[
  {
    "path": ".claude/settings.json",
    "content": "{\n  \"permissions\": {\n    \"allow\": [\n      \"Bash(pnpm typecheck:*)\",\n      \"Bash(pnpm --filter * typecheck:*)\",\n      \"Bash(pnpm lint:*)\",\n      \"Bash(pnpm --filter * lint:*)\",\n      \"Bash(pnpm format:*)\",\n      \"Bash(pnpm --filter * format:*)\",\n      \"Bash(pnpm test:*)\",\n      \"Bash(pnpm --filter * test:*)\"\n    ],\n    \"deny\": []\n  }\n}\n"
  },
  {
    "path": ".dockerignore",
    "content": "Dockerfile\n.dockerignore\n**/node_modules\nnpm-debug.log\nREADME.md\n**/.next\n**/*.db\n**/.env*\n.turbo\n.git\n./data\n\n# Files that don't need to be included in the Docker image\ndocs\nkubernetes\ncharts\napps/mobile\napps/landing\napps/browser-extension\npackages/e2e_tests\npackages/benchmarks\n\n# Aider\n.aider*\n\n# next.js\n.next/\nout/\n.claude/worktrees\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\nbuy_me_a_coffee: mbassem\ngithub: MohamedBassem\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: Bug Report\ndescription: Create a report to help us fix bugs & issues in existing supported functionality\nlabels: [\"bug\", \"status/untriaged\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for taking the time to fill out a bug report!\n        Please note that this form is for reporting bugs in existing supported functionality.\n\n        If you are reporting something that's not an issue in functionality we've previously supported and/or is simply something different to your expectations, then it may be more appropriate to raise via a feature or support request instead.\n  - type: textarea\n    id: description\n    attributes:\n      label: Describe the Bug\n      description: Provide a clear and concise description of what the bug is.\n    validations:\n      required: true\n  - type: textarea\n    id: reproduction\n    attributes:\n      label: Steps to Reproduce\n      description: Detail the steps that would replicate this issue.\n      placeholder: |\n        1. Go to '...'\n        2. Click on '....'\n        3. Scroll down to '....'\n        4. See error\n    validations:\n      required: true\n  - type: textarea\n    id: expected\n    attributes:\n      label: Expected Behaviour\n      description: Provide clear and concise description of what you expected to happen.\n    validations:\n      required: true\n  - type: textarea\n    id: context\n    attributes:\n      label: Screenshots or Additional Context\n      description: Provide any additional context and screenshots here to help us solve this issue.\n    validations:\n      required: false\n  - type: input\n    id: devicedetails\n    attributes:\n      label: Device Details\n      description: |\n        If this is an issue that occurs when using the Karakeep interface, please provide details of the device/browser used which presents the reported issue.\n      placeholder: (eg. Firefox 97 (64-bit) on Windows 11)\n    validations:\n      required: false\n  - type: input\n    id: bsversion\n    attributes:\n      label: Exact Karakeep Version\n      description: This can be found in the bottom left of the page (e.g. Karakeep v0.18.0)\n      placeholder: (eg. v0.18.0)\n    validations:\n      required: true\n\n  - type: input\n    id: environment\n    attributes:\n      label: Environment Details\n      description: |\n        Tell us where Karakeep is running, and reverse proxy (e.g. Docker, Bare linux, Proxmox, Unraid, K8s, Cloud).\n      placeholder: (eg. Docker on Ubuntu 22.04 behind Caddy, or Proxmox LXC)\n    validations:\n      required: false\n\n  - type: textarea\n    id: containerlogs\n    attributes:\n      label: Debug Logs\n      description: |\n        Please provide relevant logs where possible\n      placeholder: (paste logs here)\n    validations:\n      required: false\n\n  - type: checkboxes\n    id: confirm-troubleshooting\n    attributes:\n      label: Have you checked the troubleshooting guide?\n      description: |\n        We have a troubleshooting guide that you can find [here](https://docs.karakeep.app/administration/troubleshooting). Please check it out as you might find a solution to your problem.\n      options:\n        - label: I have checked the troubleshooting guide and I haven't found a solution to my problem\n          required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: Feature Request\ndescription: Request a new feature or idea to be added to Karakeep\nlabels: [\"feature request\", \"status/untriaged\"]\nbody:\n  - type: textarea\n    id: description\n    attributes:\n      label: Describe the feature you'd like\n      description: Provide a clear description of the feature you'd like implemented in Karakeep\n    validations:\n      required: true\n  - type: textarea\n    id: benefits\n    attributes:\n      label: Describe the benefits this would bring to existing Karakeep users\n      description: |\n        Explain the measurable benefits this feature would achieve for existing Karakeep users.\n        These benefits should details outcomes in terms of what this request solves/achieves, and should not be specific to implementation.\n        This helps us understand the core desired goal so that a variety of potential implementations could be explored.\n        This field is important. Lack of input here may lead to early issue closure.\n    validations:\n      required: true\n  - type: textarea\n    id: already_achieved\n    attributes:\n      label: Can the goal of this request already be achieved via other means?\n      description: |\n        Yes/No. If yes, please describe how the requested approach fits in with the existing method.\n    validations:\n      required: true\n  - type: checkboxes\n    id: confirm-search\n    attributes:\n      label: Have you searched for an existing open/closed issue?\n      description: |\n        To help us keep these issues under control, please ensure you have first [searched our issue list](https://github.com/karakeep-app/karakeep/issues?q=is%3Aissue) for any existing issues that cover the fundamental benefit/goal of your request.\n      options:\n        - label: I have searched for existing issues and none cover my fundamental request\n          required: true\n  - type: textarea\n    id: context\n    attributes:\n      label: Additional context\n      description: Add any other context or screenshots about the feature request here.\n    validations:\n      required: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/question.yml",
    "content": "name: Question / Support Request\ndescription: Get help with anything related to Karakeep\nlabels: [\"question\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        We use Github discussions for anything that's not a bug report or a feature request. Please ask your question in the [Q&A section](https://github.com/karakeep-app/karakeep/discussions/categories/q-a) and someone will answer it soon!\n"
  },
  {
    "path": ".github/workflows/android.yml",
    "content": "name: Android App Release Build\n\non:\n  push:\n    tags:\n      - 'android/v[0-9]+.[0-9]+.[0-9]+-[0-9]+'\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Setup repo\n        uses: actions/checkout@v4\n\n      - name: Setup\n        uses: ./tooling/github/setup\n\n      - name: Set up JDK 17\n        uses: actions/setup-java@v4\n        with:\n          java-version: '17'\n          distribution: 'temurin'\n\n      - name: Setup Android SDK\n        uses: android-actions/setup-android@v3\n\n      - name: Setup Expo\n        uses: expo/expo-github-action@v8\n        with:\n          expo-version: latest\n          eas-version: latest\n          token: ${{ secrets.EXPO_TOKEN }}\n\n      - name: Build Android app\n        working-directory: apps/mobile\n        run: eas build --platform android --local --output ${{ github.workspace }}/app-release.aab\n        env:\n          EAS_SKIP_AUTO_FINGERPRINT: \"1\"\n          SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}\n\n      - name: Upload AAB artifact\n        uses: actions/upload-artifact@v4\n        with:\n          name: karakeep-android\n          path: ${{ github.workspace }}/app-release.aab\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  pull_request:\n    branches: [\"*\"]\n  push:\n    branches: [\"main\"]\n  merge_group:\n\nconcurrency:\n  group: ${{ github.workflow }}-${{ github.ref }}\n  cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}\n\n# You can leverage Vercel Remote Caching with Turbo to speed up your builds\nenv:\n  FORCE_COLOR: 3\n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Setup\n        uses: ./tooling/github/setup\n\n      - name: Lint\n        run: pnpm lint && pnpm exec sherif\n\n  format:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Setup\n        uses: ./tooling/github/setup\n\n      - name: Format\n        run: pnpm format\n\n  typecheck:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Setup\n        uses: ./tooling/github/setup\n\n      - name: Typecheck\n        run: pnpm typecheck\n  tests:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Setup\n        uses: ./tooling/github/setup\n\n      - name: Shared Package Tests\n        working-directory: packages/shared\n        run: pnpm test\n\n      - name: TRPC Tests\n        working-directory: packages/trpc\n        run: pnpm test\n\n      - name: Workers Tests\n        working-directory: apps/workers\n        run: pnpm test\n\n      - name: E2E Tests\n        working-directory: packages/e2e_tests\n        run: pnpm test\n\n      - name: Upload Docker Logs\n        if: failure()\n        uses: actions/upload-artifact@v4\n        with:\n          name: e2e-docker-logs-${{ github.sha }}-${{ github.run_attempt }}\n          path: packages/e2e_tests/setup/docker-logs/\n          retention-days: 7\n  open-api-spec:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Setup\n        uses: ./tooling/github/setup\n\n      - name: Regenerate OpenAPI spec\n        working-directory: packages/open-api\n        run: pnpm run generate\n\n      - name: Check for changes\n        run: |\n          if [[ -n \"$(git status --porcelain)\" ]]; then\n            echo \"Error: Generated files are not up to date!\"\n            echo \"The following files have changes:\"\n            git status --porcelain\n            echo \"\"\n            echo \"Please regenerate the files locally with (pnpm run generate) and commit the changes.\"\n            git diff\n            exit 1\n          else\n            echo \"✅ Generated files are up to date!\"\n          fi\n"
  },
  {
    "path": ".github/workflows/claude.yml",
    "content": "name: Claude Code\n\non:\n  issue_comment:\n    types: [created]\n  pull_request_review_comment:\n    types: [created]\n  issues:\n    types: [opened, assigned]\n  pull_request_review:\n    types: [submitted]\n\njobs:\n  claude:\n    if: |\n      github.actor == 'MohamedBassem' && (\n        (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||\n        (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||\n        (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||\n        (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))\n      )\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n      pull-requests: write\n      issues: write\n      id-token: write\n      actions: read\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 1\n\n      - name: Run Claude Code\n        id: claude\n        uses: anthropics/claude-code-action@v1\n        with:\n          claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}\n\n          # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4)\n          # model: \"claude-opus-4-20250514\"\n\n          # Optional: Customize the trigger phrase (default: @claude)\n          # trigger_phrase: \"/claude\"\n\n          # Optional: Trigger when specific user is assigned to an issue\n          # assignee_trigger: \"claude-bot\"\n\n          # Optional: Allow Claude to run specific commands\n          allowed_tools: \"Bash(pnpm install),Bash(pnpm typecheck),Bash(pnpm test),Bash(pnpm lint:fix)\"\n\n          # Optional: Add custom instructions for Claude to customize its behavior for your project\n          # custom_instructions: |\n          #   Follow our coding standards\n          #   Ensure all new code has tests\n          #   Use TypeScript for new files\n\n          # Optional: Custom environment variables for Claude\n          # claude_env: |\n          #   NODE_ENV: test\n"
  },
  {
    "path": ".github/workflows/cli.yml",
    "content": "name: Publish CLI Package to npm\non:\n  push:\n    tags:\n      # This is a glob pattern not a regex\n      - \"cli/v[0-9]+.[0-9]+.[0-9]+\"\n\npermissions:\n  id-token: write # Required for OIDC\n  contents: read\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Setup\n        uses: ./tooling/github/setup\n\n      - name: Build CLI\n        run: pnpm build\n        working-directory: apps/cli\n\n      # npm 11.5.1 or later is required for trusted publishing\n      - run: npm install -g npm@latest\n\n      - run: pnpm publish --access public --no-git-checks\n        working-directory: apps/cli\n"
  },
  {
    "path": ".github/workflows/docker.yml",
    "content": "name: Build and Push Docker\non:\n  release:\n    types:\n      - created\n  push:\n    branches:\n      - main\n\njobs:\n  build:\n    strategy:\n      fail-fast: false\n      matrix:\n        platform: [linux/amd64, linux/arm64]\n        image: [web, workers, cli, mcp, aio]\n        include:\n          - platform: linux/amd64\n            suffix: amd64\n            os: ubuntu-latest\n          - platform: linux/arm64\n            suffix: arm64\n            os: ubuntu-24.04-arm\n          - image: web\n            name: karakeep-web\n            target: web\n            tags_latest: ghcr.io/hoarder-app/hoarder-web:latest,ghcr.io/mohamedbassem/hoarder-web:latest,ghcr.io/karakeep-app/karakeep-web:latest\n            tags_release: ghcr.io/hoarder-app/hoarder-web:${{ github.event.release.name }},ghcr.io/mohamedbassem/hoarder-web:${{ github.event.release.name }},ghcr.io/karakeep-app/karakeep-web:${{ github.event.release.name }},ghcr.io/hoarder-app/hoarder-web:release,ghcr.io/mohamedbassem/hoarder-web:release,ghcr.io/karakeep-app/karakeep-web:release\n          - image: workers\n            name: karakeep-workers\n            target: workers\n            tags_latest: ghcr.io/hoarder-app/hoarder-workers:latest,ghcr.io/mohamedbassem/hoarder-workers:latest,ghcr.io/karakeep-app/karakeep-workers:latest\n            tags_release: ghcr.io/hoarder-app/hoarder-workers:${{ github.event.release.name }},ghcr.io/mohamedbassem/hoarder-workers:${{ github.event.release.name }},ghcr.io/karakeep-app/karakeep-workers:${{ github.event.release.name }},ghcr.io/hoarder-app/hoarder-workers:release,ghcr.io/mohamedbassem/hoarder-workers:release,ghcr.io/karakeep-app/karakeep-workers:release\n          - image: cli\n            name: karakeep-cli\n            target: cli\n            tags_latest: ghcr.io/hoarder-app/hoarder-cli:latest,ghcr.io/mohamedbassem/hoarder-cli:latest,ghcr.io/karakeep-app/karakeep-cli:latest\n            tags_release: ghcr.io/hoarder-app/hoarder-cli:${{ github.event.release.name }},ghcr.io/mohamedbassem/hoarder-cli:${{ github.event.release.name }},ghcr.io/karakeep-app/karakeep-cli:${{ github.event.release.name }},ghcr.io/hoarder-app/hoarder-cli:release,ghcr.io/mohamedbassem/hoarder-cli:release,ghcr.io/karakeep-app/karakeep-cli:release\n          - image: mcp\n            name: karakeep-mcp\n            target: mcp\n            tags_latest: ghcr.io/karakeep-app/karakeep-mcp:latest\n            tags_release: ghcr.io/karakeep-app/karakeep-mcp:${{ github.event.release.name }},ghcr.io/karakeep-app/karakeep-mcp:release\n          - image: aio\n            name: karakeep-aio\n            target: aio\n            tags_latest: ghcr.io/hoarder-app/hoarder:latest,ghcr.io/karakeep-app/karakeep:latest\n            tags_release: ghcr.io/hoarder-app/hoarder:${{ github.event.release.name }},ghcr.io/karakeep-app/karakeep:${{ github.event.release.name }},ghcr.io/hoarder-app/hoarder:release,ghcr.io/karakeep-app/karakeep:release\n    runs-on: ${{ matrix.os }}\n    permissions:\n      packages: write\n      contents: read\n\n    steps:\n      - uses: actions/checkout@v4\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Login to Github Registry\n        uses: docker/login-action@v3\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GHCR_GITHUB_PAT }}\n\n      - name: Prepare tags\n        id: tags\n        run: |\n          set -euo pipefail\n          add_suffix() {\n            local value=\"$1\"\n            local out=\"\"\n            IFS=',' read -ra items <<< \"$value\"\n            for item in \"${items[@]}\"; do\n              out+=\"${item}-${{ matrix.suffix }},\"\n            done\n            echo \"${out%,}\"\n          }\n\n          all_tags=\"\"\n\n          # Only push 'latest' tags on pushes to main, not on releases\n          if [[ \"${{ github.event_name }}\" == \"push\" ]]; then\n            latest_with_suffix=$(add_suffix \"${{ matrix.tags_latest }}\")\n            all_tags=\"${latest_with_suffix}\"\n            echo \"latest=${latest_with_suffix}\" >> \"$GITHUB_OUTPUT\"\n          fi\n\n          # Only push release-specific tags on releases\n          if [[ \"${{ github.event_name }}\" == \"release\" ]]; then\n            release_with_suffix=$(add_suffix \"${{ matrix.tags_release }}\")\n            all_tags=\"${release_with_suffix}\"\n            echo \"release=${release_with_suffix}\" >> \"$GITHUB_OUTPUT\"\n          fi\n\n          echo \"all=${all_tags}\" >> \"$GITHUB_OUTPUT\"\n\n      - name: Build ${{ matrix.name }}\n        uses: docker/build-push-action@v5\n        with:\n          context: .\n          build-args: SERVER_VERSION=${{ github.event_name == 'release' && github.event.release.name || 'nightly' }}\n          file: docker/Dockerfile\n          target: ${{ matrix.target }}\n          platforms: ${{ matrix.platform }}\n          push: true\n          tags: ${{ steps.tags.outputs.all }}\n          cache-from: type=registry,ref=ghcr.io/karakeep-app/karakeep-build-cache:${{ matrix.target }}-${{ matrix.suffix }}\n          cache-to: type=registry,mode=max,ref=ghcr.io/karakeep-app/karakeep-build-cache:${{ matrix.target }}-${{ matrix.suffix }}\n\n  manifest:\n    needs: build\n    runs-on: ubuntu-latest\n    permissions:\n      packages: write\n      contents: read\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n          - name: karakeep-web\n            tags_latest: ghcr.io/hoarder-app/hoarder-web:latest,ghcr.io/mohamedbassem/hoarder-web:latest,ghcr.io/karakeep-app/karakeep-web:latest\n            tags_release: ghcr.io/hoarder-app/hoarder-web:${{ github.event.release.name }},ghcr.io/mohamedbassem/hoarder-web:${{ github.event.release.name }},ghcr.io/karakeep-app/karakeep-web:${{ github.event.release.name }},ghcr.io/hoarder-app/hoarder-web:release,ghcr.io/mohamedbassem/hoarder-web:release,ghcr.io/karakeep-app/karakeep-web:release\n          - name: karakeep-workers\n            tags_latest: ghcr.io/hoarder-app/hoarder-workers:latest,ghcr.io/mohamedbassem/hoarder-workers:latest,ghcr.io/karakeep-app/karakeep-workers:latest\n            tags_release: ghcr.io/hoarder-app/hoarder-workers:${{ github.event.release.name }},ghcr.io/mohamedbassem/hoarder-workers:${{ github.event.release.name }},ghcr.io/karakeep-app/karakeep-workers:${{ github.event.release.name }},ghcr.io/hoarder-app/hoarder-workers:release,ghcr.io/mohamedbassem/hoarder-workers:release,ghcr.io/karakeep-app/karakeep-workers:release\n          - name: karakeep-cli\n            tags_latest: ghcr.io/hoarder-app/hoarder-cli:latest,ghcr.io/mohamedbassem/hoarder-cli:latest,ghcr.io/karakeep-app/karakeep-cli:latest\n            tags_release: ghcr.io/hoarder-app/hoarder-cli:${{ github.event.release.name }},ghcr.io/mohamedbassem/hoarder-cli:${{ github.event.release.name }},ghcr.io/karakeep-app/karakeep-cli:${{ github.event.release.name }},ghcr.io/hoarder-app/hoarder-cli:release,ghcr.io/mohamedbassem/hoarder-cli:release,ghcr.io/karakeep-app/karakeep-cli:release\n          - name: karakeep-mcp\n            tags_latest: ghcr.io/karakeep-app/karakeep-mcp:latest\n            tags_release: ghcr.io/karakeep-app/karakeep-mcp:${{ github.event.release.name }},ghcr.io/karakeep-app/karakeep-mcp:release\n          - name: karakeep-aio\n            tags_latest: ghcr.io/hoarder-app/hoarder:latest,ghcr.io/karakeep-app/karakeep:latest\n            tags_release: ghcr.io/hoarder-app/hoarder:${{ github.event.release.name }},ghcr.io/karakeep-app/karakeep:${{ github.event.release.name }},ghcr.io/hoarder-app/hoarder:release,ghcr.io/karakeep-app/karakeep:release\n    steps:\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Login to Github Registry\n        uses: docker/login-action@v3\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GHCR_GITHUB_PAT }}\n\n      - name: Create manifests for ${{ matrix.name }}\n        env:\n          TAGS_LATEST: ${{ matrix.tags_latest }}\n          TAGS_RELEASE: ${{ matrix.tags_release }}\n          IS_RELEASE: ${{ github.event_name == 'release' }}\n          IS_PUSH: ${{ github.event_name == 'push' }}\n        run: |\n          set -euo pipefail\n\n          create_manifest() {\n            local tag=\"$1\"\n            docker buildx imagetools create \\\n              -t \"${tag}\" \\\n              \"${tag}-amd64\" \\\n              \"${tag}-arm64\"\n          }\n\n          # Only create 'latest' manifests on pushes to main\n          if [[ \"${IS_PUSH}\" == \"true\" ]]; then\n            IFS=',' read -ra latest_tags <<< \"${TAGS_LATEST}\"\n            for tag in \"${latest_tags[@]}\"; do\n              create_manifest \"$tag\"\n            done\n          fi\n\n          # Only create release-specific manifests on releases\n          if [[ \"${IS_RELEASE}\" == \"true\" ]]; then\n            IFS=',' read -ra release_tags <<< \"${TAGS_RELEASE}\"\n            for tag in \"${release_tags[@]}\"; do\n              create_manifest \"$tag\"\n            done\n          fi\n"
  },
  {
    "path": ".github/workflows/extension.yml",
    "content": "name: Extension Release Build\n\non:\n  push:\n    tags:\n      - 'extension/v[0-9]+.[0-9]+.[0-9]+'\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Setup repo\n        uses: actions/checkout@v4\n\n      - name: Setup\n        uses: ./tooling/github/setup\n\n      - name: Build the extension (chrome)\n        working-directory: apps/browser-extension\n        run: pnpm run build --outDir dist-chrome\n\n      - name: Build the extension (firefox)\n        env:\n          VITE_BUILD_FIREFOX: true\n        working-directory: apps/browser-extension\n        run: pnpm run build --outDir dist-firefox\n\n      - name: Upload Extension Archive (chrome)\n        uses: actions/upload-artifact@v4\n        with:\n          name: karakeep-extension-chrome\n          path: apps/browser-extension/dist-chrome/*\n\n      - name: Upload Extension Archive (firefox)\n        uses: actions/upload-artifact@v4\n        with:\n          name: karakeep-extension-firefox\n          path: apps/browser-extension/dist-firefox/*\n"
  },
  {
    "path": ".github/workflows/ios.yml",
    "content": "name: iOS App Release Build\n\non:\n  push:\n    tags:\n      - 'ios/v[0-9]+.[0-9]+.[0-9]+-[0-9]+'\n\njobs:\n  build:\n    runs-on: macos-26\n    steps:\n      - name: Setup repo\n        uses: actions/checkout@v4\n\n      - name: Setup\n        uses: ./tooling/github/setup\n\n      - name: Setup Expo\n        uses: expo/expo-github-action@v8\n        with:\n          expo-version: latest\n          eas-version: latest\n          token: ${{ secrets.EXPO_TOKEN }}\n\n      - name: Build iOS app\n        working-directory: apps/mobile\n        run: eas build --platform ios --local --non-interactive --output ${{ github.workspace }}/app-release.ipa\n        env:\n          EAS_SKIP_AUTO_FINGERPRINT: \"1\"\n          SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}\n\n      - name: Upload IPA artifact\n        uses: actions/upload-artifact@v4\n        with:\n          name: karakeep-ios\n          path: ${{ github.workspace }}/app-release.ipa\n"
  },
  {
    "path": ".github/workflows/mcp.yml",
    "content": "name: Publish MCP Package to npm\non:\n  push:\n    tags:\n      # This is a glob pattern not a regex\n      - \"mcp/v[0-9]+.[0-9]+.[0-9]+\"\n\npermissions:\n  id-token: write # Required for OIDC\n  contents: read\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Setup\n        uses: ./tooling/github/setup\n\n      - name: Build MCP\n        run: pnpm build\n        working-directory: apps/mcp\n\n      # npm 11.5.1 or later is required for trusted publishing\n      - run: npm install -g npm@latest\n\n      - run: pnpm publish --access public --no-git-checks\n        working-directory: apps/mcp\n"
  },
  {
    "path": ".github/workflows/sdk.yml",
    "content": "name: Publish CLI Package to npm\non:\n  push:\n    tags:\n      # This is a glob pattern not a regex\n      - \"sdk/v[0-9]+.[0-9]+.[0-9]+\"\n\npermissions:\n  id-token: write # Required for OIDC\n  contents: read\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Setup\n        uses: ./tooling/github/setup\n\n      - name: Build SDK\n        run: pnpm build\n        working-directory: packages/sdk\n\n      # npm 11.5.1 or later is required for trusted publishing\n      - run: npm install -g npm@latest\n\n      - run: pnpm publish --access public --no-git-checks\n        working-directory: packages/sdk\n"
  },
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\nnode_modules\n.pnp\n.pnpm-store\n.pnp.js\n.yarn/install-state.gz\n\n# testing\ncoverage\n\n# next.js\n.next/\nout/\n\n# production\nbuild\ndist\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# local env files\n.env*.local\n.env\n\n# vercel\n.vercel\n\n# typescript\n*.tsbuildinfo\n\n# The sqlite database\n**/*.db\ndata\n\n# PWA Files\n**/public/sw.js\n**/public/workbox-*.js\n**/public/worker-*.js\n**/public/sw.js.map\n**/public/workbox-*.js.map\n**/public/worker-*.js.map\n\n# Turbo\n.turbo\n\n# Idea\n.idea\n*.iml\n\n# Aider\n.aider*\n\n# VS-Code\n.vscode\nauth_failures.log\n.claude/settings.local.json\n\n# Local directory for AI agent contexts\n.aicontext/\n.codex\n.claude/worktrees\njean.json\n"
  },
  {
    "path": ".husky/pre-commit",
    "content": "pnpm preflight\npnpm exec sherif\npnpm run --filter @karakeep/open-api check\n"
  },
  {
    "path": ".npmrc",
    "content": "node-linker=hoisted\n"
  },
  {
    "path": ".oxfmtrc.json",
    "content": "{\n  \"$schema\": \"./node_modules/oxfmt/configuration_schema.json\",\n  \"sortTailwindcss\": {\n    \"config\": \"tooling/tailwind/web.ts\",\n    \"functions\": [\n      \"cn\",\n      \"cva\"\n    ]\n  },\n  \"importOrder\": [\n    \"<TYPES>\",\n    \"^(react/(.*)$)|^(react$)|^(react-native(.*)$)\",\n    \"^(next/(.*)$)|^(next$)\",\n    \"^(expo(.*)$)|^(expo$)\",\n    \"<THIRD_PARTY_MODULES>\",\n    \"\",\n    \"<TYPES>^@karakeep\",\n    \"^@karakeep/(.*)$\",\n    \"\",\n    \"<TYPES>^[.|..|~]\",\n    \"^~/\",\n    \"^[../]\",\n    \"^[./]\"\n  ],\n  \"importOrderParserPlugins\": [\n    \"typescript\",\n    \"jsx\",\n    \"decorators-legacy\"\n  ],\n  \"importOrderTypeScriptVersion\": \"4.4.0\",\n  \"printWidth\": 80,\n  \"sortPackageJson\": false,\n  \"ignorePatterns\": [\n    \".next\",\n    \"build\",\n    \"coverage\",\n    \".vscode*\",\n    \"node_modules\",\n    \"dist\",\n    \"*.md\",\n    \"*.json\",\n    \".env\",\n    \".env.*\",\n    \"**/migrations/**\",\n    \"packages/open-api/karakeep-openapi-spec.json\",\n    \"apps/web/public/workbox-*.js\",\n    \"pnpm-lock.yaml\",\n    \"package-lock.json\",\n    \"yarn.lock\",\n    \"**/.expo/**\",\n    \"apps/mobile/android\",\n    \"apps/mobile/ios\"\n  ]\n}\n"
  },
  {
    "path": ".oxlintrc.json",
    "content": "{\n  \"$schema\": \"node_modules/oxlint/configuration_schema.json\",\n  \"ignorePatterns\": [\n    \"**/*.config.js\",\n    \"**/*.config.cjs\",\n    \"**/.eslintrc.cjs\",\n    \"**/.next\",\n    \"**/dist\",\n    \"**/build\",\n    \"**/pnpm-lock.yaml\"\n  ]\n}\n"
  },
  {
    "path": "AGENTS.md",
    "content": "# Karakeep Project Overview\n\nThis document provides context about the Karakeep project for the different agents.\n\n## Project Overview\n\nKarakeep is a monorepo project managed with Turborepo. It appears to be a web application with a focus on collecting and organizing information, possibly a bookmarking or \"read-it-later\" service. The project is built with a modern tech stack, including:\n\n- **Frontend:** Next.js, React, TypeScript, Tailwind CSS\n- **Backend:** Hono (a lightweight web framework), tRPC\n- **Database:** Drizzle ORM (likely with a relational database like PostgreSQL or SQLite)\n- **Tooling:** Oxfmt, oxlint, Vitest, pnpm\n\n## Project Structure\n\nThe project is organized into `apps` and `packages`:\n\n### Applications (`apps/`)\n\n- **`web`:** The main web application, built with Next.js.\n- **`browser-extension`:** A browser extension, likely for saving content to karakeep.\n- **`cli`:** A command-line interface for interacting with the service.\n- **`landing`:** A landing page for the project.\n- **`mobile`:** A mobile application (details unknown).\n- **`mcp`:** The Model Context Protocol (MCP) server to communicate with Karakeep.\n- **`workers`:** Background workers for processing tasks.\n\n### Packages (`packages/`)\n\n- **`api`:** The main API, built with Hono and tRPC.\n- **`db`:** Database schema and migrations, using Drizzle ORM.\n- **`e2e_tests`:** End-to-end tests for the project.\n- **`open-api`:** OpenAPI specifications for the API.\n- **`sdk`:** A software development kit for interacting with the API.\n- **`shared`:** Shared code and types between packages.\n- **`shared-react`:** Shared React components and hooks.\n- **`shared-server`:** Shared logic that's meant to be used on the server-side.\n- **`trpc`:** tRPC router and procedures. Most of the business logic is here.\n\n### Docs\n\n- **docs/docs/03-configuration.md**: Explains configuration options for the project.\n\n## Development Workflow\n\n- **Package Manager:** pnpm\n- **Build System:** Turborepo\n- **Code Formatting:** Oxfmt\n- **Linting:** oxlint\n- **Testing:** Vitest\n\n## Other info\n\n- This project uses shadcn/ui. The shadcn components in the web app are in `packages/web/components/ui`.\n- This project uses Tailwind CSS.\n- For the mobile app, we use [expo](https://expo.dev/).\n\n### Common Commands\n\n- `pnpm typecheck`: Typecheck the codebase.\n- `pnpm lint`: Lint the codebase.\n- `pnpm lint:fix`: Fix linting issues.\n- `pnpm format`: Format the codebase.\n- `pnpm format:fix`: Fix formatting issues.\n- `pnpm test`: Run tests.\n- `pnpm db:generate --name description_of_schema_change`: db migration after making schema changes\n\nStarting services:\n- `pnpm web`: Start the web application (this doesn't return, unless you kill it).\n- `pnpm workers`: Starts the background workers (this doesn't return, unless you kill it).\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to Karakeep\n\nFirst off, thank you for considering contributing to our project! This document outlines our contribution process and guidelines to make it easy for you to help improve this project.\n\n## How Can I Contribute?\n\n\n### Asking Questions\n\nIf you have questions:\n\n* Use the GitHub Discussions Q&A section\n* Search existing discussions to see if your question has been answered\n* If not found, create a new discussion with a clear, descriptive title\n\n\n### Reporting Bugs\n\nIf in doubt, about whether a problem you're seeing is a bug or not, use the discussions Q&A section instead. If it turns out to be a bug, we'll promote it into an issue. If you're sure it's a bug:\n* Create a new issue using the bug report template\n* Include a clear description and steps to reproduce\n* Wait for triage and labeling by maintainers\n\n\n### Suggesting Features\n\nFor feature requests:\n\n* If you find a similar feature request, upvote it instead of creating a new one to help us prioritize it\n* Create a new issue using the feature request template\n* New features start with the `status/untriaged` label\n  * If the feature request is approved, the maintainers will add the `status/approved` label and assign a priority to the issue\n  * Other issues will get labeled with `status/icebox`. Issues in the icebox are not prioritized, until there's enough interest from the community\n\n\n### Working on Issues\n\nBefore starting to work on an issue:\n\n* Prefer working on `status/approved` issues to make sure they get prioritized for the review\n* Comment on the issue to let others know you're working on it\n* Read the [development documentation](https://docs.karakeep.app/Development/setup) to get started\n* If you need help, you can find us in the #development channel in the [Karakeep Discord](https://discord.com/invite/NrgeYywsFh).\n* Once you're done, open a PR and wait for review. Try to include a screenshot of the change in the PR description.\n\nPlease note that we're all volunteers. We'll aim to review your PR within a week from when they are opened.\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU AFFERO GENERAL PUBLIC LICENSE\n                       Version 3, 19 November 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU Affero General Public License is a free, copyleft license for\nsoftware and other kinds of works, specifically designed to ensure\ncooperation with the community in the case of network server software.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nour General Public Licenses are intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  Developers that use our General Public Licenses protect your rights\nwith two steps: (1) assert copyright on the software, and (2) offer\nyou this License which gives you legal permission to copy, distribute\nand/or modify the software.\n\n  A secondary benefit of defending all users' freedom is that\nimprovements made in alternate versions of the program, if they\nreceive widespread use, become available for other developers to\nincorporate.  Many developers of free software are heartened and\nencouraged by the resulting cooperation.  However, in the case of\nsoftware used on network servers, this result may fail to come about.\nThe GNU General Public License permits making a modified version and\nletting the public access it on a server without ever releasing its\nsource code to the public.\n\n  The GNU Affero General Public License is designed specifically to\nensure that, in such cases, the modified source code becomes available\nto the community.  It requires the operator of a network server to\nprovide the source code of the modified version running there to the\nusers of that server.  Therefore, public use of a modified version, on\na publicly accessible server, gives the public access to the source\ncode of the modified version.\n\n  An older license, called the Affero General Public License and\npublished by Affero, was designed to accomplish similar goals.  This is\na different license, not a version of the Affero GPL, but Affero has\nreleased a new version of the Affero GPL which permits relicensing under\nthis license.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU Affero General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Remote Network Interaction; Use with the GNU General Public License.\n\n  Notwithstanding any other provision of this License, if you modify the\nProgram, your modified version must prominently offer all users\ninteracting with it remotely through a computer network (if your version\nsupports such interaction) an opportunity to receive the Corresponding\nSource of your version by providing access to the Corresponding Source\nfrom a network server at no charge, through some standard or customary\nmeans of facilitating copying of software.  This Corresponding Source\nshall include the Corresponding Source for any work covered by version 3\nof the GNU General Public License that is incorporated pursuant to the\nfollowing paragraph.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the work with which it is combined will remain governed by version\n3 of the GNU General Public License.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU Affero General Public License from time to time.  Such new versions\nwill be similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU Affero General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU Affero General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU Affero General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU Affero General Public License as published\n    by the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU Affero General Public License for more details.\n\n    You should have received a copy of the GNU Affero General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If your software can interact with users remotely through a computer\nnetwork, you should also make sure that it provides a way for users to\nget its source.  For example, if your program is a web application, its\ninterface could display a \"Source\" link that leads users to an archive\nof the code.  There are many ways you could offer source, and different\nsolutions will be better for different programs; see section 13 for the\nspecific requirements.\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU AGPL, see\n<https://www.gnu.org/licenses/>.\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n    <a href=\"https://github.com/karakeep-app/karakeep/actions/workflows/ci.yml\">\n        <img alt=\"GitHub Actions Workflow Status\" src=\"https://img.shields.io/github/actions/workflow/status/karakeep-app/karakeep/ci.yml\" />\n    </a>\n    <a href=\"https://github.com/karakeep-app/karakeep/releases\">\n        <img alt=\"GitHub Release\" src=\"https://img.shields.io/github/v/release/karakeep-app/karakeep\" />\n    </a>\n    <a href=\"https://discord.gg/NrgeYywsFh\">\n        <img alt=\"Discord\" src=\"https://img.shields.io/discord/1223681308962721802?label=chat%20on%20discord\" />\n    </a>\n    <a href=\"https://hosted.weblate.org/engage/hoarder/\">\n        <img src=\"https://hosted.weblate.org/widget/hoarder/hoarder/svg-badge.svg\" alt=\"Translation status\" />\n    </a>\n</div>\n\n# <img height=\"50px\" src=\"./screenshots/logo.png\" />\n\nKarakeep (previously Hoarder) is a self-hostable bookmark-everything app with a touch of AI for the data hoarders out there.\n\n![homepage screenshot](https://github.com/karakeep-app/karakeep/blob/main/screenshots/homepage.png?raw=true)\n\n## Features\n\n- 🔗 Bookmark links, take simple notes and store images and pdfs.\n- ⬇️ Automatic fetching for link titles, descriptions and images.\n- 📋 Sort your bookmarks into lists.\n- 👥 Collaborate with others on the same list.\n- 🔎 Full text search of all the content stored.\n- ✨ AI-based (aka chatgpt) automatic tagging and summarization. With supports for local models using ollama!\n- 🤖 Rule-based engine for customized management.\n- 🎆 OCR for extracting text from images.\n- 🔖 [Chrome plugin](https://chromewebstore.google.com/detail/karakeep/kgcjekpmcjjogibpjebkhaanilehneje) and [Firefox addon](https://addons.mozilla.org/en-US/firefox/addon/karakeep/) for quick bookmarking.\n- 📱 An [iOS app](https://apps.apple.com/us/app/karakeep-app/id6479258022), and an [Android app](https://play.google.com/store/apps/details?id=app.hoarder.hoardermobile&pcampaignid=web_share).\n- 📰 Auto hoarding from RSS feeds.\n- 🔌 REST API and multiple clients.\n- 🌐 Multi-language support.\n- 🖍️ Mark and store highlights from your hoarded content.\n- 🗄️ Full page archival (using [monolith](https://github.com/Y2Z/monolith)) to protect against link rot.\n- ▶️ Auto video archiving using [yt-dlp](https://github.com/yt-dlp/yt-dlp).\n- ☑️ Bulk actions support.\n- 🔐 SSO support.\n- 🌙 Dark mode support.\n- 💾 Self-hosting first.\n- ⬇️ Bookmark importers from Chrome, Pocket, Linkwarden, Omnivore, Tab Session Manager.\n- 🔄 Automatic sync with browser bookmarks via [floccus](https://floccus.org/).\n- [Planned] Offline reading on mobile, semantic search across bookmarks, ...\n\n**⚠️ This app is under heavy development.**\n\n## Documentation\n\n- [Installation](https://docs.karakeep.app/Installation/docker)\n- [Configuration](https://docs.karakeep.app/configuration)\n- [Screenshots](https://docs.karakeep.app/screenshots)\n- [Security Considerations](https://docs.karakeep.app/security-considerations)\n- [Development](https://docs.karakeep.app/Development/setup)\n\n## Demo\n\nYou can access the demo at [https://try.karakeep.app](https://try.karakeep.app). Login with the following creds:\n\n```\nemail: demo@karakeep.app\npassword: demodemo\n```\n\nThe demo is seeded with some content, but it's in read-only mode to prevent abuse.\n\n## About the name\n\nThe name Karakeep is inspired by the Arabic word \"كراكيب\" (karakeeb), a colloquial term commonly used to refer to miscellaneous clutter, odds and ends, or items that may seem disorganized but often hold personal value or hidden usefulness. It evokes the image of a messy drawer or forgotten box, full of stuff you can't quite throw away—because somehow, it matters (or more likely, because you're a hoarder!).\n\n## Stack\n\n- [NextJS](https://nextjs.org/) for the web app. Using app router.\n- [Drizzle](https://orm.drizzle.team/) for the database and its migrations.\n- [NextAuth](https://next-auth.js.org) for authentication.\n- [tRPC](https://trpc.io) for client->server communication.\n- [Puppeteer](https://pptr.dev/) for crawling the bookmarks.\n- [OpenAI](https://openai.com/) because AI is so hot right now.\n- [Meilisearch](https://meilisearch.com) for the full content search.\n\n## Why did I build it?\n\nI browse reddit, twitter and hackernews a lot from my phone. I frequently find interesting stuff (articles, tools, etc) that I'd like to bookmark and read later when I'm in front of a laptop. Typical read-it-later apps usecase. Initially, I was using [Pocket](https://getpocket.com) for that. Then I got into self-hosting and I wanted to self-host this usecase. I used [memos](https://github.com/usememos/memos) for those quick notes and I loved it but it was lacking some features that I found important for that usecase such as link previews and automatic tagging (more on that in the next section).\n\nI'm a systems engineer in my day job (and have been for the past 7 years). I didn't want to get too detached from the web development world. I decided to build this app as a way to keep my hand dirty with web development, and at the same time, build something that I care about and use every day.\n\n## Alternatives\n\n- [memos](https://github.com/usememos/memos): I love memos. I have it running on my home server and it's one of my most used self-hosted apps. It doesn't, however, archive or preview the links shared in it. It's just that I dump a lot of links there and I'd have loved if I'd be able to figure which link is that by just looking at my timeline. Also, given the variety of things I dump there, I'd have loved if it does some sort of automatic tagging for what I save there. This is exactly the usecase that I'm trying to tackle with Karakeep.\n- [mymind](https://mymind.com/): Mymind is the closest alternative to this project and from where I drew a lot of inspirations. It's a commercial product though.\n- [raindrop](https://raindrop.io): A polished open source bookmark manager that supports links, images and files. It's not self-hostable though.\n- Bookmark managers (mostly focused on bookmarking links):\n    - [Pocket](https://getpocket.com) (Dead): Pocket is what hooked me into the whole idea of read-it-later apps. I used it [a lot](https://blog.mbassem.com/2019/01/27/favorite-articles-2018/). However, I recently got into home-labbing and became obsessed with the idea of running my services in my home server. Karakeep is meant to be a self-hosting first app. Mozilla recently announced that it's shutting down pocket.\n    - [Linkwarden](https://linkwarden.app/): An open-source self-hostable bookmark manager that I ran for a bit in my homelab. It's focused mostly on links and supports collaborative collections.\n    - [Wallabag](https://wallabag.it): Wallabag is a well-established open source read-it-later app written in php.\n    - [Shiori](https://github.com/go-shiori/shiori): Shiori is meant to be an open source pocket clone written in Go.\n\n## Translations\n\nKarakeep uses Weblate for managing translations. If you want to help translate Karakeep, you can do so [here](https://hosted.weblate.org/engage/hoarder/).\n\n## Karakeep Cloud ☁️\n\nIf you're not comfortable with self-hosting, you can use our managed Karakeep cloud at [cloud.karakeep.app](https://cloud.karakeep.app). Cloud subscriptions support the development of Karakeep.\n\n## Support\n\nIf you're enjoying using Karakeep, drop a ⭐️ on the repo!\n\n<a href=\"https://www.buymeacoffee.com/mbassem\" target=\"_blank\"><img src=\"https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png\" alt=\"Buy Me A Coffee\" style=\"height: 60px !important;width: 217px !important;\" ></a>\n\n## Community Channels\n\n- Join us on [Discord](https://discord.gg/NrgeYywsFh).\n- Follow us on Twitter: [@karakeep_app](https://x.com/karakeep_app).\n\n## License\n\nKarakeep is licensed under [AGPL-3.0](https://github.com/karakeep-app/karakeep/blob/main/LICENSE) and owned by [Localhost Labs Ltd](https://localhostlabs.co.uk).\n\n## Star History\n\n[![Star History Chart](https://api.star-history.com/svg?repos=karakeep-app/karakeep&type=Date)](https://star-history.com/#karakeep-app/karakeep&Date)\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Reporting a Vulnerability\n\nPlease report security issues to `security@karakeep.app`\n"
  },
  {
    "path": "apps/browser-extension/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
  },
  {
    "path": "apps/browser-extension/.oxlintrc.json",
    "content": "{\n  \"$schema\": \"../../node_modules/oxlint/configuration_schema.json\",\n  \"extends\": [\n    \"../../tooling/oxlint/oxlint-base.json\",\n    \"../../tooling/oxlint/oxlint-react.json\"\n  ],\n  \"env\": {\n    \"builtin\": true,\n    \"commonjs\": true,\n    \"browser\": true,\n    \"es2022\": true,\n    \"node\": true\n  },\n  \"globals\": {\n    \"React\": \"writeable\"\n  },\n  \"settings\": {\n    \"react\": {\n      \"version\": \"19\"\n    }\n  },\n  \"ignorePatterns\": [\n    \"**/*.config.js\",\n    \"**/*.config.cjs\",\n    \"**/.eslintrc.cjs\",\n    \".next\",\n    \"dist\",\n    \"build\",\n    \"pnpm-lock.yaml\"\n  ]\n}\n"
  },
  {
    "path": "apps/browser-extension/components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"default\",\n  \"rsc\": false,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"tailwind.config.js\",\n    \"css\": \"src/index.css\",\n    \"baseColor\": \"slate\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"aliases\": {\n    \"components\": \"src/components\",\n    \"utils\": \"src/utils/css\"\n  }\n}\n"
  },
  {
    "path": "apps/browser-extension/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/vite.svg\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Karakeep</title>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/browser-extension/manifest.json",
    "content": "{\n  \"manifest_version\": 3,\n  \"name\": \"Karakeep\",\n  \"description\": \"An extension to bookmark links to karakeep.app\",\n  \"version\": \"1.2.9\",\n  \"icons\": {\n    \"16\": \"public/logo-16.png\",\n    \"48\": \"public/logo-48.png\",\n    \"128\": \"public/logo-128.png\"\n  },\n  \"action\": {\n    \"default_popup\": \"index.html\",\n    \"theme_icons\": [\n      {\n        \"light\": \"logo-16-darkmode.png\",\n        \"dark\": \"logo-16.png\",\n        \"size\": 16\n      },\n      {\n        \"light\": \"logo-48-darkmode.png\",\n        \"dark\": \"logo-48.png\",\n        \"size\": 48\n      },\n      {\n        \"light\": \"logo-128-darkmode.png\",\n        \"dark\": \"logo-128.png\",\n        \"size\": 128\n      }\n    ]\n  },\n  \"background\": {\n    \"service_worker\": \"src/background/background.ts\",\n    \"scripts\": [\"src/background/background.ts\"]\n  },\n  \"options_ui\": {\n    \"page\": \"index.html#options\",\n    \"open_in_tab\": false\n  },\n  \"browser_specific_settings\": {\n    \"gecko\": {\n      \"id\": \"addon@karakeep.app\"\n    }\n  },\n  \"content_security_policy\": {\n    \"extension_pages\": \"script-src 'self'; object-src 'self'\"\n  },\n  \"permissions\": [\"storage\", \"tabs\", \"contextMenus\"],\n  \"commands\": {\n    \"add-link\": {\n      \"suggested_key\": {\n        \"default\": \"Ctrl+Shift+E\"\n      },\n      \"description\": \"Send the current page URL to Karakeep.\"\n    }\n  }\n}\n"
  },
  {
    "path": "apps/browser-extension/package.json",
    "content": "{\n  \"name\": \"@karakeep/browser-extension\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc && vite build\",\n    \"format\": \"oxfmt --check .\",\n    \"format:fix\": \"oxfmt .\",\n    \"lint\": \"oxlint .\",\n    \"lint:fix\": \"oxlint . --fix\",\n    \"preview\": \"vite preview\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@karakeep/shared\": \"workspace:^0.1.0\",\n    \"@karakeep/shared-react\": \"workspace:^0.1.0\",\n    \"@karakeep/trpc\": \"workspace:^0.1.0\",\n    \"@radix-ui/react-dialog\": \"^1.1.14\",\n    \"@radix-ui/react-popover\": \"^1.1.14\",\n    \"@radix-ui/react-select\": \"^2.2.5\",\n    \"@radix-ui/react-slot\": \"^1.2.4\",\n    \"@tanstack/query-async-storage-persister\": \"5.90.2\",\n    \"@tanstack/react-query\": \"5.90.2\",\n    \"@tanstack/react-query-persist-client\": \"5.90.2\",\n    \"@trpc/client\": \"^11.9.0\",\n    \"@trpc/server\": \"^11.9.0\",\n    \"@trpc/tanstack-react-query\": \"^11.9.0\",\n    \"@uidotdev/usehooks\": \"^2.4.1\",\n    \"class-variance-authority\": \"^0.7.0\",\n    \"clsx\": \"^2.1.0\",\n    \"cmdk\": \"^1.1.1\",\n    \"lucide-react\": \"^0.501.0\",\n    \"react\": \"^19.2.1\",\n    \"react-dom\": \"^19.2.1\",\n    \"react-router-dom\": \"^6.22.0\",\n    \"superjson\": \"^2.2.1\",\n    \"tailwind-merge\": \"^2.2.1\",\n    \"tailwindcss-animate\": \"^1.0.7\",\n    \"zod\": \"^3.24.2\"\n  },\n  \"devDependencies\": {\n    \"@crxjs/vite-plugin\": \"2.2.0\",\n    \"@karakeep/tailwind-config\": \"workspace:^0.1.0\",\n    \"@karakeep/tsconfig\": \"workspace:^0.1.0\",\n    \"@types/chrome\": \"^0.0.260\",\n    \"@types/react\": \"^19.2.14\",\n    \"@types/react-dom\": \"^19.1.6\",\n    \"@vitejs/plugin-react-swc\": \"^4.0.1\",\n    \"autoprefixer\": \"^10.4.17\",\n    \"postcss\": \"^8.4.35\",\n    \"tailwindcss\": \"^3.4.1\",\n    \"typescript\": \"^5.9\",\n    \"vite\": \"^7.0.6\"\n  }\n}\n"
  },
  {
    "path": "apps/browser-extension/postcss.config.js",
    "content": "export default {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n};\n"
  },
  {
    "path": "apps/browser-extension/src/BookmarkDeletedPage.tsx",
    "content": "export default function BookmarkDeletedPage() {\n  return <p className=\"text-xl\">Bookmark Deleted!</p>;\n}\n"
  },
  {
    "path": "apps/browser-extension/src/BookmarkSavedPage.tsx",
    "content": "import { useState } from \"react\";\nimport { ArrowUpRightFromSquare, Trash } from \"lucide-react\";\nimport { Link, useNavigate, useParams } from \"react-router-dom\";\n\nimport { useDeleteBookmark } from \"@karakeep/shared-react/hooks/bookmarks\";\n\nimport BookmarkLists from \"./components/BookmarkLists\";\nimport { ListsSelector } from \"./components/ListsSelector\";\nimport { NoteEditor } from \"./components/NoteEditor\";\nimport TagList from \"./components/TagList\";\nimport { TagsSelector } from \"./components/TagsSelector\";\nimport { Button, buttonVariants } from \"./components/ui/button\";\nimport Spinner from \"./Spinner\";\nimport { cn } from \"./utils/css\";\nimport usePluginSettings from \"./utils/settings\";\nimport { MessageType } from \"./utils/type\";\n\nexport default function BookmarkSavedPage() {\n  const { bookmarkId } = useParams();\n  const navigate = useNavigate();\n  const [error, setError] = useState(\"\");\n\n  const { mutate: deleteBookmark, isPending } = useDeleteBookmark({\n    onSuccess: async () => {\n      const [currentTab] = await chrome.tabs.query({\n        active: true,\n        lastFocusedWindow: true,\n      });\n      await chrome.runtime.sendMessage({\n        type: MessageType.BOOKMARK_REFRESH_BADGE,\n        currentTab: currentTab,\n      });\n      navigate(\"/bookmarkdeleted\");\n    },\n    onError: (e) => {\n      setError(e.message);\n    },\n  });\n\n  const { settings } = usePluginSettings();\n\n  if (!bookmarkId) {\n    return <div>NOT FOUND</div>;\n  }\n\n  return (\n    <div className=\"flex flex-col gap-2\">\n      {error && <p className=\"text-red-500\">{error}</p>}\n      <div className=\"flex items-center justify-between gap-2\">\n        <p className=\"text-xl\">Hoarded!</p>\n        <div className=\"flex gap-2\">\n          <Link\n            className={cn(\n              buttonVariants({ variant: \"link\" }),\n              \"flex gap-2 rounded-md p-3\",\n            )}\n            target=\"_blank\"\n            rel=\"noreferrer\"\n            to={`${settings.address}/dashboard/preview/${bookmarkId}`}\n          >\n            <ArrowUpRightFromSquare className=\"my-auto\" size=\"20\" />\n            <p className=\"my-auto\">Open</p>\n          </Link>\n          <Button\n            variant=\"link\"\n            onClick={() => deleteBookmark({ bookmarkId })}\n            className=\"flex gap-2 text-red-500 hover:text-red-500\"\n          >\n            {!isPending ? (\n              <>\n                <Trash className=\"my-auto\" size=\"20\" />\n                <p className=\"my-auto\">Delete</p>\n              </>\n            ) : (\n              <span className=\"m-auto\">\n                <Spinner />\n              </span>\n            )}\n          </Button>\n        </div>\n      </div>\n      <hr />\n      <p className=\"text-lg\">Notes</p>\n      <NoteEditor bookmarkId={bookmarkId} />\n      <hr />\n      <p className=\"text-lg\">Tags</p>\n      <TagList bookmarkId={bookmarkId} />\n      <TagsSelector bookmarkId={bookmarkId} />\n      <hr />\n      <p className=\"text-lg\">Lists</p>\n      <BookmarkLists bookmarkId={bookmarkId} />\n      <ListsSelector bookmarkId={bookmarkId} />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/browser-extension/src/CustomHeadersPage.tsx",
    "content": "import { useEffect, useState } from \"react\";\nimport { Plus, Trash2 } from \"lucide-react\";\nimport { useNavigate } from \"react-router-dom\";\n\nimport { Button } from \"./components/ui/button\";\nimport { Input } from \"./components/ui/input\";\nimport Logo from \"./Logo\";\nimport usePluginSettings from \"./utils/settings\";\n\nexport default function CustomHeadersPage() {\n  const navigate = useNavigate();\n  const { settings, setSettings } = usePluginSettings();\n\n  // Convert headers object to array of entries for easier manipulation\n  const [headers, setHeaders] = useState<{ key: string; value: string }[]>([]);\n  const [newHeaderKey, setNewHeaderKey] = useState(\"\");\n  const [newHeaderValue, setNewHeaderValue] = useState(\"\");\n\n  // Update headers when settings change (e.g., when loaded from storage)\n  useEffect(() => {\n    setHeaders(\n      Object.entries(settings.customHeaders || {}).map(([key, value]) => ({\n        key,\n        value,\n      })),\n    );\n  }, [settings.customHeaders]);\n\n  const handleAddHeader = () => {\n    if (!newHeaderKey.trim() || !newHeaderValue.trim()) {\n      return;\n    }\n\n    // Check if header already exists\n    const existingIndex = headers.findIndex((h) => h.key === newHeaderKey);\n    if (existingIndex >= 0) {\n      // Update existing header\n      const updatedHeaders = [...headers];\n      updatedHeaders[existingIndex].value = newHeaderValue;\n      setHeaders(updatedHeaders);\n    } else {\n      // Add new header\n      setHeaders([...headers, { key: newHeaderKey, value: newHeaderValue }]);\n    }\n\n    setNewHeaderKey(\"\");\n    setNewHeaderValue(\"\");\n  };\n\n  const handleRemoveHeader = (index: number) => {\n    setHeaders(headers.filter((_, i) => i !== index));\n  };\n\n  const handleSave = () => {\n    // Convert array back to object\n    const headersObject = headers.reduce(\n      (acc, { key, value }) => {\n        if (key.trim() && value.trim()) {\n          acc[key] = value;\n        }\n        return acc;\n      },\n      {} as Record<string, string>,\n    );\n\n    setSettings((s) => ({ ...s, customHeaders: headersObject }));\n    navigate(-1);\n  };\n\n  const handleCancel = () => {\n    navigate(-1);\n  };\n\n  return (\n    <div className=\"flex flex-col space-y-2\">\n      <Logo />\n      <span className=\"text-lg\">Custom Headers</span>\n      <p className=\"text-sm text-muted-foreground\">\n        Add custom HTTP headers that will be sent with every API request.\n      </p>\n      <hr />\n\n      {/* Existing Headers List */}\n      <div className=\"max-h-64 space-y-2 overflow-y-auto\">\n        {headers.length === 0 ? (\n          <p className=\"py-4 text-center text-sm text-muted-foreground\">\n            No custom headers configured\n          </p>\n        ) : (\n          headers.map((header, index) => (\n            <div\n              key={index}\n              className=\"flex items-center gap-2 rounded-lg border bg-background p-3\"\n            >\n              <div className=\"flex-1 space-y-1\">\n                <p className=\"text-sm font-semibold\">{header.key}</p>\n                <p className=\"truncate text-xs text-muted-foreground\">\n                  {header.value}\n                </p>\n              </div>\n              <Button\n                variant=\"ghost\"\n                size=\"sm\"\n                onClick={() => handleRemoveHeader(index)}\n                className=\"h-8 w-8 p-0 text-destructive hover:text-destructive\"\n              >\n                <Trash2 className=\"h-4 w-4\" />\n              </Button>\n            </div>\n          ))\n        )}\n      </div>\n\n      <hr />\n\n      {/* Add New Header */}\n      <div className=\"space-y-2\">\n        <p className=\"text-sm font-semibold\">Add New Header</p>\n        <Input\n          placeholder=\"Header Name (e.g., X-Custom-Header)\"\n          value={newHeaderKey}\n          onChange={(e) => setNewHeaderKey(e.target.value)}\n          autoCapitalize=\"none\"\n        />\n        <Input\n          placeholder=\"Header Value\"\n          value={newHeaderValue}\n          onChange={(e) => setNewHeaderValue(e.target.value)}\n          autoCapitalize=\"none\"\n        />\n        <Button\n          variant=\"secondary\"\n          onClick={handleAddHeader}\n          disabled={!newHeaderKey.trim() || !newHeaderValue.trim()}\n          className=\"w-full\"\n        >\n          <Plus className=\"mr-2 h-4 w-4\" />\n          Add Header\n        </Button>\n      </div>\n\n      <hr />\n\n      {/* Action Buttons */}\n      <div className=\"flex gap-2\">\n        <Button variant=\"outline\" onClick={handleCancel} className=\"flex-1\">\n          Cancel\n        </Button>\n        <Button onClick={handleSave} className=\"flex-1\">\n          Save\n        </Button>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/browser-extension/src/Layout.tsx",
    "content": "import { Home, RefreshCw, Settings, X } from \"lucide-react\";\nimport { Outlet, useNavigate } from \"react-router-dom\";\n\nimport { Button } from \"./components/ui/button\";\nimport usePluginSettings from \"./utils/settings\";\n\nexport default function Layout() {\n  const navigate = useNavigate();\n  const { settings, isPending: isInit } = usePluginSettings();\n  if (!isInit) {\n    return <div className=\"p-4\">Loading ... </div>;\n  }\n\n  if (!settings.apiKey || !settings.address) {\n    navigate(\"/notconfigured\");\n    return;\n  }\n\n  return (\n    <div className=\"flex flex-col space-y-2\">\n      <div className=\"rounded-md bg-gray-100 p-4 dark:bg-gray-900\">\n        <Outlet />\n      </div>\n      <hr />\n      <div className=\"flex justify-between space-x-3\">\n        <div className=\"my-auto\">\n          <a\n            className=\"flex gap-2 text-foreground\"\n            target=\"_blank\"\n            rel=\"noreferrer\"\n            href={`${settings.address}/dashboard/bookmarks`}\n          >\n            <Home />\n            <span className=\"text-md my-auto\">Bookmarks</span>\n          </a>\n        </div>\n        <div className=\"flex space-x-3\">\n          {process.env.NODE_ENV == \"development\" && (\n            <Button onClick={() => navigate(0)}>\n              <RefreshCw className=\"w-4\" />\n            </Button>\n          )}\n          <Button onClick={() => navigate(\"/options\")}>\n            <Settings className=\"w-4\" />\n          </Button>\n          <Button onClick={() => window.close()}>\n            <X className=\"w-4\" />\n          </Button>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/browser-extension/src/Logo.tsx",
    "content": "import logoImgWhite from \"../public/logo-full-white.png\";\nimport logoImg from \"../public/logo-full.png\";\n\nexport default function Logo() {\n  return (\n    <span className=\"flex items-center justify-center\">\n      <img src={logoImg} alt=\"karakeep logo\" className=\"h-14 dark:hidden\" />\n      <img\n        src={logoImgWhite}\n        alt=\"karakeep logo\"\n        className=\"hidden h-14 dark:block\"\n      />\n    </span>\n  );\n}\n"
  },
  {
    "path": "apps/browser-extension/src/NotConfiguredPage.tsx",
    "content": "import { useEffect, useState } from \"react\";\nimport { useNavigate } from \"react-router-dom\";\n\nimport { Button } from \"./components/ui/button\";\nimport { Input } from \"./components/ui/input\";\nimport Logo from \"./Logo\";\nimport usePluginSettings from \"./utils/settings\";\nimport { isHttpUrl } from \"./utils/url\";\n\nexport default function NotConfiguredPage() {\n  const navigate = useNavigate();\n\n  const { settings, setSettings } = usePluginSettings();\n\n  const [error, setError] = useState(\"\");\n  const [serverAddress, setServerAddress] = useState(settings.address);\n\n  useEffect(() => {\n    setServerAddress(settings.address);\n  }, [settings.address]);\n\n  const onSave = () => {\n    const input = serverAddress.trim();\n    if (input == \"\") {\n      setError(\"Server address is required\");\n      return;\n    }\n\n    // Add URL protocol validation\n    if (!isHttpUrl(input)) {\n      setError(\"Server address must start with http:// or https://\");\n      return;\n    }\n\n    setSettings((s) => ({ ...s, address: input.replace(/\\/$/, \"\") }));\n    navigate(\"/signin\");\n  };\n\n  return (\n    <div className=\"flex flex-col space-y-2\">\n      <Logo />\n      <span className=\"pt-3\">\n        To use the plugin, you need to configure it first.\n      </span>\n      <p className=\"text-red-500\">{error}</p>\n      <div className=\"flex gap-2\">\n        <label className=\"my-auto\">Server Address</label>\n        <Input\n          name=\"address\"\n          value={serverAddress}\n          className=\"h-8 flex-1 rounded-lg border border-gray-300 p-2\"\n          onChange={(e) => setServerAddress(e.target.value)}\n        />\n      </div>\n      <div className=\"flex justify-start\">\n        <button\n          type=\"button\"\n          onClick={() => navigate(\"/customheaders\")}\n          className=\"text-xs text-muted-foreground underline hover:text-foreground\"\n        >\n          Configure Custom Headers\n          {settings.customHeaders &&\n            Object.keys(settings.customHeaders).length > 0 &&\n            ` (${Object.keys(settings.customHeaders).length})`}\n        </button>\n      </div>\n      <Button onClick={onSave}>Configure</Button>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/browser-extension/src/OptionsPage.tsx",
    "content": "import React, { useEffect } from \"react\";\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { useNavigate } from \"react-router-dom\";\n\nimport { Button } from \"./components/ui/button\";\nimport { Input } from \"./components/ui/input\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"./components/ui/select\";\nimport { Switch } from \"./components/ui/switch\";\nimport Logo from \"./Logo\";\nimport Spinner from \"./Spinner\";\nimport usePluginSettings, {\n  DEFAULT_BADGE_CACHE_EXPIRE_MS,\n} from \"./utils/settings\";\nimport { useTheme } from \"./utils/ThemeProvider\";\nimport { useTRPC } from \"./utils/trpc\";\n\nexport default function OptionsPage() {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n  const navigate = useNavigate();\n  const { settings, setSettings } = usePluginSettings();\n  const { setTheme, theme } = useTheme();\n\n  const { data: whoami, error: whoAmIError } = useQuery(\n    api.users.whoami.queryOptions(undefined, {\n      enabled: settings.address != \"\",\n    }),\n  );\n\n  const { mutate: deleteKey } = useMutation(\n    api.apiKeys.revoke.mutationOptions(),\n  );\n\n  const invalidateWhoami = () => {\n    queryClient.refetchQueries(api.users.whoami.queryFilter());\n  };\n\n  useEffect(() => {\n    invalidateWhoami();\n  }, [settings]);\n\n  let loggedInMessage: React.ReactNode;\n  if (whoAmIError) {\n    if (whoAmIError.data?.code == \"UNAUTHORIZED\") {\n      loggedInMessage = <span>Not logged in</span>;\n    } else {\n      loggedInMessage = (\n        <span>Something went wrong: {whoAmIError.message}</span>\n      );\n    }\n  } else if (whoami) {\n    loggedInMessage = <span>{whoami.email}</span>;\n  } else {\n    loggedInMessage = <Spinner />;\n  }\n\n  const onLogout = () => {\n    if (settings.apiKeyId) {\n      deleteKey({ id: settings.apiKeyId });\n    }\n    setSettings((s) => ({ ...s, apiKey: \"\", apiKeyId: undefined }));\n    invalidateWhoami();\n    navigate(\"/notconfigured\");\n  };\n\n  return (\n    <div className=\"flex flex-col space-y-2\">\n      <Logo />\n      <span className=\"text-lg\">Settings</span>\n      <hr />\n      <div className=\"flex items-center justify-between gap-2\">\n        <span className=\"text-sm font-medium\">Show count badge</span>\n        <Switch\n          checked={settings.showCountBadge}\n          onCheckedChange={(checked) =>\n            setSettings((s) => ({ ...s, showCountBadge: checked }))\n          }\n        />\n      </div>\n      {settings.showCountBadge && (\n        <>\n          <div className=\"flex items-center justify-between gap-2\">\n            <span className=\"text-sm font-medium\">Use badge cache</span>\n            <Switch\n              checked={settings.useBadgeCache}\n              onCheckedChange={(checked) =>\n                setSettings((s) => ({ ...s, useBadgeCache: checked }))\n              }\n            />\n          </div>\n          {settings.useBadgeCache && (\n            <>\n              <div className=\"flex items-center justify-between gap-2\">\n                <span className=\"text-sm font-medium\">\n                  Badge cache expire time (second)\n                </span>\n                <Input\n                  type=\"number\"\n                  min=\"1\"\n                  step=\"1\"\n                  value={settings.badgeCacheExpireMs / 1000}\n                  onChange={(e) =>\n                    setSettings((s) => ({\n                      ...s,\n                      badgeCacheExpireMs:\n                        parseInt(e.target.value) * 1000 ||\n                        DEFAULT_BADGE_CACHE_EXPIRE_MS,\n                    }))\n                  }\n                  className=\"w-32\"\n                />\n              </div>\n            </>\n          )}\n        </>\n      )}\n      <hr />\n      <div className=\"flex gap-2\">\n        <span className=\"my-auto\">Server Address:</span>\n        {settings.address}\n      </div>\n      <div className=\"flex gap-2\">\n        <span className=\"my-auto\">Logged in as:</span>\n        {loggedInMessage}\n      </div>\n      <div className=\"flex gap-2\">\n        <span className=\"my-auto\">Theme:</span>\n        <Select value={theme} onValueChange={setTheme}>\n          <SelectTrigger className=\"w-24\">\n            <SelectValue placeholder=\"Theme\" />\n          </SelectTrigger>\n          <SelectContent>\n            <SelectItem value=\"light\">Light</SelectItem>\n            <SelectItem value=\"dark\">Dark</SelectItem>\n            <SelectItem value=\"system\">System</SelectItem>\n          </SelectContent>\n        </Select>\n      </div>\n      <Button onClick={onLogout}>Logout</Button>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/browser-extension/src/SavePage.tsx",
    "content": "import { useEffect, useState } from \"react\";\nimport { useMutation } from \"@tanstack/react-query\";\nimport { Navigate } from \"react-router-dom\";\n\nimport {\n  BookmarkTypes,\n  ZNewBookmarkRequest,\n  zNewBookmarkRequestSchema,\n} from \"@karakeep/shared/types/bookmarks\";\n\nimport { NEW_BOOKMARK_REQUEST_KEY_NAME } from \"./background/protocol\";\nimport Spinner from \"./Spinner\";\nimport { useTRPC } from \"./utils/trpc\";\nimport { MessageType } from \"./utils/type\";\nimport { isHttpUrl } from \"./utils/url\";\n\nexport default function SavePage() {\n  const api = useTRPC();\n  const [error, setError] = useState<string | undefined>(undefined);\n\n  const {\n    data,\n    mutate: createBookmark,\n    status,\n  } = useMutation(\n    api.bookmarks.createBookmark.mutationOptions({\n      onError: (e) => {\n        setError(\"Something went wrong: \" + e.message);\n      },\n      onSuccess: async () => {\n        // After successful creation, update badge cache and notify background\n        const [currentTab] = await chrome.tabs.query({\n          active: true,\n          lastFocusedWindow: true,\n        });\n        await chrome.runtime.sendMessage({\n          type: MessageType.BOOKMARK_REFRESH_BADGE,\n          currentTab: currentTab,\n        });\n      },\n    }),\n  );\n  useEffect(() => {\n    async function getNewBookmarkRequestFromBackgroundScriptIfAny(): Promise<ZNewBookmarkRequest | null> {\n      const { [NEW_BOOKMARK_REQUEST_KEY_NAME]: req } =\n        await chrome.storage.session.get(NEW_BOOKMARK_REQUEST_KEY_NAME);\n      if (!req) {\n        return null;\n      }\n      // Delete the request immediately to avoid issues with lingering values\n      await chrome.storage.session.remove(NEW_BOOKMARK_REQUEST_KEY_NAME);\n      return zNewBookmarkRequestSchema.parse(req);\n    }\n\n    async function runSave() {\n      let newBookmarkRequest =\n        await getNewBookmarkRequestFromBackgroundScriptIfAny();\n      if (!newBookmarkRequest) {\n        const [currentTab] = await chrome.tabs.query({\n          active: true,\n          lastFocusedWindow: true,\n        });\n        if (!currentTab.url) {\n          setError(\"Current tab has no URL to bookmark.\");\n          return;\n        }\n\n        if (!isHttpUrl(currentTab.url)) {\n          setError(\n            \"Cannot bookmark this type of URL. Only HTTP/HTTPS URLs are supported.\",\n          );\n          return;\n        }\n\n        newBookmarkRequest = {\n          type: BookmarkTypes.LINK,\n          title: currentTab.title,\n          url: currentTab.url,\n          source: \"extension\",\n        };\n      }\n\n      createBookmark({\n        ...newBookmarkRequest,\n        source: newBookmarkRequest.source || \"extension\",\n      });\n    }\n\n    runSave();\n  }, [createBookmark]);\n\n  if (error) {\n    return <div className=\"text-red-500\">{error}</div>;\n  }\n\n  switch (status) {\n    case \"error\": {\n      return <div className=\"text-red-500\">{error}</div>;\n    }\n    case \"success\": {\n      return <Navigate to={`/bookmark/${data.id}`} />;\n    }\n    case \"pending\": {\n      return (\n        <div className=\"flex justify-between text-lg\">\n          <span>Saving Bookmark </span>\n          <Spinner />\n        </div>\n      );\n    }\n    case \"idle\": {\n      return <div />;\n    }\n  }\n}\n"
  },
  {
    "path": "apps/browser-extension/src/SignInPage.tsx",
    "content": "import { useState } from \"react\";\nimport { useMutation } from \"@tanstack/react-query\";\nimport { useNavigate } from \"react-router-dom\";\n\nimport { Button } from \"./components/ui/button\";\nimport { Input } from \"./components/ui/input\";\nimport Logo from \"./Logo\";\nimport usePluginSettings from \"./utils/settings\";\nimport { useTRPC } from \"./utils/trpc\";\n\nconst enum LoginState {\n  NONE = \"NONE\",\n  USERNAME_PASSWORD = \"USERNAME/PASSWORD\",\n  API_KEY = \"API_KEY\",\n}\n\nexport default function SignInPage() {\n  const api = useTRPC();\n  const navigate = useNavigate();\n  const { settings, setSettings } = usePluginSettings();\n\n  const {\n    mutate: login,\n    error: usernamePasswordError,\n    isPending: userNamePasswordRequestIsPending,\n  } = useMutation(\n    api.apiKeys.exchange.mutationOptions({\n      onSuccess: (resp) => {\n        setSettings((s) => ({ ...s, apiKey: resp.key, apiKeyId: resp.id }));\n        navigate(\"/options\");\n      },\n    }),\n  );\n\n  const {\n    mutate: validateApiKey,\n    error: apiKeyValidationError,\n    isPending: apiKeyValueRequestIsPending,\n  } = useMutation(\n    api.apiKeys.validate.mutationOptions({\n      onSuccess: () => {\n        setSettings((s) => ({ ...s, apiKey: apiKeyFormData.apiKey }));\n        navigate(\"/options\");\n      },\n    }),\n  );\n\n  const [lastLoginAttemptSource, setLastLoginAttemptSource] =\n    useState<LoginState>(LoginState.NONE);\n\n  const [formData, setFormData] = useState<{\n    email: string;\n    password: string;\n  }>({\n    email: \"\",\n    password: \"\",\n  });\n\n  const [apiKeyFormData, setApiKeyFormData] = useState<{\n    apiKey: string;\n  }>({\n    apiKey: \"\",\n  });\n\n  const onUserNamePasswordSubmit = (e: React.FormEvent) => {\n    e.preventDefault();\n    setLastLoginAttemptSource(LoginState.USERNAME_PASSWORD);\n    const randStr = (Math.random() + 1).toString(36).substring(5);\n    login({ ...formData, keyName: `Browser extension: (${randStr})` });\n  };\n\n  const onApiKeySubmit = (e: React.FormEvent) => {\n    e.preventDefault();\n    setLastLoginAttemptSource(LoginState.API_KEY);\n    validateApiKey({ ...apiKeyFormData });\n  };\n\n  let errorMessage = \"\";\n  let loginError;\n  switch (lastLoginAttemptSource) {\n    case LoginState.USERNAME_PASSWORD:\n      loginError = usernamePasswordError;\n      break;\n    case LoginState.API_KEY:\n      loginError = apiKeyValidationError;\n      break;\n  }\n  if (loginError) {\n    errorMessage = loginError.message || \"Wrong username or password\";\n  }\n\n  return (\n    <div className=\"flex flex-col space-y-2\">\n      <Logo />\n      <p className=\"text-lg\">Login</p>\n      <p className=\"text-red-500\">{errorMessage}</p>\n      <form\n        className=\"flex flex-col gap-y-2\"\n        onSubmit={onUserNamePasswordSubmit}\n      >\n        <div className=\"flex flex-col gap-y-1\">\n          <label className=\"my-auto font-bold\">Email</label>\n          <Input\n            value={formData.email}\n            onChange={(e) =>\n              setFormData((f) => ({ ...f, email: e.target.value }))\n            }\n            type=\"text\"\n            name=\"email\"\n            className=\"h-8 flex-1 rounded-lg border border-gray-300 p-2\"\n          />\n        </div>\n        <div className=\"flex flex-col gap-y-1\">\n          <label className=\"my-auto font-bold\">Password</label>\n          <Input\n            value={formData.password}\n            onChange={(e) =>\n              setFormData((f) => ({\n                ...f,\n                password: e.target.value,\n              }))\n            }\n            type=\"password\"\n            name=\"password\"\n            className=\"h-8 flex-1 rounded-lg border border-gray-300 p-2\"\n          />\n        </div>\n        <Button\n          type=\"submit\"\n          disabled={\n            userNamePasswordRequestIsPending || apiKeyValueRequestIsPending\n          }\n        >\n          Login\n        </Button>\n      </form>\n      <div className=\"flex w-full flex-row items-center gap-3\">\n        <hr className=\"flex-1\" />\n        Or\n        <hr className=\"flex-1\" />\n      </div>\n\n      <form className=\"flex flex-col gap-y-2\" onSubmit={onApiKeySubmit}>\n        <div className=\"flex flex-col gap-y-1\">\n          <label className=\"my-auto font-bold\">API Key</label>\n          <Input\n            value={apiKeyFormData.apiKey}\n            onChange={(e) =>\n              setApiKeyFormData((f) => ({ ...f, apiKey: e.target.value }))\n            }\n            type=\"text\"\n            name=\"apiKey\"\n            className=\"h-8 flex-1 rounded-lg border border-gray-300 p-2\"\n          />\n        </div>\n        <Button\n          type=\"submit\"\n          disabled={\n            userNamePasswordRequestIsPending || apiKeyValueRequestIsPending\n          }\n        >\n          Login with API key\n        </Button>\n      </form>\n\n      <div className=\"flex justify-center pt-2\">\n        <button\n          type=\"button\"\n          onClick={() => navigate(\"/customheaders\")}\n          className=\"text-xs text-muted-foreground underline hover:text-foreground\"\n        >\n          Configure Custom Headers\n          {settings.customHeaders &&\n            Object.keys(settings.customHeaders).length > 0 &&\n            ` (${Object.keys(settings.customHeaders).length})`}\n        </button>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/browser-extension/src/Spinner.tsx",
    "content": "export default function Spinner() {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"24\"\n      height=\"24\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      className=\"animate-spin\"\n    >\n      <path d=\"M21 12a9 9 0 1 1-6.219-8.56\" />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "apps/browser-extension/src/background/background.ts",
    "content": "import {\n  BookmarkTypes,\n  ZNewBookmarkRequest,\n} from \"@karakeep/shared/types/bookmarks\";\n\nimport { clearBadgeStatus, getBadgeStatus } from \"../utils/badgeCache\";\nimport {\n  getPluginSettings,\n  Settings,\n  subscribeToSettingsChanges,\n} from \"../utils/settings\";\nimport { getApiClient, initializeClients } from \"../utils/trpc\";\nimport { MessageType } from \"../utils/type\";\nimport { isHttpUrl } from \"../utils/url\";\nimport { NEW_BOOKMARK_REQUEST_KEY_NAME } from \"./protocol\";\n\nconst OPEN_KARAKEEP_ID = \"open-karakeep\";\nconst ADD_LINK_TO_KARAKEEP_ID = \"add-link\";\nconst CLEAR_CURRENT_CACHE_ID = \"clear-current-cache\";\nconst CLEAR_ALL_CACHE_ID = \"clear-all-cache\";\nconst SEPARATOR_ID = \"separator-1\";\nconst VIEW_PAGE_IN_KARAKEEP = \"view-page-in-karakeep\";\n\n/**\n * Check the current settings state and register or remove context menus accordingly.\n * @param settings The current plugin settings.\n */\nasync function checkSettingsState(settings: Settings) {\n  await initializeClients();\n  if (settings?.address && settings?.apiKey) {\n    registerContextMenus(settings);\n  } else {\n    removeContextMenus();\n    await clearAllCache();\n  }\n}\n\n/**\n * Remove context menus from the browser.\n */\nfunction removeContextMenus() {\n  try {\n    chrome.contextMenus.removeAll();\n  } catch (error) {\n    console.error(\"Failed to remove context menus:\", error);\n  }\n}\n\n/**\n * Register context menus in the browser.\n * * A context menu button to open a tab with the currently configured karakeep instance.\n * * * If the \"show count badge\" setting is enabled, add context menu buttons to clear the cache for the current page or all pages.\n * * A context menu button to add a link to karakeep without loading the page.\n * @param settings The current plugin settings.\n */\nfunction registerContextMenus(settings: Settings) {\n  removeContextMenus();\n  chrome.contextMenus.create({\n    id: OPEN_KARAKEEP_ID,\n    title: \"Open Karakeep\",\n    contexts: [\"action\"],\n  });\n\n  chrome.contextMenus.create({\n    id: ADD_LINK_TO_KARAKEEP_ID,\n    title: \"Add to Karakeep\",\n    contexts: [\"link\", \"page\", \"selection\", \"image\"],\n  });\n\n  if (settings?.showCountBadge) {\n    chrome.contextMenus.create({\n      id: VIEW_PAGE_IN_KARAKEEP,\n      title: \"View this page in Karakeep\",\n      contexts: [\"action\", \"page\"],\n    });\n    if (settings?.useBadgeCache) {\n      // Add separator\n      chrome.contextMenus.create({\n        id: SEPARATOR_ID,\n        type: \"separator\",\n        contexts: [\"action\"],\n      });\n\n      chrome.contextMenus.create({\n        id: CLEAR_CURRENT_CACHE_ID,\n        title: \"Clear Current Page Cache\",\n        contexts: [\"action\"],\n      });\n\n      chrome.contextMenus.create({\n        id: CLEAR_ALL_CACHE_ID,\n        title: \"Clear All Cache\",\n        contexts: [\"action\"],\n      });\n    }\n  }\n}\n\n/**\n * Handle context menu clicks by opening a new tab with karakeep or adding a link to karakeep.\n * @param info Information about the context menu click event.\n * @param tab The current tab.\n */\nasync function handleContextMenuClick(\n  info: chrome.contextMenus.OnClickData,\n  tab?: chrome.tabs.Tab,\n) {\n  const { menuItemId, selectionText, srcUrl, linkUrl, pageUrl } = info;\n  if (menuItemId === OPEN_KARAKEEP_ID) {\n    getPluginSettings().then((settings: Settings) => {\n      chrome.tabs.create({ url: settings.address, active: true });\n    });\n  } else if (menuItemId === CLEAR_CURRENT_CACHE_ID) {\n    await clearCurrentPageCache();\n  } else if (menuItemId === CLEAR_ALL_CACHE_ID) {\n    await clearAllCache();\n  } else if (menuItemId === ADD_LINK_TO_KARAKEEP_ID) {\n    // Only pass the current page title when the URL being saved is the\n    // page itself. When saving a link or image, the title would\n    // incorrectly be the current page's title instead of the target's.\n    const isCurrentPage = !srcUrl && !linkUrl;\n    addLinkToKarakeep({\n      selectionText,\n      srcUrl,\n      linkUrl,\n      pageUrl,\n      title: isCurrentPage ? tab?.title : undefined,\n    });\n\n    // NOTE: Firefox only allows opening context menus if it's triggered by a user action.\n    // awaiting on any promise before calling this function will lose the \"user action\" context.\n    await chrome.action.openPopup();\n  } else if (menuItemId === VIEW_PAGE_IN_KARAKEEP) {\n    if (tab) {\n      await searchCurrentUrl(tab.url);\n    }\n  }\n}\n\n/**\n * Add a link to karakeep based on the provided information.\n * @param options An object containing information about the link to add.\n */\nfunction addLinkToKarakeep({\n  selectionText,\n  srcUrl,\n  linkUrl,\n  pageUrl,\n  title,\n}: {\n  selectionText?: string;\n  srcUrl?: string;\n  linkUrl?: string;\n  pageUrl?: string;\n  title?: string;\n}) {\n  let newBookmark: ZNewBookmarkRequest | null = null;\n  if (selectionText) {\n    newBookmark = {\n      type: BookmarkTypes.TEXT,\n      text: selectionText,\n      sourceUrl: pageUrl,\n      source: \"extension\",\n    };\n  } else {\n    const finalUrl = srcUrl ?? linkUrl ?? pageUrl;\n\n    if (finalUrl && isHttpUrl(finalUrl)) {\n      newBookmark = {\n        type: BookmarkTypes.LINK,\n        url: finalUrl,\n        source: \"extension\",\n        title,\n      };\n    } else {\n      console.warn(\"Invalid URL, bookmark not created:\", finalUrl);\n    }\n  }\n  if (newBookmark) {\n    chrome.storage.session.set({\n      [NEW_BOOKMARK_REQUEST_KEY_NAME]: newBookmark,\n    });\n  }\n}\n\n/**\n * Search current URL and open appropriate page.\n */\nasync function searchCurrentUrl(tabUrl?: string) {\n  try {\n    if (!tabUrl || !isHttpUrl(tabUrl)) {\n      console.warn(\"Invalid URL, cannot search:\", tabUrl);\n      return;\n    }\n    console.log(\"Searching bookmarks for URL:\", tabUrl);\n\n    const settings = await getPluginSettings();\n    const serverAddress = settings.address;\n\n    const matchedBookmarkId = await getBadgeStatus(tabUrl);\n    let targetUrl: string;\n    if (matchedBookmarkId) {\n      // Found exact match, open bookmark details page\n      targetUrl = `${serverAddress}/dashboard/preview/${matchedBookmarkId}`;\n      console.log(\"Opening bookmark details page:\", targetUrl);\n    } else {\n      // No exact match, open search results page\n      const searchQuery = encodeURIComponent(`url:${tabUrl}`);\n      targetUrl = `${serverAddress}/dashboard/search?q=${searchQuery}`;\n      console.log(\"Opening search results page:\", targetUrl);\n    }\n    await chrome.tabs.create({ url: targetUrl, active: true });\n  } catch (error) {\n    console.error(\"Failed to search current URL:\", error);\n  }\n}\n\n/**\n * Clear badge cache for the current active page.\n */\nasync function clearCurrentPageCache() {\n  try {\n    // Get the active tab\n    const [activeTab] = await chrome.tabs.query({\n      active: true,\n      currentWindow: true,\n    });\n\n    if (activeTab.url && activeTab.id) {\n      console.log(\"Clearing cache for current page:\", activeTab.url);\n      await clearBadgeStatus(activeTab.url);\n\n      // Refresh the badge for the current tab\n      await checkAndUpdateIcon(activeTab.id);\n    }\n  } catch (error) {\n    console.error(\"Failed to clear current page cache:\", error);\n  }\n}\n\n/**\n * Clear all badge cache and refresh badges for all active tabs.\n */\nasync function clearAllCache() {\n  try {\n    console.log(\"Clearing all badge cache\");\n    await clearBadgeStatus();\n  } catch (error) {\n    console.error(\"Failed to clear all cache:\", error);\n  }\n}\n\ngetPluginSettings().then(async (settings: Settings) => {\n  await checkSettingsState(settings);\n});\n\nsubscribeToSettingsChanges(async (settings) => {\n  await checkSettingsState(settings);\n});\n\n// eslint-disable-next-line @typescript-eslint/no-misused-promises -- Manifest V3 allows async functions for all callbacks\nchrome.contextMenus.onClicked.addListener(handleContextMenuClick);\n\n/**\n * Handle command events, such as adding a link to karakeep.\n * @param command The command to handle.\n * @param tab The current tab.\n */\nfunction handleCommand(command: string, tab: chrome.tabs.Tab) {\n  if (command === ADD_LINK_TO_KARAKEEP_ID) {\n    addLinkToKarakeep({\n      selectionText: undefined,\n      srcUrl: undefined,\n      linkUrl: undefined,\n      pageUrl: tab?.url,\n    });\n\n    // now try to open the popup\n    chrome.action.openPopup();\n  } else {\n    console.warn(`Received unknown command: ${command}`);\n  }\n}\n\nchrome.commands.onCommand.addListener(handleCommand);\n\n/**\n * Set the badge text and color based on the provided information.\n * @param badgeStatus\n * @param tabId The ID of the tab to update.\n */\nexport async function setBadge(badgeStatus: string | null, tabId?: number) {\n  if (!tabId) return;\n\n  if (badgeStatus) {\n    return await Promise.all([\n      chrome.action.setBadgeText({ tabId, text: ` ` }),\n      chrome.action.setBadgeBackgroundColor({\n        tabId,\n        color: \"#4CAF50\",\n      }),\n    ]);\n  } else {\n    await chrome.action.setBadgeText({ tabId, text: `` });\n  }\n}\n\n/**\n * Check and update the badge icon for a given tab ID.\n * @param tabId The ID of the tab to update.\n */\nasync function checkAndUpdateIcon(tabId: number) {\n  const tabInfo = await chrome.tabs.get(tabId);\n  const { showCountBadge } = await getPluginSettings();\n  const api = await getApiClient();\n  if (\n    !api ||\n    !showCountBadge ||\n    !tabInfo.url ||\n    !isHttpUrl(tabInfo.url) ||\n    tabInfo.status !== \"complete\"\n  ) {\n    await chrome.action.setBadgeText({ tabId, text: \"\" });\n    return;\n  }\n  console.log(\"Tab activated\", tabId, tabInfo);\n\n  try {\n    const status = await getBadgeStatus(tabInfo.url);\n    await setBadge(status, tabId);\n  } catch (error) {\n    console.error(\"Archive check failed:\", error);\n    await setBadge(null, tabId);\n  }\n}\n\nchrome.tabs.onActivated.addListener(async (tabActiveInfo) => {\n  await checkAndUpdateIcon(tabActiveInfo.tabId);\n});\n\nchrome.tabs.onUpdated.addListener(async (tabId) => {\n  await checkAndUpdateIcon(tabId);\n});\n\n// Listen for REFRESH_BADGE messages from popup and update badge accordingly\nchrome.runtime.onMessage.addListener(async (msg) => {\n  if (msg && msg.type) {\n    if (msg.currentTab && msg.type === MessageType.BOOKMARK_REFRESH_BADGE) {\n      console.log(\n        \"Received REFRESH_BADGE message for tab:\",\n        msg.currentTab.url,\n      );\n      if (msg.currentTab.url) {\n        await clearBadgeStatus(msg.currentTab.url);\n      }\n      if (typeof msg.currentTab.id === \"number\") {\n        await checkAndUpdateIcon(msg.currentTab.id);\n      }\n    }\n  }\n});\n"
  },
  {
    "path": "apps/browser-extension/src/background/protocol.ts",
    "content": "export const NEW_BOOKMARK_REQUEST_KEY_NAME = \"karakeep-new-bookmark\";\n"
  },
  {
    "path": "apps/browser-extension/src/components/BookmarkLists.tsx",
    "content": "import { useQuery } from \"@tanstack/react-query\";\nimport { X } from \"lucide-react\";\n\nimport {\n  useBookmarkLists,\n  useRemoveBookmarkFromList,\n} from \"@karakeep/shared-react/hooks/lists\";\n\nimport { useTRPC } from \"../utils/trpc\";\nimport { Button } from \"./ui/button\";\n\nexport default function BookmarkLists({ bookmarkId }: { bookmarkId: string }) {\n  const api = useTRPC();\n  const { data: allLists } = useBookmarkLists();\n\n  const { mutate: deleteFromList } = useRemoveBookmarkFromList();\n\n  const { data: lists } = useQuery(\n    api.lists.getListsOfBookmark.queryOptions({ bookmarkId }),\n  );\n  if (!lists || !allLists) {\n    return null;\n  }\n\n  return (\n    <ul className=\"flex flex-col gap-1\">\n      {lists.lists.map((l) => (\n        <li\n          key={l.id}\n          className=\"flex items-center justify-between rounded border border-border bg-background p-2 text-sm text-foreground\"\n        >\n          <span>\n            {allLists\n              .getPathById(l.id)!\n              .map((l) => `${l.icon} ${l.name}`)\n              .join(\" / \")}\n          </span>\n          <Button\n            variant=\"ghost\"\n            size=\"sm\"\n            onClick={() => deleteFromList({ bookmarkId, listId: l.id })}\n          >\n            <X className=\"size-4\" />\n          </Button>\n        </li>\n      ))}\n    </ul>\n  );\n}\n"
  },
  {
    "path": "apps/browser-extension/src/components/ListsSelector.tsx",
    "content": "import * as React from \"react\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { useSet } from \"@uidotdev/usehooks\";\nimport { Check, ChevronsUpDown } from \"lucide-react\";\n\nimport {\n  useAddBookmarkToList,\n  useBookmarkLists,\n  useRemoveBookmarkFromList,\n} from \"@karakeep/shared-react/hooks/lists\";\n\nimport { cn } from \"../utils/css\";\nimport { useTRPC } from \"../utils/trpc\";\nimport { Button } from \"./ui/button\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"./ui/command\";\nimport { DynamicPopoverContent } from \"./ui/dynamic-popover\";\nimport { Popover, PopoverTrigger } from \"./ui/popover\";\n\nexport function ListsSelector({ bookmarkId }: { bookmarkId: string }) {\n  const api = useTRPC();\n  const currentlyUpdating = useSet<string>();\n  const [open, setOpen] = React.useState(false);\n\n  const { mutate: addToList } = useAddBookmarkToList();\n  const { mutate: removeFromList } = useRemoveBookmarkFromList();\n  const { data: existingLists } = useQuery(\n    api.lists.getListsOfBookmark.queryOptions({\n      bookmarkId,\n    }),\n  );\n\n  const { data: allLists } = useBookmarkLists();\n\n  const existingListIds = new Set(existingLists?.lists.map((list) => list.id));\n\n  const toggleList = (listId: string) => {\n    currentlyUpdating.add(listId);\n    if (existingListIds.has(listId)) {\n      removeFromList(\n        { bookmarkId, listId },\n        {\n          onSettled: (_resp, _err, req) => currentlyUpdating.delete(req.listId),\n        },\n      );\n    } else {\n      addToList(\n        { bookmarkId, listId },\n        {\n          onSettled: (_resp, _err, req) => currentlyUpdating.delete(req.listId),\n        },\n      );\n    }\n  };\n\n  return (\n    <Popover open={open} onOpenChange={setOpen}>\n      <PopoverTrigger asChild>\n        <Button\n          variant=\"outline\"\n          role=\"combobox\"\n          aria-expanded={open}\n          className=\"justify-between\"\n        >\n          Add to List...\n          <ChevronsUpDown className=\"ml-2 h-4 w-4 shrink-0 opacity-50\" />\n        </Button>\n      </PopoverTrigger>\n      <DynamicPopoverContent className=\"w-[320px] p-0\">\n        <Command>\n          <CommandInput placeholder=\"Search Lists ...\" />\n          <CommandList>\n            <CommandEmpty>You don&apos;t have any lists.</CommandEmpty>\n            <CommandGroup>\n              {allLists?.allPaths\n                .filter((path) => path[path.length - 1].userRole !== \"viewer\")\n                .map((path) => {\n                  const lastItem = path[path.length - 1];\n\n                  return (\n                    <CommandItem\n                      key={lastItem.id}\n                      value={lastItem.id}\n                      keywords={[lastItem.name, lastItem.icon]}\n                      onSelect={toggleList}\n                      disabled={currentlyUpdating.has(lastItem.id)}\n                    >\n                      <Check\n                        className={cn(\n                          \"mr-2 size-4\",\n                          existingListIds.has(lastItem.id)\n                            ? \"opacity-100\"\n                            : \"opacity-0\",\n                        )}\n                      />\n                      {path\n                        .map((item) => `${item.icon} ${item.name}`)\n                        .join(\" / \")}\n                    </CommandItem>\n                  );\n                })}\n            </CommandGroup>\n          </CommandList>\n        </Command>\n      </DynamicPopoverContent>\n    </Popover>\n  );\n}\n"
  },
  {
    "path": "apps/browser-extension/src/components/NoteEditor.tsx",
    "content": "import { useEffect, useState } from \"react\";\nimport { Check, Save } from \"lucide-react\";\n\nimport {\n  useAutoRefreshingBookmarkQuery,\n  useUpdateBookmark,\n} from \"@karakeep/shared-react/hooks/bookmarks\";\n\nimport { Button } from \"./ui/button\";\nimport { Textarea } from \"./ui/textarea\";\n\nexport function NoteEditor({ bookmarkId }: { bookmarkId: string }) {\n  const { data: bookmark } = useAutoRefreshingBookmarkQuery({ bookmarkId });\n  const [error, setError] = useState<string | null>(null);\n  const [isSaving, setIsSaving] = useState(false);\n  const [noteValue, setNoteValue] = useState(\"\");\n  const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);\n\n  // Update local state when bookmark changes, but only if there are no unsaved changes\n  // This prevents overwriting user's edits while they're typing\n  useEffect(() => {\n    if (bookmark && !hasUnsavedChanges) {\n      setNoteValue(bookmark.note ?? \"\");\n    }\n  }, [bookmark?.note, bookmark, hasUnsavedChanges]);\n\n  const updateBookmarkMutator = useUpdateBookmark({\n    onSuccess: () => {\n      setError(null);\n      setIsSaving(false);\n      setHasUnsavedChanges(false);\n    },\n    onError: (e) => {\n      setError(e.message || \"Failed to save note\");\n      setIsSaving(false);\n    },\n  });\n\n  const handleSave = () => {\n    if (!bookmark || noteValue === bookmark.note || isSaving) {\n      return;\n    }\n    setIsSaving(true);\n    setError(null);\n    updateBookmarkMutator.mutate({\n      bookmarkId: bookmark.id,\n      note: noteValue,\n    });\n  };\n\n  if (!bookmark) {\n    return null;\n  }\n\n  return (\n    <div className=\"flex flex-col gap-2\">\n      <Textarea\n        autoFocus\n        className=\"h-32 w-full overflow-auto rounded bg-background p-2 text-sm text-gray-400 dark:text-gray-300\"\n        value={noteValue}\n        placeholder=\"Write some notes ...\"\n        onChange={(e) => {\n          setNoteValue(e.currentTarget.value);\n          setHasUnsavedChanges(e.currentTarget.value !== bookmark.note);\n        }}\n      />\n      <div className=\"flex items-center justify-between gap-2\">\n        <div className=\"flex-1\">\n          {isSaving && <p className=\"text-xs text-gray-500\">Saving note...</p>}\n          {error && <p className=\"text-xs text-red-500\">{error}</p>}\n          {!isSaving && !error && hasUnsavedChanges && (\n            <p className=\"text-xs text-amber-600 dark:text-amber-500\">\n              Unsaved changes\n            </p>\n          )}\n        </div>\n        {hasUnsavedChanges && (\n          <Button\n            onClick={handleSave}\n            disabled={isSaving}\n            size=\"sm\"\n            className=\"gap-1.5\"\n          >\n            {isSaving ? (\n              <>\n                <Save className=\"h-3.5 w-3.5 animate-pulse\" />\n                Saving...\n              </>\n            ) : (\n              <>\n                <Save className=\"h-3.5 w-3.5\" />\n                Save Note\n              </>\n            )}\n          </Button>\n        )}\n        {!hasUnsavedChanges && !isSaving && noteValue && (\n          <div className=\"flex items-center gap-1 text-xs text-green-600 dark:text-green-500\">\n            <Check className=\"h-3.5 w-3.5\" />\n            Saved\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/browser-extension/src/components/TagList.tsx",
    "content": "import { useAutoRefreshingBookmarkQuery } from \"@karakeep/shared-react/hooks/bookmarks\";\nimport { isBookmarkStillTagging } from \"@karakeep/shared/utils/bookmarkUtils\";\n\nimport { Badge } from \"./ui/badge\";\n\nexport default function TagList({ bookmarkId }: { bookmarkId: string }) {\n  const { data: bookmark } = useAutoRefreshingBookmarkQuery({ bookmarkId });\n  if (!bookmark) {\n    return null;\n  }\n\n  return (\n    <div className=\"flex flex-wrap gap-1\">\n      {bookmark.tags.length === 0 && !isBookmarkStillTagging(bookmark) && (\n        <Badge variant=\"secondary\">No tags</Badge>\n      )}\n      {[...bookmark.tags].map((tag) => (\n        <Badge\n          key={tag.id}\n          className={\n            tag.attachedBy === \"ai\" ? \"bg-purple-500 text-white\" : undefined\n          }\n        >\n          {tag.name}\n        </Badge>\n      ))}\n      {isBookmarkStillTagging(bookmark) && (\n        <Badge variant=\"secondary\">AI tags loading...</Badge>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/browser-extension/src/components/TagsSelector.tsx",
    "content": "import * as React from \"react\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { useSet } from \"@uidotdev/usehooks\";\nimport { Check, ChevronsUpDown, Plus } from \"lucide-react\";\n\nimport {\n  useAutoRefreshingBookmarkQuery,\n  useUpdateBookmarkTags,\n} from \"@karakeep/shared-react/hooks/bookmarks\";\n\nimport { cn } from \"../utils/css\";\nimport { useTRPC } from \"../utils/trpc\";\nimport { Button } from \"./ui/button\";\nimport {\n  Command,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n  CommandSeparator,\n} from \"./ui/command\";\nimport { DynamicPopoverContent } from \"./ui/dynamic-popover\";\nimport { Popover, PopoverTrigger } from \"./ui/popover\";\n\nexport function TagsSelector({ bookmarkId }: { bookmarkId: string }) {\n  const api = useTRPC();\n  const { data: allTags } = useQuery(api.tags.list.queryOptions({}));\n  const { data: bookmark } = useAutoRefreshingBookmarkQuery({ bookmarkId });\n\n  const existingTagIds = new Set(bookmark?.tags.map((t) => t.id) ?? []);\n\n  const [input, setInput] = React.useState(\"\");\n  const [open, setOpen] = React.useState(false);\n  const currentlyUpdating = useSet<string>();\n\n  const { mutate } = useUpdateBookmarkTags({\n    onMutate: (req) => {\n      req.attach.forEach((t) => currentlyUpdating.add(t.tagId ?? \"\"));\n      req.detach.forEach((t) => currentlyUpdating.add(t.tagId ?? \"\"));\n    },\n    onSettled: (_resp, _err, req) => {\n      if (!req) {\n        return;\n      }\n      req.attach.forEach((t) => currentlyUpdating.delete(t.tagId ?? \"\"));\n      req.detach.forEach((t) => currentlyUpdating.delete(t.tagId ?? \"\"));\n    },\n  });\n\n  const toggleTag = (tagId: string) => {\n    mutate({\n      bookmarkId,\n      attach: existingTagIds.has(tagId) ? [] : [{ tagId }],\n      detach: existingTagIds.has(tagId) ? [{ tagId }] : [],\n    });\n  };\n\n  return (\n    <Popover open={open} onOpenChange={setOpen}>\n      <PopoverTrigger asChild>\n        <Button\n          variant=\"outline\"\n          role=\"combobox\"\n          aria-expanded={open}\n          className=\"justify-between\"\n        >\n          Add Tags...\n          <ChevronsUpDown className=\"ml-2 h-4 w-4 shrink-0 opacity-50\" />\n        </Button>\n      </PopoverTrigger>\n      <DynamicPopoverContent className=\"w-[320px] p-0\">\n        <Command>\n          <CommandInput\n            value={input}\n            onValueChange={setInput}\n            placeholder=\"Search Tags ...\"\n          />\n          <CommandList>\n            <CommandGroup>\n              <CommandItem\n                onSelect={() =>\n                  mutate({\n                    bookmarkId,\n                    attach: [{ tagName: input }],\n                    detach: [],\n                  })\n                }\n              >\n                <Plus className=\"mr-2 size-4\" />\n                Create &quot;{input}&quot; ...\n              </CommandItem>\n            </CommandGroup>\n            <CommandSeparator />\n            <CommandGroup>\n              {allTags?.tags\n                .sort((a, b) => a.name.localeCompare(b.name))\n                .map((tag) => (\n                  <CommandItem\n                    key={tag.id}\n                    value={tag.id}\n                    keywords={[tag.name]}\n                    onSelect={toggleTag}\n                    disabled={currentlyUpdating.has(tag.id)}\n                  >\n                    <Check\n                      className={cn(\n                        \"mr-2 size-4\",\n                        existingTagIds.has(tag.id)\n                          ? \"opacity-100\"\n                          : \"opacity-0\",\n                      )}\n                    />\n                    {tag.name}\n                  </CommandItem>\n                ))}\n            </CommandGroup>\n          </CommandList>\n        </Command>\n      </DynamicPopoverContent>\n    </Popover>\n  );\n}\n"
  },
  {
    "path": "apps/browser-extension/src/components/ui/badge.tsx",
    "content": "import type { VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\nimport { cva } from \"class-variance-authority\";\n\nimport { cn } from \"../../utils/css\";\n\nconst badgeVariants = cva(\n  \"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2\",\n  {\n    variants: {\n      variant: {\n        default:\n          \"border-transparent bg-primary text-primary-foreground hover:bg-primary/80\",\n        secondary:\n          \"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n        destructive:\n          \"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80\",\n        outline: \"text-foreground\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nexport interface BadgeProps\n  extends\n    React.HTMLAttributes<HTMLDivElement>,\n    VariantProps<typeof badgeVariants> {}\n\nfunction Badge({ className, variant, ...props }: BadgeProps) {\n  return (\n    <div className={cn(badgeVariants({ variant }), className)} {...props} />\n  );\n}\n\nexport { Badge, badgeVariants };\n"
  },
  {
    "path": "apps/browser-extension/src/components/ui/button.tsx",
    "content": "import type { VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva } from \"class-variance-authority\";\n\nimport { cn } from \"../../utils/css\";\n\nconst buttonVariants = cva(\n  \"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n        destructive:\n          \"bg-destructive text-destructive-foreground hover:bg-destructive/90\",\n        outline:\n          \"border border-input bg-background hover:bg-accent hover:text-accent-foreground\",\n        secondary:\n          \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n        ghost: \"hover:bg-accent hover:text-accent-foreground\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default: \"h-10 px-4 py-2\",\n        sm: \"h-9 rounded-md px-3\",\n        lg: \"h-11 rounded-md px-8\",\n        icon: \"h-10 w-10\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nexport interface ButtonProps\n  extends\n    React.ButtonHTMLAttributes<HTMLButtonElement>,\n    VariantProps<typeof buttonVariants> {\n  asChild?: boolean;\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n  ({ className, variant, size, asChild = false, ...props }, ref) => {\n    const Comp = asChild ? Slot : \"button\";\n    return (\n      <Comp\n        className={cn(buttonVariants({ variant, size, className }))}\n        ref={ref}\n        {...props}\n      />\n    );\n  },\n);\nButton.displayName = \"Button\";\n\nexport { Button, buttonVariants };\n"
  },
  {
    "path": "apps/browser-extension/src/components/ui/command.tsx",
    "content": "import type { DialogProps } from \"@radix-ui/react-dialog\";\nimport * as React from \"react\";\nimport { Command as CommandPrimitive } from \"cmdk\";\nimport { Search } from \"lucide-react\";\n\nimport { cn } from \"../../utils/css\";\nimport { Dialog, DialogContent } from \"./dialog\";\n\nconst Command = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive\n    ref={ref}\n    className={cn(\n      \"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground\",\n      className,\n    )}\n    {...props}\n  />\n));\nCommand.displayName = CommandPrimitive.displayName;\n\ntype CommandDialogProps = DialogProps;\n\nconst CommandDialog = ({ children, ...props }: CommandDialogProps) => {\n  return (\n    <Dialog {...props}>\n      <DialogContent className=\"overflow-hidden p-0 shadow-lg\">\n        <Command className=\"[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5\">\n          {children}\n        </Command>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nconst CommandInput = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Input>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>\n>(({ className, ...props }, ref) => (\n  // https://github.com/shadcn-ui/ui/issues/3366\n  // eslint-disable-next-line react/no-unknown-property\n  <div className=\"flex items-center border-b px-3\" cmdk-input-wrapper=\"\">\n    <Search className=\"mr-2 h-4 w-4 shrink-0 opacity-50\" />\n    <CommandPrimitive.Input\n      ref={ref}\n      className={cn(\n        \"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50\",\n        className,\n      )}\n      {...props}\n    />\n  </div>\n));\n\nCommandInput.displayName = CommandPrimitive.Input.displayName;\n\nconst CommandList = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.List>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive.List\n    ref={ref}\n    className={cn(\"max-h-[300px] overflow-y-auto overflow-x-hidden\", className)}\n    {...props}\n  />\n));\n\nCommandList.displayName = CommandPrimitive.List.displayName;\n\nconst CommandEmpty = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Empty>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>\n>((props, ref) => (\n  <CommandPrimitive.Empty\n    ref={ref}\n    className=\"py-6 text-center text-sm\"\n    {...props}\n  />\n));\n\nCommandEmpty.displayName = CommandPrimitive.Empty.displayName;\n\nconst CommandGroup = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Group>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive.Group\n    ref={ref}\n    className={cn(\n      \"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground\",\n      className,\n    )}\n    {...props}\n  />\n));\n\nCommandGroup.displayName = CommandPrimitive.Group.displayName;\n\nconst CommandSeparator = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive.Separator\n    ref={ref}\n    className={cn(\"-mx-1 h-px bg-border\", className)}\n    {...props}\n  />\n));\nCommandSeparator.displayName = CommandPrimitive.Separator.displayName;\n\nconst CommandItem = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled='true']:pointer-events-none data-[disabled='true']:opacity-50\",\n      className,\n    )}\n    {...props}\n  />\n));\n\nCommandItem.displayName = CommandPrimitive.Item.displayName;\n\nconst CommandShortcut = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLSpanElement>) => {\n  return (\n    <span\n      className={cn(\n        \"ml-auto text-xs tracking-widest text-muted-foreground\",\n        className,\n      )}\n      {...props}\n    />\n  );\n};\nCommandShortcut.displayName = \"CommandShortcut\";\n\nexport {\n  Command,\n  CommandDialog,\n  CommandInput,\n  CommandList,\n  CommandEmpty,\n  CommandGroup,\n  CommandItem,\n  CommandShortcut,\n  CommandSeparator,\n};\n"
  },
  {
    "path": "apps/browser-extension/src/components/ui/dialog.tsx",
    "content": "import * as React from \"react\";\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport { X } from \"lucide-react\";\n\nimport { cn } from \"../../utils/css\";\n\nconst Dialog = DialogPrimitive.Root;\n\nconst DialogTrigger = DialogPrimitive.Trigger;\n\nconst DialogPortal = DialogPrimitive.Portal;\n\nconst DialogClose = DialogPrimitive.Close;\n\nconst DialogOverlay = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Overlay\n    ref={ref}\n    className={cn(\n      \"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n      className,\n    )}\n    {...props}\n  />\n));\nDialogOverlay.displayName = DialogPrimitive.Overlay.displayName;\n\nconst DialogContent = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>\n>(({ className, children, ...props }, ref) => (\n  <DialogPortal>\n    <DialogOverlay />\n    <DialogPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      <DialogPrimitive.Close className=\"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground\">\n        <X className=\"h-4 w-4\" />\n        <span className=\"sr-only\">Close</span>\n      </DialogPrimitive.Close>\n    </DialogPrimitive.Content>\n  </DialogPortal>\n));\nDialogContent.displayName = DialogPrimitive.Content.displayName;\n\nconst DialogHeader = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col space-y-1.5 text-center sm:text-left\",\n      className,\n    )}\n    {...props}\n  />\n);\nDialogHeader.displayName = \"DialogHeader\";\n\nconst DialogFooter = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\",\n      className,\n    )}\n    {...props}\n  />\n);\nDialogFooter.displayName = \"DialogFooter\";\n\nconst DialogTitle = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Title\n    ref={ref}\n    className={cn(\n      \"text-lg font-semibold leading-none tracking-tight\",\n      className,\n    )}\n    {...props}\n  />\n));\nDialogTitle.displayName = DialogPrimitive.Title.displayName;\n\nconst DialogDescription = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Description\n    ref={ref}\n    className={cn(\"text-sm text-muted-foreground\", className)}\n    {...props}\n  />\n));\nDialogDescription.displayName = DialogPrimitive.Description.displayName;\n\nexport {\n  Dialog,\n  DialogPortal,\n  DialogOverlay,\n  DialogClose,\n  DialogTrigger,\n  DialogContent,\n  DialogHeader,\n  DialogFooter,\n  DialogTitle,\n  DialogDescription,\n};\n"
  },
  {
    "path": "apps/browser-extension/src/components/ui/dynamic-popover.tsx",
    "content": "import * as React from \"react\";\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\";\n\nimport { cn } from \"../../utils/css\";\n\ninterface DynamicPopoverContentProps extends React.ComponentPropsWithoutRef<\n  typeof PopoverPrimitive.Content\n> {\n  /**\n   * Whether to enable dynamic height adjustment\n   * If true, use max-h when content can fit the viewport, otherwise use fixed height\n   * If false, always use h-[var(--radix-popover-content-available-height)]\n   */\n  dynamicHeight?: boolean;\n\n  /**\n   * Debounce delay for height adjustment (milliseconds)\n   * Used to optimize performance and avoid frequent recalculations\n   */\n  debounceMs?: number;\n}\n\n/**\n * Custom Hook for debouncing\n */\nfunction useDebounce<T>(value: T, delay: number): T {\n  const [debouncedValue, setDebouncedValue] = React.useState<T>(value);\n\n  React.useEffect(() => {\n    const handler = setTimeout(() => {\n      setDebouncedValue(value);\n    }, delay);\n\n    return () => {\n      clearTimeout(handler);\n    };\n  }, [value, delay]);\n\n  return debouncedValue;\n}\n\n/**\n * Utility function to get available height\n */\nfunction getAvailableHeight(element: HTMLElement): number {\n  try {\n    const cssValue = getComputedStyle(element).getPropertyValue(\n      \"--radix-popover-content-available-height\",\n    );\n\n    const parsedValue = parseInt(cssValue, 10);\n\n    // If CSS variable value cannot be obtained, fallback to 80% of viewport height\n    return !isNaN(parsedValue) && parsedValue > 0\n      ? parsedValue\n      : Math.floor(window.innerHeight * 0.8);\n  } catch (error) {\n    console.warn(\"Failed to get available height from CSS variable:\", error);\n    return Math.floor(window.innerHeight * 0.8);\n  }\n}\n\n/**\n * Utility function to calculate content height\n */\nfunction getContentHeight(element: HTMLElement): number {\n  try {\n    return element.scrollHeight;\n  } catch (error) {\n    console.warn(\"Failed to get content height:\", error);\n    return 0;\n  }\n}\n\nconst DynamicPopoverContent = React.forwardRef<\n  React.ElementRef<typeof PopoverPrimitive.Content>,\n  DynamicPopoverContentProps\n>(\n  (\n    {\n      className,\n      align = \"center\",\n      sideOffset = 4,\n      dynamicHeight = true,\n      debounceMs = 100,\n      children,\n      ...props\n    },\n    ref,\n  ) => {\n    const contentRef = React.useRef<HTMLDivElement>(null);\n\n    // Use state to manage height class name\n    const [heightClass, setHeightClass] = React.useState<string>(\n      \"max-h-[var(--radix-popover-content-available-height)]\",\n    );\n\n    // Create a dependency to trigger recalculation\n    const [childrenKey, setChildrenKey] = React.useState(0);\n\n    // Use debounce to optimize performance\n    const debouncedChildrenKey = useDebounce(childrenKey, debounceMs);\n\n    // Listen for children changes\n    React.useEffect(() => {\n      setChildrenKey((prev) => prev + 1);\n    }, [children]);\n\n    // Utility function to merge refs\n    const setRefs = React.useCallback(\n      (node: HTMLDivElement | null) => {\n        // Set internal ref\n        contentRef.current = node;\n\n        // Set external ref\n        if (typeof ref === \"function\") {\n          ref(node);\n        } else if (ref) {\n          ref.current = node;\n        }\n      },\n      [ref],\n    );\n\n    // Core logic for calculating height\n    const calculateHeight = React.useCallback(() => {\n      if (!dynamicHeight || !contentRef.current) {\n        return;\n      }\n\n      const element = contentRef.current;\n      const availableHeight = getAvailableHeight(element);\n      const contentHeight = getContentHeight(element);\n\n      // Add some buffer to avoid edge cases\n      const BUFFER = 10;\n\n      if (contentHeight + BUFFER > availableHeight) {\n        setHeightClass(\"h-[var(--radix-popover-content-available-height)]\");\n      } else {\n        setHeightClass(\"max-h-[var(--radix-popover-content-available-height)]\");\n      }\n    }, [dynamicHeight]);\n\n    // Use useLayoutEffect to avoid layout flickering\n    React.useLayoutEffect(() => {\n      calculateHeight();\n    }, [calculateHeight, debouncedChildrenKey]);\n\n    // Handle window resize\n    React.useEffect(() => {\n      if (!dynamicHeight) return;\n\n      const handleResize = () => {\n        calculateHeight();\n      };\n\n      window.addEventListener(\"resize\", handleResize);\n      return () => {\n        window.removeEventListener(\"resize\", handleResize);\n      };\n    }, [calculateHeight, dynamicHeight]);\n\n    // Define all styles as a single constant for better performance and simplicity\n    const POPOVER_STYLES =\n      \"z-50 w-72 overflow-y-auto rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\";\n\n    // Determine final height class name\n    const finalHeightClass = React.useMemo(() => {\n      return dynamicHeight\n        ? heightClass\n        : \"h-[var(--radix-popover-content-available-height)]\";\n    }, [dynamicHeight, heightClass]);\n\n    // Memoize the complete class name for performance\n    const popoverClassName = React.useMemo(\n      () => cn(POPOVER_STYLES, finalHeightClass, className),\n      [finalHeightClass, className],\n    );\n\n    return (\n      <PopoverPrimitive.Portal>\n        <PopoverPrimitive.Content\n          ref={setRefs}\n          align={align}\n          sideOffset={sideOffset}\n          className={popoverClassName}\n          {...props}\n        >\n          {children}\n        </PopoverPrimitive.Content>\n      </PopoverPrimitive.Portal>\n    );\n  },\n);\n\nDynamicPopoverContent.displayName = PopoverPrimitive.Content.displayName;\n\nexport { DynamicPopoverContent };\n"
  },
  {
    "path": "apps/browser-extension/src/components/ui/input.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"../../utils/css\";\n\nexport type InputProps = React.InputHTMLAttributes<HTMLInputElement>;\n\nconst Input = React.forwardRef<HTMLInputElement, InputProps>(\n  ({ className, type, ...props }, ref) => {\n    return (\n      <input\n        type={type}\n        className={cn(\n          \"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\",\n          className,\n        )}\n        ref={ref}\n        {...props}\n      />\n    );\n  },\n);\nInput.displayName = \"Input\";\n\nexport { Input };\n"
  },
  {
    "path": "apps/browser-extension/src/components/ui/popover.tsx",
    "content": "import * as React from \"react\";\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\";\n\nimport { cn } from \"../../utils/css\";\n\nconst Popover = PopoverPrimitive.Root;\n\nconst PopoverTrigger = PopoverPrimitive.Trigger;\n\nconst PopoverContent = React.forwardRef<\n  React.ElementRef<typeof PopoverPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>\n>(({ className, align = \"center\", sideOffset = 4, ...props }, ref) => (\n  <PopoverPrimitive.Portal>\n    <PopoverPrimitive.Content\n      ref={ref}\n      align={align}\n      sideOffset={sideOffset}\n      className={cn(\n        \"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        className,\n      )}\n      {...props}\n    />\n  </PopoverPrimitive.Portal>\n));\nPopoverContent.displayName = PopoverPrimitive.Content.displayName;\n\nexport { Popover, PopoverTrigger, PopoverContent };\n"
  },
  {
    "path": "apps/browser-extension/src/components/ui/select.tsx",
    "content": "import * as React from \"react\";\nimport * as SelectPrimitive from \"@radix-ui/react-select\";\nimport { Check, ChevronDown, ChevronUp } from \"lucide-react\";\n\nimport { cn } from \"../../utils/css\";\n\nconst Select = SelectPrimitive.Root;\n\nconst SelectGroup = SelectPrimitive.Group;\n\nconst SelectValue = SelectPrimitive.Value;\n\nconst SelectTrigger = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Trigger\n    ref={ref}\n    className={cn(\n      \"flex h-8 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1\",\n      className,\n    )}\n    {...props}\n  >\n    {children}\n    <SelectPrimitive.Icon asChild>\n      <ChevronDown className=\"size-4 opacity-50\" />\n    </SelectPrimitive.Icon>\n  </SelectPrimitive.Trigger>\n));\nSelectTrigger.displayName = SelectPrimitive.Trigger.displayName;\n\nconst SelectScrollUpButton = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollUpButton\n    ref={ref}\n    className={cn(\n      \"flex cursor-default items-center justify-center py-1\",\n      className,\n    )}\n    {...props}\n  >\n    <ChevronUp className=\"size-4\" />\n  </SelectPrimitive.ScrollUpButton>\n));\nSelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;\n\nconst SelectScrollDownButton = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollDownButton\n    ref={ref}\n    className={cn(\n      \"flex cursor-default items-center justify-center py-1\",\n      className,\n    )}\n    {...props}\n  >\n    <ChevronDown className=\"size-4\" />\n  </SelectPrimitive.ScrollDownButton>\n));\nSelectScrollDownButton.displayName =\n  SelectPrimitive.ScrollDownButton.displayName;\n\nconst SelectContent = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>\n>(({ className, children, position = \"popper\", ...props }, ref) => (\n  <SelectPrimitive.Portal>\n    <SelectPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        position === \"popper\" &&\n          \"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1\",\n        className,\n      )}\n      position={position}\n      {...props}\n    >\n      <SelectScrollUpButton />\n      <SelectPrimitive.Viewport\n        className={cn(\n          \"p-1\",\n          position === \"popper\" &&\n            \"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]\",\n        )}\n      >\n        {children}\n      </SelectPrimitive.Viewport>\n      <SelectScrollDownButton />\n    </SelectPrimitive.Content>\n  </SelectPrimitive.Portal>\n));\nSelectContent.displayName = SelectPrimitive.Content.displayName;\n\nconst SelectLabel = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Label\n    ref={ref}\n    className={cn(\"py-1.5 pl-8 pr-2 text-sm font-semibold\", className)}\n    {...props}\n  />\n));\nSelectLabel.displayName = SelectPrimitive.Label.displayName;\n\nconst SelectItem = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className,\n    )}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex size-3.5 items-center justify-center\">\n      <SelectPrimitive.ItemIndicator>\n        <Check className=\"size-4\" />\n      </SelectPrimitive.ItemIndicator>\n    </span>\n\n    <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n  </SelectPrimitive.Item>\n));\nSelectItem.displayName = SelectPrimitive.Item.displayName;\n\nconst SelectSeparator = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Separator\n    ref={ref}\n    className={cn(\"-mx-1 my-1 h-px bg-muted\", className)}\n    {...props}\n  />\n));\nSelectSeparator.displayName = SelectPrimitive.Separator.displayName;\n\nexport {\n  Select,\n  SelectGroup,\n  SelectValue,\n  SelectTrigger,\n  SelectContent,\n  SelectLabel,\n  SelectItem,\n  SelectSeparator,\n  SelectScrollUpButton,\n  SelectScrollDownButton,\n};\n"
  },
  {
    "path": "apps/browser-extension/src/components/ui/switch.tsx",
    "content": "import * as React from \"react\";\nimport * as SwitchPrimitives from \"@radix-ui/react-switch\";\n\nimport { cn } from \"../../utils/css\";\n\nconst Switch = React.forwardRef<\n  React.ElementRef<typeof SwitchPrimitives.Root>,\n  React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>\n>(({ className, ...props }, ref) => (\n  <SwitchPrimitives.Root\n    className={cn(\n      \"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input\",\n      className,\n    )}\n    {...props}\n    ref={ref}\n  >\n    <SwitchPrimitives.Thumb\n      className={cn(\n        \"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0\",\n      )}\n    />\n  </SwitchPrimitives.Root>\n));\nSwitch.displayName = SwitchPrimitives.Root.displayName;\n\nexport { Switch };\n"
  },
  {
    "path": "apps/browser-extension/src/components/ui/textarea.tsx",
    "content": "import * as React from \"react\";\n\nimport { cn } from \"../../utils/css\";\n\nexport type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>;\n\nconst Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(\n  ({ className, ...props }, ref) => {\n    return (\n      <textarea\n        className={cn(\n          \"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\",\n          className,\n        )}\n        ref={ref}\n        {...props}\n      />\n    );\n  },\n);\nTextarea.displayName = \"Textarea\";\n\nexport { Textarea };\n"
  },
  {
    "path": "apps/browser-extension/src/index.css",
    "content": "@import \"@karakeep/tailwind-config/globals.css\";\n"
  },
  {
    "path": "apps/browser-extension/src/main.tsx",
    "content": "import ReactDOM from \"react-dom/client\";\n\nimport \"./index.css\";\n\nimport { HashRouter, Route, Routes } from \"react-router-dom\";\n\nimport BookmarkDeletedPage from \"./BookmarkDeletedPage.tsx\";\nimport BookmarkSavedPage from \"./BookmarkSavedPage.tsx\";\nimport CustomHeadersPage from \"./CustomHeadersPage.tsx\";\nimport Layout from \"./Layout.tsx\";\nimport NotConfiguredPage from \"./NotConfiguredPage.tsx\";\nimport OptionsPage from \"./OptionsPage.tsx\";\nimport SavePage from \"./SavePage.tsx\";\nimport SignInPage from \"./SignInPage.tsx\";\nimport { Providers } from \"./utils/providers.tsx\";\n\nfunction App() {\n  return (\n    <div className=\"w-96 p-4\">\n      <Providers>\n        <HashRouter>\n          <Routes>\n            <Route element={<Layout />}>\n              <Route path=\"/\" element={<SavePage />} />\n              <Route\n                path=\"/bookmark/:bookmarkId\"\n                element={<BookmarkSavedPage />}\n              />\n              <Route\n                path=\"/bookmarkdeleted\"\n                element={<BookmarkDeletedPage />}\n              />\n            </Route>\n            <Route path=\"/notconfigured\" element={<NotConfiguredPage />} />\n            <Route path=\"/options\" element={<OptionsPage />} />\n            <Route path=\"/signin\" element={<SignInPage />} />\n            <Route path=\"/customheaders\" element={<CustomHeadersPage />} />\n          </Routes>\n        </HashRouter>\n      </Providers>\n    </div>\n  );\n}\n\nReactDOM.createRoot(document.getElementById(\"root\")!).render(<App />);\n"
  },
  {
    "path": "apps/browser-extension/src/utils/ThemeProvider.tsx",
    "content": "import { createContext, useContext, useEffect } from \"react\";\n\nimport usePluginSettings from \"./settings\";\n\ntype Theme = \"dark\" | \"light\" | \"system\";\n\ninterface ThemeProviderProps {\n  children: React.ReactNode;\n}\n\ninterface ThemeProviderState {\n  theme: Theme;\n  setTheme: (theme: Theme) => void;\n}\n\nconst initialState: ThemeProviderState = {\n  theme: \"system\",\n  setTheme: () => null,\n};\n\nconst ThemeProviderContext = createContext<ThemeProviderState>(initialState);\n\nexport function ThemeProvider({ children, ...props }: ThemeProviderProps) {\n  const { settings, setSettings } = usePluginSettings();\n  const theme = settings.theme;\n\n  useEffect(() => {\n    const root = window.document.documentElement;\n\n    const updateIcon = (useDarkModeIcons: boolean) => {\n      const iconSuffix = useDarkModeIcons ? \"-darkmode.png\" : \".png\";\n\n      const iconPaths = {\n        \"16\": `logo-16${iconSuffix}`,\n        \"48\": `logo-48${iconSuffix}`,\n        \"128\": `logo-128${iconSuffix}`,\n      };\n      chrome.action.setIcon({ path: iconPaths });\n    };\n\n    const applyThemeAndIcon = () => {\n      root.classList.remove(\"light\", \"dark\");\n\n      let currentTheme: \"light\" | \"dark\";\n      if (theme === \"system\") {\n        currentTheme = window.matchMedia(\"(prefers-color-scheme: dark)\").matches\n          ? \"dark\"\n          : \"light\";\n      } else {\n        currentTheme = theme;\n      }\n\n      root.classList.add(currentTheme);\n      updateIcon(currentTheme === \"dark\");\n    };\n\n    applyThemeAndIcon();\n  }, [theme]);\n\n  const value = {\n    theme,\n    setTheme: (newTheme: Theme) => {\n      setSettings((s) => ({ ...s, theme: newTheme }));\n    },\n  };\n\n  return (\n    <ThemeProviderContext.Provider {...props} value={value}>\n      {children}\n    </ThemeProviderContext.Provider>\n  );\n}\n\nexport const useTheme = () => {\n  const context = useContext(ThemeProviderContext);\n\n  if (context === undefined)\n    throw new Error(\"useTheme must be used within a ThemeProvider\");\n\n  return context;\n};\n"
  },
  {
    "path": "apps/browser-extension/src/utils/badgeCache.ts",
    "content": "// Badge count cache helpers\nimport { getPluginSettings } from \"./settings\";\nimport { getApiClient, getQueryClient } from \"./trpc\";\n\n/**\n * Fetches the bookmark status for a given URL from the API.\n * This function will be used by our cache as the \"fetcher\".\n * @param url The URL to check.\n * @returns The bookmark id if found, null if not found.\n */\nasync function fetchBadgeStatus(url: string): Promise<string | null> {\n  const api = await getApiClient();\n  if (!api) {\n    // This case should ideally not happen if settings are correct\n    throw new Error(\"[badgeCache] API client not configured\");\n  }\n  try {\n    const data = await api.bookmarks.checkUrl.query({ url });\n    return data.bookmarkId;\n  } catch (error) {\n    console.error(`[badgeCache] Failed to fetch status for ${url}:`, error);\n    // In case of API error, return a non-cacheable empty status\n    // Propagate so cache treats this as a miss and doesn't store\n    throw error;\n  }\n}\n\n/**\n * Get badge status for a URL using the SWR cache.\n * @param url The URL to get the status for.\n */\nexport async function getBadgeStatus(url: string): Promise<string | null> {\n  const { useBadgeCache, badgeCacheExpireMs } = await getPluginSettings();\n  if (!useBadgeCache) return fetchBadgeStatus(url);\n\n  const queryClient = await getQueryClient();\n  if (!queryClient) return fetchBadgeStatus(url);\n\n  return await queryClient.fetchQuery({\n    queryKey: [\"badgeStatus\", url],\n    queryFn: () => fetchBadgeStatus(url),\n    // Keep in memory for twice as long as stale time\n    gcTime: badgeCacheExpireMs * 2,\n    // Use the user-configured cache expire time\n    staleTime: badgeCacheExpireMs,\n  });\n}\n\n/**\n * Clear badge status cache for a specific URL or all URLs.\n * @param url The URL to clear. If not provided, clears the entire cache.\n */\nexport async function clearBadgeStatus(url?: string): Promise<void> {\n  const queryClient = await getQueryClient();\n  if (!queryClient) return;\n\n  if (url) {\n    await queryClient.invalidateQueries({ queryKey: [\"badgeStatus\", url] });\n  } else {\n    await queryClient.invalidateQueries({ queryKey: [\"badgeStatus\"] });\n  }\n  console.log(`[badgeCache] Invalidated cache for: ${url || \"all\"}`);\n}\n"
  },
  {
    "path": "apps/browser-extension/src/utils/css.ts",
    "content": "import type { ClassValue } from \"clsx\";\nimport { clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
  },
  {
    "path": "apps/browser-extension/src/utils/providers.tsx",
    "content": "import { TRPCSettingsProvider } from \"@karakeep/shared-react/providers/trpc-provider\";\n\nimport usePluginSettings from \"./settings\";\nimport { ThemeProvider } from \"./ThemeProvider\";\n\nexport function Providers({ children }: { children: React.ReactNode }) {\n  const { settings } = usePluginSettings();\n\n  return (\n    <TRPCSettingsProvider settings={settings}>\n      <ThemeProvider>{children}</ThemeProvider>\n    </TRPCSettingsProvider>\n  );\n}\n"
  },
  {
    "path": "apps/browser-extension/src/utils/settings.ts",
    "content": "import React from \"react\";\nimport { z } from \"zod\";\n\nexport const DEFAULT_BADGE_CACHE_EXPIRE_MS = 60 * 60 * 1000; // 1 hour\nexport const DEFAULT_SHOW_COUNT_BADGE = false;\n\nconst zSettingsSchema = z.object({\n  apiKey: z.string(),\n  apiKeyId: z.string().optional(),\n  address: z.string().optional().default(\"https://cloud.karakeep.app\"),\n  theme: z.enum([\"light\", \"dark\", \"system\"]).optional().default(\"system\"),\n  showCountBadge: z.boolean().default(DEFAULT_SHOW_COUNT_BADGE),\n  useBadgeCache: z.boolean().default(true),\n  badgeCacheExpireMs: z.number().min(0).default(DEFAULT_BADGE_CACHE_EXPIRE_MS),\n  customHeaders: z.record(z.string(), z.string()).optional().default({}),\n});\n\nconst DEFAULT_SETTINGS: Settings = {\n  apiKey: \"\",\n  address: \"https://cloud.karakeep.app\",\n  theme: \"system\",\n  showCountBadge: DEFAULT_SHOW_COUNT_BADGE,\n  useBadgeCache: true,\n  badgeCacheExpireMs: DEFAULT_BADGE_CACHE_EXPIRE_MS,\n  customHeaders: {},\n};\n\nexport type Settings = z.infer<typeof zSettingsSchema>;\n\nconst STORAGE = chrome.storage.sync;\n\nexport default function usePluginSettings() {\n  const [settings, setSettingsInternal] =\n    React.useState<Settings>(DEFAULT_SETTINGS);\n\n  const [isInit, setIsInit] = React.useState(false);\n\n  React.useEffect(() => {\n    if (!isInit) {\n      getPluginSettings().then((settings) => {\n        setSettingsInternal(settings);\n        setIsInit(true);\n      });\n    }\n    const onChange = (\n      changes: Record<string, chrome.storage.StorageChange>,\n    ) => {\n      if (changes.settings === undefined) {\n        return;\n      }\n      const parsedSettings = zSettingsSchema.safeParse(\n        changes.settings.newValue,\n      );\n      if (parsedSettings.success) {\n        setSettingsInternal(parsedSettings.data);\n      }\n    };\n    STORAGE.onChanged.addListener(onChange);\n    return () => {\n      STORAGE.onChanged.removeListener(onChange);\n    };\n  }, []);\n\n  const setSettings = async (s: (_: Settings) => Settings) => {\n    const newVal = s(settings);\n    await STORAGE.set({ settings: newVal });\n  };\n\n  return { settings, setSettings, isPending: isInit };\n}\n\nexport async function getPluginSettings() {\n  const parsedSettings = zSettingsSchema.safeParse(\n    (await STORAGE.get(\"settings\")).settings,\n  );\n  if (parsedSettings.success) {\n    return parsedSettings.data;\n  } else {\n    return DEFAULT_SETTINGS;\n  }\n}\n\nexport function subscribeToSettingsChanges(\n  callback: (settings: Settings) => void,\n) {\n  STORAGE.onChanged.addListener((changes) => {\n    if (changes.settings === undefined) {\n      return;\n    }\n    const parsedSettings = zSettingsSchema.safeParse(changes.settings.newValue);\n    if (parsedSettings.success) {\n      callback(parsedSettings.data);\n    } else {\n      callback(DEFAULT_SETTINGS);\n    }\n  });\n}\n"
  },
  {
    "path": "apps/browser-extension/src/utils/storagePersister.ts",
    "content": "import {\n  PersistedClient,\n  Persister,\n} from \"@tanstack/react-query-persist-client\";\n\nexport const TANSTACK_QUERY_CACHE_KEY = \"tanstack-query-cache-key\";\n\n// Declare chrome namespace for TypeScript\ndeclare const chrome: {\n  storage: {\n    local: {\n      set: (items: Record<string, string>) => Promise<void>;\n      get: (keys: string | string[]) => Promise<Record<string, string>>;\n      remove: (keys: string | string[]) => Promise<void>;\n    };\n  };\n};\n\n/**\n * Creates an AsyncStorage-like interface for Chrome's extension storage.\n *\n * @param storage The Chrome storage area to use (e.g., `chrome.storage.local`).\n * @returns An object that mimics the AsyncStorage interface.\n */\nexport const createChromeStorage = (\n  storage: typeof chrome.storage.local = globalThis.chrome?.storage?.local,\n): Persister => {\n  // Check if we are in a Chrome extension environment\n  if (typeof chrome === \"undefined\" || !chrome.storage) {\n    // Return a noop persister for non-extension environments\n    return {\n      persistClient: async () => {\n        return;\n      },\n      restoreClient: async () => undefined,\n      removeClient: async () => {\n        return;\n      },\n    };\n  }\n\n  return {\n    persistClient: async (client: PersistedClient) => {\n      await storage.set({ [TANSTACK_QUERY_CACHE_KEY]: JSON.stringify(client) });\n    },\n    restoreClient: async () => {\n      const result = await storage.get(TANSTACK_QUERY_CACHE_KEY);\n      return result[TANSTACK_QUERY_CACHE_KEY]\n        ? JSON.parse(result[TANSTACK_QUERY_CACHE_KEY])\n        : undefined;\n    },\n    removeClient: async () => {\n      await storage.remove(TANSTACK_QUERY_CACHE_KEY);\n    },\n  };\n};\n"
  },
  {
    "path": "apps/browser-extension/src/utils/trpc.ts",
    "content": "import { QueryClient } from \"@tanstack/react-query\";\nimport { persistQueryClient } from \"@tanstack/react-query-persist-client\";\nimport { createTRPCClient, httpBatchLink } from \"@trpc/client\";\nimport superjson from \"superjson\";\n\nimport type { AppRouter } from \"@karakeep/trpc/routers/_app\";\n\nimport { getPluginSettings } from \"./settings\";\nimport { createChromeStorage } from \"./storagePersister\";\n\nexport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\nlet apiClient: ReturnType<typeof createTRPCClient<AppRouter>> | null = null;\nlet queryClient: QueryClient | null = null;\nlet currentSettings: {\n  address: string;\n  apiKey: string;\n  badgeCacheExpireMs: number;\n  useBadgeCache: boolean;\n  customHeaders: Record<string, string>;\n} | null = null;\n\nexport async function initializeClients() {\n  const { address, apiKey, badgeCacheExpireMs, useBadgeCache, customHeaders } =\n    await getPluginSettings();\n\n  if (currentSettings) {\n    const addressChanged = currentSettings.address !== address;\n    const apiKeyChanged = currentSettings.apiKey !== apiKey;\n    const cacheTimeChanged =\n      currentSettings.badgeCacheExpireMs !== badgeCacheExpireMs;\n    const useBadgeCacheChanged =\n      currentSettings.useBadgeCache !== useBadgeCache;\n    const customHeadersChanged =\n      JSON.stringify(currentSettings.customHeaders) !==\n      JSON.stringify(customHeaders);\n\n    if (!address || !apiKey) {\n      // Invalid configuration, clean\n      const persisterForCleanup = createChromeStorage();\n      await persisterForCleanup.removeClient();\n      cleanupApiClient();\n      return;\n    }\n\n    if (addressChanged || apiKeyChanged || customHeadersChanged) {\n      // Switch context completely → discard the old instance and wipe persisted cache\n      const persisterForCleanup = createChromeStorage();\n      await persisterForCleanup.removeClient();\n      cleanupApiClient();\n    } else if ((cacheTimeChanged || useBadgeCacheChanged) && queryClient) {\n      // Change the cache policy only → Clean up the data, but reuse the instance\n      queryClient.clear();\n    }\n\n    // If there is already existing and there is no major change in settings, reuse it\n    if (\n      queryClient &&\n      apiClient &&\n      currentSettings &&\n      !addressChanged &&\n      !apiKeyChanged &&\n      !cacheTimeChanged &&\n      !useBadgeCacheChanged &&\n      !customHeadersChanged\n    ) {\n      return;\n    }\n  }\n\n  if (address && apiKey) {\n    // Store current settings\n    currentSettings = {\n      address,\n      apiKey,\n      badgeCacheExpireMs,\n      useBadgeCache,\n      customHeaders,\n    };\n\n    // Create new QueryClient with updated settings\n    queryClient = new QueryClient();\n\n    const persister = createChromeStorage();\n    if (useBadgeCache) {\n      persistQueryClient({\n        queryClient,\n        persister,\n        // Avoid restoring very old data and bust on policy changes\n        maxAge: badgeCacheExpireMs * 2,\n        buster: `badge:${address}:${badgeCacheExpireMs}`,\n      });\n    } else {\n      // Ensure disk cache is cleared when caching is disabled\n      await persister.removeClient();\n    }\n\n    apiClient = createTRPCClient<AppRouter>({\n      links: [\n        httpBatchLink({\n          url: `${address}/api/trpc`,\n          headers() {\n            return {\n              Authorization: `Bearer ${apiKey}`,\n              ...customHeaders,\n            };\n          },\n          transformer: superjson,\n        }),\n      ],\n    });\n  }\n}\n\nexport async function getApiClient() {\n  if (!apiClient) {\n    await initializeClients();\n  }\n  return apiClient;\n}\n\nexport async function getQueryClient() {\n  // Check if settings have changed and reinitialize if needed\n  await initializeClients();\n  return queryClient;\n}\n\nexport function cleanupApiClient() {\n  apiClient = null;\n  queryClient = null;\n  currentSettings = null;\n}\n"
  },
  {
    "path": "apps/browser-extension/src/utils/type.ts",
    "content": "export const enum MessageType {\n  BOOKMARK_REFRESH_BADGE = 1,\n}\n"
  },
  {
    "path": "apps/browser-extension/src/utils/url.ts",
    "content": "/**\n * Check if a URL is an HTTP or HTTPS URL.\n * @param url The URL to check.\n * @returns True if the URL starts with \"http://\" or \"https://\", false otherwise.\n */\nexport function isHttpUrl(url: string) {\n  const lower = url.toLowerCase();\n  return lower.startsWith(\"http://\") || lower.startsWith(\"https://\");\n}\n\n/**\n * Normalize a URL by removing the hash and trailing slash.\n * @param url The URL to process.\n * @param base Optional base URL for relative URLs.\n * @returns Normalized URL as string.\n */\nexport function normalizeUrl(url: string, base?: string): string {\n  const u = new URL(url, base);\n  u.hash = \"\"; // Remove hash fragment\n  let pathname = u.pathname;\n  if (pathname.endsWith(\"/\") && pathname !== \"/\") {\n    pathname = pathname.slice(0, -1); // Remove trailing slash except for root \"/\"\n  }\n  u.pathname = pathname;\n  return u.toString();\n}\n\n/**\n * Compare two URLs ignoring hash and trailing slash.\n * @param url1 First URL.\n * @param url2 Second URL.\n * @param base Optional base URL for relative URLs.\n * @returns True if URLs match after normalization.\n */\nexport function urlsMatchIgnoringAnchorAndTrailingSlash(\n  url1: string,\n  url2: string,\n  base?: string,\n): boolean {\n  return normalizeUrl(url1, base) === normalizeUrl(url2, base);\n}\n"
  },
  {
    "path": "apps/browser-extension/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "apps/browser-extension/tailwind.config.js",
    "content": "import web from \"@karakeep/tailwind-config/web\";\n\nconst config = {\n  darkMode: \"selector\",\n  content: web.content,\n  presets: [web],\n};\n\nexport default config;\n"
  },
  {
    "path": "apps/browser-extension/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n\n    \"types\": [\"chrome\"],\n\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n\n    \"strict\": true,\n    \"noUnusedLocals\": false,\n    \"noUnusedParameters\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"tsBuildInfoFile\": \"node_modules/.cache/tsbuildinfo.json\"\n  },\n  \"include\": [\"src\", \"vite.config.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "apps/browser-extension/vite.config.ts",
    "content": "import { crx } from \"@crxjs/vite-plugin\";\nimport react from \"@vitejs/plugin-react-swc\";\nimport { defineConfig } from \"vite\";\n\nimport manifest from \"./manifest.json\";\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  plugins: [\n    react(),\n    crx({\n      manifest,\n      browser: process.env.VITE_BUILD_FIREFOX ? \"firefox\" : \"chrome\",\n    }),\n  ],\n  server: {\n    cors: {\n      origin: [/chrome-extension:\\/\\//],\n    },\n  },\n});\n"
  },
  {
    "path": "apps/cli/.gitignore",
    "content": "dist\n"
  },
  {
    "path": "apps/cli/.npmignore",
    "content": ".turbo/**\nsrc/**\nvite.config.mts\ntsconfig.json\n"
  },
  {
    "path": "apps/cli/.oxlintrc.json",
    "content": "{\n  \"$schema\": \"../../node_modules/oxlint/configuration_schema.json\",\n  \"extends\": [\n    \"../../tooling/oxlint/oxlint-base.json\"\n  ],\n  \"env\": {\n    \"builtin\": true,\n    \"commonjs\": true\n  },\n  \"ignorePatterns\": [\n    \"**/*.config.js\",\n    \"**/*.config.cjs\",\n    \"**/.eslintrc.cjs\",\n    \"**/.next\",\n    \"**/dist\",\n    \"**/build\",\n    \"**/pnpm-lock.yaml\"\n  ]\n}\n"
  },
  {
    "path": "apps/cli/package.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/package.json\",\n  \"name\": \"@karakeep/cli\",\n  \"version\": \"0.31.0\",\n  \"description\": \"Command Line Interface (CLI) for Karakeep\",\n  \"license\": \"GNU Affero General Public License version 3\",\n  \"type\": \"module\",\n  \"keywords\": [\n    \"hoarder\",\n    \"karakeep\",\n    \"cli\"\n  ],\n  \"exports\": \"./dist/index.mjs\",\n  \"bin\": {\n    \"karakeep\": \"dist/index.mjs\"\n  },\n  \"devDependencies\": {\n    \"@commander-js/extra-typings\": \"^12.0.1\",\n    \"@karakeep/shared\": \"workspace:^0.1.0\",\n    \"@karakeep/trpc\": \"workspace:^0.1.0\",\n    \"@karakeep/tsconfig\": \"workspace:^0.1.0\",\n    \"@trpc/client\": \"^11.9.0\",\n    \"@trpc/server\": \"^11.9.0\",\n    \"@tsconfig/node22\": \"^22.0.0\",\n    \"chalk\": \"^5.3.0\",\n    \"commander\": \"^12.0.0\",\n    \"superjson\": \"^2.2.1\",\n    \"table\": \"^6.8.2\",\n    \"tsx\": \"^4.8.1\",\n    \"vite\": \"^7.0.6\",\n    \"vite-tsconfig-paths\": \"^4.3.1\"\n  },\n  \"scripts\": {\n    \"build\": \"vite build && chmod +x dist/index.mjs\",\n    \"run\": \"tsx src/index.ts\",\n    \"lint\": \"oxlint .\",\n    \"lint:fix\": \"oxlint . --fix\",\n    \"format\": \"oxfmt --check .\",\n    \"format:fix\": \"oxfmt .\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/karakeep-app/karakeep.git\",\n    \"directory\": \"apps/cli\"\n  }\n}\n"
  },
  {
    "path": "apps/cli/src/commands/admin.ts",
    "content": "import { getGlobalOptions } from \"@/lib/globals\";\nimport {\n  printErrorMessageWithReason,\n  printObject,\n  printStatusMessage,\n} from \"@/lib/output\";\nimport { getAPIClient } from \"@/lib/trpc\";\nimport { Command } from \"@commander-js/extra-typings\";\nimport { getBorderCharacters, table } from \"table\";\n\nexport const adminCmd = new Command()\n  .name(\"admin\")\n  .description(\"admin commands\");\n\nfunction toHumanReadableSize(size: number): string {\n  const sizes = [\"Bytes\", \"KB\", \"MB\", \"GB\", \"TB\"];\n  if (size === 0) return \"0 Bytes\";\n  const i = Math.floor(Math.log(size) / Math.log(1024));\n  return (size / Math.pow(1024, i)).toFixed(2) + \" \" + sizes[i];\n}\n\n// --- Users subcommand ---\n\nconst usersCmd = new Command()\n  .name(\"users\")\n  .description(\"user management commands\");\n\nusersCmd\n  .command(\"list\")\n  .description(\"list all users\")\n  .action(async () => {\n    const api = getAPIClient();\n\n    try {\n      const [usersResp, userStats] = await Promise.all([\n        api.users.list.query(),\n        api.admin.userStats.query(),\n      ]);\n\n      if (getGlobalOptions().json) {\n        printObject({\n          users: usersResp.users.map((u) => ({\n            ...u,\n            numBookmarks: userStats[u.id]?.numBookmarks ?? 0,\n            assetSizes: userStats[u.id]?.assetSizes ?? 0,\n          })),\n        });\n      } else {\n        const data: string[][] = [\n          [\n            \"Name\",\n            \"Email\",\n            \"Num Bookmarks\",\n            \"Asset Sizes\",\n            \"Role\",\n            \"Local User\",\n          ],\n        ];\n\n        usersResp.users.forEach((user) => {\n          const stats = userStats[user.id] ?? {\n            numBookmarks: 0,\n            assetSizes: 0,\n          };\n\n          const numBookmarksDisplay = `${stats.numBookmarks} / ${user.bookmarkQuota?.toString() ?? \"Unlimited\"}`;\n          const assetSizesDisplay = `${toHumanReadableSize(stats.assetSizes)} / ${user.storageQuota ? toHumanReadableSize(user.storageQuota) : \"Unlimited\"}`;\n\n          data.push([\n            user.name,\n            user.email,\n            numBookmarksDisplay,\n            assetSizesDisplay,\n            user.role ?? \"\",\n            user.localUser ? \"✓\" : \"✗\",\n          ]);\n        });\n\n        console.log(\n          table(data, {\n            border: getBorderCharacters(\"ramac\"),\n            drawHorizontalLine: (lineIndex, rowCount) => {\n              return (\n                lineIndex === 0 || lineIndex === 1 || lineIndex === rowCount\n              );\n            },\n          }),\n        );\n      }\n    } catch (error) {\n      printErrorMessageWithReason(\"Failed to list all users\", error as object);\n    }\n  });\n\nadminCmd.addCommand(usersCmd);\n\n// --- Bookmarks subcommand ---\n\nconst bookmarksCmd = new Command()\n  .name(\"bookmarks\")\n  .description(\"admin bookmark management commands\");\n\nbookmarksCmd\n  .command(\"debug\")\n  .description(\"get debug info for a bookmark\")\n  .argument(\"<bookmarkId>\", \"the id of the bookmark to debug\")\n  .action(async (bookmarkId) => {\n    const api = getAPIClient();\n\n    try {\n      const debugInfo = await api.admin.getBookmarkDebugInfo.query({\n        bookmarkId,\n      });\n\n      if (getGlobalOptions().json) {\n        printObject(debugInfo);\n      } else {\n        const basicData: string[][] = [[\"Field\", \"Value\"]];\n        basicData.push([\"ID\", debugInfo.id]);\n        basicData.push([\"Type\", debugInfo.type]);\n        basicData.push([\"Source\", debugInfo.source ?? \"N/A\"]);\n        basicData.push([\"Owner User ID\", debugInfo.userId]);\n        basicData.push([\n          \"Created At\",\n          new Date(debugInfo.createdAt).toISOString(),\n        ]);\n        basicData.push([\n          \"Modified At\",\n          debugInfo.modifiedAt\n            ? new Date(debugInfo.modifiedAt).toISOString()\n            : \"N/A\",\n        ]);\n        basicData.push([\"Title\", debugInfo.title ?? \"N/A\"]);\n        basicData.push([\"Summary\", debugInfo.summary ?? \"N/A\"]);\n        basicData.push([\"Tagging Status\", debugInfo.taggingStatus ?? \"N/A\"]);\n        basicData.push([\n          \"Summarization Status\",\n          debugInfo.summarizationStatus ?? \"N/A\",\n        ]);\n\n        if (debugInfo.linkInfo) {\n          basicData.push([\"URL\", debugInfo.linkInfo.url]);\n          basicData.push([\"Crawl Status\", debugInfo.linkInfo.crawlStatus]);\n          basicData.push([\n            \"Crawl Status Code\",\n            debugInfo.linkInfo.crawlStatusCode?.toString() ?? \"N/A\",\n          ]);\n          basicData.push([\n            \"Crawled At\",\n            debugInfo.linkInfo.crawledAt\n              ? new Date(debugInfo.linkInfo.crawledAt).toISOString()\n              : \"N/A\",\n          ]);\n          basicData.push([\n            \"Has HTML Content\",\n            debugInfo.linkInfo.hasHtmlContent ? \"Yes\" : \"No\",\n          ]);\n          basicData.push([\n            \"Has Content Asset\",\n            debugInfo.linkInfo.hasContentAsset ? \"Yes\" : \"No\",\n          ]);\n        }\n\n        if (debugInfo.textInfo) {\n          basicData.push([\n            \"Has Text\",\n            debugInfo.textInfo.hasText ? \"Yes\" : \"No\",\n          ]);\n          basicData.push([\"Source URL\", debugInfo.textInfo.sourceUrl ?? \"N/A\"]);\n        }\n\n        if (debugInfo.assetInfo) {\n          basicData.push([\"Asset Type\", debugInfo.assetInfo.assetType]);\n          basicData.push([\n            \"Has Content\",\n            debugInfo.assetInfo.hasContent ? \"Yes\" : \"No\",\n          ]);\n          basicData.push([\"File Name\", debugInfo.assetInfo.fileName ?? \"N/A\"]);\n        }\n\n        console.log(\n          table(basicData, {\n            border: getBorderCharacters(\"ramac\"),\n            drawHorizontalLine: (lineIndex, rowCount) =>\n              lineIndex === 0 || lineIndex === 1 || lineIndex === rowCount,\n          }),\n        );\n\n        if (debugInfo.tags.length > 0) {\n          console.log(\"Tags:\");\n          const tagsData: string[][] = [[\"Name\", \"Attached By\"]];\n          debugInfo.tags.forEach((tag) => {\n            tagsData.push([tag.name, tag.attachedBy]);\n          });\n          console.log(\n            table(tagsData, {\n              border: getBorderCharacters(\"ramac\"),\n              drawHorizontalLine: (lineIndex, rowCount) =>\n                lineIndex === 0 || lineIndex === 1 || lineIndex === rowCount,\n            }),\n          );\n        }\n\n        if (debugInfo.assets.length > 0) {\n          console.log(\"Assets:\");\n          const assetsData: string[][] = [[\"Type\", \"Size\", \"URL\"]];\n          debugInfo.assets.forEach((asset) => {\n            assetsData.push([\n              asset.assetType,\n              toHumanReadableSize(asset.size),\n              asset.url ?? \"N/A\",\n            ]);\n          });\n          console.log(\n            table(assetsData, {\n              border: getBorderCharacters(\"ramac\"),\n              drawHorizontalLine: (lineIndex, rowCount) =>\n                lineIndex === 0 || lineIndex === 1 || lineIndex === rowCount,\n            }),\n          );\n        }\n      }\n    } catch (error) {\n      printErrorMessageWithReason(\n        \"Failed to get bookmark debug info\",\n        error as object,\n      );\n    }\n  });\n\nbookmarksCmd\n  .command(\"recrawl\")\n  .description(\"trigger a recrawl for a link bookmark\")\n  .argument(\"<bookmarkId>\", \"the id of the bookmark to recrawl\")\n  .action(async (bookmarkId) => {\n    const api = getAPIClient();\n    try {\n      await api.admin.adminRecrawlBookmark.mutate({ bookmarkId });\n      printStatusMessage(true, \"Recrawl queued successfully\");\n    } catch (error) {\n      printErrorMessageWithReason(\"Failed to queue recrawl\", error as object);\n    }\n  });\n\nbookmarksCmd\n  .command(\"reindex\")\n  .description(\"trigger a search reindex for a bookmark\")\n  .argument(\"<bookmarkId>\", \"the id of the bookmark to reindex\")\n  .action(async (bookmarkId) => {\n    const api = getAPIClient();\n    try {\n      await api.admin.adminReindexBookmark.mutate({ bookmarkId });\n      printStatusMessage(true, \"Reindex queued successfully\");\n    } catch (error) {\n      printErrorMessageWithReason(\"Failed to queue reindex\", error as object);\n    }\n  });\n\nbookmarksCmd\n  .command(\"retag\")\n  .description(\"trigger AI retagging for a bookmark\")\n  .argument(\"<bookmarkId>\", \"the id of the bookmark to retag\")\n  .action(async (bookmarkId) => {\n    const api = getAPIClient();\n    try {\n      await api.admin.adminRetagBookmark.mutate({ bookmarkId });\n      printStatusMessage(true, \"Retag queued successfully\");\n    } catch (error) {\n      printErrorMessageWithReason(\"Failed to queue retag\", error as object);\n    }\n  });\n\nbookmarksCmd\n  .command(\"resummarize\")\n  .description(\"trigger AI resummarization for a link bookmark\")\n  .argument(\"<bookmarkId>\", \"the id of the bookmark to resummarize\")\n  .action(async (bookmarkId) => {\n    const api = getAPIClient();\n    try {\n      await api.admin.adminResummarizeBookmark.mutate({ bookmarkId });\n      printStatusMessage(true, \"Resummarize queued successfully\");\n    } catch (error) {\n      printErrorMessageWithReason(\n        \"Failed to queue resummarize\",\n        error as object,\n      );\n    }\n  });\n\nadminCmd.addCommand(bookmarksCmd);\n\n// --- Jobs subcommand ---\n\nconst jobsCmd = new Command()\n  .name(\"jobs\")\n  .description(\"background job management commands\");\n\njobsCmd\n  .command(\"stats\")\n  .description(\"show background job queue statistics\")\n  .action(async () => {\n    const api = getAPIClient();\n\n    try {\n      const stats = await api.admin.backgroundJobsStats.query();\n\n      if (getGlobalOptions().json) {\n        printObject(stats);\n      } else {\n        const data: string[][] = [[\"Queue\", \"Queued\", \"Unprocessed\", \"Failed\"]];\n\n        data.push([\n          \"Crawling\",\n          stats.crawlStats.queued.toString(),\n          stats.crawlStats.pending.toString(),\n          stats.crawlStats.failed.toString(),\n        ]);\n        data.push([\n          \"Inference (Tag/Summarize)\",\n          stats.inferenceStats.queued.toString(),\n          stats.inferenceStats.pending.toString(),\n          stats.inferenceStats.failed.toString(),\n        ]);\n        data.push([\n          \"Search Indexing\",\n          stats.indexingStats.queued.toString(),\n          \"-\",\n          \"-\",\n        ]);\n        data.push([\n          \"Video Processing\",\n          stats.videoStats.queued.toString(),\n          \"-\",\n          \"-\",\n        ]);\n        data.push([\"Webhooks\", stats.webhookStats.queued.toString(), \"-\", \"-\"]);\n        data.push([\n          \"Asset Preprocessing\",\n          stats.assetPreprocessingStats.queued.toString(),\n          \"-\",\n          \"-\",\n        ]);\n        data.push([\"Feeds\", stats.feedStats.queued.toString(), \"-\", \"-\"]);\n        data.push([\n          \"Admin Maintenance\",\n          stats.adminMaintenanceStats.queued.toString(),\n          \"-\",\n          \"-\",\n        ]);\n\n        console.log(\n          table(data, {\n            border: getBorderCharacters(\"ramac\"),\n            drawHorizontalLine: (lineIndex, rowCount) =>\n              lineIndex === 0 || lineIndex === 1 || lineIndex === rowCount,\n          }),\n        );\n      }\n    } catch (error) {\n      printErrorMessageWithReason(\n        \"Failed to get background job stats\",\n        error as object,\n      );\n    }\n  });\n\njobsCmd\n  .command(\"recrawl-links\")\n  .description(\"recrawl all link bookmarks matching a crawl status\")\n  .requiredOption(\n    \"--status <status>\",\n    \"filter by crawl status (success, failure, pending, all)\",\n  )\n  .option(\"--run-inference\", \"also re-run inference after crawling\", false)\n  .action(async (opts) => {\n    const api = getAPIClient();\n    const status = opts.status as \"success\" | \"failure\" | \"pending\" | \"all\";\n    try {\n      await api.admin.recrawlLinks.mutate({\n        crawlStatus: status,\n        runInference: opts.runInference,\n      });\n      printStatusMessage(\n        true,\n        `Recrawl queued for all links with crawl status: ${status}`,\n      );\n    } catch (error) {\n      printErrorMessageWithReason(\n        \"Failed to queue mass recrawl\",\n        error as object,\n      );\n    }\n  });\n\njobsCmd\n  .command(\"reindex-all\")\n  .description(\"reindex all bookmarks for search\")\n  .action(async () => {\n    const api = getAPIClient();\n    try {\n      await api.admin.reindexAllBookmarks.mutate();\n      printStatusMessage(true, \"Reindex queued for all bookmarks\");\n    } catch (error) {\n      printErrorMessageWithReason(\n        \"Failed to queue mass reindex\",\n        error as object,\n      );\n    }\n  });\n\njobsCmd\n  .command(\"retag-all\")\n  .description(\"re-run AI tagging on all bookmarks matching a status\")\n  .requiredOption(\n    \"--status <status>\",\n    \"filter by tagging status (success, failure, pending, all)\",\n  )\n  .action(async (opts) => {\n    const api = getAPIClient();\n    const status = opts.status as \"success\" | \"failure\" | \"pending\" | \"all\";\n    try {\n      await api.admin.reRunInferenceOnAllBookmarks.mutate({\n        type: \"tag\",\n        status,\n      });\n      printStatusMessage(\n        true,\n        `Retag queued for all bookmarks with tagging status: ${status}`,\n      );\n    } catch (error) {\n      printErrorMessageWithReason(\n        \"Failed to queue mass retag\",\n        error as object,\n      );\n    }\n  });\n\njobsCmd\n  .command(\"resummarize-all\")\n  .description(\"re-run AI summarization on all bookmarks matching a status\")\n  .requiredOption(\n    \"--status <status>\",\n    \"filter by summarization status (success, failure, pending, all)\",\n  )\n  .action(async (opts) => {\n    const api = getAPIClient();\n    const status = opts.status as \"success\" | \"failure\" | \"pending\" | \"all\";\n    try {\n      await api.admin.reRunInferenceOnAllBookmarks.mutate({\n        type: \"summarize\",\n        status,\n      });\n      printStatusMessage(\n        true,\n        `Resummarize queued for all bookmarks with summarization status: ${status}`,\n      );\n    } catch (error) {\n      printErrorMessageWithReason(\n        \"Failed to queue mass resummarize\",\n        error as object,\n      );\n    }\n  });\n\njobsCmd\n  .command(\"reprocess-assets\")\n  .description(\"reprocess all asset bookmarks in fix mode\")\n  .action(async () => {\n    const api = getAPIClient();\n    try {\n      await api.admin.reprocessAssetsFixMode.mutate();\n      printStatusMessage(true, \"Asset reprocessing queued for all assets\");\n    } catch (error) {\n      printErrorMessageWithReason(\n        \"Failed to queue asset reprocessing\",\n        error as object,\n      );\n    }\n  });\n\nadminCmd.addCommand(jobsCmd);\n"
  },
  {
    "path": "apps/cli/src/commands/bookmarks.ts",
    "content": "import * as fs from \"node:fs\";\nimport { addToList } from \"@/commands/lists\";\nimport {\n  printError,\n  printObject,\n  printStatusMessage,\n  printSuccess,\n} from \"@/lib/output\";\nimport { getAPIClient } from \"@/lib/trpc\";\nimport { Command } from \"@commander-js/extra-typings\";\n\nimport type { ZBookmark } from \"@karakeep/shared/types/bookmarks\";\nimport {\n  BookmarkTypes,\n  MAX_NUM_BOOKMARKS_PER_PAGE,\n} from \"@karakeep/shared/types/bookmarks\";\n\nexport const bookmarkCmd = new Command()\n  .name(\"bookmarks\")\n  .description(\"manipulating bookmarks\");\n\nfunction collect<T>(val: T, acc: T[]) {\n  acc.push(val);\n  return acc;\n}\n\ntype Bookmark = Omit<ZBookmark, \"tags\"> & {\n  tags: string[];\n};\n\nfunction normalizeBookmark(bookmark: ZBookmark): Bookmark {\n  return {\n    ...bookmark,\n    tags: bookmark.tags.map((t) => t.name),\n  };\n}\n\nfunction printBookmark(bookmark: ZBookmark) {\n  printObject(normalizeBookmark(bookmark));\n}\n\nbookmarkCmd\n  .command(\"add\")\n  .description(\"creates a new bookmark\")\n  .option(\n    \"--link <link>\",\n    \"the link to add. Specify multiple times to add multiple links\",\n    collect<string>,\n    [],\n  )\n  .option(\n    \"--note <note>\",\n    \"the note text to add. Specify multiple times to add multiple notes\",\n    collect<string>,\n    [],\n  )\n  .option(\"--stdin\", \"reads the data from stdin and store it as a note\")\n  .option(\n    \"--list-id <id>\",\n    \"if set, the bookmark(s) will be added to this list\",\n  )\n  .option(\n    \"--tag-name <tag>\",\n    \"if set, this tag will be added to the bookmark(s). Specify multiple times to add multiple tags\",\n    collect<string>,\n    [],\n  )\n  .option(\n    \"--title <title>\",\n    \"if set, this will be used as the bookmark's title\",\n  )\n  .action(async (opts) => {\n    const api = getAPIClient();\n\n    const results: Bookmark[] = [];\n\n    const promises = [\n      ...opts.link.map((url) =>\n        api.bookmarks.createBookmark\n          .mutate({\n            type: BookmarkTypes.LINK,\n            url,\n            title: opts.title,\n            source: \"cli\",\n          })\n          .then((bookmark: ZBookmark) => {\n            results.push(normalizeBookmark(bookmark));\n          })\n          .catch(printError(`Failed to add a link bookmark for url \"${url}\"`)),\n      ),\n      ...opts.note.map((text) =>\n        api.bookmarks.createBookmark\n          .mutate({\n            type: BookmarkTypes.TEXT,\n            text,\n            title: opts.title,\n            source: \"cli\",\n          })\n          .then((bookmark: ZBookmark) => {\n            results.push(normalizeBookmark(bookmark));\n          })\n          .catch(\n            printError(\n              `Failed to add a text bookmark with text \"${text.substring(0, 50)}\"`,\n            ),\n          ),\n      ),\n    ];\n\n    if (opts.stdin) {\n      const text = fs.readFileSync(0, \"utf-8\");\n      promises.push(\n        api.bookmarks.createBookmark\n          .mutate({\n            type: BookmarkTypes.TEXT,\n            text,\n            title: opts.title,\n            source: \"cli\",\n          })\n          .then((bookmark: ZBookmark) => {\n            results.push(normalizeBookmark(bookmark));\n          })\n          .catch(\n            printError(\n              `Failed to add a text bookmark with text \"${text.substring(0, 50)}\"`,\n            ),\n          ),\n      );\n    }\n\n    await Promise.allSettled(promises);\n    printObject(results);\n\n    await Promise.allSettled(\n      results.flatMap((r) => [\n        updateTags(opts.tagName, [], r.id),\n        opts.listId ? addToList(opts.listId, r.id) : Promise.resolve(),\n      ]),\n    );\n  });\n\nbookmarkCmd\n  .command(\"get\")\n  .description(\"fetch information about a bookmark\")\n  .argument(\"<id>\", \"The id of the bookmark to get\")\n  .option(\n    \"--include-content\",\n    \"include full bookmark content in results\",\n    false,\n  )\n  .action(async (id, opts) => {\n    const api = getAPIClient();\n    await api.bookmarks.getBookmark\n      .query({ bookmarkId: id, includeContent: opts.includeContent })\n      .then(printBookmark)\n      .catch(printError(`Failed to get the bookmark with id \"${id}\"`));\n  });\n\nfunction printTagMessage(\n  tags: { tagName: string }[],\n  bookmarkId: string,\n  action: \"Added\" | \"Removed\",\n) {\n  tags.forEach((tag) => {\n    printStatusMessage(\n      true,\n      `${action} the tag ${tag.tagName} ${action === \"Added\" ? \"to\" : \"from\"} the bookmark with id ${bookmarkId}`,\n    );\n  });\n}\n\nasync function updateTags(addTags: string[], removeTags: string[], id: string) {\n  const tagsToAdd = addTags.map((addTag) => {\n    return { tagName: addTag };\n  });\n\n  const tagsToRemove = removeTags.map((removeTag) => {\n    return { tagName: removeTag };\n  });\n\n  if (tagsToAdd.length > 0 || tagsToRemove.length > 0) {\n    const api = getAPIClient();\n    await api.bookmarks.updateTags\n      .mutate({\n        bookmarkId: id,\n        attach: tagsToAdd,\n        detach: tagsToRemove,\n      })\n      .then(() => {\n        printTagMessage(tagsToAdd, id, \"Added\");\n        printTagMessage(tagsToRemove, id, \"Removed\");\n      })\n      .catch(\n        printError(\n          `Failed to add/remove tags to/from bookmark with id \"${id}\"`,\n        ),\n      );\n  }\n}\n\nbookmarkCmd\n  .command(\"update\")\n  .description(\"update a bookmark\")\n  .option(\"--title <title>\", \"if set, the bookmark's title will be updated\")\n  .option(\"--note <note>\", \"if set, the bookmark's note will be updated\")\n  .option(\"--archive\", \"if set, the bookmark will be archived\")\n  .option(\"--no-archive\", \"if set, the bookmark will be unarchived\")\n  .option(\"--favourite\", \"if set, the bookmark will be favourited\")\n  .option(\"--no-favourite\", \"if set, the bookmark will be unfavourited\")\n  .argument(\"<id>\", \"the id of the bookmark to update\")\n  .action(async (id, opts) => {\n    const api = getAPIClient();\n    await api.bookmarks.updateBookmark\n      .mutate({\n        bookmarkId: id,\n        archived: opts.archive,\n        favourited: opts.favourite,\n        title: opts.title,\n        note: opts.note,\n      })\n      .then(printObject)\n      .catch(printError(`Failed to update bookmark with id \"${id}\"`));\n  });\n\nbookmarkCmd\n  .command(\"update-tags\")\n  .description(\"update the tags of a bookmark\")\n  .option(\n    \"--add-tag <tag>\",\n    \"if set, this tag will be added to the bookmark. Specify multiple times to add multiple tags\",\n    collect<string>,\n    [],\n  )\n  .option(\n    \"--remove-tag <tag>\",\n    \"if set, this tag will be removed from the bookmark. Specify multiple times to remove multiple tags\",\n    collect<string>,\n    [],\n  )\n  .argument(\"<id>\", \"the id of the bookmark to update\")\n  .action(async (id, opts) => {\n    await updateTags(opts.addTag, opts.removeTag, id);\n  });\n\nbookmarkCmd\n  .command(\"list\")\n  .description(\"list all bookmarks\")\n  .option(\n    \"--include-archived\",\n    \"If set, archived bookmarks will be fetched as well\",\n    false,\n  )\n  .option(\"--list-id <id>\", \"if set, only items from that list will be fetched\")\n  .option(\n    \"--include-content\",\n    \"include full bookmark content in results\",\n    false,\n  )\n  .action(async (opts) => {\n    const api = getAPIClient();\n\n    const request = {\n      archived: opts.includeArchived ? undefined : false,\n      listId: opts.listId,\n      limit: MAX_NUM_BOOKMARKS_PER_PAGE,\n      useCursorV2: true,\n      includeContent: opts.includeContent,\n    };\n\n    try {\n      let resp = await api.bookmarks.getBookmarks.query(request);\n      let results: ZBookmark[] = resp.bookmarks;\n\n      while (resp.nextCursor) {\n        resp = await api.bookmarks.getBookmarks.query({\n          ...request,\n          cursor: resp.nextCursor,\n        });\n        results = [...results, ...resp.bookmarks];\n      }\n      printObject(results.map(normalizeBookmark), { maxArrayLength: null });\n    } catch {\n      printStatusMessage(false, \"Failed to query bookmarks\");\n    }\n  });\n\nbookmarkCmd\n  .command(\"search\")\n  .description(\"search bookmarks using query matchers\")\n  .argument(\n    \"<query>\",\n    \"the search query (supports matchers like tag:name, is:fav, etc.)\",\n  )\n  .option(\n    \"--limit <limit>\",\n    \"number of results per page\",\n    (val) => parseInt(val, 10),\n    50,\n  )\n  .option(\n    \"--sort-order <order>\",\n    \"sort order for results\",\n    (val) => {\n      if (val !== \"relevance\" && val !== \"asc\" && val !== \"desc\") {\n        throw new Error(\"sort-order must be one of: relevance, asc, desc\");\n      }\n      return val;\n    },\n    \"relevance\",\n  )\n  .option(\n    \"--include-content\",\n    \"include full bookmark content in results\",\n    false,\n  )\n  .option(\"--all\", \"fetch all results (paginate through all pages)\", false)\n  .action(async (query, opts) => {\n    const api = getAPIClient();\n\n    const request = {\n      text: query,\n      limit: opts.limit,\n      sortOrder: opts.sortOrder as \"relevance\" | \"asc\" | \"desc\",\n      includeContent: opts.includeContent,\n    };\n\n    try {\n      let resp = await api.bookmarks.searchBookmarks.query(request);\n      let results: ZBookmark[] = resp.bookmarks;\n\n      // If --all flag is set, fetch all pages\n      if (opts.all) {\n        while (resp.nextCursor) {\n          resp = await api.bookmarks.searchBookmarks.query({\n            ...request,\n            cursor: resp.nextCursor,\n          });\n          results = [...results, ...resp.bookmarks];\n        }\n      }\n\n      printObject(results.map(normalizeBookmark), { maxArrayLength: null });\n    } catch (error) {\n      printStatusMessage(false, \"Failed to search bookmarks\");\n      if (error instanceof Error) {\n        printStatusMessage(false, error.message);\n      }\n    }\n  });\n\nbookmarkCmd\n  .command(\"delete\")\n  .description(\"delete a bookmark\")\n  .argument(\"<id>\", \"the id of the bookmark to delete\")\n  .action(async (id) => {\n    const api = getAPIClient();\n    await api.bookmarks.deleteBookmark\n      .mutate({ bookmarkId: id })\n      .then(printSuccess(`Bookmark with id '${id}' got deleted`))\n      .catch(printError(`Failed to delete bookmark with id \"${id}\"`));\n  });\n"
  },
  {
    "path": "apps/cli/src/commands/dump.ts",
    "content": "import { spawn } from \"node:child_process\";\nimport fsp from \"node:fs/promises\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport { getGlobalOptions } from \"@/lib/globals\";\nimport { printErrorMessageWithReason, printStatusMessage } from \"@/lib/output\";\nimport { getAPIClient } from \"@/lib/trpc\";\nimport { Command } from \"@commander-js/extra-typings\";\nimport chalk from \"chalk\";\n\nimport type { ZBookmark } from \"@karakeep/shared/types/bookmarks\";\nimport type { ZBookmarkList } from \"@karakeep/shared/types/lists\";\nimport { MAX_NUM_BOOKMARKS_PER_PAGE } from \"@karakeep/shared/types/bookmarks\";\nimport { ZCursor } from \"@karakeep/shared/types/pagination\";\nimport { MAX_NUM_TAGS_PER_PAGE } from \"@karakeep/shared/types/tags\";\n\nconst OK = chalk.green(\"✓\");\nconst FAIL = chalk.red(\"✗\");\nconst DOTS = chalk.gray(\"…\");\n\nfunction line(msg: string) {\n  console.log(msg);\n}\n\nfunction stepStart(title: string) {\n  console.log(`${chalk.cyan(title)} ${DOTS}`);\n}\n\nfunction stepEndSuccess(extra?: string) {\n  process.stdout.write(`${OK}${extra ? \" \" + chalk.gray(extra) : \"\"}\\n`);\n}\n\nfunction stepEndFail(extra?: string) {\n  process.stdout.write(`${FAIL}${extra ? \" \" + chalk.gray(extra) : \"\"}\\n`);\n}\n\nfunction progressUpdate(\n  prefix: string,\n  current: number,\n  total?: number,\n  suffix?: string,\n) {\n  const totalPart = total != null ? `/${total}` : \"\";\n  const text = `${chalk.gray(prefix)} ${current}${totalPart}${suffix ? \" \" + chalk.gray(suffix) : \"\"}`;\n  if (process.stdout.isTTY) {\n    try {\n      process.stdout.clearLine(0);\n      process.stdout.cursorTo(0);\n      process.stdout.write(text);\n      return;\n    } catch {\n      // ignore failures\n    }\n  }\n  console.log(text);\n}\n\nfunction progressDone() {\n  process.stdout.write(\"\\n\");\n}\n\nasync function ensureDir(p: string) {\n  await fsp.mkdir(p, { recursive: true });\n}\n\nasync function writeJson(filePath: string, data: unknown) {\n  const dir = path.dirname(filePath);\n  await ensureDir(dir);\n  await fsp.writeFile(filePath, JSON.stringify(data, null, 2), \"utf-8\");\n}\n\nasync function writeJsonl(\n  filePath: string,\n  items: AsyncIterable<unknown> | Iterable<unknown>,\n) {\n  const dir = path.dirname(filePath);\n  await ensureDir(dir);\n  const fh = await fsp.open(filePath, \"w\");\n  try {\n    for await (const item of items) {\n      await fh.write(JSON.stringify(item) + \"\\n\");\n    }\n  } finally {\n    await fh.close();\n  }\n}\n\nasync function createTarGz(srcDir: string, outFile: string): Promise<void> {\n  await ensureDir(path.dirname(outFile));\n  await new Promise<void>((resolve, reject) => {\n    const tar = spawn(\"tar\", [\"-czf\", outFile, \"-C\", srcDir, \".\"], {\n      stdio: \"inherit\",\n    });\n    tar.on(\"error\", reject);\n    tar.on(\"close\", (code) => {\n      if (code === 0) resolve();\n      else reject(new Error(`tar exited with code ${code}`));\n    });\n  });\n}\n\nexport const dumpCmd = new Command()\n  .name(\"dump\")\n  .description(\"dump all account data and assets into an archive\")\n  .option(\"--output <file>\", \"output archive path (.tar.gz)\")\n  .option(\n    \"--exclude-assets\",\n    \"exclude binary assets (skip assets index and files)\",\n  )\n  .option(\"--exclude-bookmarks\", \"exclude bookmarks (metadata/content)\")\n  .option(\"--exclude-lists\", \"exclude lists and list membership\")\n  .option(\"--exclude-tags\", \"exclude tags\")\n  .option(\"--exclude-ai-prompts\", \"exclude AI prompts\")\n  .option(\"--exclude-rules\", \"exclude rule engine rules\")\n  .option(\"--exclude-feeds\", \"exclude RSS feeds\")\n  .option(\"--exclude-webhooks\", \"exclude webhooks\")\n  .option(\"--exclude-user-settings\", \"exclude user settings\")\n  .option(\"--exclude-link-content\", \"exclude link content\")\n  .option(\n    \"--batch-size <n>\",\n    `number of bookmarks per page (max ${MAX_NUM_BOOKMARKS_PER_PAGE})`,\n    (v) => Math.min(Number(v || 50), MAX_NUM_BOOKMARKS_PER_PAGE),\n    50,\n  )\n  .action(async (opts) => {\n    const api = getAPIClient();\n    const globals = getGlobalOptions();\n\n    const ts = new Date()\n      .toISOString()\n      .replace(/[:.]/g, \"-\")\n      .replace(\"T\", \"_\")\n      .replace(\"Z\", \"Z\");\n    const workRoot = await fsp.mkdtemp(\n      path.join(os.tmpdir(), `karakeep-dump-${ts}-`),\n    );\n    const outFile = opts.output ?? path.resolve(`karakeep-dump-${ts}.tar.gz`);\n\n    try {\n      line(\"\");\n      line(`${chalk.bold(\"Karakeep Dump\")}`);\n      line(`${chalk.gray(\"Server:\")} ${globals.serverAddr}`);\n      line(`${chalk.gray(\"Output:\")} ${outFile}`);\n      line(\"\");\n\n      // Manifest skeleton\n      const whoami = await api.users.whoami.query();\n      const manifest = {\n        format: \"karakeep.dump\",\n        version: 1,\n        exportedAt: new Date().toISOString(),\n        server: globals.serverAddr,\n        user: {\n          id: whoami.id,\n          email: whoami.email,\n          name: whoami.name,\n        },\n        counts: {\n          bookmarks: 0,\n          assets: 0,\n          lists: 0,\n          tags: 0,\n          rules: 0,\n          feeds: 0,\n          webhooks: 0,\n          prompts: 0,\n        },\n      };\n\n      // 1) User settings\n      if (!opts.excludeUserSettings) {\n        stepStart(\"Exporting user settings\");\n        const settings = await api.users.settings.query();\n        await writeJson(\n          path.join(workRoot, \"users\", \"settings.json\"),\n          settings,\n        );\n        stepEndSuccess();\n      }\n\n      // 2) Lists\n      let lists: ZBookmarkList[] | undefined;\n      if (!opts.excludeLists) {\n        stepStart(\"Exporting lists\");\n        const resp = await api.lists.list.query();\n        lists = resp.lists;\n        await writeJson(path.join(workRoot, \"lists\", \"index.json\"), lists);\n        manifest.counts.lists = lists.length;\n        stepEndSuccess();\n      }\n\n      // 3) Tags\n      if (!opts.excludeTags) {\n        stepStart(\"Exporting tags\");\n\n        let cursor = null;\n        let allTags = [];\n        do {\n          const { tags, nextCursor } = await api.tags.list.query({\n            limit: MAX_NUM_TAGS_PER_PAGE,\n            cursor,\n          });\n          allTags.push(...tags);\n          cursor = nextCursor;\n        } while (cursor);\n        await writeJson(path.join(workRoot, \"tags\", \"index.json\"), allTags);\n        manifest.counts.tags = allTags.length;\n        stepEndSuccess();\n      }\n\n      // 4) Rules\n      if (!opts.excludeRules) {\n        stepStart(\"Exporting rules\");\n        const { rules } = await api.rules.list.query();\n        await writeJson(path.join(workRoot, \"rules\", \"index.json\"), rules);\n        manifest.counts.rules = rules.length;\n        stepEndSuccess();\n      }\n\n      // 5) Feeds\n      if (!opts.excludeFeeds) {\n        stepStart(\"Exporting feeds\");\n        const { feeds } = await api.feeds.list.query();\n        await writeJson(path.join(workRoot, \"feeds\", \"index.json\"), feeds);\n        manifest.counts.feeds = feeds.length;\n        stepEndSuccess();\n      }\n\n      // 6) Prompts\n      if (!opts.excludeAiPrompts) {\n        stepStart(\"Exporting AI prompts\");\n        const prompts = await api.prompts.list.query();\n        await writeJson(path.join(workRoot, \"prompts\", \"index.json\"), prompts);\n        manifest.counts.prompts = prompts.length;\n        stepEndSuccess();\n      }\n\n      // 7) Webhooks\n      if (!opts.excludeWebhooks) {\n        stepStart(\"Exporting webhooks\");\n        const webhooks = await api.webhooks.list.query();\n        await writeJson(\n          path.join(workRoot, \"webhooks\", \"index.json\"),\n          webhooks.webhooks,\n        );\n        manifest.counts.webhooks = webhooks.webhooks.length;\n        stepEndSuccess();\n      }\n\n      // 8) Bookmarks (JSONL + list membership)\n      if (!opts.excludeBookmarks) {\n        stepStart(\"Exporting bookmarks (metadata/content)\");\n        const bookmarkJsonl = path.join(workRoot, \"bookmarks\", \"index.jsonl\");\n        let bookmarksExported = 0;\n        const bookmarkIterator = async function* (): AsyncGenerator<ZBookmark> {\n          let cursor: ZCursor | null = null;\n          do {\n            const resp = await api.bookmarks.getBookmarks.query({\n              includeContent: !opts.excludeLinkContent,\n              limit: Number(opts.batchSize) || 50,\n              cursor,\n              useCursorV2: true,\n            });\n            for (const b of resp.bookmarks) {\n              yield b;\n              bookmarksExported++;\n              progressUpdate(\"Bookmarks\", bookmarksExported);\n            }\n            cursor = resp.nextCursor;\n          } while (cursor);\n        };\n        await writeJsonl(bookmarkJsonl, bookmarkIterator());\n        progressDone();\n        manifest.counts.bookmarks = bookmarksExported;\n        stepEndSuccess();\n      }\n\n      // 9) List membership (listId -> [bookmarkId])\n      if (!opts.excludeLists && !opts.excludeBookmarks && lists) {\n        stepStart(\"Exporting list membership\");\n        const membership = await buildListMembership(api, lists, (p, t) =>\n          progressUpdate(\"Lists scanned\", p, t),\n        );\n        progressDone();\n        await writeJson(\n          path.join(workRoot, \"lists\", \"membership.json\"),\n          Object.fromEntries(membership.entries()),\n        );\n        stepEndSuccess();\n      }\n\n      // 10) Assets: index + files\n      if (!opts.excludeAssets) {\n        stepStart(\"Exporting assets (binary files)\");\n        const assetsDir = path.join(workRoot, \"assets\", \"files\");\n        await ensureDir(assetsDir);\n        const assetsIndex: {\n          id: string;\n          assetType: string;\n          size: number;\n          contentType: string | null;\n          fileName: string | null;\n          bookmarkId: string | null;\n          filePath: string; // relative inside archive\n        }[] = [];\n        let assetPageCursor: number | null | undefined = null;\n        let downloaded = 0;\n        let totalAssets: number | undefined = undefined;\n        do {\n          const resp = await api.assets.list.query({\n            limit: 50,\n            cursor: assetPageCursor ?? undefined,\n          });\n          if (totalAssets == null) totalAssets = resp.totalCount;\n          for (const a of resp.assets) {\n            const relPath = path.join(\"assets\", \"files\", a.id);\n            const absPath = path.join(workRoot, relPath);\n            try {\n              await downloadAsset(\n                globals.serverAddr,\n                globals.apiKey,\n                a.id,\n                absPath,\n              );\n              assetsIndex.push({\n                id: a.id,\n                assetType: a.assetType,\n                size: a.size,\n                contentType: a.contentType,\n                fileName: a.fileName,\n                bookmarkId: a.bookmarkId,\n                filePath: relPath.replace(/\\\\/g, \"/\"),\n              });\n              downloaded++;\n              progressUpdate(\"Assets\", downloaded, totalAssets);\n            } catch (e) {\n              printErrorMessageWithReason(\n                `Failed to download asset \"${a.id}\"`,\n                e as object,\n              );\n            }\n          }\n          assetPageCursor = resp.nextCursor;\n        } while (assetPageCursor);\n        progressDone();\n        manifest.counts.assets = downloaded;\n        await writeJson(\n          path.join(workRoot, \"assets\", \"index.json\"),\n          assetsIndex,\n        );\n        stepEndSuccess();\n      }\n\n      // 11) Manifest\n      stepStart(\"Writing manifest\");\n      await writeJson(path.join(workRoot, \"manifest.json\"), manifest);\n      stepEndSuccess();\n\n      // 12) Create archive\n      stepStart(\"Creating archive\");\n      await createTarGz(workRoot, outFile);\n      stepEndSuccess();\n\n      printStatusMessage(true, `Dump completed. File: ${outFile}`);\n    } catch (error) {\n      stepEndFail();\n      printErrorMessageWithReason(\"Dump failed\", error as object);\n      throw error;\n    } finally {\n      // Best-effort cleanup of temp directory\n      try {\n        await fsp.rm(workRoot, { recursive: true, force: true });\n      } catch {\n        // ignore\n      }\n    }\n  });\n\nasync function buildListMembership(\n  api: ReturnType<typeof getAPIClient>,\n  lists: ZBookmarkList[],\n  onProgress?: (processed: number, total: number) => void,\n) {\n  const result = new Map<string, string[]>(); // listId -> [bookmarkId]\n  let processed = 0;\n  for (const l of lists) {\n    // Only manual lists have explicit membership\n    if (l.type !== \"manual\") {\n      processed++;\n      onProgress?.(processed, lists.length);\n      continue;\n    }\n    let cursor: ZCursor | null = null;\n    const ids: string[] = [];\n    do {\n      const resp = await api.bookmarks.getBookmarks.query({\n        listId: l.id,\n        limit: MAX_NUM_BOOKMARKS_PER_PAGE,\n        cursor,\n        includeContent: false,\n        useCursorV2: true,\n      });\n      for (const b of resp.bookmarks) ids.push(b.id);\n      cursor = resp.nextCursor;\n    } while (cursor);\n    result.set(l.id, ids);\n    processed++;\n    onProgress?.(processed, lists.length);\n  }\n  return result;\n}\n\nasync function downloadAsset(\n  serverAddr: string,\n  apiKey: string,\n  assetId: string,\n  destFile: string,\n) {\n  const url = `${serverAddr}/api/assets/${assetId}`;\n  const resp = await fetch(url, {\n    headers: { authorization: `Bearer ${apiKey}` },\n  });\n  if (!resp.ok) {\n    throw new Error(`HTTP ${resp.status} ${resp.statusText}`);\n  }\n  const arrayBuf = await resp.arrayBuffer();\n  await ensureDir(path.dirname(destFile));\n  await fsp.writeFile(destFile, Buffer.from(arrayBuf));\n}\n"
  },
  {
    "path": "apps/cli/src/commands/lists.ts",
    "content": "import { getGlobalOptions } from \"@/lib/globals\";\nimport {\n  printError,\n  printErrorMessageWithReason,\n  printObject,\n  printSuccess,\n} from \"@/lib/output\";\nimport { getAPIClient } from \"@/lib/trpc\";\nimport { Command } from \"@commander-js/extra-typings\";\nimport { getBorderCharacters, table } from \"table\";\n\nimport { listsToTree } from \"@karakeep/shared/utils/listUtils\";\n\nexport const listsCmd = new Command()\n  .name(\"lists\")\n  .description(\"manipulating lists\");\n\nlistsCmd\n  .command(\"list\")\n  .description(\"lists all lists\")\n  .action(async () => {\n    const api = getAPIClient();\n\n    try {\n      const resp = await api.lists.list.query();\n\n      if (getGlobalOptions().json) {\n        printObject(resp);\n      } else {\n        const { allPaths } = listsToTree(resp.lists);\n        const data: string[][] = [[\"Id\", \"Name\"]];\n\n        allPaths.forEach((path) => {\n          const name = path.map((p) => `${p.icon} ${p.name}`).join(\" / \");\n          const id = path[path.length - 1].id;\n          data.push([id, name]);\n        });\n        console.log(\n          table(data, {\n            border: getBorderCharacters(\"ramac\"),\n            singleLine: true,\n          }),\n        );\n      }\n    } catch (error) {\n      printErrorMessageWithReason(\"Failed to list all lists\", error as object);\n    }\n  });\n\nlistsCmd\n  .command(\"delete\")\n  .description(\"deletes a list\")\n  .argument(\"<id>\", \"the id of the list\")\n  .action(async (id) => {\n    const api = getAPIClient();\n\n    await api.lists.delete\n      .mutate({\n        listId: id,\n      })\n      .then(printSuccess(`Successfully deleted list with id \"${id}\"`))\n      .catch(printError(`Failed to delete list with id \"${id}\"`));\n  });\n\nexport async function addToList(listId: string, bookmarkId: string) {\n  const api = getAPIClient();\n\n  await api.lists.addToList\n    .mutate({\n      listId,\n      bookmarkId,\n    })\n    .then(\n      printSuccess(\n        `Successfully added bookmark \"${bookmarkId}\" to list with id \"${listId}\"`,\n      ),\n    )\n    .catch(\n      printError(\n        `Failed to add bookmark \"${bookmarkId}\" to list with id \"${listId}\"`,\n      ),\n    );\n}\n\nlistsCmd\n  .command(\"get\")\n  .description(\"gets all the ids of the bookmarks assigned to the list\")\n  .requiredOption(\"--list <id>\", \"the id of the list\")\n  .option(\n    \"--include-content\",\n    \"include full bookmark content in results\",\n    false,\n  )\n  .action(async (opts) => {\n    const api = getAPIClient();\n    try {\n      let resp = await api.bookmarks.getBookmarks.query({\n        listId: opts.list,\n        includeContent: opts.includeContent,\n      });\n      let results: string[] = resp.bookmarks.map((b) => b.id);\n      while (resp.nextCursor) {\n        resp = await api.bookmarks.getBookmarks.query({\n          listId: opts.list,\n          cursor: resp.nextCursor,\n          includeContent: opts.includeContent,\n        });\n        results = [...results, ...resp.bookmarks.map((b) => b.id)];\n      }\n\n      printObject(results);\n    } catch (error) {\n      printErrorMessageWithReason(\n        \"Failed to get the ids of the bookmarks in the list\",\n        error as object,\n      );\n    }\n  });\n\nlistsCmd\n  .command(\"add-bookmark\")\n  .description(\"add a bookmark to list\")\n  .requiredOption(\"--list <id>\", \"the id of the list\")\n  .requiredOption(\"--bookmark <bookmark>\", \"the id of the bookmark\")\n  .action(async (opts) => {\n    await addToList(opts.list, opts.bookmark);\n  });\n\nlistsCmd\n  .command(\"remove-bookmark\")\n  .description(\"remove a bookmark from list\")\n  .requiredOption(\"--list <id>\", \"the id of the list\")\n  .requiredOption(\"--bookmark <bookmark>\", \"the id of the bookmark\")\n  .action(async (opts) => {\n    const api = getAPIClient();\n\n    await api.lists.removeFromList\n      .mutate({\n        listId: opts.list,\n        bookmarkId: opts.bookmark,\n      })\n      .then(\n        printSuccess(\n          `Successfully removed bookmark \"${opts.bookmark}\" from list with id \"${opts.list}\"`,\n        ),\n      )\n      .catch(\n        printError(\n          `Failed to remove bookmark \"${opts.bookmark}\" from list with id \"${opts.list}\"`,\n        ),\n      );\n  });\n"
  },
  {
    "path": "apps/cli/src/commands/migrate.ts",
    "content": "import { stdin as input, stdout as output } from \"node:process\";\nimport readline from \"node:readline/promises\";\nimport { getGlobalOptions } from \"@/lib/globals\";\nimport { printErrorMessageWithReason, printStatusMessage } from \"@/lib/output\";\nimport { getAPIClient, getAPIClientFor } from \"@/lib/trpc\";\nimport { Command } from \"@commander-js/extra-typings\";\nimport chalk from \"chalk\";\n\nimport type { ZBookmark } from \"@karakeep/shared/types/bookmarks\";\nimport type { ZBookmarkList } from \"@karakeep/shared/types/lists\";\nimport type { ZPrompt } from \"@karakeep/shared/types/prompts\";\nimport type { RuleEngineRule } from \"@karakeep/shared/types/rules\";\nimport type { ZGetTagResponse } from \"@karakeep/shared/types/tags\";\nimport {\n  BookmarkTypes,\n  MAX_NUM_BOOKMARKS_PER_PAGE,\n} from \"@karakeep/shared/types/bookmarks\";\nimport { ZCursor } from \"@karakeep/shared/types/pagination\";\n\nconst OK = chalk.green(\"✓\");\nconst FAIL = chalk.red(\"✗\");\nconst DOTS = chalk.gray(\"…\");\n\nfunction line(msg: string) {\n  console.log(msg);\n}\n\nfunction stepStart(title: string) {\n  console.log(`${chalk.cyan(title)} ${DOTS}`);\n}\n\nfunction stepEndSuccess(extra?: string) {\n  process.stdout.write(`${OK}${extra ? \" \" + chalk.gray(extra) : \"\"}\\n`);\n}\n\nfunction stepEndFail(extra?: string) {\n  process.stdout.write(`${FAIL}${extra ? \" \" + chalk.gray(extra) : \"\"}\\n`);\n}\n\nfunction progressUpdate(\n  prefix: string,\n  current: number,\n  total?: number,\n  suffix?: string,\n) {\n  const totalPart = total != null ? `/${total}` : \"\";\n  const text = `${chalk.gray(prefix)} ${current}${totalPart}${suffix ? \" \" + chalk.gray(suffix) : \"\"}`;\n  if (process.stdout.isTTY) {\n    try {\n      process.stdout.clearLine(0);\n      process.stdout.cursorTo(0);\n      process.stdout.write(text);\n      return;\n    } catch {\n      // ignore failures\n    }\n  }\n  console.log(text);\n}\n\nfunction progressDone() {\n  process.stdout.write(\"\\n\");\n}\n\nexport const migrateCmd = new Command()\n  .name(\"migrate\")\n  .description(\"migrate data from a source server to a destination server\")\n  .requiredOption(\n    \"--dest-server <url>\",\n    \"destination server base URL, e.g. https://dest.example.com\",\n  )\n  .requiredOption(\"--dest-api-key <key>\", \"API key for the destination server\")\n  .option(\"-y, --yes\", \"skip confirmation prompt\")\n  .option(\"--exclude-assets\", \"exclude assets (skip asset bookmarks)\")\n  .option(\"--exclude-lists\", \"exclude lists and list membership\")\n  .option(\"--exclude-ai-prompts\", \"exclude AI prompts\")\n  .option(\"--exclude-rules\", \"exclude rule engine rules\")\n  .option(\"--exclude-feeds\", \"exclude RSS feeds\")\n  .option(\"--exclude-webhooks\", \"exclude webhooks\")\n  .option(\"--exclude-bookmarks\", \"exclude bookmarks migration\")\n  .option(\"--exclude-tags\", \"exclude tags migration\")\n  .option(\"--exclude-user-settings\", \"exclude user settings migration\")\n  .option(\n    \"--batch-size <n>\",\n    `number of bookmarks per page (max ${MAX_NUM_BOOKMARKS_PER_PAGE})`,\n    (v) => Math.min(Number(v || 50), MAX_NUM_BOOKMARKS_PER_PAGE),\n    50,\n  )\n  .action(async (opts) => {\n    const globals = getGlobalOptions();\n    const src = getAPIClient();\n    const dest = getAPIClientFor({\n      serverAddr: opts.destServer,\n      apiKey: opts.destApiKey,\n    });\n\n    if (!opts.yes) {\n      const rl = readline.createInterface({ input, output });\n      const answer = (\n        await rl.question(\n          `About to migrate data from \"${globals.serverAddr}\" to \"${opts.destServer}\". Proceed? (yes/no): `,\n        )\n      )\n        .trim()\n        .toLowerCase();\n      rl.close();\n      if (answer !== \"y\" && answer !== \"yes\") {\n        printStatusMessage(false, \"Migration aborted by user\");\n        return;\n      }\n    }\n\n    try {\n      line(\"\");\n      line(`${chalk.bold(\"Karakeep Migration\")}`);\n      line(`${chalk.gray(\"From:\")} ${globals.serverAddr}`);\n      line(`${chalk.gray(\"To:  \")} ${opts.destServer}`);\n      line(\"\");\n\n      // Pre-fetch totals for progress\n      let totalBookmarks: number | undefined = undefined;\n      try {\n        const stats = await src.users.stats.query();\n        totalBookmarks = stats.numBookmarks;\n      } catch {\n        // ignore stats errors; progress will show without total\n      }\n\n      // 1) User settings\n      if (!opts.excludeUserSettings) {\n        stepStart(\"Migrating user settings\");\n        await migrateUserSettings(src, dest);\n        stepEndSuccess();\n      }\n\n      // 2) Lists (and mapping)\n      let lists: ZBookmarkList[] = [];\n      let listIdMap = new Map<string, string>();\n      if (!opts.excludeLists) {\n        stepStart(\"Migrating lists\");\n        const listsStart = Date.now();\n        const listsRes = await migrateLists(\n          src,\n          dest,\n          (created, alreadyExists, total) => {\n            progressUpdate(\"Lists (created)\", created + alreadyExists, total);\n          },\n        );\n        lists = listsRes.lists;\n        listIdMap = listsRes.listIdMap;\n        progressDone();\n        stepEndSuccess(\n          `${listsRes.createdCount} created in ${Math.round((Date.now() - listsStart) / 1000)}s`,\n        );\n      }\n\n      // 3) Feeds\n      let feedIdMap = new Map<string, string>();\n      if (!opts.excludeFeeds) {\n        stepStart(\"Migrating feeds\");\n        const feedsStart = Date.now();\n        const res = await migrateFeeds(src, dest, (created, total) => {\n          progressUpdate(\"Feeds\", created, total);\n        });\n        feedIdMap = res.idMap;\n        progressDone();\n        stepEndSuccess(\n          `${res.count} migrated in ${Math.round((Date.now() - feedsStart) / 1000)}s`,\n        );\n      }\n\n      // 4) AI settings (custom prompts)\n      if (!opts.excludeAiPrompts) {\n        stepStart(\"Migrating AI prompts\");\n        const promptsStart = Date.now();\n        const promptsCount = await migratePrompts(\n          src,\n          dest,\n          (created, total) => {\n            progressUpdate(\"Prompts\", created, total);\n          },\n        );\n        progressDone();\n        stepEndSuccess(\n          `${promptsCount} migrated in ${Math.round((Date.now() - promptsStart) / 1000)}s`,\n        );\n      }\n\n      // 5) Webhooks (tokens cannot be read; created without token)\n      if (!opts.excludeWebhooks) {\n        stepStart(\"Migrating webhooks\");\n        const webhooksStart = Date.now();\n        const webhooksCount = await migrateWebhooks(\n          src,\n          dest,\n          (created, total) => {\n            progressUpdate(\"Webhooks\", created, total);\n          },\n        );\n        progressDone();\n        stepEndSuccess(\n          `${webhooksCount} migrated in ${Math.round((Date.now() - webhooksStart) / 1000)}s`,\n        );\n      }\n\n      // 6) Tags (build id map for rules)\n      let tagIdMap = new Map<string, string>();\n      if (!opts.excludeTags) {\n        stepStart(\"Ensuring tags on destination\");\n        const tagsStart = Date.now();\n        const res = await migrateTags(src, dest, (ensured, total) => {\n          progressUpdate(\"Tags\", ensured, total);\n        });\n        tagIdMap = res.idMap;\n        progressDone();\n        stepEndSuccess(\n          `${res.count} ensured in ${Math.round((Date.now() - tagsStart) / 1000)}s`,\n        );\n      }\n\n      // 7) Rules (requires tag/list/feed id maps)\n      if (\n        !opts.excludeRules &&\n        !opts.excludeLists &&\n        !opts.excludeFeeds &&\n        !opts.excludeTags\n      ) {\n        stepStart(\"Migrating rule engine rules\");\n        const rulesStart = Date.now();\n        const rulesCount = await migrateRules(\n          src,\n          dest,\n          { tagIdMap, listIdMap, feedIdMap },\n          (created, total) => {\n            progressUpdate(\"Rules\", created, total);\n          },\n        );\n        progressDone();\n        stepEndSuccess(\n          `${rulesCount} migrated in ${Math.round((Date.now() - rulesStart) / 1000)}s`,\n        );\n      }\n\n      // 8) Bookmarks (with list membership + tags)\n      let bookmarkListsMap = new Map<string, string[]>();\n      if (!opts.excludeLists && !opts.excludeBookmarks) {\n        stepStart(\"Building list membership for bookmarks\");\n        const blmStart = Date.now();\n        const res = await buildBookmarkListMembership(\n          src,\n          lists,\n          (processed, total) => {\n            progressUpdate(\"Scanning lists\", processed, total);\n          },\n        );\n        bookmarkListsMap = res.bookmarkListsMap;\n        progressDone();\n        stepEndSuccess(\n          `${res.scannedLists} lists scanned in ${Math.round((Date.now() - blmStart) / 1000)}s`,\n        );\n      }\n      if (!opts.excludeBookmarks) {\n        stepStart(\"Migrating bookmarks\");\n        const bmStart = Date.now();\n        const res = await migrateBookmarks(src, dest, {\n          pageSize: Number(opts.batchSize) || 50,\n          listIdMap,\n          bookmarkListsMap,\n          total: totalBookmarks,\n          onProgress: (migrated, skipped, total) => {\n            const suffix =\n              skipped > 0 ? `(skipped ${skipped} assets)` : undefined;\n            progressUpdate(\"Bookmarks\", migrated, total, suffix);\n          },\n          srcServer: globals.serverAddr,\n          srcApiKey: globals.apiKey,\n          destServer: opts.destServer,\n          destApiKey: opts.destApiKey,\n          excludeAssets: !!opts.excludeAssets,\n          excludeLists: !!opts.excludeLists,\n        });\n        progressDone();\n        stepEndSuccess(\n          `${res.migrated} migrated${res.skippedAssets ? `, ${res.skippedAssets} skipped` : \"\"} in ${Math.round((Date.now() - bmStart) / 1000)}s`,\n        );\n      }\n\n      printStatusMessage(true, \"Migration completed successfully\");\n    } catch (error) {\n      stepEndFail();\n      printErrorMessageWithReason(\"Migration failed\", error as object);\n    }\n  });\n\nasync function migrateUserSettings(\n  src: ReturnType<typeof getAPIClientFor>,\n  dest: ReturnType<typeof getAPIClientFor>,\n) {\n  try {\n    const settings = await src.users.settings.query();\n    await dest.users.updateSettings.mutate(settings);\n  } catch (error) {\n    printErrorMessageWithReason(\n      \"Failed migrating user settings\",\n      error as object,\n    );\n    throw error;\n  }\n}\n\nasync function migrateLists(\n  src: ReturnType<typeof getAPIClientFor>,\n  dest: ReturnType<typeof getAPIClientFor>,\n  onProgress?: (created: number, alreadyExists: number, total: number) => void,\n) {\n  try {\n    const { lists } = await src.lists.list.query();\n    const destListsResp = await dest.lists.list.query();\n    const destLists = destListsResp.lists.slice();\n\n    // Create lists in parent-first order\n    const remaining = new Map<string, ZBookmarkList>(\n      lists.map((l) => [l.id, l]),\n    );\n    const created = new Map<string, string>(); // srcId -> destId\n    let createdCount = 0;\n    let alreadyExistsCount = 0;\n    let progress = true;\n\n    while (remaining.size > 0 && progress) {\n      progress = false;\n      for (const [id, l] of Array.from(remaining.entries())) {\n        const parentOk = !l.parentId || created.has(l.parentId);\n        if (!parentOk) continue;\n        const parentDestId = l.parentId ? created.get(l.parentId)! : undefined;\n\n        // Try to find an existing destination list with the same properties (including parent)\n        const match = destLists.find(\n          (dl) =>\n            dl.name === l.name &&\n            dl.icon === l.icon &&\n            (dl.description ?? null) === (l.description ?? null) &&\n            dl.type === l.type &&\n            (dl.query ?? null) === (l.query ?? null) &&\n            (dl.parentId ?? undefined) === (parentDestId ?? undefined),\n        );\n\n        if (match) {\n          created.set(id, match.id);\n          // Align public flag if required (best-effort)\n          if (typeof l.public === \"boolean\" && match.public !== l.public) {\n            try {\n              await dest.lists.edit.mutate({\n                listId: match.id,\n                public: l.public,\n              });\n            } catch {\n              // ignore failures\n            }\n          }\n          remaining.delete(id);\n          progress = true;\n          alreadyExistsCount++;\n          onProgress?.(createdCount, alreadyExistsCount, lists.length);\n        } else {\n          const createdList = await dest.lists.create.mutate({\n            name: l.name,\n            description: l.description ?? undefined,\n            icon: l.icon,\n            type: l.type,\n            query: l.query ?? undefined,\n            parentId: parentDestId,\n          });\n          // Apply visibility if needed\n          if (typeof l.public === \"boolean\") {\n            try {\n              await dest.lists.edit.mutate({\n                listId: createdList.id,\n                public: l.public,\n              });\n            } catch {\n              // ignore failures\n            }\n          }\n          // Make newly created list available for subsequent matches\n          destLists.push(createdList);\n\n          created.set(id, createdList.id);\n          remaining.delete(id);\n          progress = true;\n          createdCount++;\n          onProgress?.(createdCount, alreadyExistsCount, lists.length);\n        }\n      }\n    }\n\n    if (remaining.size > 0) {\n      throw new Error(\n        \"Could not resolve list hierarchy due to missing parents\",\n      );\n    }\n\n    return { lists, listIdMap: created, createdCount };\n  } catch (error) {\n    printErrorMessageWithReason(\"Failed migrating lists\", error as object);\n    throw error;\n  }\n}\n\nasync function migrateFeeds(\n  src: ReturnType<typeof getAPIClientFor>,\n  dest: ReturnType<typeof getAPIClientFor>,\n  onProgress?: (created: number, total: number) => void,\n) {\n  try {\n    const { feeds } = await src.feeds.list.query();\n    const idMap = new Map<string, string>();\n    let created = 0;\n    for (const f of feeds) {\n      const nf = await dest.feeds.create.mutate({\n        name: f.name,\n        url: f.url,\n        enabled: f.enabled,\n      });\n      idMap.set(f.id, nf.id);\n      created++;\n      onProgress?.(created, feeds.length);\n    }\n    return { idMap, count: feeds.length };\n  } catch (error) {\n    printErrorMessageWithReason(\"Failed migrating feeds\", error as object);\n    throw error;\n  }\n}\n\nasync function migratePrompts(\n  src: ReturnType<typeof getAPIClientFor>,\n  dest: ReturnType<typeof getAPIClientFor>,\n  onProgress?: (created: number, total: number) => void,\n) {\n  try {\n    const prompts: ZPrompt[] = await src.prompts.list.query();\n    let created = 0;\n    for (const p of prompts) {\n      const np = await dest.prompts.create.mutate({\n        text: p.text,\n        appliesTo: p.appliesTo,\n      });\n      if (p.enabled !== np.enabled) {\n        await dest.prompts.update.mutate({\n          promptId: np.id,\n          enabled: p.enabled,\n        });\n      }\n      created++;\n      onProgress?.(created, prompts.length);\n    }\n    // keep output concise; step wrapper prints totals\n    return created;\n  } catch (error) {\n    printErrorMessageWithReason(\"Failed migrating AI prompts\", error as object);\n    throw error;\n  }\n}\n\nasync function migrateWebhooks(\n  src: ReturnType<typeof getAPIClientFor>,\n  dest: ReturnType<typeof getAPIClientFor>,\n  onProgress?: (created: number, total: number) => void,\n) {\n  try {\n    const { webhooks } = await src.webhooks.list.query();\n    onProgress?.(0, webhooks.length);\n    let created = 0;\n    for (const w of webhooks) {\n      await dest.webhooks.create.mutate({ url: w.url, events: w.events });\n      created++;\n      onProgress?.(created, webhooks.length);\n    }\n    return created;\n  } catch (error) {\n    printErrorMessageWithReason(\"Failed migrating webhooks\", error as object);\n    throw error;\n  }\n}\n\nasync function migrateTags(\n  src: ReturnType<typeof getAPIClientFor>,\n  dest: ReturnType<typeof getAPIClientFor>,\n  onProgress?: (ensured: number, total: number) => void,\n) {\n  try {\n    const { tags: srcTags } = await src.tags.list.query({});\n    // Create tags by name; ignore if exist\n    let ensured = 0;\n    for (const t of srcTags) {\n      try {\n        await dest.tags.create.mutate({ name: t.name });\n      } catch {\n        // Ignore duplicate errors\n      }\n      ensured++;\n      onProgress?.(ensured, srcTags.length);\n    }\n    // Build id map using destination's current tags\n    const { tags: destTags } = await dest.tags.list.query({});\n    const nameToDestId = destTags.reduce<Record<string, string>>((acc, t) => {\n      acc[t.name] = t.id;\n      return acc;\n    }, {});\n    const idMap = new Map<string, string>();\n    srcTags.forEach((t: ZGetTagResponse) => {\n      const destId = nameToDestId[t.name];\n      if (destId) idMap.set(t.id, destId);\n    });\n    return { idMap, count: srcTags.length };\n  } catch (error) {\n    printErrorMessageWithReason(\"Failed migrating tags\", error as object);\n    throw error;\n  }\n}\n\ninterface RuleIdMaps {\n  tagIdMap: Map<string, string>;\n  listIdMap: Map<string, string>;\n  feedIdMap: Map<string, string>;\n}\n\nasync function migrateRules(\n  src: ReturnType<typeof getAPIClientFor>,\n  dest: ReturnType<typeof getAPIClientFor>,\n  maps: RuleIdMaps,\n  onProgress?: (created: number, total: number) => void,\n) {\n  try {\n    const { rules } = await src.rules.list.query();\n    let migrated = 0;\n    for (const r of rules) {\n      try {\n        const nr = remapRuleIds(r, maps);\n        await dest.rules.create.mutate(nr);\n        migrated++;\n        onProgress?.(migrated, rules.length);\n      } catch (e) {\n        printErrorMessageWithReason(\n          `Failed migrating rule \"${r.id}\"`,\n          e as object,\n        );\n      }\n    }\n    return migrated;\n  } catch (error) {\n    printErrorMessageWithReason(\n      \"Failed migrating rule engine rules\",\n      error as object,\n    );\n    throw error;\n  }\n}\n\nfunction remapRuleIds(\n  rule: RuleEngineRule,\n  maps: RuleIdMaps,\n): Omit<RuleEngineRule, \"id\"> {\n  const mapTag = (id: string) => maps.tagIdMap.get(id) ?? id;\n  const mapList = (id: string) => maps.listIdMap.get(id) ?? id;\n  const mapFeed = (id: string) => maps.feedIdMap.get(id) ?? id;\n\n  const mapCondition = (\n    c: RuleEngineRule[\"condition\"],\n  ): RuleEngineRule[\"condition\"] => {\n    switch (c.type) {\n      case \"hasTag\":\n        return { ...c, tagId: mapTag(c.tagId) };\n      case \"importedFromFeed\":\n        return { ...c, feedId: mapFeed(c.feedId) };\n      case \"and\":\n      case \"or\":\n        return { ...c, conditions: c.conditions.map(mapCondition) };\n      default:\n        return c;\n    }\n  };\n\n  const mapEvent = (e: RuleEngineRule[\"event\"]): RuleEngineRule[\"event\"] => {\n    switch (e.type) {\n      case \"tagAdded\":\n      case \"tagRemoved\":\n        return { ...e, tagId: mapTag(e.tagId) };\n      case \"addedToList\":\n      case \"removedFromList\":\n        return { ...e, listId: mapList(e.listId) };\n      default:\n        return e;\n    }\n  };\n\n  const mapAction = (\n    a: RuleEngineRule[\"actions\"][number],\n  ): RuleEngineRule[\"actions\"][number] => {\n    switch (a.type) {\n      case \"addTag\":\n      case \"removeTag\":\n        return { ...a, tagId: mapTag(a.tagId) };\n      case \"addToList\":\n      case \"removeFromList\":\n        return { ...a, listId: mapList(a.listId) };\n      default:\n        return a;\n    }\n  };\n\n  return {\n    name: rule.name,\n    description: rule.description,\n    enabled: rule.enabled,\n    event: mapEvent(rule.event),\n    condition: mapCondition(rule.condition),\n    actions: rule.actions.map(mapAction),\n  };\n}\n\nasync function buildBookmarkListMembership(\n  src: ReturnType<typeof getAPIClientFor>,\n  srcLists: ZBookmarkList[],\n  onProgress?: (processedLists: number, totalLists: number) => void,\n) {\n  // Build mapping: oldBookmarkId -> [srcListIds]\n  const bookmarkToLists = new Map<string, string[]>();\n  let processed = 0;\n  for (const l of srcLists) {\n    if (l.type != \"manual\") {\n      processed++;\n      onProgress?.(processed, srcLists.length);\n      continue;\n    }\n    let cursor: ZCursor | null = null;\n    do {\n      const resp = await src.bookmarks.getBookmarks.query({\n        listId: l.id,\n        cursor,\n        limit: MAX_NUM_BOOKMARKS_PER_PAGE,\n        includeContent: false,\n      });\n      for (const b of resp.bookmarks) {\n        if (!bookmarkToLists.has(b.id)) bookmarkToLists.set(b.id, []);\n        bookmarkToLists.get(b.id)!.push(l.id);\n      }\n      cursor = resp.nextCursor;\n    } while (cursor);\n    processed++;\n    onProgress?.(processed, srcLists.length);\n  }\n  return { bookmarkListsMap: bookmarkToLists, scannedLists: processed };\n}\n\nasync function migrateBookmarks(\n  src: ReturnType<typeof getAPIClientFor>,\n  dest: ReturnType<typeof getAPIClientFor>,\n  opts: {\n    pageSize: number;\n    listIdMap: Map<string, string>;\n    bookmarkListsMap: Map<string, string[]>; // srcBookmarkId -> srcListIds\n    total?: number;\n    onProgress?: (\n      migrated: number,\n      skippedAssets: number,\n      total?: number,\n    ) => void;\n    srcServer: string;\n    srcApiKey: string;\n    destServer: string;\n    destApiKey: string;\n    excludeAssets: boolean;\n    excludeLists: boolean;\n  },\n) {\n  let cursor: ZCursor | null = null;\n  let migrated = 0;\n  let skippedAssets = 0;\n  while (true) {\n    const resp = await src.bookmarks.getBookmarks.query({\n      limit: opts.pageSize,\n      cursor,\n      includeContent: false,\n    });\n    for (const b of resp.bookmarks as ZBookmark[]) {\n      // Create bookmark on destination\n      try {\n        const common = {\n          title: b.title ?? undefined,\n          archived: b.archived,\n          favourited: b.favourited,\n          note: b.note ?? undefined,\n          summary: b.summary ?? undefined,\n          createdAt: b.createdAt,\n          crawlPriority: \"low\" as const,\n          source: b.source === null ? undefined : b.source,\n        };\n        let createdId: string | null = null;\n        switch (b.content.type) {\n          case BookmarkTypes.LINK: {\n            const nb = await dest.bookmarks.createBookmark.mutate({\n              ...common,\n              type: BookmarkTypes.LINK,\n              url: b.content.url,\n            });\n            createdId = nb.id;\n            break;\n          }\n          case BookmarkTypes.TEXT: {\n            const nb = await dest.bookmarks.createBookmark.mutate({\n              ...common,\n              type: BookmarkTypes.TEXT,\n              text: b.content.text,\n              sourceUrl: b.content.sourceUrl ?? undefined,\n            });\n            createdId = nb.id;\n            break;\n          }\n          case BookmarkTypes.ASSET: {\n            if (opts.excludeAssets) {\n              // Skip migrating asset bookmarks when excluded\n              skippedAssets++;\n              continue;\n            }\n            // Download from source and re-upload to destination\n            try {\n              const downloadResp = await fetch(\n                `${opts.srcServer}/api/assets/${b.content.assetId}`,\n                {\n                  headers: { authorization: `Bearer ${opts.srcApiKey}` },\n                },\n              );\n              if (!downloadResp.ok) {\n                throw new Error(\n                  `Failed to download asset: ${downloadResp.status} ${downloadResp.statusText}`,\n                );\n              }\n              const srcContentType =\n                downloadResp.headers.get(\"content-type\") ??\n                \"application/octet-stream\";\n              const arrayBuf = await downloadResp.arrayBuffer();\n              const blob = new Blob([arrayBuf], { type: srcContentType });\n              const fileName = b.content.fileName ?? `asset-${b.id}`;\n\n              const form = new FormData();\n              form.append(\"file\", blob, fileName);\n              const uploadResp = await fetch(`${opts.destServer}/api/assets`, {\n                method: \"POST\",\n                headers: { authorization: `Bearer ${opts.destApiKey}` },\n                body: form,\n              });\n              if (!uploadResp.ok) {\n                throw new Error(\n                  `Failed to upload asset: ${uploadResp.status} ${uploadResp.statusText}`,\n                );\n              }\n              const uploaded: {\n                assetId: string;\n                contentType: string | null;\n                size: number | null;\n                fileName: string | null;\n              } = await uploadResp.json();\n\n              const nb = await dest.bookmarks.createBookmark.mutate({\n                ...common,\n                type: BookmarkTypes.ASSET,\n                assetType: b.content.assetType,\n                assetId: uploaded.assetId,\n                fileName: uploaded.fileName ?? fileName,\n                sourceUrl: b.content.sourceUrl ?? undefined,\n              });\n              createdId = nb.id;\n              break;\n            } catch {\n              skippedAssets++;\n              // Continue with next bookmark after reporting error\n              // Optional: print concise error per asset\n              // printErrorMessageWithReason(`Failed migrating asset for bookmark \"${b.id}\"`, e as object);\n              continue;\n            }\n          }\n          default: {\n            continue;\n          }\n        }\n\n        // Attach tags by name\n        if (b.tags.length > 0) {\n          await dest.bookmarks.updateTags.mutate({\n            bookmarkId: createdId!,\n            attach: b.tags.map((t) => ({\n              tagName: t.name,\n              attachedBy: t.attachedBy,\n            })),\n            detach: [],\n          });\n        }\n\n        // Add to lists (map src -> dest list ids)\n        if (!opts.excludeLists) {\n          const srcListIds = opts.bookmarkListsMap.get(b.id) ?? [];\n          for (const srcListId of srcListIds) {\n            const destListId = opts.listIdMap.get(srcListId);\n            if (destListId) {\n              await dest.lists.addToList.mutate({\n                listId: destListId,\n                bookmarkId: createdId!,\n              });\n            }\n          }\n        }\n        migrated++;\n        opts.onProgress?.(migrated, skippedAssets, opts.total);\n      } catch (error) {\n        printErrorMessageWithReason(\n          `Failed migrating bookmark \"${b.id}\"`,\n          error as object,\n        );\n      }\n    }\n    cursor = resp.nextCursor;\n    if (!cursor) break;\n    // Update progress after each page\n    opts.onProgress?.(migrated, skippedAssets, opts.total);\n  }\n  return { migrated, skippedAssets };\n}\n"
  },
  {
    "path": "apps/cli/src/commands/tags.ts",
    "content": "import { getGlobalOptions } from \"@/lib/globals\";\nimport {\n  printError,\n  printErrorMessageWithReason,\n  printObject,\n  printSuccess,\n} from \"@/lib/output\";\nimport { getAPIClient } from \"@/lib/trpc\";\nimport { Command } from \"@commander-js/extra-typings\";\nimport { getBorderCharacters, table } from \"table\";\n\nexport const tagsCmd = new Command()\n  .name(\"tags\")\n  .description(\"manipulating tags\");\n\ntagsCmd\n  .command(\"list\")\n  .description(\"lists all tags\")\n  .action(async () => {\n    const api = getAPIClient();\n\n    try {\n      const tags = (await api.tags.list.query({})).tags;\n      tags.sort((a, b) => b.numBookmarks - a.numBookmarks);\n      if (getGlobalOptions().json) {\n        printObject(tags);\n      } else {\n        const data: string[][] = [[\"Id\", \"Name\", \"Num bookmarks\"]];\n\n        tags.forEach((tag) => {\n          data.push([tag.id, tag.name, tag.numBookmarks.toString()]);\n        });\n        console.log(\n          table(data, {\n            border: getBorderCharacters(\"ramac\"),\n            singleLine: true,\n          }),\n        );\n      }\n    } catch (error) {\n      printErrorMessageWithReason(\"Failed to list all tags\", error as object);\n    }\n  });\n\ntagsCmd\n  .command(\"delete\")\n  .description(\"delete a tag\")\n  .argument(\"<id>\", \"the id of the tag\")\n  .action(async (id) => {\n    const api = getAPIClient();\n\n    await api.tags.delete\n      .mutate({\n        tagId: id,\n      })\n      .then(printSuccess(`Successfully deleted the tag with the id \"${id}\"`))\n      .catch(printError(`Failed to delete the tag with the id \"${id}\"`));\n  });\n"
  },
  {
    "path": "apps/cli/src/commands/whoami.ts",
    "content": "import { printError, printObject } from \"@/lib/output\";\nimport { getAPIClient } from \"@/lib/trpc\";\nimport { Command } from \"@commander-js/extra-typings\";\n\nexport const whoamiCmd = new Command()\n  .name(\"whoami\")\n  .description(\"returns info about the owner of this API key\")\n  .action(async () => {\n    await getAPIClient()\n      .users.whoami.query()\n      .then(printObject)\n      .catch(\n        printError(\n          `Unable to fetch information about the owner of this API key`,\n        ),\n      );\n  });\n"
  },
  {
    "path": "apps/cli/src/commands/wipe.ts",
    "content": "import { stdin as input, stdout as output } from \"node:process\";\nimport readline from \"node:readline/promises\";\nimport { getGlobalOptions } from \"@/lib/globals\";\nimport { printErrorMessageWithReason, printStatusMessage } from \"@/lib/output\";\nimport { getAPIClient } from \"@/lib/trpc\";\nimport { Command } from \"@commander-js/extra-typings\";\nimport chalk from \"chalk\";\n\nimport { MAX_NUM_BOOKMARKS_PER_PAGE } from \"@karakeep/shared/types/bookmarks\";\nimport { ZCursor } from \"@karakeep/shared/types/pagination\";\n\nconst OK = chalk.green(\"✓\");\nconst FAIL = chalk.red(\"✗\");\nconst DOTS = chalk.gray(\"…\");\n\nfunction line(msg: string) {\n  console.log(msg);\n}\n\nfunction stepStart(title: string) {\n  console.log(`${chalk.cyan(title)} ${DOTS}`);\n}\n\nfunction stepEndSuccess(extra?: string) {\n  process.stdout.write(`${OK}${extra ? \" \" + chalk.gray(extra) : \"\"}\\n`);\n}\n\nfunction stepEndFail(extra?: string) {\n  process.stdout.write(`${FAIL}${extra ? \" \" + chalk.gray(extra) : \"\"}\\n`);\n}\n\nfunction progressUpdate(\n  prefix: string,\n  current: number,\n  total?: number,\n  suffix?: string,\n) {\n  const totalPart = total != null ? `/${total}` : \"\";\n  const text = `${chalk.gray(prefix)} ${current}${totalPart}${suffix ? \" \" + chalk.gray(suffix) : \"\"}`;\n  if (process.stdout.isTTY) {\n    try {\n      process.stdout.clearLine(0);\n      process.stdout.cursorTo(0);\n      process.stdout.write(text);\n      return;\n    } catch {\n      // ignore failures\n    }\n  }\n  console.log(text);\n}\n\nfunction progressDone() {\n  process.stdout.write(\"\\n\");\n}\n\nexport const wipeCmd = new Command()\n  .name(\"wipe\")\n  .description(\"wipe all data for the current user from the server\")\n  .option(\"-y, --yes\", \"skip confirmation prompt\")\n  .option(\"--exclude-lists\", \"exclude lists from deletion\")\n  .option(\"--exclude-ai-prompts\", \"exclude AI prompts from deletion\")\n  .option(\"--exclude-rules\", \"exclude rules from deletion\")\n  .option(\"--exclude-feeds\", \"exclude RSS feeds from deletion\")\n  .option(\"--exclude-webhooks\", \"exclude webhooks from deletion\")\n  .option(\"--exclude-bookmarks\", \"exclude bookmarks from deletion\")\n  .option(\"--exclude-tags\", \"exclude tags cleanup from deletion\")\n  .option(\"--exclude-user-settings\", \"exclude user settings (no-op)\")\n  .option(\n    \"--batch-size <n>\",\n    `number of bookmarks per page (max ${MAX_NUM_BOOKMARKS_PER_PAGE})`,\n    (v) => Math.min(Number(v || 50), MAX_NUM_BOOKMARKS_PER_PAGE),\n    50,\n  )\n  .action(async (opts) => {\n    const globals = getGlobalOptions();\n    const api = getAPIClient();\n\n    if (!opts.yes) {\n      const rl = readline.createInterface({ input, output });\n      const answer = (\n        await rl.question(\n          `This will permanently delete ALL your data on \"${globals.serverAddr}\". Proceed? (yes/no): `,\n        )\n      )\n        .trim()\n        .toLowerCase();\n      rl.close();\n      if (answer !== \"y\" && answer !== \"yes\") {\n        printStatusMessage(false, \"Wipe aborted by user\");\n        return;\n      }\n    }\n\n    try {\n      line(\"\");\n      line(`${chalk.bold(\"Karakeep Wipe\")}`);\n      line(`${chalk.gray(\"Server:\")} ${globals.serverAddr}`);\n      line(\"\");\n\n      // Pre-fetch stats for user feedback\n      let totalBookmarks: number | undefined = undefined;\n      try {\n        const stats = await api.users.stats.query();\n        totalBookmarks = stats.numBookmarks;\n      } catch {\n        // ignore stats errors; progress will show without total\n      }\n\n      // 1) Rules\n      if (!opts.excludeRules) {\n        stepStart(\"Deleting rule engine rules\");\n        const rulesStart = Date.now();\n        const rulesDeleted = await wipeRules(api, (deleted, total) => {\n          progressUpdate(\"Rules\", deleted, total);\n        });\n        progressDone();\n        stepEndSuccess(\n          `${rulesDeleted} deleted in ${Math.round((Date.now() - rulesStart) / 1000)}s`,\n        );\n      }\n\n      // 2) Feeds\n      if (!opts.excludeFeeds) {\n        stepStart(\"Deleting feeds\");\n        const feedsStart = Date.now();\n        const feedsDeleted = await wipeFeeds(api, (deleted, total) => {\n          progressUpdate(\"Feeds\", deleted, total);\n        });\n        progressDone();\n        stepEndSuccess(\n          `${feedsDeleted} deleted in ${Math.round((Date.now() - feedsStart) / 1000)}s`,\n        );\n      }\n\n      // 3) Webhooks\n      if (!opts.excludeWebhooks) {\n        stepStart(\"Deleting webhooks\");\n        const webhooksStart = Date.now();\n        const webhooksDeleted = await wipeWebhooks(api, (deleted, total) => {\n          progressUpdate(\"Webhooks\", deleted, total);\n        });\n        progressDone();\n        stepEndSuccess(\n          `${webhooksDeleted} deleted in ${Math.round((Date.now() - webhooksStart) / 1000)}s`,\n        );\n      }\n\n      // 4) Prompts\n      if (!opts.excludeAiPrompts) {\n        stepStart(\"Deleting AI prompts\");\n        const promptsStart = Date.now();\n        const promptsDeleted = await wipePrompts(api, (deleted, total) => {\n          progressUpdate(\"Prompts\", deleted, total);\n        });\n        progressDone();\n        stepEndSuccess(\n          `${promptsDeleted} deleted in ${Math.round((Date.now() - promptsStart) / 1000)}s`,\n        );\n      }\n\n      // 5) Bookmarks\n      if (!opts.excludeBookmarks) {\n        stepStart(\"Deleting bookmarks\");\n        const bmStart = Date.now();\n        const bookmarksDeleted = await wipeBookmarks(api, {\n          pageSize: Number(opts.batchSize) || 50,\n          total: totalBookmarks,\n          onProgress: (deleted, total) => {\n            progressUpdate(\"Bookmarks\", deleted, total);\n          },\n        });\n        progressDone();\n        stepEndSuccess(\n          `${bookmarksDeleted} deleted in ${Math.round((Date.now() - bmStart) / 1000)}s`,\n        );\n      }\n\n      // 6) Lists\n      if (!opts.excludeLists) {\n        stepStart(\"Deleting lists\");\n        const listsStart = Date.now();\n        const listsDeleted = await wipeLists(api, (deleted, total) => {\n          progressUpdate(\"Lists\", deleted, total);\n        });\n        progressDone();\n        stepEndSuccess(\n          `${listsDeleted} deleted in ${Math.round((Date.now() - listsStart) / 1000)}s`,\n        );\n      }\n\n      // 7) Tags (unused)\n      if (!opts.excludeTags) {\n        stepStart(\"Deleting unused tags\");\n        const tagsStart = Date.now();\n        const deletedTags = await wipeTags(api);\n        stepEndSuccess(\n          `${deletedTags} deleted in ${Math.round((Date.now() - tagsStart) / 1000)}s`,\n        );\n      }\n\n      printStatusMessage(true, \"Wipe completed successfully\");\n    } catch (error) {\n      stepEndFail();\n      printErrorMessageWithReason(\"Wipe failed\", error as object);\n    }\n  });\n\nasync function wipeRules(\n  api: ReturnType<typeof getAPIClient>,\n  onProgress?: (deleted: number, total: number) => void,\n) {\n  try {\n    const { rules } = await api.rules.list.query();\n    let deleted = 0;\n    for (const r of rules) {\n      try {\n        await api.rules.delete.mutate({ id: r.id });\n        deleted++;\n        onProgress?.(deleted, rules.length);\n      } catch (e) {\n        printErrorMessageWithReason(\n          `Failed deleting rule \"${r.id}\"`,\n          e as object,\n        );\n      }\n    }\n    return deleted;\n  } catch (error) {\n    printErrorMessageWithReason(\"Failed deleting rules\", error as object);\n    throw error;\n  }\n}\n\nasync function wipeFeeds(\n  api: ReturnType<typeof getAPIClient>,\n  onProgress?: (deleted: number, total: number) => void,\n) {\n  try {\n    const { feeds } = await api.feeds.list.query();\n    let deleted = 0;\n    for (const f of feeds) {\n      try {\n        await api.feeds.delete.mutate({ feedId: f.id });\n        deleted++;\n        onProgress?.(deleted, feeds.length);\n      } catch (e) {\n        printErrorMessageWithReason(\n          `Failed deleting feed \"${f.id}\"`,\n          e as object,\n        );\n      }\n    }\n    return deleted;\n  } catch (error) {\n    printErrorMessageWithReason(\"Failed deleting feeds\", error as object);\n    throw error;\n  }\n}\n\nasync function wipeWebhooks(\n  api: ReturnType<typeof getAPIClient>,\n  onProgress?: (deleted: number, total: number) => void,\n) {\n  try {\n    const { webhooks } = await api.webhooks.list.query();\n    let deleted = 0;\n    for (const w of webhooks) {\n      try {\n        await api.webhooks.delete.mutate({ webhookId: w.id });\n        deleted++;\n        onProgress?.(deleted, webhooks.length);\n      } catch (e) {\n        printErrorMessageWithReason(\n          `Failed deleting webhook \"${w.id}\"`,\n          e as object,\n        );\n      }\n    }\n    return deleted;\n  } catch (error) {\n    printErrorMessageWithReason(\"Failed deleting webhooks\", error as object);\n    throw error;\n  }\n}\n\nasync function wipePrompts(\n  api: ReturnType<typeof getAPIClient>,\n  onProgress?: (deleted: number, total: number) => void,\n) {\n  try {\n    const prompts = await api.prompts.list.query();\n    let deleted = 0;\n    for (const p of prompts) {\n      try {\n        await api.prompts.delete.mutate({ promptId: p.id });\n        deleted++;\n        onProgress?.(deleted, prompts.length);\n      } catch (e) {\n        printErrorMessageWithReason(\n          `Failed deleting prompt \"${p.id}\"`,\n          e as object,\n        );\n      }\n    }\n    return deleted;\n  } catch (error) {\n    printErrorMessageWithReason(\"Failed deleting AI prompts\", error as object);\n    throw error;\n  }\n}\n\nasync function wipeBookmarks(\n  api: ReturnType<typeof getAPIClient>,\n  opts: {\n    pageSize: number;\n    total?: number;\n    onProgress?: (deleted: number, total?: number) => void;\n  },\n) {\n  try {\n    let cursor: ZCursor | null | undefined = undefined;\n    let deleted = 0;\n    while (true) {\n      const resp = await api.bookmarks.getBookmarks.query({\n        limit: opts.pageSize,\n        cursor,\n        useCursorV2: true,\n        includeContent: false,\n      });\n      for (const b of resp.bookmarks) {\n        try {\n          await api.bookmarks.deleteBookmark.mutate({ bookmarkId: b.id });\n          deleted++;\n          opts.onProgress?.(deleted, opts.total);\n        } catch (e) {\n          printErrorMessageWithReason(\n            `Failed deleting bookmark \"${b.id}\"`,\n            e as object,\n          );\n        }\n      }\n      cursor = resp.nextCursor;\n      if (!cursor) break;\n      opts.onProgress?.(deleted, opts.total);\n    }\n    return deleted;\n  } catch (error) {\n    printErrorMessageWithReason(\"Failed deleting bookmarks\", error as object);\n    throw error;\n  }\n}\n\nasync function wipeLists(\n  api: ReturnType<typeof getAPIClient>,\n  onProgress?: (deleted: number, total: number) => void,\n) {\n  try {\n    const { lists } = await api.lists.list.query();\n    // Delete child lists first (deepest first)\n    const depthCache = new Map<string, number>();\n    const byId = new Map(lists.map((l) => [l.id, l]));\n    const getDepth = (id: string): number => {\n      const cached = depthCache.get(id);\n      if (cached != null) return cached;\n      let d = 0;\n      let cur = byId.get(id);\n      const visited = new Set<string>();\n      while (cur?.parentId) {\n        if (visited.has(cur.parentId)) break; // cycle guard\n        visited.add(cur.parentId);\n        d++;\n        cur = byId.get(cur.parentId);\n      }\n      depthCache.set(id, d);\n      return d;\n    };\n    const ordered = lists\n      .slice()\n      .sort((a, b) => getDepth(b.id) - getDepth(a.id));\n    let deleted = 0;\n    for (const l of ordered) {\n      try {\n        await api.lists.delete.mutate({ listId: l.id });\n        deleted++;\n        onProgress?.(deleted, lists.length);\n      } catch (e) {\n        printErrorMessageWithReason(\n          `Failed deleting list \"${l.id}\"`,\n          e as object,\n        );\n      }\n    }\n    return deleted;\n  } catch (error) {\n    printErrorMessageWithReason(\"Failed deleting lists\", error as object);\n    throw error;\n  }\n}\n\nasync function wipeTags(api: ReturnType<typeof getAPIClient>) {\n  try {\n    const res = await api.tags.deleteUnused.mutate();\n    return res.deletedTags;\n  } catch (error) {\n    printErrorMessageWithReason(\"Failed deleting tags\", error as object);\n    throw error;\n  }\n}\n"
  },
  {
    "path": "apps/cli/src/index.ts",
    "content": "import { adminCmd } from \"@/commands/admin\";\nimport { bookmarkCmd } from \"@/commands/bookmarks\";\nimport { dumpCmd } from \"@/commands/dump\";\nimport { listsCmd } from \"@/commands/lists\";\nimport { migrateCmd } from \"@/commands/migrate\";\nimport { tagsCmd } from \"@/commands/tags\";\nimport { whoamiCmd } from \"@/commands/whoami\";\nimport { wipeCmd } from \"@/commands/wipe\";\nimport { setGlobalOptions } from \"@/lib/globals\";\nimport { Command, Option } from \"@commander-js/extra-typings\";\n\nconst program = new Command()\n  .name(\"karakeep\")\n  .description(\"A CLI interface to interact with the karakeep api\")\n  .addOption(\n    new Option(\"--api-key <key>\", \"the API key to interact with the API\")\n      .makeOptionMandatory(true)\n      .env(\"KARAKEEP_API_KEY\"),\n  )\n  .addOption(\n    new Option(\n      \"--server-addr <addr>\",\n      \"the address of the server to connect to\",\n    )\n      .makeOptionMandatory(true)\n      .env(\"KARAKEEP_SERVER_ADDR\"),\n  )\n  .addOption(new Option(\"--json\", \"to output the result as JSON\"))\n  .version(\n    import.meta.env && \"CLI_VERSION\" in import.meta.env\n      ? import.meta.env.CLI_VERSION\n      : \"0.0.0\",\n  );\n\nprogram.addCommand(adminCmd);\nprogram.addCommand(bookmarkCmd);\nprogram.addCommand(listsCmd);\nprogram.addCommand(tagsCmd);\nprogram.addCommand(whoamiCmd);\nprogram.addCommand(migrateCmd);\nprogram.addCommand(wipeCmd);\nprogram.addCommand(dumpCmd);\n\nsetGlobalOptions(program.opts());\n\nprogram.parse();\n"
  },
  {
    "path": "apps/cli/src/lib/globals.ts",
    "content": "export interface GlobalOptions {\n  apiKey: string;\n  serverAddr: string;\n  json?: true;\n}\n\nexport let globalOpts: GlobalOptions | undefined = undefined;\n\nexport function setGlobalOptions(opts: GlobalOptions) {\n  globalOpts = opts;\n}\n\nexport function getGlobalOptions() {\n  if (!globalOpts) {\n    throw new Error(\"Global options are not initalized yet\");\n  }\n  return globalOpts;\n}\n"
  },
  {
    "path": "apps/cli/src/lib/output.ts",
    "content": "import { InspectOptions } from \"util\";\nimport chalk from \"chalk\";\n\nimport { getGlobalOptions } from \"./globals\";\n\n/**\n * Prints an object either in a nicely formatted way or as JSON (depending on the command flag --json)\n *\n * @param output\n */\nexport function printObject(\n  output: object,\n  extraOptions?: InspectOptions,\n): void {\n  if (getGlobalOptions().json) {\n    console.log(JSON.stringify(output, undefined, 4));\n  } else {\n    console.dir(output, extraOptions);\n  }\n}\n\n/**\n * Used to output a status (success/error) and a message either as string or as JSON (depending on the command flag --json)\n *\n * @param success if the message is a successful message or an error\n * @param output the message to output\n */\nexport function printStatusMessage(success: boolean, message: unknown): void {\n  const status = success ? \"Success\" : \"Error\";\n  const colorFunction = success ? chalk.green : chalk.red;\n  console.error(colorFunction(`${status}: ${message}`));\n}\n\n/**\n * @param message The message that will be printed as a successful message\n * @returns a function that can be used in a Promise on success\n */\nexport function printSuccess(message: string) {\n  return () => {\n    printStatusMessage(true, message);\n  };\n}\n\n/**\n * @param message The message that will be printed as an error message\n * @returns a function that can be used in a Promise on rejection\n */\nexport function printError(message: string) {\n  return (error: object) => {\n    printErrorMessageWithReason(message, error);\n  };\n}\n\n/**\n * @param message The message that will be printed as an error message\n * @param error an error object with the reason for the error\n */\nexport function printErrorMessageWithReason(message: string, error: object) {\n  const errorMessage = \"message\" in error ? error.message : error;\n  printStatusMessage(false, `${message}. Reason: ${errorMessage}`);\n}\n"
  },
  {
    "path": "apps/cli/src/lib/trpc.ts",
    "content": "import { getGlobalOptions } from \"@/lib/globals\";\nimport { createTRPCClient, httpBatchLink } from \"@trpc/client\";\nimport superjson from \"superjson\";\n\nimport type { AppRouter } from \"@karakeep/trpc/routers/_app\";\nimport { TRPC_MAX_URL_LENGTH_INTERNAL } from \"@karakeep/shared/trpc\";\n\nexport function getAPIClient() {\n  const globals = getGlobalOptions();\n  return createTRPCClient<AppRouter>({\n    links: [\n      httpBatchLink({\n        url: `${globals.serverAddr}/api/trpc`,\n        maxURLLength: TRPC_MAX_URL_LENGTH_INTERNAL,\n        transformer: superjson,\n        headers() {\n          return {\n            authorization: `Bearer ${globals.apiKey}`,\n          };\n        },\n      }),\n    ],\n  });\n}\n\nexport function getAPIClientFor(opts: { serverAddr: string; apiKey: string }) {\n  return createTRPCClient<AppRouter>({\n    links: [\n      httpBatchLink({\n        url: `${opts.serverAddr}/api/trpc`,\n        maxURLLength: TRPC_MAX_URL_LENGTH_INTERNAL,\n        transformer: superjson,\n        headers() {\n          return {\n            authorization: `Bearer ${opts.apiKey}`,\n          };\n        },\n      }),\n    ],\n  });\n}\n"
  },
  {
    "path": "apps/cli/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n\ninterface ImportMetaEnv {\n  readonly CLI_VERSION: string;\n}\n\ninterface ImportMeta {\n  readonly env: ImportMetaEnv;\n}\n"
  },
  {
    "path": "apps/cli/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@karakeep/tsconfig/node.json\",\n  \"include\": [\"src\", \"vite.config.mts\"],\n  \"exclude\": [\"node_modules\", \"dist\"],\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"tsBuildInfoFile\": \"node_modules/.cache/tsbuildinfo.json\",\n    \"strictNullChecks\": true,\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    },\n    \"types\": [\"vite/client\"]\n  }\n}\n"
  },
  {
    "path": "apps/cli/vite.config.mts",
    "content": "// This file is shamelessly copied from immich's CLI vite config\n// https://github.com/immich-app/immich/blob/main/cli/vite.config.ts\nimport { defineConfig } from \"vite\";\nimport tsconfigPaths from \"vite-tsconfig-paths\";\n\nexport default defineConfig({\n  build: {\n    rollupOptions: {\n      input: \"src/index.ts\",\n      output: {\n        dir: \"dist\",\n        format: \"es\",\n        entryFileNames: \"index.mjs\",\n        banner: \"#!/usr/bin/env node\",\n      },\n      external: [\"node:fs\", \"node:path\", \"node:url\", \"node:process\"],\n    },\n    ssr: true,\n    target: \"node18\",\n  },\n  ssr: {\n    // bundle everything except for Node built-ins\n    noExternal: /^(?!node:).*$/,\n  },\n  plugins: [tsconfigPaths()],\n  define: {\n    \"import.meta.env.CLI_VERSION\": JSON.stringify(\n      process.env.npm_package_version,\n    ),\n  },\n  esbuild: {\n    // Handle shebang in source files\n    banner: \"\",\n  },\n});\n"
  },
  {
    "path": "apps/landing/.oxlintrc.json",
    "content": "{\n  \"$schema\": \"../../node_modules/oxlint/configuration_schema.json\",\n  \"extends\": [\n    \"../../tooling/oxlint/oxlint-base.json\",\n    \"../../tooling/oxlint/oxlint-react.json\"\n  ],\n  \"env\": {\n    \"builtin\": true,\n    \"commonjs\": true,\n    \"browser\": true,\n    \"es2022\": true,\n    \"node\": true\n  },\n  \"globals\": {\n    \"React\": \"writeable\"\n  },\n  \"settings\": {\n    \"react\": {\n      \"version\": \"19\"\n    }\n  },\n  \"ignorePatterns\": [\n    \"**/*.config.js\",\n    \"**/*.config.cjs\",\n    \"**/.eslintrc.cjs\",\n    \".next\",\n    \"dist\",\n    \"build\",\n    \"pnpm-lock.yaml\"\n  ]\n}\n"
  },
  {
    "path": "apps/landing/README.md",
    "content": "This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).\n\n## Getting Started\n\nFirst, run the development server:\n\n```bash\nnpm run dev\n# or\nyarn dev\n# or\npnpm dev\n# or\nbun dev\n```\n\nOpen [http://localhost:3000](http://localhost:3000) with your browser to see the result.\n\nYou can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.\n\nThis project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.\n\n## Learn More\n\nTo learn more about Next.js, take a look at the following resources:\n\n- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.\n- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.\n\nYou can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!\n\n## Deploy on Vercel\n\nThe easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.\n\nCheck out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.\n"
  },
  {
    "path": "apps/landing/components/ui/button.tsx",
    "content": "import type { VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva } from \"class-variance-authority\";\n\nconst buttonVariants = cva(\n  \"inline-flex items-center justify-center whitespace-nowrap rounded-lg text-base font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n        destructive:\n          \"bg-destructive text-destructive-foreground hover:bg-destructive/90\",\n        outline:\n          \"border border-input bg-background hover:bg-accent hover:text-accent-foreground\",\n        secondary:\n          \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n        ghost: \"hover:bg-accent hover:text-accent-foreground\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default: \"h-10 px-4 py-2\",\n        sm: \"h-9 rounded-lg px-3\",\n        lg: \"h-12 rounded-lg px-8\",\n        icon: \"size-10\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nexport interface ButtonProps\n  extends\n    React.ButtonHTMLAttributes<HTMLButtonElement>,\n    VariantProps<typeof buttonVariants> {\n  asChild?: boolean;\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n  ({ className, variant, size, asChild = false, ...props }, ref) => {\n    const Comp = asChild ? Slot : \"button\";\n    return (\n      <Comp\n        className={cn(buttonVariants({ variant, size, className }))}\n        ref={ref}\n        {...props}\n      />\n    );\n  },\n);\nButton.displayName = \"Button\";\n\nexport { Button, buttonVariants };\n"
  },
  {
    "path": "apps/landing/components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"default\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"tailwind.config.ts\",\n    \"css\": \"src/styles.css\",\n    \"baseColor\": \"slate\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/lib/utils\"\n  }\n}\n"
  },
  {
    "path": "apps/landing/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>\n      Karakeep - The Bookmark Everything App | Save, Organize & Tag with AI\n    </title>\n    <meta\n      name=\"description\"\n      content=\"Karakeep is the open-source bookmark manager for links, notes, and images. Automatically organize and tag your bookmarks with AI. Self-hostable, with apps for iOS, Android, Chrome, and Firefox.\"\n    />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <link rel=\"canonical\" href=\"https://karakeep.app/\" />\n    <meta name=\"robots\" content=\"index, follow\" />\n    <meta name=\"theme-color\" content=\"#7c3aed\" />\n\n    <!-- Open Graph -->\n    <meta property=\"og:type\" content=\"website\" />\n    <meta property=\"og:site_name\" content=\"Karakeep\" />\n    <meta property=\"og:url\" content=\"https://karakeep.app/\" />\n    <meta\n      property=\"og:title\"\n      content=\"Karakeep - The Bookmark Everything App\"\n    />\n    <meta\n      property=\"og:description\"\n      content=\"Karakeep is the open-source bookmark manager for links, notes, and images. Automatically organize and tag your bookmarks with AI.\"\n    />\n    <meta\n      property=\"og:image\"\n      content=\"https://karakeep.app/opengraph-image.png\"\n    />\n    <meta property=\"og:image:type\" content=\"image/png\" />\n    <meta property=\"og:image:width\" content=\"3624\" />\n    <meta property=\"og:image:height\" content=\"1956\" />\n    <meta\n      property=\"og:image:alt\"\n      content=\"Karakeep bookmark manager app screenshot\"\n    />\n\n    <!-- Twitter Card -->\n    <meta name=\"twitter:card\" content=\"summary_large_image\" />\n    <meta\n      name=\"twitter:title\"\n      content=\"Karakeep - The Bookmark Everything App\"\n    />\n    <meta\n      name=\"twitter:description\"\n      content=\"Karakeep is the open-source bookmark manager for links, notes, and images. Automatically organize and tag your bookmarks with AI.\"\n    />\n    <meta\n      name=\"twitter:image\"\n      content=\"https://karakeep.app/twitter-image.png\"\n    />\n    <meta\n      name=\"twitter:image:alt\"\n      content=\"Karakeep bookmark manager app screenshot\"\n    />\n\n    <link rel=\"icon\" href=\"/favicon.ico\" type=\"image/x-icon\" sizes=\"16x16\" />\n    <link rel=\"apple-touch-icon\" href=\"/apple-icon.png\" />\n\n    <!-- Structured Data -->\n    <script type=\"application/ld+json\">\n      {\n        \"@context\": \"https://schema.org\",\n        \"@type\": \"SoftwareApplication\",\n        \"name\": \"Karakeep\",\n        \"url\": \"https://karakeep.app\",\n        \"description\": \"Karakeep is the open-source bookmark manager for links, notes, and images. Automatically organize and tag your bookmarks with AI.\",\n        \"applicationCategory\": \"Productivity\",\n        \"operatingSystem\": \"Web, iOS, Android\",\n        \"offers\": [\n          {\n            \"@type\": \"Offer\",\n            \"price\": \"0\",\n            \"priceCurrency\": \"USD\",\n            \"description\": \"Free plan with 10 bookmarks and 20MB storage\"\n          },\n          {\n            \"@type\": \"Offer\",\n            \"price\": \"4\",\n            \"priceCurrency\": \"USD\",\n            \"billingIncrement\": \"P1M\",\n            \"description\": \"Pro plan with 50,000 bookmarks, 50GB storage, and AI-powered tagging\"\n          }\n        ],\n        \"featureList\": [\n          \"AI-powered bookmark tagging\",\n          \"Full text search\",\n          \"Collaborative lists\",\n          \"RSS feed integration\",\n          \"Browser extensions\",\n          \"Self-hosting support\",\n          \"REST API and webhooks\",\n          \"Dark mode\"\n        ],\n        \"screenshot\": \"https://karakeep.app/hero.webp\",\n        \"softwareVersion\": \"latest\",\n        \"author\": {\n          \"@type\": \"Organization\",\n          \"name\": \"Localhost Labs Ltd\",\n          \"url\": \"https://localhostlabs.co.uk\"\n        }\n      }\n    </script>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "apps/landing/lib/utils.ts",
    "content": "import type { ClassValue } from \"clsx\";\nimport { clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
  },
  {
    "path": "apps/landing/package.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/package.json\",\n  \"name\": \"@karakeep/landing\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc -b && vite build\",\n    \"preview\": \"vite preview\",\n    \"lint\": \"oxlint .\",\n    \"lint:fix\": \"oxlint . --fix\",\n    \"format\": \"oxfmt --check .\",\n    \"format:fix\": \"oxfmt .\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@radix-ui/react-slot\": \"^1.2.4\",\n    \"@svgr/webpack\": \"^8.1.0\",\n    \"class-variance-authority\": \"^0.7.0\",\n    \"clsx\": \"^2.1.0\",\n    \"lucide-react\": \"^0.501.0\",\n    \"react\": \"^19.2.1\",\n    \"react-dom\": \"^19.2.1\",\n    \"react-router\": \"^7.7.1\",\n    \"sharp\": \"^0.33.3\",\n    \"tailwind-merge\": \"^2.2.1\",\n    \"tailwindcss-animate\": \"^1.0.7\"\n  },\n  \"devDependencies\": {\n    \"@karakeep/tailwind-config\": \"workspace:^0.1.0\",\n    \"@karakeep/tsconfig\": \"workspace:^0.1.0\",\n    \"@tailwindcss/typography\": \"^0.5.10\",\n    \"@types/react\": \"^19.2.14\",\n    \"@types/react-dom\": \"^19.1.6\",\n    \"@vitejs/plugin-react\": \"^4.7.0\",\n    \"autoprefixer\": \"^10.4.17\",\n    \"postcss\": \"^8.4.35\",\n    \"tailwindcss\": \"^3.4.1\",\n    \"vite\": \"^7.0.6\",\n    \"vite-plugin-svgr\": \"^4.3.0\"\n  }\n}\n"
  },
  {
    "path": "apps/landing/postcss.config.cjs",
    "content": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n};\n"
  },
  {
    "path": "apps/landing/public/robots.txt",
    "content": "User-agent: *\nAllow: /\n\nSitemap: https://karakeep.app/sitemap.xml\n"
  },
  {
    "path": "apps/landing/public/sitemap.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n  <url>\n    <loc>https://karakeep.app/</loc>\n    <changefreq>weekly</changefreq>\n    <priority>1.0</priority>\n  </url>\n  <url>\n    <loc>https://karakeep.app/pricing</loc>\n    <changefreq>monthly</changefreq>\n    <priority>0.8</priority>\n  </url>\n  <url>\n    <loc>https://karakeep.app/apps</loc>\n    <changefreq>monthly</changefreq>\n    <priority>0.8</priority>\n  </url>\n  <url>\n    <loc>https://karakeep.app/privacy</loc>\n    <changefreq>yearly</changefreq>\n    <priority>0.3</priority>\n  </url>\n  <url>\n    <loc>https://karakeep.app/terms</loc>\n    <changefreq>yearly</changefreq>\n    <priority>0.3</priority>\n  </url>\n</urlset>\n"
  },
  {
    "path": "apps/landing/src/App.tsx",
    "content": "import Apps from \"@/src/Apps\";\nimport Homepage from \"@/src/Homepage\";\nimport Pricing from \"@/src/Pricing\";\nimport Privacy from \"@/src/Privacy\";\nimport Terms from \"@/src/Terms\";\nimport { BrowserRouter, Route, Routes } from \"react-router\";\n\nimport Layout from \"./components/Layout\";\n\nimport \"@karakeep/tailwind-config/globals.css\";\n\nexport default function App() {\n  return (\n    <BrowserRouter>\n      <Routes>\n        <Route element={<Layout />}>\n          <Route path=\"/\" element={<Homepage />} />\n          <Route path=\"/apps\" element={<Apps />} />\n          <Route path=\"/pricing\" element={<Pricing />} />\n        </Route>\n        <Route path=\"/privacy\" element={<Privacy />} />\n        <Route path=\"/terms\" element={<Terms />} />\n      </Routes>\n    </BrowserRouter>\n  );\n}\n"
  },
  {
    "path": "apps/landing/src/Apps.tsx",
    "content": "import SEO from \"./SEO\";\nimport appleIcon from \"/apple-icon.svg?url\";\nimport chromeIcon from \"/chrome-icon.svg?url\";\nimport firefoxIcon from \"/firefox-icon.svg?url\";\nimport googlePlayIcon from \"/google-play-icon.svg?url\";\nimport obsidianIcon from \"/obsidian-icon.svg?url\";\nimport raycastIcon from \"/raycast-icon.svg?url\";\n\ninterface Listing {\n  name: string;\n  description: string;\n  url: string;\n  badge: string;\n}\n\nconst mobileApps: Listing[] = [\n  {\n    name: \"iOS App\",\n    description: \"Save links and notes from your iPhone and iPad.\",\n    url: \"https://apps.apple.com/us/app/karakeep-app/id6479258022\",\n    badge: appleIcon,\n  },\n  {\n    name: \"Android App\",\n    description: \"Capture and organize content on Android devices.\",\n    url: \"https://play.google.com/store/apps/details?id=app.hoarder.hoardermobile&pcampaignid=web_share\",\n    badge: googlePlayIcon,\n  },\n];\n\nconst browserExtensions: Listing[] = [\n  {\n    name: \"Chrome Extension\",\n    description: \"One-click saving from Chrome.\",\n    url: \"https://chromewebstore.google.com/detail/karakeep/kgcjekpmcjjogibpjebkhaanilehneje\",\n    badge: chromeIcon,\n  },\n  {\n    name: \"Firefox Add-on\",\n    description: \"Save pages directly from Firefox.\",\n    url: \"https://addons.mozilla.org/en-US/firefox/addon/karakeep/\",\n    badge: firefoxIcon,\n  },\n];\n\nconst communityProjects: Listing[] = [\n  {\n    name: \"Raycast Extension\",\n    description: \"Manage your Karakeep bookmarks directly from Raycast.\",\n    url: \"https://www.raycast.com/luolei/karakeep\",\n    badge: raycastIcon,\n  },\n  {\n    name: \"Obsidian Plugin\",\n    description: \"Sync your Karakeep bookmarks to Obsidian as markdown notes.\",\n    url: \"https://obsidian.md/plugins?id=hoarder-sync\",\n    badge: obsidianIcon,\n  },\n];\n\nfunction ListingSection({\n  title,\n  description,\n  items,\n}: {\n  title: string;\n  description: string;\n  items: Listing[];\n}) {\n  return (\n    <section className=\"rounded-2xl border border-gray-200 bg-white p-6 shadow-sm sm:p-8\">\n      <h2 className=\"text-2xl font-semibold text-gray-900\">{title}</h2>\n      <p className=\"mt-2 text-gray-600\">{description}</p>\n      <div className=\"mt-6 grid grid-cols-1 gap-5 sm:grid-cols-2\">\n        {items.map((item) => (\n          <a\n            key={item.name}\n            href={item.url}\n            target=\"_blank\"\n            rel=\"noreferrer\"\n            className=\"flex flex-row items-center gap-4 rounded-xl border border-gray-200 p-4 transition-colors hover:border-gray-300\"\n          >\n            <div className=\"h-10 w-10 shrink-0\">\n              <img\n                className=\"h-full w-full object-contain\"\n                alt={item.name}\n                src={item.badge}\n              />\n            </div>\n            <div>\n              <h3 className=\"font-semibold text-gray-900\">{item.name}</h3>\n              <p className=\"mt-1 text-sm text-gray-600\">{item.description}</p>\n            </div>\n          </a>\n        ))}\n      </div>\n    </section>\n  );\n}\n\nexport default function Apps() {\n  return (\n    <>\n      <SEO\n        title=\"Apps & Extensions\"\n        description=\"Download Karakeep for iOS, Android, Chrome, and Firefox. Save bookmarks from any device with our mobile apps and browser extensions.\"\n        path=\"/apps\"\n      />\n      <div className=\"container mx-auto pb-16\">\n        <main className=\"px-4 py-8 sm:px-6 sm:py-14\">\n          <h1 className=\"text-4xl font-bold text-gray-900 sm:text-5xl\">\n            Apps & Extensions\n          </h1>\n          <p className=\"mt-3 max-w-2xl text-base text-gray-600 sm:text-lg\">\n            Use Karakeep anywhere with our mobile apps and browser extensions.\n          </p>\n          <div className=\"mt-10 space-y-6\">\n            <ListingSection\n              title=\"Mobile Apps\"\n              description=\"Take your bookmarks with you on iOS and Android.\"\n              items={mobileApps}\n            />\n            <ListingSection\n              title=\"Browser Extensions\"\n              description=\"Save content from your browser in one click.\"\n              items={browserExtensions}\n            />\n            <ListingSection\n              title=\"Community Projects\"\n              description=\"Integrations built by the Karakeep community.\"\n              items={communityProjects}\n            />\n          </div>\n        </main>\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "apps/landing/src/Homepage.tsx",
    "content": "import {\n  ArrowDownNarrowWide,\n  BookOpen,\n  Bookmark,\n  BrainCircuit,\n  CheckCheck,\n  FileText,\n  Highlighter,\n  Link2,\n  Plug,\n  Rss,\n  Server,\n  SunMoon,\n  Tag,\n  TextSearch,\n  Users,\n  Workflow,\n  Zap,\n} from \"lucide-react\";\n\nimport FeaturesGrid from \"./components/FeaturesGrid\";\nimport Hero from \"./components/Hero\";\nimport OpenSource from \"./components/OpenSource\";\nimport Platforms from \"./components/Platforms\";\nimport SEO from \"./SEO\";\nimport readerViewScreenshot from \"/screenshots/reader-view.webp?url\";\nimport ruleEngineScreenshot from \"/screenshots/rule-engine.webp?url\";\nimport searchScreenshot from \"/screenshots/search.webp?url\";\nimport tagsScreenshot from \"/screenshots/tags.webp?url\";\n\nconst featuresList = [\n  {\n    icon: Bookmark,\n    title: \"Bookmark\",\n    description: \"Bookmark links, take simple notes and store images and PDFs.\",\n  },\n  {\n    icon: BrainCircuit,\n    title: \"AI Tagging\",\n    description:\n      \"Automatically tags your bookmarks using AI for faster retrieval.\",\n  },\n  {\n    icon: Users,\n    title: \"Collaborative Lists\",\n    description:\n      \"Collaborate with others on shared lists for team bookmarking.\",\n  },\n  {\n    icon: Rss,\n    title: \"RSS Feeds\",\n    description:\n      \"Auto-hoard content from RSS feeds to stay updated effortlessly.\",\n  },\n  {\n    icon: Workflow,\n    title: \"Rule Engine\",\n    description:\n      \"Customize bookmark management with powerful automation rules.\",\n  },\n  {\n    icon: Highlighter,\n    title: \"Highlights\",\n    description:\n      \"Highlight text on any saved page and keep your highlights organized for quick reference.\",\n  },\n  {\n    icon: Plug,\n    title: \"API & Webhooks\",\n    description: \"Integrate with other services using REST API and webhooks.\",\n  },\n  {\n    icon: TextSearch,\n    title: \"Full Text Search\",\n    description: \"Search through all your bookmarks using full text search.\",\n  },\n  {\n    icon: Server,\n    title: \"Self Hosting\",\n    description: \"Easy self hosting with Docker for privacy and control.\",\n  },\n  {\n    icon: CheckCheck,\n    title: \"Bulk Actions\",\n    description: \"Quickly manage your bookmarks with bulk actions.\",\n  },\n  {\n    icon: ArrowDownNarrowWide,\n    title: \"Auto Fetch\",\n    description:\n      \"Automatically fetches title, description and images for links.\",\n  },\n  {\n    icon: SunMoon,\n    title: \"Dark Mode\",\n    description: \"Karakeep supports dark mode for better reading experience.\",\n  },\n];\n\nconst _showcases = [\n  {\n    label: \"BOOKMARKING\",\n    title: \"Save Everything\",\n    headline: \"One place for all your bookmarks\",\n    description:\n      \"Save links, notes, images, and PDFs from any device. Karakeep automatically fetches titles, descriptions, and images so you never lose context.\",\n    bullets: [\n      { icon: Link2, text: \"Save any link with one click\" },\n      { icon: FileText, text: \"Store notes, images, and PDFs\" },\n      {\n        icon: ArrowDownNarrowWide,\n        text: \"Auto-fetch metadata and thumbnails\",\n      },\n    ],\n    screenshot: tagsScreenshot,\n    screenshotAlt: \"Karakeep tags view\",\n    reverse: false,\n  },\n  {\n    label: \"ORGANIZATION\",\n    title: \"AI-Powered Organization\",\n    headline: \"Let AI organize your bookmarks\",\n    description:\n      \"Karakeep uses AI to automatically tag and categorize your bookmarks. Stop spending time filing things away — just save and let AI do the work.\",\n    bullets: [\n      { icon: BrainCircuit, text: \"Automatic AI-powered tagging\" },\n      { icon: Tag, text: \"Smart categorization\" },\n      { icon: Zap, text: \"Instant organization as you save\" },\n    ],\n    screenshot: searchScreenshot,\n    screenshotAlt: \"Karakeep search view\",\n    reverse: true,\n  },\n  {\n    label: \"READING\",\n    title: \"Reader View & Highlights\",\n    headline: \"Read and highlight with ease\",\n    description:\n      \"Enjoy saved articles in a clean, distraction-free reader view. Highlight important passages and keep them organized for quick reference.\",\n    bullets: [\n      { icon: BookOpen, text: \"Distraction-free reader view for articles\" },\n      { icon: Highlighter, text: \"Highlight text on any saved page\" },\n    ],\n    screenshot: readerViewScreenshot,\n    screenshotAlt: \"Karakeep reader view\",\n    reverse: false,\n  },\n  {\n    label: \"AUTOMATION\",\n    title: \"Rule Engine\",\n    headline: \"Automate your workflow\",\n    description:\n      \"Create powerful automation rules to manage your bookmarks. Automatically tag, move, or organize content based on custom conditions.\",\n    bullets: [\n      { icon: Workflow, text: \"Build custom automation rules\" },\n      { icon: Zap, text: \"Trigger actions on new bookmarks\" },\n      { icon: Tag, text: \"Auto-tag based on URL patterns or content\" },\n    ],\n    screenshot: ruleEngineScreenshot,\n    screenshotAlt: \"Karakeep rule engine\",\n    reverse: true,\n  },\n];\n\nexport default function Homepage() {\n  return (\n    <>\n      <SEO path=\"/\" />\n      <Hero />\n      <FeaturesGrid features={featuresList} />\n      <Platforms />\n      <OpenSource />\n    </>\n  );\n}\n"
  },
  {
    "path": "apps/landing/src/Navbar.tsx",
    "content": "import { useState } from \"react\";\nimport { buttonVariants } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport { Menu, X } from \"lucide-react\";\nimport { Link } from \"react-router\";\n\nimport { CLOUD_SIGNUP_LINK, DOCS_LINK, GITHUB_LINK } from \"./constants\";\nimport Logo from \"/icons/karakeep-full.svg?url\";\n\nexport default function NavBar() {\n  const [mobileOpen, setMobileOpen] = useState(false);\n\n  return (\n    <nav className=\"sticky top-0 z-50 border-b border-gray-200/50 bg-white/70 backdrop-blur-xl\">\n      <div className=\"container flex items-center justify-between px-4 py-3\">\n        <Link to=\"/\">\n          <img src={Logo} alt=\"logo\" className=\"w-36\" />\n        </Link>\n\n        {/* Desktop navigation */}\n        <div className=\"hidden items-center gap-6 md:flex\">\n          <Link\n            to=\"/pricing\"\n            className=\"text-sm text-gray-600 transition-colors hover:text-gray-900\"\n          >\n            Pricing\n          </Link>\n          <a\n            href={DOCS_LINK}\n            target=\"_blank\"\n            className=\"text-sm text-gray-600 transition-colors hover:text-gray-900\"\n            rel=\"noreferrer\"\n          >\n            Docs\n          </a>\n          <a\n            href={GITHUB_LINK}\n            target=\"_blank\"\n            className=\"text-sm text-gray-600 transition-colors hover:text-gray-900\"\n            rel=\"noreferrer\"\n          >\n            GitHub\n          </a>\n          <a\n            href=\"https://cloud.karakeep.app\"\n            target=\"_blank\"\n            className={cn(\n              \"text flex h-full w-20 gap-2\",\n              buttonVariants({ variant: \"outline\" }),\n            )}\n            rel=\"noreferrer\"\n          >\n            Login\n          </a>\n          <a\n            href={CLOUD_SIGNUP_LINK}\n            target=\"_blank\"\n            className={cn(\n              \"text flex h-full w-32 gap-2\",\n              buttonVariants({ variant: \"default\" }),\n            )}\n            rel=\"noreferrer\"\n          >\n            Get Started\n          </a>\n        </div>\n\n        {/* Mobile hamburger */}\n        <button\n          className=\"md:hidden\"\n          onClick={() => setMobileOpen(!mobileOpen)}\n          aria-label=\"Toggle menu\"\n        >\n          {mobileOpen ? (\n            <X className=\"size-6 text-gray-700\" />\n          ) : (\n            <Menu className=\"size-6 text-gray-700\" />\n          )}\n        </button>\n      </div>\n\n      {/* Mobile menu */}\n      {mobileOpen && (\n        <div className=\"border-t border-gray-200/50 bg-white/95 px-4 pb-4 pt-2 backdrop-blur-xl md:hidden\">\n          <div className=\"flex flex-col gap-3\">\n            <Link\n              to=\"/pricing\"\n              className=\"text-sm text-gray-600 hover:text-gray-900\"\n              onClick={() => setMobileOpen(false)}\n            >\n              Pricing\n            </Link>\n            <a\n              href={DOCS_LINK}\n              target=\"_blank\"\n              className=\"text-sm text-gray-600 hover:text-gray-900\"\n              rel=\"noreferrer\"\n            >\n              Docs\n            </a>\n            <a\n              href={GITHUB_LINK}\n              target=\"_blank\"\n              className=\"text-sm text-gray-600 hover:text-gray-900\"\n              rel=\"noreferrer\"\n            >\n              GitHub\n            </a>\n            <div className=\"mt-2 flex gap-3\">\n              <a\n                href=\"https://cloud.karakeep.app\"\n                target=\"_blank\"\n                className={cn(\n                  \"flex-1\",\n                  buttonVariants({ variant: \"outline\", size: \"sm\" }),\n                )}\n                rel=\"noreferrer\"\n              >\n                Login\n              </a>\n              <a\n                href={CLOUD_SIGNUP_LINK}\n                target=\"_blank\"\n                className={cn(\n                  \"flex-1\",\n                  buttonVariants({ variant: \"default\", size: \"sm\" }),\n                )}\n                rel=\"noreferrer\"\n              >\n                Get Started\n              </a>\n            </div>\n          </div>\n        </div>\n      )}\n    </nav>\n  );\n}\n"
  },
  {
    "path": "apps/landing/src/Pricing.tsx",
    "content": "import { buttonVariants } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport { Check, ExternalLink } from \"lucide-react\";\n\nimport { CLOUD_SIGNUP_LINK, GITHUB_LINK } from \"./constants\";\nimport SEO from \"./SEO\";\n\nconst CONTACT_EMAIL = \"mailto:support@karakeep.app\";\n\nconst pricingTiers = [\n  {\n    name: \"Free\",\n    price: \"$0\",\n    period: \"\",\n    description: \"Trying Karakeep out\",\n    features: [\n      \"10 bookmarks\",\n      \"20MB storage\",\n      \"Mobile & web apps\",\n      \"Browser extensions\",\n    ],\n    buttonText: \"Get Started\",\n    buttonVariant: \"outline\" as const,\n    popular: false,\n  },\n  {\n    name: \"Pro\",\n    price: \"$4\",\n    period: \"per month\",\n    description: \"For serious bookmark collectors\",\n    features: [\n      \"50,000 bookmarks\",\n      \"50GB storage\",\n      \"AI-powered tagging\",\n      \"Full-text search\",\n      \"Mobile & web apps\",\n      \"Browser extensions\",\n    ],\n    buttonText: \"Get Started\",\n    buttonVariant: \"default\" as const,\n    popular: true,\n  },\n  {\n    name: \"Self-Hosted\",\n    price: \"Free\",\n    period: \"forever\",\n    description: \"Complete control and privacy\",\n    features: [\n      \"Unlimited bookmarks\",\n      \"Unlimited storage\",\n      \"Complete data control\",\n      \"Mobile & web apps\",\n      \"Browser extensions\",\n      \"Community support\",\n    ],\n    buttonText: \"View on GitHub\",\n    buttonVariant: \"outline\" as const,\n    popular: false,\n    isGitHub: true,\n  },\n  {\n    name: \"Corporate\",\n    price: \"Custom\",\n    period: \"per seat\",\n    description: \"For teams and organizations\",\n    features: [\n      \"Everything in Pro\",\n      \"Custom deployment & domain\",\n      \"Single Sign-On (SSO)\",\n      \"User management\",\n      \"Priority support\",\n    ],\n    buttonText: \"Contact Us\",\n    buttonVariant: \"outline\" as const,\n    popular: false,\n    isContact: true,\n  },\n];\n\nfunction PricingHeader() {\n  return (\n    <div className=\"text-center\">\n      <h1 className=\"text-4xl font-bold sm:text-6xl\">Simple Pricing</h1>\n      <p className=\"mt-4 text-lg text-gray-600\">\n        Choose the plan that works best for you\n      </p>\n    </div>\n  );\n}\n\nfunction PricingCards() {\n  const renderCard = (tier: (typeof pricingTiers)[number]) => (\n    <div\n      key={tier.name}\n      className={cn(\n        \"relative rounded-2xl border bg-white p-8 shadow-sm\",\n        tier.popular && \"border-purple-500 shadow-lg\",\n      )}\n    >\n      <div className=\"text-center\">\n        <h3 className=\"text-xl font-semibold\">{tier.name}</h3>\n        <div className=\"mt-4 flex items-baseline justify-center\">\n          <span className=\"text-4xl font-bold\">{tier.price}</span>\n          {tier.period && (\n            <span className=\"ml-1 text-gray-500\">/{tier.period}</span>\n          )}\n        </div>\n        <p className=\"mt-2 text-gray-600\">{tier.description}</p>\n      </div>\n\n      <ul className=\"mt-8 space-y-3\">\n        {tier.features.map((feature) => (\n          <li key={feature} className=\"flex items-center\">\n            <Check className=\"h-5 w-5 text-green-500\" />\n            <span className=\"ml-3 text-gray-700\">{feature}</span>\n          </li>\n        ))}\n      </ul>\n\n      <div className=\"mt-8\">\n        {tier.isContact ? (\n          <a\n            href={CONTACT_EMAIL}\n            className={cn(\n              \"flex w-full items-center justify-center\",\n              buttonVariants({ variant: tier.buttonVariant, size: \"lg\" }),\n            )}\n          >\n            {tier.buttonText}\n          </a>\n        ) : tier.isGitHub ? (\n          <a\n            href={GITHUB_LINK}\n            target=\"_blank\"\n            className={cn(\n              \"flex w-full items-center justify-center gap-2\",\n              buttonVariants({ variant: tier.buttonVariant, size: \"lg\" }),\n            )}\n            rel=\"noreferrer\"\n          >\n            <ExternalLink className=\"h-4 w-4\" />\n            {tier.buttonText}\n          </a>\n        ) : (\n          <a\n            href={CLOUD_SIGNUP_LINK}\n            target=\"_blank\"\n            className={cn(\n              \"flex w-full items-center justify-center\",\n              buttonVariants({ variant: tier.buttonVariant, size: \"lg\" }),\n            )}\n            rel=\"noreferrer\"\n          >\n            {tier.buttonText}\n          </a>\n        )}\n      </div>\n    </div>\n  );\n\n  return (\n    <div className=\"mx-auto mt-16 px-6\">\n      <div className=\"grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-4\">\n        {pricingTiers.map(renderCard)}\n      </div>\n    </div>\n  );\n}\n\nfunction FAQ() {\n  const faqs = [\n    {\n      question: \"What happens to my data if I cancel?\",\n      answer:\n        \"Your data will be available for 30 days after cancellation. You can export your bookmarks at any time.\",\n    },\n    {\n      question: \"Are there any restrictions in the self-hosted version?\",\n      answer:\n        \"No. The selhosted version is completely free, fully-featured, and open source. You just need to provide your own hosting infrastructure.\",\n    },\n    {\n      question: \"Do you offer refunds?\",\n      answer: \"Yes, we offer a 7-day money-back guarantee for all paid plans.\",\n    },\n    {\n      question: \"How should I contact you if I have any questions?\",\n      answer: \"You can reach us at support@karakeep.app\",\n    },\n  ];\n\n  return (\n    <div className=\"mx-auto mt-24 max-w-4xl px-6\">\n      <h2 className=\"text-center text-3xl font-bold\">\n        Frequently Asked Questions\n      </h2>\n      <div className=\"mt-12 grid gap-8 md:grid-cols-2\">\n        {faqs.map((faq) => (\n          <div key={faq.question}>\n            <h3 className=\"text-lg font-semibold\">{faq.question}</h3>\n            <p className=\"mt-2 text-gray-600\">{faq.answer}</p>\n          </div>\n        ))}\n      </div>\n    </div>\n  );\n}\n\nexport default function Pricing() {\n  return (\n    <>\n      <SEO\n        title=\"Pricing\"\n        description=\"Simple, transparent pricing for Karakeep. Free plan available. Pro plan at $4/month with AI tagging, full-text search, and 50GB storage. Self-hosted option is free forever.\"\n        path=\"/pricing\"\n      />\n      <div className=\"container mx-auto\">\n        <div className=\"py-16\">\n          <PricingHeader />\n          <PricingCards />\n          <FAQ />\n        </div>\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "apps/landing/src/Privacy.tsx",
    "content": "import SEO from \"./SEO\";\n\nexport default function PrivacyPolicy() {\n  return (\n    <div className=\"mx-auto max-w-3xl p-4\">\n      <SEO\n        title=\"Privacy Policy\"\n        description=\"Karakeep Privacy Policy. Learn how we collect, use, and protect your data. We store primary data in Europe and do not sell personal data.\"\n        path=\"/privacy\"\n      />\n      <main className=\"space-y-6\">\n        <h1 className=\"text-3xl font-bold text-gray-900\">\n          Karakeep - Privacy Policy\n        </h1>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          <em className=\"italic\">Effective date: 2026-02-15</em>\n        </p>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          <strong>In short:</strong> we collect the minimum data needed to run\n          your account, store your saved content, and provide features like\n          search and AI tagging. We store primary data in Europe and do not sell\n          personal data.\n        </p>\n        <h2 className=\"text-lg font-semibold text-gray-900\">1. Scope</h2>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          This page explains what data we collect, how we use it, and when we\n          share it for Karakeep Cloud. Localhost Labs Ltd is the data controller\n          for personal data processed under this policy. Karakeep Cloud is\n          operated by Localhost Labs Ltd (England & Wales, Company No.\n          16403882).\n        </p>\n        <h2 className=\"text-lg font-semibold text-gray-900\">\n          2. Data We Collect\n        </h2>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          We collect the following types of data:\n        </p>\n        <ul className=\"ml-6 list-disc space-y-2 text-gray-700\">\n          <li>\n            <strong>Account information</strong>: email address, name, profile\n            image, authentication identifiers.\n          </li>\n          <li>\n            <strong>Content you provide</strong>: links, notes, uploads, and\n            other data you choose to save in the Service. We may process this\n            content — including via third-party artificial intelligence\n            providers — to provide features such as full-text search, automatic\n            tagging, and AI-powered summaries.\n          </li>\n          <li>\n            <strong>Billing and subscription data</strong>: plan status, billing\n            events, and limited payment metadata from payment providers.\n          </li>\n          <li>\n            <strong>Technical and usage data</strong>: logs, device/browser\n            details, IP address, and service events needed to run, secure, and\n            troubleshoot Karakeep.\n          </li>\n          <li>\n            <strong>Support communications</strong>: information you send when\n            contacting support.\n          </li>\n        </ul>\n        <h2 className=\"text-lg font-semibold text-gray-900\">\n          3. How We Use Data\n        </h2>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          We use your data to run and improve Karakeep Cloud, including to:\n        </p>\n        <ul className=\"ml-6 list-disc space-y-2 text-gray-700\">\n          <li>create and manage your account;</li>\n          <li>store and present your content;</li>\n          <li>manage subscriptions and payments;</li>\n          <li>detect abuse, fraud, and security incidents;</li>\n          <li>monitor reliability and fix issues;</li>\n          <li>comply with legal obligations.</li>\n        </ul>\n\n        <h2 className=\"text-lg font-semibold text-gray-900\">\n          4. Legal Bases (EEA/UK)\n        </h2>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          If you are in the EEA or UK, we rely on the following legal bases:\n        </p>\n        <ul className=\"ml-6 list-disc space-y-2 text-gray-700\">\n          <li>\n            <strong>Contract</strong>: to create and manage your account, store\n            your content, and provide core product features.\n          </li>\n          <li>\n            <strong>Legitimate interests</strong>: to keep the service reliable\n            and secure, prevent abuse, and provide support.\n          </li>\n          <li>\n            <strong>Legal obligation</strong>: to comply with tax, accounting,\n            and lawful requests from authorities.\n          </li>\n          <li>\n            <strong>Consent (where required)</strong>: for optional processing\n            where consent is the lawful basis; you can withdraw consent at any\n            time.\n          </li>\n        </ul>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          Where we rely on legitimate interests, we balance those interests\n          against your rights and freedoms.\n        </p>\n\n        <h2 className=\"text-lg font-semibold text-gray-900\">\n          5. Sharing and Processors\n        </h2>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          We use subprocessors to run Karakeep Cloud. They only process data on\n          our instructions and under contractual safeguards. Current key\n          providers include:\n        </p>\n        <ul className=\"ml-6 list-disc space-y-2 text-gray-700\">\n          <li>\n            <strong>Hetzner</strong> (hosting and infrastructure, with primary\n            data storage in Europe).\n          </li>\n          <li>\n            <strong>Stripe</strong> (payments and billing events).\n          </li>\n          <li>\n            <strong>OpenAI</strong> (content processing for features such as\n            automatic tagging and summaries).\n          </li>\n        </ul>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          We do not voluntarily disclose data to law enforcement. We require\n          valid legal process (such as a court order or warrant) before\n          providing any user data, and we will notify affected users unless\n          legally prohibited from doing so.\n        </p>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          We do not sell your personal data. We only access account content when\n          needed to help with support requests, investigate abuse or security\n          issues, or meet legal obligations.\n        </p>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          For paid plans, payments are handled by our payment provider (for\n          example, Stripe). We do not store full payment card details on our\n          systems.\n        </p>\n\n        <h2 className=\"text-lg font-semibold text-gray-900\">\n          6. International Transfers\n        </h2>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          Karakeep Cloud stores primary data in Europe. Some subprocessors may\n          process data outside your country (including outside the EEA/UK).\n          Where required, we rely on recognised safeguards for cross-border\n          transfers, such as Standard Contractual Clauses (SCCs) or adequacy\n          decisions.\n        </p>\n\n        <h2 className=\"text-lg font-semibold text-gray-900\">\n          7. Cookies and Similar Technologies\n        </h2>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          We use essential cookies and similar technologies to keep you signed\n          in, protect against cross-site request forgery, and remember basic\n          preferences. We do not use third-party advertising or tracking\n          cookies. You can adjust cookie settings in your browser, but disabling\n          essential cookies may prevent parts of the Service from working\n          properly.\n        </p>\n\n        <h2 className=\"text-lg font-semibold text-gray-900\">\n          8. Data Security\n        </h2>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          We implement appropriate technical and organisational measures to\n          protect your personal data against unauthorised access, loss, or\n          misuse. These include encryption of data in transit, access controls,\n          and regular security reviews. No system is completely secure, and we\n          cannot guarantee the absolute security of your data.\n        </p>\n\n        <h2 className=\"text-lg font-semibold text-gray-900\">\n          9. Data Retention and Deletion\n        </h2>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          We keep personal data only as long as needed to run the Service and\n          meet legal obligations. When you delete your account, we delete your\n          data. Residual copies may remain in encrypted backups for up to 90\n          days before being purged.\n        </p>\n\n        <h2 className=\"text-lg font-semibold text-gray-900\">10. Your Rights</h2>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          Regardless of where you live, we extend the following rights to all\n          Karakeep Cloud users:\n        </p>\n        <ul className=\"ml-6 list-disc space-y-2 text-gray-700\">\n          <li>\n            <strong>Access</strong>: request a copy of the personal data we hold\n            about you.\n          </li>\n          <li>\n            <strong>Correction</strong>: ask us to correct inaccurate or\n            incomplete data.\n          </li>\n          <li>\n            <strong>Erasure</strong>: ask us to delete your personal data.\n          </li>\n          <li>\n            <strong>Restriction</strong>: ask us to restrict certain processing\n            of your data.\n          </li>\n          <li>\n            <strong>Portability</strong>: receive your data in a structured,\n            commonly used format.\n          </li>\n          <li>\n            <strong>Objection</strong>: object to processing based on legitimate\n            interests.\n          </li>\n        </ul>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          To submit a rights request, email us using the contact details below.\n          We may ask for additional information to verify your identity before\n          handling some requests.\n        </p>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          You can submit privacy requests by emailing info@localhostlabs.co.uk\n          or support@karakeep.app. We respond within timelines required by\n          applicable law.\n        </p>\n\n        <h2 className=\"text-lg font-semibold text-gray-900\">11. Children</h2>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          Karakeep Cloud is not intended for children under 16. If you believe a\n          child gave us personal data in violation of this policy, contact us\n          and we will take appropriate action.\n        </p>\n\n        <h2 className=\"text-lg font-semibold text-gray-900\">\n          12. Browser Extension\n        </h2>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          The Karakeep browser extension works with your Karakeep account. It\n          includes an optional feature that, when enabled, sends URLs of pages\n          you visit to check whether they already exist in your library. This\n          feature is off by default and requires you to opt in. The data is sent\n          only to Karakeep Cloud and is not shared with third parties. Content\n          is saved to your account only when you explicitly choose to save it.\n        </p>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          URLs sent for existence checking are not stored and are not used for\n          analytics or profiling. Only content you explicitly choose to save is\n          added to your account.\n        </p>\n\n        <h2 className=\"text-lg font-semibold text-gray-900\">\n          13. Self-Hosted Deployments\n        </h2>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          This policy applies to Karakeep Cloud. If you self-host Karakeep, your\n          data stays entirely on your own infrastructure and we do not collect\n          or have access to any of it. You are responsible for your own data\n          practices and compliance.\n        </p>\n\n        <h2 className=\"text-lg font-semibold text-gray-900\">\n          14. Changes to This Policy\n        </h2>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          We may update this Privacy Policy from time to time. Changes will be\n          published on this page with an updated effective date. Your continued\n          use of the Service after a revised policy is published constitutes\n          your acceptance of the changes.\n        </p>\n\n        <h2 className=\"text-lg font-semibold text-gray-900\">15. Contact Us</h2>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          For privacy questions or requests, contact us at:\n          <br />\n          Localhost Labs Ltd (England & Wales, Company No. 16403882)\n          <br />\n          Email:{\" \"}\n          <a\n            href=\"mailto:info@localhostlabs.co.uk\"\n            className=\"text-blue-600 hover:underline\"\n          >\n            info@localhostlabs.co.uk\n          </a>\n          <br />\n          Support:{\" \"}\n          <a\n            href=\"mailto:support@karakeep.app\"\n            className=\"text-blue-600 hover:underline\"\n          >\n            support@karakeep.app\n          </a>\n        </p>\n      </main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/landing/src/SEO.tsx",
    "content": "const BASE_URL = \"https://karakeep.app\";\nconst DEFAULT_DESCRIPTION =\n  \"Karakeep is the open-source bookmark manager for links, notes, and images. Automatically organize and tag your bookmarks with AI.\";\n\ninterface SEOProps {\n  title?: string;\n  description?: string;\n  path?: string;\n}\n\nexport default function SEO({\n  title,\n  description = DEFAULT_DESCRIPTION,\n  path = \"/\",\n}: SEOProps) {\n  const fullTitle = title\n    ? `${title} | Karakeep`\n    : \"Karakeep - The Bookmark Everything App | Save, Organize & Tag with AI\";\n  const url = `${BASE_URL}${path}`;\n\n  return (\n    <>\n      <title>{fullTitle}</title>\n      <meta name=\"description\" content={description} />\n      <link rel=\"canonical\" href={url} />\n\n      <meta property=\"og:title\" content={fullTitle} />\n      <meta property=\"og:description\" content={description} />\n      <meta property=\"og:url\" content={url} />\n\n      <meta name=\"twitter:title\" content={fullTitle} />\n      <meta name=\"twitter:description\" content={description} />\n    </>\n  );\n}\n"
  },
  {
    "path": "apps/landing/src/Terms.tsx",
    "content": "import SEO from \"./SEO\";\n\nexport default function Terms() {\n  return (\n    <div className=\"mx-auto max-w-3xl p-4\">\n      <SEO\n        title=\"Terms of Service\"\n        description=\"Karakeep Terms of Service. Read our terms governing the use of Karakeep Cloud, operated by Localhost Labs Ltd.\"\n        path=\"/terms\"\n      />\n      <main className=\"space-y-6\">\n        <h1 className=\"text-3xl font-bold text-gray-900\">\n          Karakeep – Terms of Service\n        </h1>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          <em className=\"italic\">Effective date: 2026-02-15</em>\n        </p>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          These Terms of Service (&ldquo;Terms&rdquo;) govern your use of\n          Karakeep Cloud (&ldquo;Service&rdquo;), operated by Localhost Labs Ltd\n          (&ldquo;We&rdquo;, &ldquo;Us&rdquo;, &ldquo;Our&rdquo;), a company\n          incorporated in England & Wales (Company No. 16403882). By accessing\n          or using the Service, you agree to be bound by these Terms.\n        </p>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          <strong>In short:</strong> you own your content, we use it only to run\n          Karakeep Cloud (including search and tagging features), and you need\n          to use the service responsibly. If you use a paid plan, it renews\n          until you cancel.\n        </p>\n\n        <h2 className=\"text-lg font-semibold text-gray-900\">\n          1. Acceptance of Terms\n        </h2>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          By creating an account or using the Service, you confirm that you have\n          read, understood, and agree to these Terms and our Privacy Policy. If\n          you do not agree, you must not use the Service. You must be at least\n          16 years old to use the Service.\n        </p>\n\n        <h2 className=\"text-lg font-semibold text-gray-900\">\n          2. Description of Service\n        </h2>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          Karakeep Cloud is a hosted bookmarking and content-saving service that\n          allows you to save links, notes, and images, with automatic AI-powered\n          tagging and full-text search. These Terms apply to the cloud-hosted\n          version of Karakeep. Self-hosted instances of Karakeep are governed by\n          the open-source license under which the software is distributed.\n        </p>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          The Service uses automated systems, including third-party artificial\n          intelligence providers, to process Your Content for features such as\n          automatic tagging and summarization. Details of how we handle your\n          data are set out in our Privacy Policy.\n        </p>\n\n        <h2 className=\"text-lg font-semibold text-gray-900\">\n          3. Account Terms\n        </h2>\n        <ul className=\"ml-6 list-disc space-y-2 text-gray-700\">\n          <li>Each account is intended for use by a single individual.</li>\n          <li>\n            You are responsible for maintaining the security of your account and\n            password.\n          </li>\n          <li>\n            You must provide accurate and complete information when creating\n            your account.\n          </li>\n          <li>\n            You must notify us immediately at{\" \"}\n            <a\n              href=\"mailto:support@karakeep.app\"\n              className=\"text-blue-600 hover:underline\"\n            >\n              support@karakeep.app\n            </a>{\" \"}\n            if you become aware of any unauthorized access to your account.\n          </li>\n          <li>\n            We are not liable for any loss or damage arising from your failure\n            to protect your account credentials.\n          </li>\n        </ul>\n\n        <h2 className=\"text-lg font-semibold text-gray-900\">\n          4. Subscriptions & Payments\n        </h2>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          The Service offers a free tier with limited features. Paid\n          subscription plans are available for additional features and higher\n          usage limits.\n        </p>\n        <ul className=\"ml-6 list-disc space-y-2 text-gray-700\">\n          <li>\n            Paid plans are billed on a recurring basis (monthly or annually)\n            depending on the plan you select.\n          </li>\n          <li>\n            Subscriptions automatically renew at the end of each billing period\n            unless you cancel before the renewal date.\n          </li>\n          <li>\n            You may cancel your subscription at any time through your account\n            settings. Cancellation takes effect at the end of the current\n            billing period.\n          </li>\n          <li>\n            Refunds are available if requested within 7 days of your first\n            payment. To request a refund, contact us at{\" \"}\n            <a\n              href=\"mailto:support@karakeep.app\"\n              className=\"text-blue-600 hover:underline\"\n            >\n              support@karakeep.app\n            </a>\n            .\n          </li>\n          <li>\n            We reserve the right to change pricing for paid plans. We will\n            provide at least 30 days&apos; notice before any price changes take\n            effect.\n          </li>\n          <li>\n            Nothing in these Terms affects your statutory rights as a consumer.\n          </li>\n        </ul>\n\n        <h2 className=\"text-lg font-semibold text-gray-900\">\n          5. Content & Ownership\n        </h2>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          You retain all rights to the content you save, upload, or create using\n          the Service (&ldquo;Your Content&rdquo;). We do not claim ownership\n          over Your Content. By using the Service, you grant us a limited,\n          non-exclusive license to host, store, process, and display Your\n          Content so we can operate the Service for you, including search,\n          tagging, and summarization features, customer support, security\n          monitoring, and legal compliance. We do not sell Your Content or use\n          it for advertising. You are responsible for what you save and must\n          ensure you have the right to store it.\n        </p>\n\n        <h2 className=\"text-lg font-semibold text-gray-900\">\n          6. Copyright & Takedown\n        </h2>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          If you believe that content stored or displayed through the Service\n          infringes your copyright, please contact us at{\" \"}\n          <a\n            href=\"mailto:support@karakeep.app\"\n            className=\"text-blue-600 hover:underline\"\n          >\n            support@karakeep.app\n          </a>{\" \"}\n          with sufficient information to identify the material and demonstrate\n          your ownership or authorization to act on behalf of the rights holder.\n          We will review all valid notices and may remove or disable access to\n          allegedly infringing content. We may terminate the accounts of users\n          who repeatedly infringe the intellectual property rights of others.\n        </p>\n\n        <h2 className=\"text-lg font-semibold text-gray-900\">\n          7. Acceptable Use\n        </h2>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          You agree not to use the Service to:\n        </p>\n        <ul className=\"ml-6 list-disc space-y-2 text-gray-700\">\n          <li>\n            Store, distribute, or share illegal content or content that violates\n            any applicable law.\n          </li>\n          <li>Upload or transmit malware, viruses, or other harmful code.</li>\n          <li>\n            Abuse, harass, or send unsolicited bulk messages (spam) through the\n            Service.\n          </li>\n          <li>\n            Use automated tools to scrape, crawl, or extract data from the\n            Service beyond what is provided through our API.\n          </li>\n          <li>\n            Impersonate any person or entity, or misrepresent your affiliation\n            with any person or entity.\n          </li>\n        </ul>\n\n        <h2 className=\"text-lg font-semibold text-gray-900\">\n          8. Content Moderation\n        </h2>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          We may review, remove, or restrict access to content that violates\n          these Terms or applicable law, including without prior notice when\n          needed. We may investigate suspected violations and cooperate with law\n          enforcement when legally required or when reasonably necessary to\n          protect users or the public.\n        </p>\n\n        <h2 className=\"text-lg font-semibold text-gray-900\">9. API Usage</h2>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          Access to the Karakeep API is governed by these Terms. We may impose\n          rate limits on API usage to ensure fair access for all users. We\n          reserve the right to suspend or revoke API access if we determine that\n          usage is abusive, excessive, or otherwise harmful to the Service.\n        </p>\n\n        <h2 className=\"text-lg font-semibold text-gray-900\">\n          10. Intellectual Property\n        </h2>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          The Karakeep name, logo, and service features are the property of\n          Localhost Labs Ltd. The open-source code that powers Karakeep is\n          governed by its own license terms, which are separate from these\n          Terms. Nothing in these Terms grants you any right to use our\n          trademarks, logos, or branding without our prior written consent.\n        </p>\n\n        <h2 className=\"text-lg font-semibold text-gray-900\">\n          11. Third-Party Services\n        </h2>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          The Service may integrate with or link to third-party services (e.g.,\n          for content fetching, AI processing, or authentication). We are not\n          responsible for the availability, accuracy, or content of third-party\n          services. Your use of third-party services is subject to their\n          respective terms and policies.\n        </p>\n\n        <h2 className=\"text-lg font-semibold text-gray-900\">12. Termination</h2>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          You may close your account at any time through account settings (where\n          available) or by contacting us. We may suspend or terminate access if\n          you violate these Terms or engage in conduct we reasonably believe is\n          harmful to the Service or other users. When your account is closed, we\n          delete active account data. Encrypted backup copies may remain for up\n          to 90 days before being purged. We may retain limited data for longer\n          if required by law.\n        </p>\n\n        <h2 className=\"text-lg font-semibold text-gray-900\">\n          13. Disclaimer of Warranties\n        </h2>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          The Service is provided on an &ldquo;as is&rdquo; and &ldquo;as\n          available&rdquo; basis. We make no warranties, express or implied,\n          regarding the Service, including but not limited to warranties of\n          merchantability, fitness for a particular purpose, or\n          non-infringement. We do not guarantee that the Service will be\n          uninterrupted, error-free, or that your data will be preserved\n          indefinitely.\n        </p>\n\n        <h2 className=\"text-lg font-semibold text-gray-900\">\n          14. Limitation of Liability\n        </h2>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          Nothing in these Terms limits or excludes liability for death or\n          personal injury caused by negligence, fraud or fraudulent\n          misrepresentation, or any liability that cannot be limited under\n          applicable law. Subject to that, to the maximum extent permitted by\n          law, our total liability to you for claims arising out of or relating\n          to the Service will not exceed the total amount you paid us in the 12\n          months before the claim. We are not liable for indirect, incidental,\n          special, consequential, or punitive damages, including loss of data,\n          revenue, or business opportunities.\n        </p>\n\n        <h2 className=\"text-lg font-semibold text-gray-900\">\n          15. Indemnification\n        </h2>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          If your misuse of the Service, your violation of these Terms, or your\n          infringement of third-party rights causes a legal claim against us,\n          you agree to cover related losses and reasonable legal costs.\n        </p>\n\n        <h2 className=\"text-lg font-semibold text-gray-900\">\n          16. Changes to Terms\n        </h2>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          We may update these Terms from time to time. Changes will be published\n          on this page with an updated effective date. Your continued use of the\n          Service after revised Terms are published constitutes your acceptance\n          of the changes. If you do not agree to the updated Terms, you must\n          stop using the Service.\n        </p>\n\n        <h2 className=\"text-lg font-semibold text-gray-900\">\n          17. Governing Law\n        </h2>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          These Terms shall be governed by and construed in accordance with the\n          laws of England & Wales, without regard to conflict of law principles.\n          Any disputes arising under these Terms shall be subject to the\n          exclusive jurisdiction of the courts of England & Wales.\n        </p>\n\n        <h2 className=\"text-lg font-semibold text-gray-900\">18. Contact Us</h2>\n        <p className=\"text-base leading-relaxed text-gray-700\">\n          If you have any questions about these Terms, please contact us at:\n          <br />\n          Localhost Labs Ltd (England & Wales, Company No. 16403882)\n          <br />\n          Email:{\" \"}\n          <a\n            href=\"mailto:support@karakeep.app\"\n            className=\"text-blue-600 hover:underline\"\n          >\n            support@karakeep.app\n          </a>\n        </p>\n      </main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/landing/src/components/Banner.tsx",
    "content": "import { Rocket } from \"lucide-react\";\n\nimport { CLOUD_SIGNUP_LINK } from \"../constants\";\n\nexport default function Banner() {\n  return (\n    <div className=\"border-b border-amber-200 bg-gradient-to-r from-amber-50 via-orange-50 to-rose-50 px-3 py-2 text-center sm:px-4 sm:py-3\">\n      <div className=\"container flex flex-wrap items-center justify-center gap-x-2 gap-y-1 text-xs text-slate-700 sm:gap-3 sm:text-base\">\n        <div className=\"flex flex-wrap items-center justify-center gap-1 px-2 py-0.5 sm:px-3 sm:py-1\">\n          <Rocket className=\"size-4 text-amber-600 sm:size-5\" />\n          <span className=\"font-semibold text-slate-800\">\n            Karakeep Cloud Public Beta is Now Live\n          </span>\n        </div>\n        <a\n          href={CLOUD_SIGNUP_LINK}\n          target=\"_blank\"\n          rel=\"noreferrer\"\n          className=\"inline-flex items-center justify-center gap-1 text-xs font-semibold text-amber-700 underline decoration-amber-400 underline-offset-2 transition-all hover:text-amber-800 sm:rounded-full sm:border sm:border-amber-300 sm:bg-amber-500 sm:px-3 sm:py-1 sm:text-sm sm:text-white sm:no-underline sm:shadow-sm sm:hover:border-amber-400 sm:hover:bg-amber-600\"\n        >\n          Join Now <span className=\"hidden sm:inline\">&rarr;</span>\n        </a>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/landing/src/components/CallToAction.tsx",
    "content": "import { buttonVariants } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\nimport { CLOUD_SIGNUP_LINK, DEMO_LINK } from \"../constants\";\n\nexport default function CallToAction() {\n  return (\n    <section className=\"px-4 py-16 sm:py-24\">\n      <div className=\"mx-auto max-w-4xl overflow-hidden rounded-2xl bg-gradient-to-r from-purple-600 to-red-600 px-8 py-16 text-center shadow-2xl sm:px-16\">\n        <h2 className=\"text-3xl font-bold tracking-tight text-white sm:text-4xl\">\n          Start hoarding today\n        </h2>\n        <p className=\"mx-auto mt-4 max-w-xl text-lg text-purple-100\">\n          Join thousands of users who trust Karakeep to save and organize their\n          digital life.\n        </p>\n        <div className=\"mt-8 flex flex-col items-center justify-center gap-4 sm:flex-row\">\n          <a\n            href={CLOUD_SIGNUP_LINK}\n            target=\"_blank\"\n            rel=\"noreferrer\"\n            className={cn(\n              \"w-full gap-2 bg-white px-8 text-purple-700 hover:bg-gray-100 sm:w-auto\",\n              buttonVariants({ size: \"lg\" }),\n            )}\n          >\n            Get Started Free\n          </a>\n          <a\n            href={DEMO_LINK}\n            target=\"_blank\"\n            rel=\"noreferrer\"\n            className={cn(\n              \"w-full gap-2 border-white/30 px-8 text-white hover:bg-white/10 sm:w-auto\",\n              buttonVariants({ variant: \"outline\", size: \"lg\" }),\n            )}\n          >\n            Try the Demo\n          </a>\n        </div>\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "apps/landing/src/components/FeatureShowcase.tsx",
    "content": "import type { LucideIcon } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\ninterface FeatureBullet {\n  icon: LucideIcon;\n  text: string;\n}\n\ninterface FeatureShowcaseProps {\n  label: string;\n  title: string;\n  headline: string;\n  description: string;\n  bullets: FeatureBullet[];\n  screenshot: string;\n  screenshotAlt: string;\n  reverse?: boolean;\n}\n\nexport default function FeatureShowcase({\n  label,\n  headline,\n  description,\n  bullets,\n  screenshot,\n  screenshotAlt,\n  reverse = false,\n}: FeatureShowcaseProps) {\n  return (\n    <section className=\"px-4 py-8 sm:py-12\">\n      <div className=\"mx-auto max-w-6xl rounded-2xl bg-gray-50/80 px-6 py-10 sm:px-12 sm:py-14\">\n        <div\n          className={cn(\n            \"flex flex-col items-center gap-10 lg:flex-row lg:gap-14\",\n            reverse && \"lg:flex-row-reverse\",\n          )}\n        >\n          {/* Text side */}\n          <div className=\"flex flex-1 flex-col justify-center space-y-5\">\n            <span className=\"text-xs font-bold uppercase tracking-widest text-gray-500\">\n              {label}\n            </span>\n            <h2 className=\"text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl\">\n              {headline}\n            </h2>\n            <p className=\"text-base font-medium text-gray-500\">{description}</p>\n            <ul className=\"space-y-3 pt-2\">\n              {bullets.map((bullet) => (\n                <li key={bullet.text} className=\"flex items-start gap-3\">\n                  <div className=\"mt-0.5 rounded-lg bg-gray-100 p-1.5\">\n                    <bullet.icon className=\"size-4 text-gray-500\" />\n                  </div>\n                  <span className=\"text-gray-700\">{bullet.text}</span>\n                </li>\n              ))}\n            </ul>\n          </div>\n\n          {/* Screenshot side */}\n          <div className=\"flex flex-1 items-center justify-center\">\n            <div className=\"relative p-4\">\n              {/* Glow behind card */}\n              <div className=\"absolute inset-2 rounded-2xl bg-gradient-to-br from-gray-200/40 via-gray-100/30 to-transparent blur-2xl\" />\n              <div className=\"relative overflow-hidden rounded-xl border border-gray-200/80 bg-white shadow-lg\">\n                <img src={screenshot} alt={screenshotAlt} className=\"w-full\" />\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "apps/landing/src/components/FeaturesGrid.tsx",
    "content": "import type { LucideIcon } from \"lucide-react\";\n\ninterface Feature {\n  icon: LucideIcon;\n  title: string;\n  description: string;\n}\n\nexport default function FeaturesGrid({ features }: { features: Feature[] }) {\n  return (\n    <section className=\"bg-gray-50 px-4 py-16 sm:py-24\">\n      <div className=\"mx-auto max-w-6xl\">\n        <div className=\"text-center\">\n          <h2 className=\"text-3xl font-bold tracking-tight sm:text-4xl\">\n            Everything you need\n          </h2>\n          <p className=\"mx-auto mt-4 max-w-2xl text-lg text-gray-600\">\n            A complete toolkit for saving, organizing, and rediscovering your\n            content.\n          </p>\n        </div>\n        <div className=\"mt-12 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\">\n          {features.map((feature) => (\n            <div\n              key={feature.title}\n              className=\"group rounded-xl border border-gray-200 bg-white p-6 transition-all hover:border-purple-200 hover:shadow-lg\"\n            >\n              <feature.icon className=\"mb-4 size-6 text-gray-800\" />\n              <h3 className=\"mb-2 font-semibold text-gray-900\">\n                {feature.title}\n              </h3>\n              <p className=\"text-sm text-gray-600\">{feature.description}</p>\n            </div>\n          ))}\n        </div>\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "apps/landing/src/components/Footer.tsx",
    "content": "import { Link } from \"react-router\";\n\nimport { DOCS_LINK, GITHUB_LINK } from \"../constants\";\nimport Logo from \"/icons/karakeep-full.svg?url\";\n\nconst currentYear = new Date().getFullYear();\n\nconst footerLinks = {\n  Product: [\n    { label: \"Pricing\", href: \"/pricing\", internal: true },\n    { label: \"Apps & Extensions\", href: \"/apps\", internal: true },\n    { label: \"Try Demo\", href: \"https://try.karakeep.app\" },\n    { label: \"Karakeep Cloud\", href: \"https://cloud.karakeep.app\" },\n  ],\n  Resources: [\n    { label: \"Documentation\", href: DOCS_LINK },\n    { label: \"GitHub\", href: GITHUB_LINK },\n    { label: \"Self-hosting Guide\", href: `${DOCS_LINK}/installation/docker` },\n    { label: \"API Reference\", href: `${DOCS_LINK}/api/karakeep-api` },\n  ],\n  Legal: [\n    { label: \"Terms of Service\", href: \"/terms\", internal: true },\n    { label: \"Privacy Policy\", href: \"/privacy\", internal: true },\n  ],\n};\n\nexport default function Footer() {\n  return (\n    <footer className=\"border-t border-gray-200 bg-white px-4 py-12\">\n      <div className=\"mx-auto max-w-6xl\">\n        <div className=\"grid grid-cols-2 gap-8 md:grid-cols-4\">\n          {/* Brand */}\n          <div className=\"col-span-2 md:col-span-1\">\n            <img src={Logo} alt=\"Karakeep\" className=\"w-32\" />\n            <p className=\"mt-3 text-sm text-gray-500\">\n              The Bookmark Everything App. Save, organize, and rediscover your\n              content.\n            </p>\n          </div>\n\n          {/* Link columns */}\n          {Object.entries(footerLinks).map(([heading, links]) => (\n            <div key={heading}>\n              <h3 className=\"mb-3 text-sm font-semibold text-gray-900\">\n                {heading}\n              </h3>\n              <ul className=\"space-y-2\">\n                {links.map((link) => (\n                  <li key={link.label}>\n                    {\"internal\" in link && link.internal ? (\n                      <Link\n                        to={link.href}\n                        className=\"text-sm text-gray-500 transition-colors hover:text-gray-900\"\n                      >\n                        {link.label}\n                      </Link>\n                    ) : (\n                      <a\n                        href={link.href}\n                        target=\"_blank\"\n                        rel=\"noreferrer\"\n                        className=\"text-sm text-gray-500 transition-colors hover:text-gray-900\"\n                      >\n                        {link.label}\n                      </a>\n                    )}\n                  </li>\n                ))}\n              </ul>\n            </div>\n          ))}\n        </div>\n\n        <div className=\"mt-10 border-t border-gray-200 pt-6 text-center text-sm text-gray-500\">\n          &copy; 2024-{currentYear}{\" \"}\n          <a\n            href=\"https://localhostlabs.co.uk\"\n            target=\"_blank\"\n            rel=\"noreferrer\"\n            className=\"hover:text-gray-900\"\n          >\n            Localhost Labs Ltd\n          </a>\n        </div>\n      </div>\n    </footer>\n  );\n}\n"
  },
  {
    "path": "apps/landing/src/components/Hero.tsx",
    "content": "import { buttonVariants } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport { Github, Star } from \"lucide-react\";\n\nimport { DEMO_LINK, GITHUB_LINK } from \"../constants\";\nimport heroImage from \"/hero.webp?url\";\n\nexport default function Hero() {\n  return (\n    <section className=\"relative overflow-hidden px-4 pb-20 pt-16 sm:pt-24\">\n      <div className=\"animate-fade-in-up mx-auto max-w-5xl text-center\">\n        <h1 className=\"text-3xl font-bold tracking-tight sm:text-5xl md:text-6xl lg:text-7xl\">\n          The{\" \"}\n          <span className=\"bg-gradient-to-r from-purple-600 to-red-600 bg-clip-text text-transparent\">\n            Bookmark Everything\n          </span>{\" \"}\n          App\n        </h1>\n        <p className=\"mx-auto mt-6 max-w-2xl text-lg text-gray-600\">\n          Quickly save links, notes, and images and Karakeep will automatically\n          tag them for you using AI for faster retrieval. Built for the data\n          hoarders out there!\n        </p>\n        <div className=\"mt-6 flex items-center justify-center\">\n          <a\n            href={GITHUB_LINK}\n            target=\"_blank\"\n            rel=\"noreferrer\"\n            className=\"inline-flex items-center gap-2 rounded-full border border-gray-200 bg-white px-4 py-2 text-sm text-gray-700 shadow-sm transition-all hover:border-gray-300 hover:shadow-md\"\n          >\n            <Star className=\"size-4 fill-yellow-400 text-yellow-400\" />\n            <span className=\"font-semibold\">24k+</span>\n            <span className=\"text-gray-500\">stars on GitHub</span>\n          </a>\n        </div>\n        <div className=\"mt-8 flex items-center justify-center gap-4\">\n          <a\n            href={DEMO_LINK}\n            target=\"_blank\"\n            className={cn(\n              \"gap-2 px-8\",\n              buttonVariants({ variant: \"default\", size: \"lg\" }),\n            )}\n            rel=\"noreferrer\"\n          >\n            Try Demo\n          </a>\n          <a\n            href={GITHUB_LINK}\n            target=\"_blank\"\n            className={cn(\n              \"gap-2 px-8\",\n              buttonVariants({ variant: \"outline\", size: \"lg\" }),\n            )}\n            rel=\"noreferrer\"\n          >\n            <Github className=\"size-5\" /> GitHub\n          </a>\n        </div>\n      </div>\n\n      {/* Hero screenshot with browser mockup */}\n      <div className=\"animate-fade-in relative mx-auto mt-16 max-w-screen-2xl px-4\">\n        {/* Glow effect */}\n        <div className=\"animate-glow-pulse absolute -inset-4 rounded-3xl bg-gradient-to-r from-purple-400/20 via-pink-400/20 to-red-400/20 blur-3xl\" />\n\n        <img\n          src={heroImage}\n          alt=\"Karakeep dashboard\"\n          className=\"relative w-full\"\n        />\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "apps/landing/src/components/Layout.tsx",
    "content": "import { Outlet } from \"react-router\";\n\nimport Banner from \"./Banner\";\nimport Footer from \"./Footer\";\nimport NavBar from \"../Navbar\";\n\nexport default function Layout() {\n  return (\n    <div className=\"flex min-h-screen flex-col bg-gray-50\">\n      <Banner />\n      <NavBar />\n      <div className=\"flex-1\">\n        <Outlet />\n      </div>\n      <Footer />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/landing/src/components/OpenSource.tsx",
    "content": "import { buttonVariants } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport { Github } from \"lucide-react\";\n\nimport { DOCS_LINK, GITHUB_LINK } from \"../constants\";\n\nexport default function OpenSource() {\n  return (\n    <section className=\"bg-gray-900 px-4 py-16 sm:py-24\">\n      <div className=\"mx-auto max-w-4xl text-center\">\n        <Github className=\"mx-auto size-12 text-white\" />\n        <h2 className=\"mt-6 text-3xl font-bold tracking-tight text-white sm:text-4xl\">\n          Open Source & Self-Hostable\n        </h2>\n        <p className=\"mx-auto mt-4 max-w-2xl text-lg text-gray-400\">\n          Karakeep is fully open source. Run it on your own server with Docker,\n          keep full control of your data, and contribute to the project.\n        </p>\n\n        <div className=\"mt-8 flex items-center justify-center gap-4\">\n          <a\n            href={GITHUB_LINK}\n            target=\"_blank\"\n            rel=\"noreferrer\"\n            className={cn(\n              \"gap-2 bg-white px-8 text-gray-900 hover:bg-gray-100\",\n              buttonVariants({ size: \"lg\" }),\n            )}\n          >\n            <Github className=\"size-5\" /> View on GitHub\n          </a>\n          <a\n            href={`${DOCS_LINK}/installation/docker`}\n            target=\"_blank\"\n            rel=\"noreferrer\"\n            className=\"inline-flex h-12 items-center justify-center gap-2 rounded-lg border border-gray-600 px-8 text-base font-medium text-white transition-colors hover:bg-gray-800\"\n          >\n            Self-hosting docs\n          </a>\n        </div>\n\n        <div className=\"mt-12 flex flex-wrap items-center justify-center gap-8 text-sm\">\n          <div className=\"text-center\">\n            <div className=\"text-3xl font-bold text-white\">24k+</div>\n            <div className=\"mt-1 text-gray-400\">GitHub Stars</div>\n          </div>\n          <div className=\"h-8 w-px bg-gray-700\" />\n          <div className=\"text-center\">\n            <div className=\"text-3xl font-bold text-white\">150+</div>\n            <div className=\"mt-1 text-gray-400\">Contributors</div>\n          </div>\n        </div>\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "apps/landing/src/components/Platforms.tsx",
    "content": "import { Code2, Globe, Terminal, Webhook } from \"lucide-react\";\n\nimport appStoreBadge from \"/app-store-badge.png?url\";\nimport chromeExtensionBadge from \"/chrome-extension-badge.png?url\";\nimport firefoxAddonBadge from \"/firefox-addon.png?url\";\nimport playStoreBadge from \"/google-play-badge.webp?url\";\n\nconst platforms = [\n  {\n    name: \"iOS\",\n    url: \"https://apps.apple.com/us/app/karakeep-app/id6479258022\",\n    badge: appStoreBadge,\n  },\n  {\n    name: \"Android\",\n    url: \"https://play.google.com/store/apps/details?id=app.hoarder.hoardermobile&pcampaignid=web_share\",\n    badge: playStoreBadge,\n  },\n  {\n    name: \"Chrome Extension\",\n    url: \"https://chromewebstore.google.com/detail/karakeep/kgcjekpmcjjogibpjebkhaanilehneje\",\n    badge: chromeExtensionBadge,\n  },\n  {\n    name: \"Firefox Addon\",\n    url: \"https://addons.mozilla.org/en-US/firefox/addon/karakeep/\",\n    badge: firefoxAddonBadge,\n  },\n];\n\nconst extras = [\n  { icon: Terminal, label: \"CLI\" },\n  { icon: Code2, label: \"REST API\" },\n  { icon: Webhook, label: \"Webhooks\" },\n  { icon: Globe, label: \"Web App\" },\n];\n\nexport default function Platforms() {\n  return (\n    <section className=\"bg-gray-50 px-4 py-16 sm:py-24\">\n      <div className=\"mx-auto max-w-6xl\">\n        <div className=\"text-center\">\n          <h2 className=\"text-3xl font-bold tracking-tight sm:text-4xl\">\n            Apps & Extensions for Seamless Access\n          </h2>\n          <p className=\"mx-auto mt-4 max-w-2xl text-lg text-gray-600\">\n            Access your bookmarks from anywhere with native apps and browser\n            extensions.\n          </p>\n        </div>\n\n        {/* Badges */}\n        <div className=\"mt-10 flex flex-wrap items-center justify-center gap-6\">\n          {platforms.map((platform) => (\n            <a\n              key={platform.name}\n              href={platform.url}\n              target=\"_blank\"\n              rel=\"noreferrer\"\n              className=\"transition-transform hover:scale-105\"\n            >\n              <img\n                src={platform.badge}\n                alt={platform.name}\n                className=\"h-14 w-auto rounded-lg\"\n              />\n            </a>\n          ))}\n        </div>\n\n        {/* Extra integrations */}\n        <div className=\"mt-10 flex flex-wrap items-center justify-center gap-4\">\n          {extras.map((extra) => (\n            <div\n              key={extra.label}\n              className=\"flex items-center gap-2 rounded-full border border-gray-200 bg-white px-4 py-2 text-sm text-gray-700\"\n            >\n              <extra.icon className=\"size-4 text-gray-500\" />\n              {extra.label}\n            </div>\n          ))}\n        </div>\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "apps/landing/src/constants.ts",
    "content": "export const GITHUB_LINK = \"https://github.com/karakeep-app/karakeep\";\nexport const DOCS_LINK = \"https://docs.karakeep.app\";\nexport const DEMO_LINK = \"https://try.karakeep.app\";\nexport const CLOUD_SIGNUP_LINK = \"https://cloud.karakeep.app/signup\";\n"
  },
  {
    "path": "apps/landing/src/main.tsx",
    "content": "import { StrictMode } from \"react\";\nimport App from \"@/src/App\";\nimport { createRoot } from \"react-dom/client\";\n\ncreateRoot(document.getElementById(\"root\")!).render(\n  <StrictMode>\n    <App />\n  </StrictMode>,\n);\n"
  },
  {
    "path": "apps/landing/tailwind.config.ts",
    "content": "import type { Config } from \"tailwindcss\";\n\nimport web from \"@karakeep/tailwind-config/web\";\n\nconst config = {\n  content: web.content,\n  presets: [web],\n  theme: {\n    extend: {\n      keyframes: {\n        \"fade-in-up\": {\n          \"0%\": { opacity: \"0\", transform: \"translateY(20px)\" },\n          \"100%\": { opacity: \"1\", transform: \"translateY(0)\" },\n        },\n        \"fade-in\": {\n          \"0%\": { opacity: \"0\" },\n          \"100%\": { opacity: \"1\" },\n        },\n        \"glow-pulse\": {\n          \"0%, 100%\": { opacity: \"0.4\" },\n          \"50%\": { opacity: \"0.7\" },\n        },\n      },\n      animation: {\n        \"fade-in-up\": \"fade-in-up 0.6s ease-out forwards\",\n        \"fade-in\": \"fade-in 0.8s ease-out forwards\",\n        \"glow-pulse\": \"glow-pulse 3s ease-in-out infinite\",\n      },\n    },\n  },\n} satisfies Config;\n\nexport default config;\n"
  },
  {
    "path": "apps/landing/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@karakeep/tsconfig/base.json\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"./*\"]\n    },\n    \"tsBuildInfoFile\": \"node_modules/.cache/tsbuildinfo.json\"\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\", \"vite.config.ts\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "apps/landing/vite-env.d.ts",
    "content": "/// <reference types=\"vite-plugin-svgr/client\" />\n/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "apps/landing/vite.config.ts",
    "content": "import { fileURLToPath, URL } from \"node:url\";\nimport react from \"@vitejs/plugin-react\";\nimport { defineConfig } from \"vite\";\nimport svgr from \"vite-plugin-svgr\";\n\n// https://vite.dev/config/\nexport default defineConfig({\n  resolve: {\n    alias: {\n      \"@/\": fileURLToPath(new URL(\"./\", import.meta.url)),\n    },\n  },\n  plugins: [react(), svgr()],\n});\n"
  },
  {
    "path": "apps/mcp/.gitignore",
    "content": "dist\n"
  },
  {
    "path": "apps/mcp/.npmignore",
    "content": ".turbo/**\nsrc/**\nvite.config.mts\ntsconfig.json\n"
  },
  {
    "path": "apps/mcp/.oxlintrc.json",
    "content": "{\n  \"$schema\": \"../../node_modules/oxlint/configuration_schema.json\",\n  \"extends\": [\n    \"../../tooling/oxlint/oxlint-base.json\"\n  ],\n  \"env\": {\n    \"builtin\": true,\n    \"commonjs\": true\n  },\n  \"ignorePatterns\": [\n    \"**/*.config.js\",\n    \"**/*.config.cjs\",\n    \"**/.eslintrc.cjs\",\n    \"**/.next\",\n    \"**/dist\",\n    \"**/build\",\n    \"**/pnpm-lock.yaml\"\n  ]\n}\n"
  },
  {
    "path": "apps/mcp/README.md",
    "content": "# Karakeep MCP Server\n\nThis is the Karakeep MCP server, which is a server that can be used to interact\nwith Karakeep from other tools.\n\n## Supported Tools\n\n- Searching bookmarks\n- Adding and removing bookmarks from lists\n- Attaching and detaching tags to bookmarks\n- Creating new lists\n- Creating text and URL bookmarks\n\nCurrently, the MCP server only exposes tools (no resources).\n\n## Usage with Claude Desktop\n\nFrom NPM:\n\n```json\n{\n  \"mcpServers\": {\n    \"karakeep\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"@karakeep/mcp\"\n      ],\n      \"env\": {\n        \"KARAKEEP_API_ADDR\": \"https://<YOUR_SERVER_ADDR>\",\n        \"KARAKEEP_API_KEY\": \"<YOUR_TOKEN>\",\n        \"KARAKEEP_CUSTOM_HEADERS\": \"{\\\"CF-Access-Client-Id\\\": \\\"...\\\", \\\"CF-Access-Client-Secret\\\": \\\"...\\\"}\"\n      }\n    }\n  }\n}\n```\n\nFrom Docker:\n\n```json\n{\n  \"mcpServers\": {\n    \"karakeep\": {\n      \"command\": \"docker\",\n      \"args\": [\n        \"run\",\n        \"-e\",\n        \"KARAKEEP_API_ADDR=https://<YOUR_SERVER_ADDR>\",\n        \"-e\",\n        \"KARAKEEP_API_KEY=<YOUR_TOKEN>\",\n        \"-e\",\n        \"KARAKEEP_CUSTOM_HEADERS={\\\"CF-Access-Client-Id\\\": \\\"...\\\", \\\"CF-Access-Client-Secret\\\": \\\"...\\\"}\",\n        \"ghcr.io/karakeep-app/karakeep-mcp:latest\"\n      ]\n    }\n  }\n}\n```\n"
  },
  {
    "path": "apps/mcp/package.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/package.json\",\n  \"name\": \"@karakeep/mcp\",\n  \"version\": \"0.31.0\",\n  \"description\": \"MCP server for Karakeep\",\n  \"license\": \"GNU Affero General Public License version 3\",\n  \"type\": \"module\",\n  \"keywords\": [\n    \"hoarder\",\n    \"karakeep\",\n    \"mcp\"\n  ],\n  \"bin\": {\n    \"karakeep-mcp\": \"dist/index.js\"\n  },\n  \"devDependencies\": {\n    \"@karakeep/tsconfig\": \"workspace:^0.1.0\",\n    \"@tsconfig/node22\": \"^22.0.0\",\n    \"@types/turndown\": \"^5.0.5\",\n    \"shx\": \"^0.4.0\",\n    \"tsx\": \"^4.8.1\",\n    \"vite\": \"^7.0.6\"\n  },\n  \"scripts\": {\n    \"build\": \"vite build && shx chmod +x dist/index.js\",\n    \"run\": \"tsx src/index.ts\",\n    \"lint\": \"oxlint .\",\n    \"lint:fix\": \"oxlint . --fix\",\n    \"format\": \"oxfmt --check .\",\n    \"format:fix\": \"oxfmt .\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/karakeep-app/karakeep.git\",\n    \"directory\": \"apps/mcp\"\n  },\n  \"dependencies\": {\n    \"@karakeep/sdk\": \"workspace:*\",\n    \"@modelcontextprotocol/sdk\": \"^1.9.0\",\n    \"turndown\": \"^7.2.0\",\n    \"zod\": \"^3.24.2\"\n  }\n}\n"
  },
  {
    "path": "apps/mcp/src/bookmarks.ts",
    "content": "import { CallToolResult } from \"@modelcontextprotocol/sdk/types\";\nimport { z } from \"zod\";\n\nimport { karakeepClient, mcpServer, turndownService } from \"./shared\";\nimport { compactBookmark, toMcpToolError } from \"./utils\";\n\n// Tools\nmcpServer.tool(\n  \"search-bookmarks\",\n  `Search for bookmarks matching a specific a query.\n`,\n  {\n    query: z.string().describe(`\n    By default, this will do a full-text search, but you can also use qualifiers to filter the results.\nYou can search bookmarks using specific qualifiers. is:fav finds favorited bookmarks,\nis:archived searches archived bookmarks, is:tagged finds those with tags,\nis:inlist finds those in lists, and is:link, is:text, and is:media filter by bookmark type.\nurl:<value> searches for URL substrings, #<tag> searches for bookmarks with a specific tag,\nlist:<name> searches for bookmarks in a specific list given its name (without the icon),\nafter:<date> finds bookmarks created on or after a date (YYYY-MM-DD), and before:<date> finds bookmarks created on or before a date (YYYY-MM-DD).\nIf you need to pass names with spaces, you can quote them with double quotes. If you want to negate a qualifier, prefix it with a minus sign.\n## Examples:\n\n### Find favorited bookmarks from 2023 that are tagged \"important\"\nis:fav after:2023-01-01 before:2023-12-31 #important\n\n### Find archived bookmarks that are either in \"reading\" list or tagged \"work\"\nis:archived and (list:reading or #work)\n\n### Combine text search with qualifiers\nmachine learning is:fav`),\n    limit: z\n      .number()\n      .optional()\n      .describe(`The number of results to return in a single query.`)\n      .default(10),\n    nextCursor: z\n      .string()\n      .optional()\n      .describe(\n        `The next cursor to use for pagination. The value for this is returned from a previous call to this tool.`,\n      ),\n  },\n  async ({ query, limit, nextCursor }): Promise<CallToolResult> => {\n    const res = await karakeepClient.GET(\"/bookmarks/search\", {\n      params: {\n        query: {\n          q: query,\n          limit: limit,\n          includeContent: false,\n          cursor: nextCursor,\n        },\n      },\n    });\n    if (!res.data) {\n      return toMcpToolError(res.error);\n    }\n    return {\n      content: [\n        {\n          type: \"text\",\n          text: `\n${res.data.bookmarks.map(compactBookmark).join(\"\\n\\n\")}\n\nNext cursor: ${res.data.nextCursor ? `'${res.data.nextCursor}'` : \"no more pages\"}\n`,\n        },\n      ],\n    };\n  },\n);\n\nmcpServer.tool(\n  \"get-bookmark\",\n  `Get a bookmark by id.`,\n  {\n    bookmarkId: z.string().describe(`The bookmarkId to get.`),\n  },\n  async ({ bookmarkId }): Promise<CallToolResult> => {\n    const res = await karakeepClient.GET(`/bookmarks/{bookmarkId}`, {\n      params: {\n        path: {\n          bookmarkId,\n        },\n        query: {\n          includeContent: false,\n        },\n      },\n    });\n    if (res.error) {\n      return toMcpToolError(res.error);\n    }\n    return {\n      content: [\n        {\n          type: \"text\",\n          text: compactBookmark(res.data),\n        },\n      ],\n    };\n  },\n);\n\nmcpServer.tool(\n  \"create-bookmark\",\n  `Create a link bookmark or a text bookmark`,\n  {\n    type: z.enum([\"link\", \"text\"]).describe(`The type of bookmark to create.`),\n    title: z.string().optional().describe(`The title of the bookmark`),\n    content: z\n      .string()\n      .describe(\n        \"If type is text, the text to be bookmarked. If the type is link, then it's the URL to be bookmarked.\",\n      ),\n  },\n  async ({ title, type, content }): Promise<CallToolResult> => {\n    const res = await karakeepClient.POST(`/bookmarks`, {\n      body: (\n        {\n          link: {\n            type: \"link\",\n            title,\n            url: content,\n          },\n          text: {\n            type: \"text\",\n            title,\n            text: content,\n          },\n        } as const\n      )[type],\n    });\n    if (res.error) {\n      return toMcpToolError(res.error);\n    }\n    return {\n      content: [\n        {\n          type: \"text\",\n          text: compactBookmark(res.data),\n        },\n      ],\n    };\n  },\n);\n\nmcpServer.tool(\n  \"get-bookmark-content\",\n  `Get the content of the bookmark in markdown`,\n  {\n    bookmarkId: z.string().describe(`The bookmarkId to get content for.`),\n  },\n  async ({ bookmarkId }): Promise<CallToolResult> => {\n    const res = await karakeepClient.GET(`/bookmarks/{bookmarkId}`, {\n      params: {\n        path: {\n          bookmarkId,\n        },\n        query: {\n          includeContent: true,\n        },\n      },\n    });\n    if (res.error) {\n      return toMcpToolError(res.error);\n    }\n    let content;\n    if (res.data.content.type === \"link\") {\n      const htmlContent = res.data.content.htmlContent;\n      content = turndownService.turndown(htmlContent);\n    } else if (res.data.content.type === \"text\") {\n      content = res.data.content.text;\n    } else if (res.data.content.type === \"asset\") {\n      content = res.data.content.content;\n    }\n    return {\n      content: [\n        {\n          type: \"text\",\n          text: content ?? \"\",\n        },\n      ],\n    };\n  },\n);\n"
  },
  {
    "path": "apps/mcp/src/index.ts",
    "content": "#!/usr/bin/env node\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\n\nimport { mcpServer } from \"./shared\";\n\nimport \"./bookmarks.ts\";\nimport \"./lists.ts\";\nimport \"./tags.ts\";\n\nasync function run() {\n  // Start receiving messages on stdin and sending messages on stdout\n  const transport = new StdioServerTransport();\n  await mcpServer.connect(transport);\n}\n\nrun();\n"
  },
  {
    "path": "apps/mcp/src/lists.ts",
    "content": "import { CallToolResult } from \"@modelcontextprotocol/sdk/types\";\nimport { z } from \"zod\";\n\nimport { karakeepClient, mcpServer } from \"./shared\";\nimport { toMcpToolError } from \"./utils\";\n\nmcpServer.tool(\n  \"get-lists\",\n  `Retrieves a list of lists.`,\n  async (): Promise<CallToolResult> => {\n    const res = await karakeepClient.GET(\"/lists\", {\n      params: {},\n    });\n    if (!res.data) {\n      return toMcpToolError(res.error);\n    }\n    return {\n      content: [\n        {\n          type: \"text\",\n          text: res.data.lists\n            .map(\n              (list) => `List ID: ${list.id}\nName: ${list.name}\nIcon: ${list.icon}\nDescription: ${list.description ?? \"\"}\nParent ID: ${list.parentId}`,\n            )\n            .join(\"\\n\\n\"),\n        },\n      ],\n    };\n  },\n);\n\nmcpServer.tool(\n  \"add-bookmark-to-list\",\n  `Add a bookmark to a list.`,\n  {\n    listId: z.string().describe(`The listId to add the bookmark to.`),\n    bookmarkId: z.string().describe(`The bookmarkId to add.`),\n  },\n  async ({ listId, bookmarkId }): Promise<CallToolResult> => {\n    const res = await karakeepClient.PUT(\n      `/lists/{listId}/bookmarks/{bookmarkId}`,\n      {\n        params: {\n          path: {\n            listId,\n            bookmarkId,\n          },\n        },\n      },\n    );\n    if (res.error) {\n      return toMcpToolError(res.error);\n    }\n    return {\n      content: [\n        {\n          type: \"text\",\n          text: `Bookmark ${bookmarkId} added to list ${listId}`,\n        },\n      ],\n    };\n  },\n);\n\nmcpServer.tool(\n  \"remove-bookmark-from-list\",\n  `Remove a bookmark from a list.`,\n  {\n    listId: z.string().describe(`The listId to remove the bookmark from.`),\n    bookmarkId: z.string().describe(`The bookmarkId to remove.`),\n  },\n  async ({ listId, bookmarkId }): Promise<CallToolResult> => {\n    const res = await karakeepClient.DELETE(\n      `/lists/{listId}/bookmarks/{bookmarkId}`,\n      {\n        params: {\n          path: {\n            listId,\n            bookmarkId,\n          },\n        },\n      },\n    );\n    if (res.error) {\n      return toMcpToolError(res.error);\n    }\n    return {\n      content: [\n        {\n          type: \"text\",\n          text: `Bookmark ${bookmarkId} removed from list ${listId}`,\n        },\n      ],\n    };\n  },\n);\n\nmcpServer.tool(\n  \"create-list\",\n  `Create a list.`,\n  {\n    name: z.string().describe(`The name of the list.`),\n    icon: z.string().describe(`The emoji icon of the list.`),\n    parentId: z\n      .string()\n      .optional()\n      .describe(`The parent list id of this list.`),\n  },\n  async ({ name, icon, parentId }): Promise<CallToolResult> => {\n    const res = await karakeepClient.POST(\"/lists\", {\n      body: {\n        name,\n        icon,\n        parentId,\n      },\n    });\n    if (res.error) {\n      return toMcpToolError(res.error);\n    }\n    return {\n      content: [\n        {\n          type: \"text\",\n          text: `List ${name} created with id ${res.data.id}`,\n        },\n      ],\n    };\n  },\n);\n"
  },
  {
    "path": "apps/mcp/src/shared.ts",
    "content": "import { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport TurndownService from \"turndown\";\n\nimport { createKarakeepClient } from \"@karakeep/sdk\";\n\nconst addr = process.env.KARAKEEP_API_ADDR;\nconst apiKey = process.env.KARAKEEP_API_KEY;\n\nconst getCustomHeaders = () => {\n  try {\n    return process.env.KARAKEEP_CUSTOM_HEADERS\n      ? JSON.parse(process.env.KARAKEEP_CUSTOM_HEADERS)\n      : {};\n  } catch (e) {\n    console.error(\"Failed to parse KARAKEEP_CUSTOM_HEADERS\", e);\n    return {};\n  }\n};\n\nexport const karakeepClient = createKarakeepClient({\n  baseUrl: `${addr}/api/v1`,\n  headers: {\n    ...getCustomHeaders(),\n    \"Content-Type\": \"application/json\",\n    authorization: `Bearer ${apiKey}`,\n  },\n});\n\nexport const mcpServer = new McpServer({\n  name: \"Karakeep\",\n  version: \"0.23.0\",\n});\n\nexport const turndownService = new TurndownService();\n"
  },
  {
    "path": "apps/mcp/src/tags.ts",
    "content": "import { CallToolResult } from \"@modelcontextprotocol/sdk/types\";\nimport { z } from \"zod\";\n\nimport { karakeepClient, mcpServer } from \"./shared\";\nimport { toMcpToolError } from \"./utils\";\n\nmcpServer.tool(\n  \"attach-tag-to-bookmark\",\n  `Attach a tag to a bookmark.`,\n  {\n    bookmarkId: z.string().describe(`The bookmarkId to attach the tag to.`),\n    tagsToAttach: z.array(z.string()).describe(`The tag names to attach.`),\n  },\n  async ({ bookmarkId, tagsToAttach }): Promise<CallToolResult> => {\n    const res = await karakeepClient.POST(`/bookmarks/{bookmarkId}/tags`, {\n      params: {\n        path: {\n          bookmarkId,\n        },\n      },\n      body: {\n        tags: tagsToAttach.map((tag) => ({ tagName: tag })),\n      },\n    });\n    if (res.error) {\n      return toMcpToolError(res.error);\n    }\n    return {\n      content: [\n        {\n          type: \"text\",\n          text: `Tags ${JSON.stringify(tagsToAttach)} attached to bookmark ${bookmarkId}`,\n        },\n      ],\n    };\n  },\n);\n\nmcpServer.tool(\n  \"detach-tag-from-bookmark\",\n  `Detach a tag from a bookmark.`,\n  {\n    bookmarkId: z.string().describe(`The bookmarkId to detach the tag from.`),\n    tagsToDetach: z.array(z.string()).describe(`The tag names to detach.`),\n  },\n  async ({ bookmarkId, tagsToDetach }): Promise<CallToolResult> => {\n    const res = await karakeepClient.DELETE(`/bookmarks/{bookmarkId}/tags`, {\n      params: {\n        path: {\n          bookmarkId,\n        },\n      },\n      body: {\n        tags: tagsToDetach.map((tag) => ({ tagName: tag })),\n      },\n    });\n    if (res.error) {\n      return toMcpToolError(res.error);\n    }\n    return {\n      content: [\n        {\n          type: \"text\",\n          text: `Tags ${JSON.stringify(tagsToDetach)} detached from bookmark ${bookmarkId}`,\n        },\n      ],\n    };\n  },\n);\n"
  },
  {
    "path": "apps/mcp/src/utils.ts",
    "content": "import { CallToolResult } from \"@modelcontextprotocol/sdk/types\";\n\nimport { KarakeepAPISchemas } from \"@karakeep/sdk\";\n\nexport function toMcpToolError(\n  error: { code: string; message: string } | undefined,\n): CallToolResult {\n  return {\n    isError: true,\n    content: [\n      {\n        type: \"text\",\n        text: error ? JSON.stringify(error) : `Something went wrong`,\n      },\n    ],\n  };\n}\n\nexport function compactBookmark(\n  bookmark: KarakeepAPISchemas[\"Bookmark\"],\n): string {\n  let content: string;\n  if (bookmark.content.type === \"link\") {\n    content = `Bookmark type: link\nBookmarked URL: ${bookmark.content.url}\ndescription: ${bookmark.content.description ?? \"\"}\nauthor: ${bookmark.content.author ?? \"\"}\npublisher: ${bookmark.content.publisher ?? \"\"}`;\n  } else if (bookmark.content.type === \"text\") {\n    content = `Bookmark type: text\n  Source URL: ${bookmark.content.sourceUrl ?? \"\"}`;\n  } else if (bookmark.content.type === \"asset\") {\n    content = `Bookmark type: media\nAsset ID: ${bookmark.content.assetId}\nAsset type: ${bookmark.content.assetType}\nSource URL: ${bookmark.content.sourceUrl ?? \"\"}`;\n  } else {\n    content = `Bookmark type: unknown`;\n  }\n\n  return `Bookmark ID: ${bookmark.id}\n  Created at: ${bookmark.createdAt}\n  Title: ${\n    bookmark.title\n      ? bookmark.title\n      : ((bookmark.content.type === \"link\" ? bookmark.content.title : \"\") ?? \"\")\n  }\n  Summary: ${bookmark.summary ?? \"\"}\n  Note: ${bookmark.note ?? \"\"}\n  ${content}\n  Tags: ${bookmark.tags.map((t) => t.name).join(\", \")}`;\n}\n"
  },
  {
    "path": "apps/mcp/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@karakeep/tsconfig/node.json\",\n  \"include\": [\"src\", \"vite.config.mts\"],\n  \"exclude\": [\"node_modules\", \"dist\"],\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"tsBuildInfoFile\": \"node_modules/.cache/tsbuildinfo.json\",\n    \"strictNullChecks\": true,\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    },\n    \"types\": [\"vite/client\"]\n  }\n}\n"
  },
  {
    "path": "apps/mcp/vite.config.mts",
    "content": "// This file is shamelessly copied from immich's CLI vite config\n// https://github.com/immich-app/immich/blob/main/cli/vite.config.ts\nimport { defineConfig } from \"vite\";\nimport tsconfigPaths from \"vite-tsconfig-paths\";\n\nexport default defineConfig({\n  build: {\n    rollupOptions: {\n      input: \"src/index.ts\",\n      output: {\n        dir: \"dist\",\n      },\n    },\n    commonjsOptions: {\n      transformMixedEsModules: true,\n    },\n    ssr: true,\n  },\n  ssr: {\n    // bundle everything except for Node built-ins\n    noExternal: /^(?!node:).*$/,\n  },\n  plugins: [tsconfigPaths()],\n});\n"
  },
  {
    "path": "apps/mobile/.gitignore",
    "content": "# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files\n\n# dependencies\nnode_modules/\n\n# Expo\n.expo/\ndist/\nweb-build/\n\n# Native\n*.orig.*\n*.jks\n*.p8\n*.p12\n*.key\n*.mobileprovision\n\n# Metro\n.metro-health-check*\n\n# debug\nnpm-debug.*\nyarn-debug.*\nyarn-error.*\n\n# macOS\n.DS_Store\n*.pem\n\n# local env files\n.env*.local\n\n# typescript\n*.tsbuildinfo\n\n#build files\nios/\nandroid/\n\n.env.local\n"
  },
  {
    "path": "apps/mobile/.npmrc",
    "content": "node-linker=hoisted\n"
  },
  {
    "path": "apps/mobile/.oxlintrc.json",
    "content": "{\n  \"$schema\": \"../../node_modules/oxlint/configuration_schema.json\",\n  \"extends\": [\n    \"../../tooling/oxlint/oxlint-base.json\",\n    \"../../tooling/oxlint/oxlint-react.json\"\n  ],\n  \"env\": {\n    \"builtin\": true,\n    \"commonjs\": true,\n    \"browser\": true,\n    \"es2022\": true,\n    \"node\": true\n  },\n  \"globals\": {\n    \"React\": \"writeable\"\n  },\n  \"settings\": {\n    \"react\": {\n      \"version\": \"19\"\n    }\n  },\n  \"ignorePatterns\": [\n    \"**/*.config.js\",\n    \"**/*.config.cjs\",\n    \"**/.eslintrc.cjs\",\n    \"tailwind.config.ts\",\n    \".next\",\n    \"dist\",\n    \"build\",\n    \"pnpm-lock.yaml\",\n    \"expo-plugins/**\",\n    \"ios/**\",\n    \"android/**\",\n    \"plugins/**\"\n  ]\n}\n"
  },
  {
    "path": "apps/mobile/app/+not-found.tsx",
    "content": "import { View } from \"react-native\";\n\n// This is kinda important given that the sharing modal always resolve to an unknown route\nexport default function NotFound() {\n  return <View />;\n}\n"
  },
  {
    "path": "apps/mobile/app/_layout.tsx",
    "content": "import \"@/globals.css\";\nimport \"expo-dev-client\";\n\nimport { useEffect } from \"react\";\nimport { Platform } from \"react-native\";\nimport { GestureHandlerRootView } from \"react-native-gesture-handler\";\nimport { KeyboardProvider } from \"react-native-keyboard-controller\";\nimport { SafeAreaProvider } from \"react-native-safe-area-context\";\nimport { useRouter } from \"expo-router\";\nimport { Stack } from \"expo-router/stack\";\nimport { ShareIntentProvider, useShareIntent } from \"expo-share-intent\";\nimport { StatusBar } from \"expo-status-bar\";\nimport { StyledStack } from \"@/components/navigation/stack\";\nimport SplashScreenController from \"@/components/SplashScreenController\";\nimport { Providers } from \"@/lib/providers\";\nimport { useColorScheme, useInitialAndroidBarSync } from \"@/lib/useColorScheme\";\nimport { cn } from \"@/lib/utils\";\nimport { NAV_THEME } from \"@/theme\";\nimport { ThemeProvider as NavThemeProvider } from \"@react-navigation/native\";\nimport * as Sentry from \"@sentry/react-native\";\n\nSentry.init({\n  dsn: \"https://a61d93ed65066ed54c8566ba6b6a01d2@o4511008866172928.ingest.de.sentry.io/4511008868270160\",\n\n  // Adds more context data to events (IP address, cookies, user, etc.)\n  // For more information, visit: https://docs.sentry.io/platforms/react-native/data-management/data-collected/\n  sendDefaultPii: false,\n\n  // Enable Logs\n  enableLogs: false,\n});\n\nexport default Sentry.wrap(function RootLayout() {\n  useInitialAndroidBarSync();\n  const router = useRouter();\n  const { hasShareIntent } = useShareIntent();\n  const { colorScheme } = useColorScheme();\n\n  useEffect(() => {\n    if (hasShareIntent) {\n      router.replace({\n        pathname: \"sharing\",\n      });\n    }\n  }, [hasShareIntent]);\n\n  return (\n    <SafeAreaProvider>\n      <KeyboardProvider\n        statusBarTranslucent={Platform.OS !== \"android\" ? true : undefined}\n        navigationBarTranslucent={Platform.OS !== \"android\" ? true : undefined}\n      >\n        <NavThemeProvider value={NAV_THEME[colorScheme]}>\n          <SplashScreenController />\n          <StyledStack\n            layout={(props) => {\n              return (\n                <GestureHandlerRootView style={{ flex: 1 }}>\n                  <ShareIntentProvider>\n                    <Providers>{props.children}</Providers>\n                  </ShareIntentProvider>\n                </GestureHandlerRootView>\n              );\n            }}\n            contentClassName={cn(\n              \"w-full flex-1 bg-gray-100 text-foreground dark:bg-background\",\n              colorScheme == \"dark\" ? \"dark\" : \"light\",\n            )}\n            screenOptions={{\n              ...Platform.select({\n                ios: {\n                  headerTransparent: true,\n                  headerBlurEffect: \"systemMaterial\",\n                  headerLargeTitle: true,\n                  headerLargeTitleShadowVisible: false,\n                  headerLargeStyle: { backgroundColor: \"transparent\" },\n                },\n              }),\n              headerShadowVisible: false,\n            }}\n          >\n            <Stack.Screen\n              name=\"dashboard\"\n              options={{\n                headerShown: false,\n              }}\n            />\n            <Stack.Screen\n              name=\"index\"\n              options={{\n                headerShown: false,\n              }}\n            />\n            <Stack.Screen\n              name=\"signin\"\n              options={{\n                headerShown: true,\n                headerBackVisible: true,\n                headerBackTitle: \"Back\",\n                title: \"\",\n              }}\n            />\n            <Stack.Screen\n              name=\"sharing\"\n              options={{\n                headerShown: false,\n              }}\n            />\n            <Stack.Screen\n              name=\"+not-found\"\n              options={{\n                headerShown: false,\n              }}\n            />\n            <Stack.Screen\n              name=\"server-address\"\n              options={{\n                title: \"Server Address\",\n                headerShown: true,\n                headerTransparent: false,\n                headerLargeTitle: false,\n                presentation: Platform.select({\n                  ios: \"formSheet\" as const,\n                  default: \"modal\" as const,\n                }),\n              }}\n            />\n            <Stack.Screen\n              name=\"test-connection\"\n              options={{\n                title: \"Test Connection\",\n                headerShown: true,\n                headerTransparent: false,\n                headerLargeTitle: false,\n                presentation: Platform.select({\n                  ios: \"formSheet\" as const,\n                  default: \"modal\" as const,\n                }),\n              }}\n            />\n          </StyledStack>\n        </NavThemeProvider>\n      </KeyboardProvider>\n      <StatusBar style=\"auto\" />\n    </SafeAreaProvider>\n  );\n});\n"
  },
  {
    "path": "apps/mobile/app/dashboard/(tabs)/(highlights)/_layout.tsx",
    "content": "import { Platform } from \"react-native\";\nimport { Stack } from \"expo-router/stack\";\n\nexport default function Layout() {\n  return (\n    <Stack\n      screenOptions={{\n        ...Platform.select({\n          ios: {\n            headerLargeTitle: true,\n            headerTransparent: true,\n            headerBlurEffect: \"systemMaterial\",\n            headerLargeTitleShadowVisible: false,\n            headerLargeStyle: { backgroundColor: \"transparent\" },\n          },\n          android: {\n            headerStyle: {\n              backgroundColor: \"transparent\",\n            },\n            contentStyle: {\n              // Manual padding to avoid the native tabbar until expo fixes this in sdk 55.\n              paddingBottom: 100,\n            },\n          },\n        }),\n        headerShadowVisible: false,\n      }}\n    >\n      <Stack.Screen name=\"index\" options={{ title: \"Highlights\" }} />\n    </Stack>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/app/dashboard/(tabs)/(highlights)/index.tsx",
    "content": "import FullPageError from \"@/components/FullPageError\";\nimport HighlightList from \"@/components/highlights/HighlightList\";\nimport FullPageSpinner from \"@/components/ui/FullPageSpinner\";\nimport { useInfiniteQuery, useQueryClient } from \"@tanstack/react-query\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\nexport default function Highlights() {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n  const {\n    data,\n    isPending,\n    isPlaceholderData,\n    error,\n    fetchNextPage,\n    isFetchingNextPage,\n    refetch,\n  } = useInfiniteQuery(\n    api.highlights.getAll.infiniteQueryOptions(\n      {},\n      {\n        initialCursor: null,\n        getNextPageParam: (lastPage) => lastPage.nextCursor,\n      },\n    ),\n  );\n\n  if (error) {\n    return <FullPageError error={error.message} onRetry={() => refetch()} />;\n  }\n\n  if (isPending || !data) {\n    return <FullPageSpinner />;\n  }\n\n  const onRefresh = () => {\n    queryClient.invalidateQueries(api.highlights.getAll.pathFilter());\n  };\n\n  return (\n    <HighlightList\n      highlights={data.pages.flatMap((p) => p.highlights)}\n      onRefresh={onRefresh}\n      fetchNextPage={fetchNextPage}\n      isFetchingNextPage={isFetchingNextPage}\n      isRefreshing={isPending || isPlaceholderData}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/mobile/app/dashboard/(tabs)/(home)/_layout.tsx",
    "content": "import { Platform } from \"react-native\";\nimport { Stack } from \"expo-router/stack\";\n\nexport default function Layout() {\n  return (\n    <Stack\n      screenOptions={{\n        ...Platform.select({\n          ios: {\n            headerLargeTitle: true,\n            headerTransparent: true,\n            headerBlurEffect: \"systemMaterial\",\n            headerLargeTitleShadowVisible: false,\n            headerLargeStyle: { backgroundColor: \"transparent\" },\n          },\n          android: {\n            headerStyle: {\n              backgroundColor: \"transparent\",\n            },\n            contentStyle: {\n              // Manual padding to avoid the native tabbar until expo fixes this in sdk 55.\n              paddingBottom: 100,\n            },\n          },\n        }),\n        headerShadowVisible: false,\n      }}\n    >\n      <Stack.Screen name=\"index\" options={{ title: \"Home\" }} />\n    </Stack>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/app/dashboard/(tabs)/(home)/index.tsx",
    "content": "import { useRef } from \"react\";\nimport { Platform, Pressable, View } from \"react-native\";\nimport * as Haptics from \"expo-haptics\";\nimport * as ImagePicker from \"expo-image-picker\";\nimport { router, Stack } from \"expo-router\";\nimport UpdatingBookmarkList from \"@/components/bookmarks/UpdatingBookmarkList\";\nimport { TailwindResolver } from \"@/components/TailwindResolver\";\nimport { Text } from \"@/components/ui/Text\";\nimport useAppSettings from \"@/lib/settings\";\nimport { useUploadAsset } from \"@/lib/upload\";\nimport { useMenuIconColors } from \"@/lib/useMenuIconColors\";\nimport { MenuView } from \"@react-native-menu/menu\";\nimport { Plus, Search } from \"lucide-react-native\";\nimport { toast as sonnerToast } from \"sonner-native\";\n\nfunction HeaderRight({\n  openNewBookmarkModal,\n}: {\n  openNewBookmarkModal: () => void;\n}) {\n  const { settings } = useAppSettings();\n  const { menuIconColor } = useMenuIconColors();\n  const uploadToastIdRef = useRef<string | number | null>(null);\n  const { uploadAsset } = useUploadAsset(settings, {\n    onSuccess: () => {\n      if (uploadToastIdRef.current !== null) {\n        sonnerToast.success(\"Image saved!\", { id: uploadToastIdRef.current });\n        uploadToastIdRef.current = null;\n      }\n    },\n    onError: (e) => {\n      if (uploadToastIdRef.current !== null) {\n        sonnerToast.error(e, { id: uploadToastIdRef.current });\n        uploadToastIdRef.current = null;\n      } else {\n        sonnerToast.error(e);\n      }\n    },\n  });\n  return (\n    <MenuView\n      onPressAction={async ({ nativeEvent }) => {\n        Haptics.selectionAsync();\n        if (nativeEvent.event === \"new\") {\n          openNewBookmarkModal();\n        } else if (nativeEvent.event === \"library\") {\n          try {\n            const result = await ImagePicker.launchImageLibraryAsync({\n              mediaTypes: [\"images\"],\n              quality: settings.imageQuality,\n              allowsMultipleSelection: false,\n            });\n            if (!result.canceled) {\n              const asset = result.assets[0];\n              if (!asset) {\n                uploadToastIdRef.current = null;\n                return;\n              }\n              uploadToastIdRef.current =\n                sonnerToast.loading(\"Uploading image...\");\n              uploadAsset({\n                type: asset.mimeType ?? \"\",\n                name: asset.fileName ?? \"\",\n                uri: asset.uri,\n              });\n            }\n          } catch {\n            sonnerToast.error(\"Failed to open photo library\", {\n              id:\n                uploadToastIdRef.current !== null\n                  ? uploadToastIdRef.current\n                  : undefined,\n            });\n            uploadToastIdRef.current = null;\n          }\n        }\n      }}\n      actions={[\n        {\n          id: \"new\",\n          title: \"New Bookmark\",\n          image: Platform.select({\n            ios: \"square.and.pencil\",\n          }),\n          imageColor: Platform.select({\n            ios: menuIconColor,\n          }),\n        },\n        {\n          id: \"library\",\n          title: \"Photo Library\",\n          image: Platform.select({\n            ios: \"photo\",\n          }),\n          imageColor: Platform.select({\n            ios: menuIconColor,\n          }),\n        },\n      ]}\n      shouldOpenOnLongPress={false}\n    >\n      <View className=\"my-auto px-4\">\n        <Plus\n          color=\"rgb(0, 122, 255)\"\n          onPress={() => Haptics.selectionAsync()}\n        />\n      </View>\n    </MenuView>\n  );\n}\n\nexport default function Home() {\n  return (\n    <>\n      <Stack.Screen\n        options={{\n          headerRight: () => (\n            <HeaderRight\n              openNewBookmarkModal={() =>\n                router.push(\"/dashboard/bookmarks/new\")\n              }\n            />\n          ),\n        }}\n      />\n      <UpdatingBookmarkList\n        query={{ archived: false }}\n        header={\n          <Pressable\n            className=\"flex flex-row items-center gap-1 rounded-lg border border-input bg-card px-4 py-1\"\n            onPress={() => router.push(\"/dashboard/search\")}\n          >\n            <TailwindResolver\n              className=\"text-muted\"\n              comp={(styles) => (\n                <Search size={16} color={styles?.color?.toString()} />\n              )}\n            />\n            <Text className=\"flex-1 text-muted\" numberOfLines={1}>\n              Search\n            </Text>\n          </Pressable>\n        }\n      />\n    </>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/app/dashboard/(tabs)/(lists)/_layout.tsx",
    "content": "import { Platform } from \"react-native\";\nimport { Stack } from \"expo-router/stack\";\n\nexport default function Layout() {\n  return (\n    <Stack\n      screenOptions={{\n        ...Platform.select({\n          ios: {\n            headerLargeTitle: true,\n            headerTransparent: true,\n            headerBlurEffect: \"systemMaterial\",\n            headerLargeTitleShadowVisible: false,\n            headerLargeStyle: { backgroundColor: \"transparent\" },\n          },\n          android: {\n            headerStyle: {\n              backgroundColor: \"transparent\",\n            },\n            contentStyle: {\n              // Manual padding to avoid the native tabbar until expo fixes this in sdk 55.\n              paddingBottom: 100,\n            },\n          },\n        }),\n        headerShadowVisible: false,\n      }}\n    >\n      <Stack.Screen name=\"index\" options={{ title: \"Lists\" }} />\n    </Stack>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/app/dashboard/(tabs)/(lists)/index.tsx",
    "content": "import { useEffect, useMemo, useState } from \"react\";\nimport { FlatList, Pressable, View } from \"react-native\";\nimport * as Haptics from \"expo-haptics\";\nimport { Link, router, Stack } from \"expo-router\";\nimport FullPageError from \"@/components/FullPageError\";\nimport ChevronRight from \"@/components/ui/ChevronRight\";\nimport FullPageSpinner from \"@/components/ui/FullPageSpinner\";\nimport { Text } from \"@/components/ui/Text\";\nimport { useColorScheme } from \"@/lib/useColorScheme\";\nimport { condProps } from \"@/lib/utils\";\nimport { useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { Plus } from \"lucide-react-native\";\n\nimport { useBookmarkLists } from \"@karakeep/shared-react/hooks/lists\";\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\nimport { ZBookmarkListTreeNode } from \"@karakeep/shared/utils/listUtils\";\n\nfunction HeaderRight({ openNewListModal }: { openNewListModal: () => void }) {\n  return (\n    <Pressable\n      className=\"my-auto px-4\"\n      onPress={() => {\n        Haptics.selectionAsync();\n        openNewListModal();\n      }}\n    >\n      <Plus color=\"rgb(0, 122, 255)\" />\n    </Pressable>\n  );\n}\n\ninterface ListLink {\n  id: string;\n  logo: string;\n  name: string;\n  href: string;\n  level: number;\n  parent?: string;\n  numChildren: number;\n  collapsed: boolean;\n  isSharedSection?: boolean;\n  numBookmarks?: number;\n}\n\nfunction traverseTree(\n  node: ZBookmarkListTreeNode,\n  links: ListLink[],\n  showChildrenOf: Record<string, boolean>,\n  listStats?: Map<string, number>,\n  parent?: string,\n  level = 0,\n) {\n  links.push({\n    id: node.item.id,\n    logo: node.item.icon,\n    name: node.item.name,\n    href: `/dashboard/lists/${node.item.id}`,\n    level,\n    parent,\n    numChildren: node.children?.length ?? 0,\n    collapsed: !showChildrenOf[node.item.id],\n    numBookmarks: listStats?.get(node.item.id),\n  });\n\n  if (node.children && showChildrenOf[node.item.id]) {\n    node.children.forEach((child) =>\n      traverseTree(\n        child,\n        links,\n        showChildrenOf,\n        listStats,\n        node.item.id,\n        level + 1,\n      ),\n    );\n  }\n}\n\nexport default function Lists() {\n  const { colors } = useColorScheme();\n  const [refreshing, setRefreshing] = useState(false);\n  const { data: lists, isPending, error, refetch } = useBookmarkLists();\n  const [showChildrenOf, setShowChildrenOf] = useState<Record<string, boolean>>(\n    {},\n  );\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n  const { data: listStats } = useQuery(api.lists.stats.queryOptions());\n\n  // Check if there are any shared lists\n  const hasSharedLists = useMemo(() => {\n    return lists?.data.some((list) => list.userRole !== \"owner\") ?? false;\n  }, [lists?.data]);\n\n  // Check if any list has children to determine if we need chevron spacing\n  const hasAnyListsWithChildren = useMemo(() => {\n    const checkForChildren = (node: ZBookmarkListTreeNode): boolean => {\n      if (node.children && node.children.length > 0) return true;\n      return false;\n    };\n    return (\n      Object.values(lists?.root ?? {}).some(checkForChildren) || hasSharedLists\n    );\n  }, [lists?.root, hasSharedLists]);\n\n  useEffect(() => {\n    setRefreshing(isPending);\n  }, [isPending]);\n\n  if (error) {\n    return <FullPageError error={error.message} onRetry={() => refetch()} />;\n  }\n\n  if (!lists) {\n    return <FullPageSpinner />;\n  }\n\n  const onRefresh = () => {\n    queryClient.invalidateQueries(api.lists.list.pathFilter());\n    queryClient.invalidateQueries(api.lists.stats.pathFilter());\n  };\n\n  const links: ListLink[] = [\n    {\n      id: \"fav\",\n      logo: \"⭐️\",\n      name: \"Favourites\",\n      href: \"/dashboard/favourites\",\n      level: 0,\n      numChildren: 0,\n      collapsed: false,\n    },\n    {\n      id: \"arch\",\n      logo: \"🗄️\",\n      name: \"Archive\",\n      href: \"/dashboard/archive\",\n      level: 0,\n      numChildren: 0,\n      collapsed: false,\n    },\n  ];\n\n  // Add shared lists section if there are any\n  if (hasSharedLists) {\n    // Count shared lists to determine if section has children\n    const sharedListsCount = Object.values(lists.root).filter(\n      (list) => list.item.userRole !== \"owner\",\n    ).length;\n\n    links.push({\n      id: \"shared-section\",\n      logo: \"👥\",\n      name: \"Shared Lists\",\n      href: \"#\",\n      level: 0,\n      numChildren: sharedListsCount,\n      collapsed: !showChildrenOf[\"shared-section\"],\n      isSharedSection: true,\n    });\n\n    // Add shared lists as children if section is expanded\n    if (showChildrenOf[\"shared-section\"]) {\n      Object.values(lists.root).forEach((list) => {\n        if (list.item.userRole !== \"owner\") {\n          traverseTree(\n            list,\n            links,\n            showChildrenOf,\n            listStats?.stats,\n            \"shared-section\",\n            1,\n          );\n        }\n      });\n    }\n  }\n\n  // Add owned lists only\n  Object.values(lists.root).forEach((list) => {\n    if (list.item.userRole === \"owner\") {\n      traverseTree(list, links, showChildrenOf, listStats?.stats);\n    }\n  });\n\n  return (\n    <>\n      <Stack.Screen\n        options={{\n          headerRight: () => (\n            <HeaderRight\n              openNewListModal={() => router.push(\"/dashboard/lists/new\")}\n            />\n          ),\n        }}\n      />\n      <FlatList\n        className=\"h-full\"\n        contentInsetAdjustmentBehavior=\"automatic\"\n        contentContainerStyle={{\n          gap: 6,\n          paddingBottom: 20,\n        }}\n        renderItem={(l) => (\n          <View\n            className=\"mx-2 flex flex-row items-center rounded-xl bg-card px-4 py-2\"\n            style={{\n              borderCurve: \"continuous\",\n              ...condProps({\n                condition: l.item.level > 0,\n                props: { marginLeft: l.item.level * 20 },\n              }),\n            }}\n          >\n            {hasAnyListsWithChildren && (\n              <View style={{ width: 32 }}>\n                {l.item.numChildren > 0 && (\n                  <Pressable\n                    className=\"pr-2\"\n                    onPress={() => {\n                      setShowChildrenOf((prev) => ({\n                        ...prev,\n                        [l.item.id]: !prev[l.item.id],\n                      }));\n                    }}\n                  >\n                    <ChevronRight\n                      color={colors.foreground}\n                      style={{\n                        transform: [\n                          { rotate: l.item.collapsed ? \"0deg\" : \"90deg\" },\n                        ],\n                      }}\n                    />\n                  </Pressable>\n                )}\n              </View>\n            )}\n\n            {l.item.isSharedSection ? (\n              <Pressable\n                className=\"flex flex-1 flex-row items-center justify-between\"\n                onPress={() => {\n                  setShowChildrenOf((prev) => ({\n                    ...prev,\n                    [l.item.id]: !prev[l.item.id],\n                  }));\n                }}\n              >\n                <Text className=\"mr-2 flex-1\" numberOfLines={1}>\n                  {l.item.logo} {l.item.name}\n                </Text>\n              </Pressable>\n            ) : (\n              <Link\n                asChild\n                key={l.item.id}\n                href={l.item.href}\n                className=\"flex-1\"\n              >\n                <Pressable className=\"flex flex-row items-center justify-between\">\n                  <Text className=\"mr-2 flex-1\" numberOfLines={1}>\n                    {l.item.logo} {l.item.name}\n                  </Text>\n                  <View className=\"flex flex-row items-center\">\n                    {l.item.numBookmarks !== undefined && (\n                      <Text className=\"mr-2 text-xs text-muted-foreground\">\n                        {l.item.numBookmarks}\n                      </Text>\n                    )}\n                    <ChevronRight />\n                  </View>\n                </Pressable>\n              </Link>\n            )}\n          </View>\n        )}\n        data={links}\n        refreshing={refreshing}\n        onRefresh={onRefresh}\n      />\n    </>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/app/dashboard/(tabs)/(settings)/_layout.tsx",
    "content": "import { Platform } from \"react-native\";\nimport { Stack } from \"expo-router/stack\";\n\nexport default function Layout() {\n  return (\n    <Stack\n      screenOptions={{\n        ...Platform.select({\n          ios: {\n            headerLargeTitle: true,\n            headerTransparent: true,\n            headerBlurEffect: \"systemMaterial\",\n            headerLargeTitleShadowVisible: false,\n            headerLargeStyle: { backgroundColor: \"transparent\" },\n          },\n          android: {\n            headerStyle: {\n              backgroundColor: \"transparent\",\n            },\n            contentStyle: {\n              // Manual padding to avoid the native tabbar until expo fixes this in sdk 55.\n              paddingBottom: 100,\n            },\n          },\n        }),\n        headerShadowVisible: false,\n      }}\n    >\n      <Stack.Screen name=\"index\" options={{ title: \"Settings\" }} />\n    </Stack>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/app/dashboard/(tabs)/(settings)/index.tsx",
    "content": "import { useEffect, useState } from \"react\";\nimport {\n  ActivityIndicator,\n  Alert,\n  Modal,\n  Pressable,\n  ScrollView,\n  Switch,\n  TextInput,\n  View,\n} from \"react-native\";\nimport Slider from \"@react-native-community/slider\";\nimport Constants from \"expo-constants\";\nimport { Link } from \"expo-router\";\nimport { UserProfileHeader } from \"@/components/settings/UserProfileHeader\";\nimport ChevronRight from \"@/components/ui/ChevronRight\";\nimport { Divider } from \"@/components/ui/Divider\";\nimport { Text } from \"@/components/ui/Text\";\nimport { useServerVersion } from \"@/lib/hooks\";\nimport { useSession } from \"@/lib/session\";\nimport useAppSettings from \"@/lib/settings\";\nimport { useMutation, useQuery } from \"@tanstack/react-query\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\nfunction SectionHeader({ title }: { title: string }) {\n  return (\n    <Text className=\"px-4 pb-1 pt-4 text-xs uppercase tracking-wide text-muted-foreground\">\n      {title}\n    </Text>\n  );\n}\n\nexport default function Settings() {\n  const { logout } = useSession();\n  const {\n    settings,\n    setSettings,\n    isLoading: isSettingsLoading,\n  } = useAppSettings();\n  const api = useTRPC();\n\n  const [imageQuality, setImageQuality] = useState<number | null>(null);\n\n  useEffect(() => {\n    setImageQuality(settings.imageQuality * 100);\n  }, [settings.imageQuality]);\n\n  const { data, error } = useQuery(api.users.whoami.queryOptions());\n  const {\n    data: serverVersion,\n    isLoading: isServerVersionLoading,\n    error: serverVersionError,\n  } = useServerVersion();\n\n  const [showPasswordModal, setShowPasswordModal] = useState(false);\n  const [password, setPassword] = useState(\"\");\n\n  const { mutate: deleteAccount, isPending: isDeleting } = useMutation(\n    api.users.deleteAccount.mutationOptions({\n      onSuccess: () => {\n        setShowPasswordModal(false);\n        setPassword(\"\");\n        Alert.alert(\n          \"Account Deleted\",\n          \"Your account has been successfully deleted.\",\n          [{ text: \"OK\", onPress: logout }],\n        );\n      },\n      onError: (e) => {\n        if (e.data?.code === \"UNAUTHORIZED\") {\n          Alert.alert(\"Error\", \"Invalid password. Please try again.\");\n        } else {\n          Alert.alert(\"Error\", \"Failed to delete account. Please try again.\");\n        }\n      },\n    }),\n  );\n\n  const isLocalUser = data?.localUser ?? false;\n\n  const handleDeleteAccount = () => {\n    Alert.alert(\n      \"Delete Account\",\n      \"Are you sure you want to delete your account? All your bookmarks, lists, tags, highlights, and other data will be permanently deleted. This action cannot be undone.\",\n      [\n        { text: \"Cancel\", style: \"cancel\" },\n        {\n          text: \"Delete\",\n          style: \"destructive\",\n          onPress: () => {\n            if (isLocalUser) {\n              setShowPasswordModal(true);\n            } else {\n              deleteAccount({});\n            }\n          },\n        },\n      ],\n    );\n  };\n\n  if (error?.data?.code === \"UNAUTHORIZED\") {\n    logout();\n  }\n\n  return (\n    <ScrollView\n      contentInsetAdjustmentBehavior=\"automatic\"\n      contentContainerStyle={{ paddingHorizontal: 16, paddingBottom: 40 }}\n    >\n      <UserProfileHeader\n        image={data?.image}\n        name={data?.name}\n        email={data?.email}\n      />\n\n      <SectionHeader title=\"Appearance\" />\n      <View\n        className=\"w-full rounded-xl bg-card py-2\"\n        style={{ borderCurve: \"continuous\" }}\n      >\n        <View className=\"flex flex-row items-center justify-between gap-8 px-4 py-1\">\n          <Link asChild href=\"/dashboard/settings/theme\" className=\"flex-1\">\n            <Pressable className=\"flex flex-row items-center\">\n              <Text className=\"mr-2 flex-1\" numberOfLines={1}>\n                Theme\n              </Text>\n              <Text className=\"mr-1 text-muted-foreground\" numberOfLines={1}>\n                {\n                  { light: \"Light\", dark: \"Dark\", system: \"System\" }[\n                    settings.theme\n                  ]\n                }\n              </Text>\n              <ChevronRight />\n            </Pressable>\n          </Link>\n        </View>\n        <Divider orientation=\"horizontal\" className=\"mx-6 my-1\" />\n        <View className=\"flex flex-row items-center justify-between gap-8 px-4 py-1\">\n          <Link\n            asChild\n            href=\"/dashboard/settings/bookmark-default-view\"\n            className=\"flex-1\"\n          >\n            <Pressable className=\"flex flex-row items-center\">\n              <Text className=\"mr-2 flex-1\" numberOfLines={1}>\n                Default Bookmark View\n              </Text>\n              {isSettingsLoading ? (\n                <ActivityIndicator size=\"small\" />\n              ) : (\n                <Text className=\"mr-1 text-muted-foreground\" numberOfLines={1}>\n                  {\n                    {\n                      reader: \"Reader\",\n                      browser: \"Browser\",\n                      externalBrowser: \"External Browser\",\n                    }[settings.defaultBookmarkView]\n                  }\n                </Text>\n              )}\n              <ChevronRight />\n            </Pressable>\n          </Link>\n        </View>\n      </View>\n\n      <SectionHeader title=\"Reading\" />\n      <View\n        className=\"w-full rounded-xl bg-card py-2\"\n        style={{ borderCurve: \"continuous\" }}\n      >\n        <View className=\"flex flex-row items-center justify-between gap-8 px-4 py-1\">\n          <Link\n            asChild\n            href=\"/dashboard/settings/reader-settings\"\n            className=\"flex-1\"\n          >\n            <Pressable className=\"flex flex-row items-center\">\n              <Text className=\"mr-2 flex-1\" numberOfLines={1}>\n                Reader Text Settings\n              </Text>\n              <ChevronRight />\n            </Pressable>\n          </Link>\n        </View>\n        <Divider orientation=\"horizontal\" className=\"mx-6 my-1\" />\n        <View className=\"flex flex-row items-center justify-between gap-8 px-4 py-1\">\n          <Text className=\"flex-1\" numberOfLines={1}>\n            Show notes in bookmark card\n          </Text>\n          <Switch\n            className=\"shrink-0\"\n            value={settings.showNotes}\n            onValueChange={(value) =>\n              setSettings({\n                ...settings,\n                showNotes: value,\n              })\n            }\n          />\n        </View>\n      </View>\n\n      <SectionHeader title=\"Media\" />\n      <View\n        className=\"w-full rounded-xl bg-card py-2\"\n        style={{ borderCurve: \"continuous\" }}\n      >\n        <View className=\"flex w-full flex-row items-center justify-between gap-8 px-4 py-1\">\n          <Text>Upload Image Quality</Text>\n          <View className=\"flex flex-1 flex-row items-center justify-center gap-2\">\n            <Text className=\"text-foreground\">\n              {Math.round(imageQuality ?? 0)}%\n            </Text>\n\n            {imageQuality === null ? (\n              <ActivityIndicator size=\"small\" />\n            ) : (\n              <Slider\n                style={{ height: 40, flex: 1 }}\n                onSlidingComplete={(value) =>\n                  setSettings({\n                    ...settings,\n                    imageQuality: Math.round(value) / 100,\n                  })\n                }\n                onValueChange={(value) => setImageQuality(value)}\n                value={imageQuality}\n                minimumValue={0}\n                maximumValue={100}\n              />\n            )}\n          </View>\n        </View>\n      </View>\n\n      <SectionHeader title=\"Account\" />\n      <View\n        className=\"w-full rounded-xl bg-card py-2\"\n        style={{ borderCurve: \"continuous\" }}\n      >\n        <Pressable\n          className=\"flex flex-row items-center px-4 py-1\"\n          onPress={logout}\n        >\n          <Text className=\"flex-1 text-destructive\">Log Out</Text>\n        </Pressable>\n        <Divider orientation=\"horizontal\" className=\"mx-6 my-1\" />\n        <Pressable\n          className=\"flex flex-row items-center px-4 py-1\"\n          onPress={handleDeleteAccount}\n          disabled={isDeleting}\n        >\n          {isDeleting ? (\n            <ActivityIndicator size=\"small\" />\n          ) : (\n            <Text className=\"flex-1 text-destructive\">Delete Account</Text>\n          )}\n        </Pressable>\n      </View>\n\n      <Modal\n        visible={showPasswordModal}\n        transparent\n        animationType=\"fade\"\n        onRequestClose={() => {\n          setShowPasswordModal(false);\n          setPassword(\"\");\n        }}\n      >\n        <Pressable\n          className=\"flex-1 items-center justify-center bg-black/50\"\n          onPress={() => {\n            setShowPasswordModal(false);\n            setPassword(\"\");\n          }}\n        >\n          <Pressable className=\"mx-8 w-full max-w-sm rounded-2xl bg-card p-6\">\n            <Text className=\"mb-2 text-lg font-bold\">Enter Password</Text>\n            <Text className=\"mb-4 text-sm text-muted-foreground\">\n              Please enter your password to confirm account deletion.\n            </Text>\n            <TextInput\n              className=\"mb-4 rounded-lg border border-input bg-background px-3 py-2 text-foreground\"\n              placeholder=\"Password\"\n              secureTextEntry\n              value={password}\n              onChangeText={setPassword}\n              autoFocus\n            />\n            <View className=\"flex flex-row justify-end gap-3\">\n              <Pressable\n                className=\"rounded-lg px-4 py-2\"\n                onPress={() => {\n                  setShowPasswordModal(false);\n                  setPassword(\"\");\n                }}\n              >\n                <Text className=\"text-muted-foreground\">Cancel</Text>\n              </Pressable>\n              <Pressable\n                className=\"rounded-lg bg-destructive px-4 py-2\"\n                onPress={() => deleteAccount({ password })}\n                disabled={isDeleting || !password}\n              >\n                {isDeleting ? (\n                  <ActivityIndicator size=\"small\" color=\"white\" />\n                ) : (\n                  <Text className=\"font-medium text-destructive-foreground\">\n                    Delete\n                  </Text>\n                )}\n              </Pressable>\n            </View>\n          </Pressable>\n        </Pressable>\n      </Modal>\n\n      <SectionHeader title=\"About\" />\n      <View\n        className=\"w-full rounded-xl bg-card py-2\"\n        style={{ borderCurve: \"continuous\" }}\n      >\n        <View className=\"flex flex-row items-center justify-between px-4 py-1\">\n          <Text className=\"text-muted-foreground\" numberOfLines={1}>\n            Server\n          </Text>\n          <Text\n            className=\"flex-1 text-right text-sm text-muted-foreground\"\n            numberOfLines={1}\n          >\n            {isSettingsLoading ? \"Loading...\" : settings.address}\n          </Text>\n        </View>\n        <Divider orientation=\"horizontal\" className=\"mx-6 my-1\" />\n        <View className=\"flex flex-row items-center justify-between px-4 py-1\">\n          <Text className=\"w-fit text-muted-foreground\" numberOfLines={1}>\n            App Version\n          </Text>\n          <Text\n            className=\"flex-1 text-right text-sm text-muted-foreground\"\n            numberOfLines={1}\n          >\n            {Constants.expoConfig?.version ?? \"unknown\"}\n          </Text>\n        </View>\n        <Divider orientation=\"horizontal\" className=\"mx-6 my-1\" />\n        <View className=\"flex flex-row items-center justify-between px-4 py-1\">\n          <Text className=\"text-muted-foreground\" numberOfLines={1}>\n            Server Version\n          </Text>\n          <Text\n            className=\"flex-1 text-right text-sm text-muted-foreground\"\n            numberOfLines={1}\n          >\n            {isServerVersionLoading\n              ? \"Loading...\"\n              : serverVersionError\n                ? \"unavailable\"\n                : (serverVersion ?? \"unknown\")}\n          </Text>\n        </View>\n      </View>\n    </ScrollView>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/app/dashboard/(tabs)/(tags)/_layout.tsx",
    "content": "import { Platform } from \"react-native\";\nimport { Stack } from \"expo-router/stack\";\n\nexport default function Layout() {\n  return (\n    <Stack\n      screenOptions={{\n        ...Platform.select({\n          ios: {\n            headerLargeTitle: true,\n            headerTransparent: true,\n            headerBlurEffect: \"systemMaterial\",\n            headerLargeTitleShadowVisible: false,\n            headerLargeStyle: { backgroundColor: \"transparent\" },\n          },\n          android: {\n            headerStyle: {\n              backgroundColor: \"transparent\",\n            },\n            contentStyle: {\n              // Manual padding to avoid the native tabbar until expo fixes this in sdk 55.\n              paddingBottom: 100,\n            },\n          },\n        }),\n        headerShadowVisible: false,\n      }}\n    >\n      <Stack.Screen name=\"index\" options={{ title: \"Tags\" }} />\n    </Stack>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/app/dashboard/(tabs)/(tags)/index.tsx",
    "content": "import { useEffect, useState } from \"react\";\nimport { FlatList, Pressable, View } from \"react-native\";\nimport { Link } from \"expo-router\";\nimport FullPageError from \"@/components/FullPageError\";\nimport ChevronRight from \"@/components/ui/ChevronRight\";\nimport EmptyState from \"@/components/ui/EmptyState\";\nimport FullPageSpinner from \"@/components/ui/FullPageSpinner\";\nimport { SearchInput } from \"@/components/ui/SearchInput\";\nimport { Text } from \"@/components/ui/Text\";\nimport { useQueryClient } from \"@tanstack/react-query\";\nimport { Tag } from \"lucide-react-native\";\n\nimport { usePaginatedSearchTags } from \"@karakeep/shared-react/hooks/tags\";\nimport { useDebounce } from \"@karakeep/shared-react/hooks/use-debounce\";\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\ninterface TagItem {\n  id: string;\n  name: string;\n  numBookmarks: number;\n  href: string;\n}\n\nexport default function Tags() {\n  const [refreshing, setRefreshing] = useState(false);\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n\n  // Debounce search query to avoid too many API calls\n  const debouncedSearch = useDebounce(searchQuery, 300);\n\n  // Fetch tags sorted by usage (most used first)\n  const {\n    data,\n    isPending,\n    error,\n    refetch,\n    fetchNextPage,\n    hasNextPage,\n    isFetchingNextPage,\n  } = usePaginatedSearchTags({\n    limit: 50,\n    sortBy: debouncedSearch ? \"relevance\" : \"usage\",\n    nameContains: debouncedSearch,\n  });\n\n  useEffect(() => {\n    setRefreshing(isPending);\n  }, [isPending]);\n\n  if (error) {\n    return <FullPageError error={error.message} onRetry={() => refetch()} />;\n  }\n\n  if (!data) {\n    return <FullPageSpinner />;\n  }\n\n  const onRefresh = () => {\n    queryClient.invalidateQueries(api.tags.list.pathFilter());\n  };\n\n  const tags: TagItem[] = data.tags.map((tag) => ({\n    id: tag.id,\n    name: tag.name,\n    numBookmarks: tag.numBookmarks,\n    href: `/dashboard/tags/${tag.id}`,\n  }));\n\n  const handleLoadMore = () => {\n    if (hasNextPage && !isFetchingNextPage) {\n      fetchNextPage();\n    }\n  };\n\n  return (\n    <FlatList\n      className=\"h-full\"\n      contentInsetAdjustmentBehavior=\"automatic\"\n      ListHeaderComponent={\n        <SearchInput\n          containerClassName=\"mx-2 mb-2\"\n          placeholder=\"Search tags...\"\n          value={searchQuery}\n          onChangeText={setSearchQuery}\n        />\n      }\n      contentContainerStyle={{\n        gap: 6,\n        paddingBottom: 20,\n      }}\n      renderItem={(item) => (\n        <View\n          className=\"mx-2 flex flex-row items-center rounded-xl bg-card px-4 py-2\"\n          style={{ borderCurve: \"continuous\" }}\n        >\n          <Link\n            asChild\n            key={item.item.id}\n            href={item.item.href}\n            className=\"flex-1\"\n          >\n            <Pressable className=\"flex flex-row items-center justify-between\">\n              <View className=\"flex-1\">\n                <Text className=\"font-medium\">{item.item.name}</Text>\n                <Text className=\"text-sm text-muted-foreground\">\n                  {item.item.numBookmarks}{\" \"}\n                  {item.item.numBookmarks === 1 ? \"bookmark\" : \"bookmarks\"}\n                </Text>\n              </View>\n              <ChevronRight />\n            </Pressable>\n          </Link>\n        </View>\n      )}\n      data={tags}\n      refreshing={refreshing}\n      onRefresh={onRefresh}\n      onEndReached={handleLoadMore}\n      onEndReachedThreshold={0.5}\n      ListFooterComponent={\n        isFetchingNextPage ? (\n          <View className=\"py-4\">\n            <Text className=\"text-center text-muted-foreground\">\n              Loading more...\n            </Text>\n          </View>\n        ) : null\n      }\n      ListEmptyComponent={\n        !isPending ? (\n          <EmptyState\n            icon={Tag}\n            title=\"No Tags\"\n            subtitle=\"Tags will appear as you organize your bookmarks\"\n          />\n        ) : null\n      }\n    />\n  );\n}\n"
  },
  {
    "path": "apps/mobile/app/dashboard/(tabs)/_layout.tsx",
    "content": "import React from \"react\";\nimport {\n  Icon,\n  Label,\n  NativeTabs,\n  VectorIcon,\n} from \"expo-router/unstable-native-tabs\";\nimport { useColorScheme } from \"@/lib/useColorScheme\";\nimport MaterialCommunityIcons from \"@expo/vector-icons/MaterialCommunityIcons\";\n\nexport default function TabLayout() {\n  const { colors } = useColorScheme();\n  return (\n    <NativeTabs backgroundColor={colors.grey6} minimizeBehavior=\"onScrollDown\">\n      <NativeTabs.Trigger name=\"(home)\">\n        <Icon\n          sf=\"house.fill\"\n          androidSrc={\n            <VectorIcon family={MaterialCommunityIcons} name=\"home\" />\n          }\n        />\n        <Label>Home</Label>\n      </NativeTabs.Trigger>\n\n      <NativeTabs.Trigger name=\"(lists)\">\n        <Icon\n          sf=\"list.clipboard.fill\"\n          androidSrc={\n            <VectorIcon family={MaterialCommunityIcons} name=\"clipboard-list\" />\n          }\n        />\n        <Label>Lists</Label>\n      </NativeTabs.Trigger>\n\n      <NativeTabs.Trigger name=\"(tags)\">\n        <Icon\n          sf=\"tag.fill\"\n          androidSrc={<VectorIcon family={MaterialCommunityIcons} name=\"tag\" />}\n        />\n        <Label>Tags</Label>\n      </NativeTabs.Trigger>\n\n      <NativeTabs.Trigger name=\"(highlights)\">\n        <Icon\n          sf=\"highlighter\"\n          androidSrc={\n            <VectorIcon family={MaterialCommunityIcons} name=\"marker\" />\n          }\n        />\n        <Label>Highlights</Label>\n      </NativeTabs.Trigger>\n\n      <NativeTabs.Trigger name=\"(settings)\">\n        <Icon\n          sf=\"gearshape.fill\"\n          androidSrc={<VectorIcon family={MaterialCommunityIcons} name=\"cog\" />}\n        />\n        <Label>Settings</Label>\n      </NativeTabs.Trigger>\n    </NativeTabs>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/app/dashboard/(tabs)/index.tsx",
    "content": "import { Redirect } from \"expo-router\";\n\nexport default function TabIndex() {\n  return <Redirect href=\"/dashboard/(home)\" />;\n}\n"
  },
  {
    "path": "apps/mobile/app/dashboard/_layout.tsx",
    "content": "import type { AppStateStatus } from \"react-native\";\nimport { useEffect } from \"react\";\nimport { AppState, Platform } from \"react-native\";\nimport { useRouter } from \"expo-router\";\nimport { Stack } from \"expo-router/stack\";\nimport { useIsLoggedIn } from \"@/lib/session\";\nimport { focusManager } from \"@tanstack/react-query\";\n\nfunction onAppStateChange(status: AppStateStatus) {\n  if (Platform.OS !== \"web\") {\n    focusManager.setFocused(status === \"active\");\n  }\n}\n\nexport default function Dashboard() {\n  const router = useRouter();\n\n  const isLoggedIn = useIsLoggedIn();\n  useEffect(() => {\n    if (isLoggedIn !== undefined && !isLoggedIn) {\n      return router.replace(\"signin\");\n    }\n  }, [isLoggedIn]);\n\n  useEffect(() => {\n    const subscription = AppState.addEventListener(\"change\", onAppStateChange);\n\n    return () => subscription.remove();\n  }, []);\n\n  return (\n    <Stack\n      screenOptions={{\n        ...Platform.select({\n          ios: {\n            headerTransparent: true,\n            headerBlurEffect: \"systemMaterial\",\n            headerLargeTitle: true,\n            headerLargeTitleShadowVisible: false,\n            headerLargeStyle: { backgroundColor: \"transparent\" },\n          },\n          android: {\n            headerStyle: {\n              backgroundColor: \"transparent\",\n            },\n          },\n        }),\n        headerShadowVisible: false,\n      }}\n    >\n      <Stack.Screen\n        name=\"(tabs)\"\n        options={{ headerShown: false, title: \"Home\" }}\n      />\n      <Stack.Screen\n        name=\"favourites\"\n        options={{\n          headerTitle: \"⭐️ Favourites\",\n          headerBackTitle: \"Back\",\n        }}\n      />\n      <Stack.Screen\n        name=\"bookmarks/[slug]/index\"\n        options={{\n          headerTitle: \"\",\n          headerBackTitle: \"Back\",\n          headerLargeTitle: false,\n        }}\n      />\n      <Stack.Screen\n        name=\"bookmarks/new\"\n        options={{\n          headerTitle: \"New Bookmark\",\n          headerBackTitle: \"Back\",\n          headerTransparent: false,\n          headerLargeTitle: false,\n          presentation: Platform.select({\n            ios: \"formSheet\" as const,\n            default: \"modal\" as const,\n          }),\n          sheetGrabberVisible: true,\n          sheetAllowedDetents: [0.35, 0.7],\n        }}\n      />\n      <Stack.Screen\n        name=\"bookmarks/[slug]/manage_tags\"\n        options={{\n          headerTitle: \"Manage Tags\",\n          headerTransparent: false,\n          headerLargeTitle: false,\n          presentation: Platform.select({\n            ios: \"formSheet\" as const,\n            default: \"modal\" as const,\n          }),\n          sheetGrabberVisible: true,\n        }}\n      />\n      <Stack.Screen\n        name=\"bookmarks/[slug]/manage_lists\"\n        options={{\n          headerTitle: \"Manage Lists\",\n          headerTransparent: false,\n          headerLargeTitle: false,\n          presentation: Platform.select({\n            ios: \"formSheet\" as const,\n            default: \"modal\" as const,\n          }),\n          sheetGrabberVisible: true,\n        }}\n      />\n      <Stack.Screen\n        name=\"bookmarks/[slug]/info\"\n        options={{\n          headerTitle: \"Edit Bookmark\",\n          headerTransparent: false,\n          headerLargeTitle: false,\n          presentation: Platform.select({\n            ios: \"formSheet\" as const,\n            default: \"modal\" as const,\n          }),\n          sheetGrabberVisible: true,\n        }}\n      />\n      <Stack.Screen\n        name=\"lists/new\"\n        options={{\n          headerTitle: \"New List\",\n          headerBackTitle: \"Back\",\n          headerLargeTitle: false,\n          headerTransparent: false,\n          presentation: Platform.select({\n            ios: \"formSheet\" as const,\n            default: \"modal\" as const,\n          }),\n          sheetGrabberVisible: true,\n        }}\n      />\n      <Stack.Screen\n        name=\"lists/[slug]/edit\"\n        options={{\n          headerTitle: \"Edit List\",\n          headerBackTitle: \"Back\",\n          headerLargeTitle: false,\n          headerTransparent: false,\n          presentation: Platform.select({\n            ios: \"formSheet\" as const,\n            default: \"modal\" as const,\n          }),\n          sheetGrabberVisible: true,\n        }}\n      />\n      <Stack.Screen\n        name=\"archive\"\n        options={{\n          headerTitle: \"🗄️ Archive\",\n          headerBackTitle: \"Back\",\n        }}\n      />\n      <Stack.Screen\n        name=\"search\"\n        options={{\n          headerTitle: \"\",\n          headerBackTitle: \"\",\n          headerShown: true,\n          headerTransparent: false,\n          headerLargeTitle: false,\n          animation: \"fade_from_bottom\",\n          animationDuration: 100,\n        }}\n      />\n      <Stack.Screen\n        name=\"settings/theme\"\n        options={{\n          title: \"Theme\",\n          headerTitle: \"Theme\",\n          headerBackTitle: \"Back\",\n        }}\n      />\n      <Stack.Screen\n        name=\"settings/bookmark-default-view\"\n        options={{\n          title: \"Bookmark View Mode\",\n          headerTitle: \"Bookmark View Mode\",\n          headerBackTitle: \"Back\",\n        }}\n      />\n      <Stack.Screen\n        name=\"settings/reader-settings\"\n        options={{\n          title: \"Reader Settings\",\n          headerTitle: \"Reader Settings\",\n          headerBackTitle: \"Back\",\n        }}\n      />\n    </Stack>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/app/dashboard/archive.tsx",
    "content": "import UpdatingBookmarkList from \"@/components/bookmarks/UpdatingBookmarkList\";\n\nexport default function Archive() {\n  return <UpdatingBookmarkList query={{ archived: true }} />;\n}\n"
  },
  {
    "path": "apps/mobile/app/dashboard/bookmarks/[slug]/index.tsx",
    "content": "import { useState } from \"react\";\nimport { KeyboardAvoidingView, Pressable, View } from \"react-native\";\nimport { useSafeAreaInsets } from \"react-native-safe-area-context\";\nimport { Stack, useLocalSearchParams, useRouter } from \"expo-router\";\nimport BookmarkAssetView from \"@/components/bookmarks/BookmarkAssetView\";\nimport BookmarkLinkTypeSelector, {\n  BookmarkLinkType,\n} from \"@/components/bookmarks/BookmarkLinkTypeSelector\";\nimport BookmarkLinkView from \"@/components/bookmarks/BookmarkLinkView\";\nimport BookmarkTextView from \"@/components/bookmarks/BookmarkTextView\";\nimport BottomActions from \"@/components/bookmarks/BottomActions\";\nimport FullPageError from \"@/components/FullPageError\";\nimport FullPageSpinner from \"@/components/ui/FullPageSpinner\";\nimport useAppSettings from \"@/lib/settings\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { Settings } from \"lucide-react-native\";\nimport { useColorScheme } from \"nativewind\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\nimport { BookmarkTypes } from \"@karakeep/shared/types/bookmarks\";\n\nexport default function BookmarkView() {\n  const insets = useSafeAreaInsets();\n  const router = useRouter();\n  const { slug } = useLocalSearchParams();\n  const { colorScheme } = useColorScheme();\n  const isDark = colorScheme === \"dark\";\n  const { settings } = useAppSettings();\n  const api = useTRPC();\n\n  const [bookmarkLinkType, setBookmarkLinkType] = useState<BookmarkLinkType>(\n    settings.defaultBookmarkView === \"externalBrowser\"\n      ? \"browser\"\n      : settings.defaultBookmarkView,\n  );\n\n  if (typeof slug !== \"string\") {\n    throw new Error(\"Unexpected param type\");\n  }\n\n  const {\n    data: bookmark,\n    error,\n    refetch,\n  } = useQuery(\n    api.bookmarks.getBookmark.queryOptions({\n      bookmarkId: slug,\n      includeContent: false,\n    }),\n  );\n\n  if (error) {\n    return <FullPageError error={error.message} onRetry={refetch} />;\n  }\n\n  if (!bookmark) {\n    return <FullPageSpinner />;\n  }\n\n  let comp;\n  let title = null;\n  switch (bookmark.content.type) {\n    case BookmarkTypes.LINK:\n      title = bookmark.title ?? bookmark.content.title;\n      comp = (\n        <BookmarkLinkView\n          bookmark={bookmark}\n          bookmarkPreviewType={bookmarkLinkType}\n        />\n      );\n      break;\n    case BookmarkTypes.TEXT:\n      title = bookmark.title;\n      comp = <BookmarkTextView bookmark={bookmark} />;\n      break;\n    case BookmarkTypes.ASSET:\n      title = bookmark.title ?? bookmark.content.fileName;\n      comp = <BookmarkAssetView bookmark={bookmark} />;\n      break;\n  }\n  return (\n    <KeyboardAvoidingView\n      style={{ flex: 1, paddingBottom: insets.bottom + 8 }}\n      behavior=\"height\"\n    >\n      <Stack.Screen\n        options={{\n          headerTitle: title ?? \"\",\n          headerBackTitle: \"Back\",\n          headerTransparent: false,\n          headerShown: true,\n          headerStyle: {\n            backgroundColor: isDark ? \"#000\" : \"#fff\",\n          },\n          headerTintColor: isDark ? \"#fff\" : \"#000\",\n          headerRight: () =>\n            bookmark.content.type === BookmarkTypes.LINK ? (\n              <View className=\"flex-row items-center gap-3 px-4\">\n                {bookmarkLinkType === \"reader\" && (\n                  <Pressable\n                    onPress={() =>\n                      router.push(\"/dashboard/settings/reader-settings\")\n                    }\n                  >\n                    <Settings size={20} color=\"gray\" />\n                  </Pressable>\n                )}\n                <BookmarkLinkTypeSelector\n                  type={bookmarkLinkType}\n                  onChange={(type) => setBookmarkLinkType(type)}\n                  bookmark={bookmark}\n                />\n              </View>\n            ) : undefined,\n        }}\n      />\n      {comp}\n      <BottomActions bookmark={bookmark} />\n    </KeyboardAvoidingView>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/app/dashboard/bookmarks/[slug]/info.tsx",
    "content": "import React from \"react\";\nimport {\n  ActivityIndicator,\n  Alert,\n  Pressable,\n  TextInput,\n  View,\n} from \"react-native\";\nimport {\n  KeyboardAwareScrollView,\n  KeyboardGestureArea,\n} from \"react-native-keyboard-controller\";\nimport * as Haptics from \"expo-haptics\";\nimport { router, Stack, useLocalSearchParams } from \"expo-router\";\nimport BookmarkTextMarkdown from \"@/components/bookmarks/BookmarkTextMarkdown\";\nimport TagPill from \"@/components/bookmarks/TagPill\";\nimport FullPageError from \"@/components/FullPageError\";\nimport FullPageSpinner from \"@/components/ui/FullPageSpinner\";\nimport {\n  GroupedSection,\n  NavigationRow,\n  RowSeparator,\n} from \"@/components/ui/GroupedList\";\nimport { Skeleton } from \"@/components/ui/Skeleton\";\nimport { Text } from \"@/components/ui/Text\";\nimport { useToast } from \"@/components/ui/Toast\";\nimport { useColorScheme } from \"@/lib/useColorScheme\";\nimport { ChevronUp, RefreshCw, Sparkles, Trash2 } from \"lucide-react-native\";\n\nimport {\n  useAutoRefreshingBookmarkQuery,\n  useDeleteBookmark,\n  useSummarizeBookmark,\n  useUpdateBookmark,\n} from \"@karakeep/shared-react/hooks/bookmarks\";\nimport { useWhoAmI } from \"@karakeep/shared-react/hooks/users\";\nimport { BookmarkTypes, ZBookmark } from \"@karakeep/shared/types/bookmarks\";\nimport { isBookmarkStillTagging } from \"@karakeep/shared/utils/bookmarkUtils\";\n\n// --- Section Components ---\n\nfunction TitleEditor({\n  title,\n  setTitle,\n  isPending,\n  disabled,\n}: {\n  title: string | null | undefined;\n  setTitle: (title: string | null) => void;\n  isPending: boolean;\n  disabled?: boolean;\n}) {\n  const { colors } = useColorScheme();\n  return (\n    <GroupedSection header=\"Title\">\n      <TextInput\n        editable={!isPending && !disabled}\n        placeholder=\"Untitled\"\n        placeholderTextColor={colors.grey}\n        onChangeText={(text) => setTitle(text)}\n        defaultValue={title ?? \"\"}\n        className=\"px-4 py-3 text-[17px] leading-6 text-foreground\"\n      />\n    </GroupedSection>\n  );\n}\n\nfunction NotesEditor({\n  notes,\n  setNotes,\n  isPending,\n  disabled,\n}: {\n  notes: string | null | undefined;\n  setNotes: (note: string | null) => void;\n  isPending: boolean;\n  disabled?: boolean;\n}) {\n  const { colors } = useColorScheme();\n  return (\n    <GroupedSection header=\"Notes\">\n      <TextInput\n        editable={!isPending && !disabled}\n        multiline\n        placeholder=\"Add notes...\"\n        placeholderTextColor={colors.grey}\n        onChangeText={(text) => setNotes(text)}\n        textAlignVertical=\"top\"\n        defaultValue={notes ?? \"\"}\n        className=\"min-h-[100px] px-4 py-3 text-[17px] leading-6 text-foreground\"\n      />\n    </GroupedSection>\n  );\n}\n\nfunction TagList({\n  bookmark,\n  readOnly,\n}: {\n  bookmark: ZBookmark;\n  readOnly: boolean;\n}) {\n  const hasTags = bookmark.tags.length > 0;\n  const isTagging = isBookmarkStillTagging(bookmark);\n\n  if (!isTagging && !hasTags && readOnly) {\n    return null;\n  }\n\n  return (\n    <GroupedSection header=\"Tags\">\n      {isTagging ? (\n        <View className=\"gap-3 p-4\">\n          <Skeleton className=\"h-4 w-full\" />\n          <Skeleton className=\"h-4 w-3/4\" />\n        </View>\n      ) : (\n        hasTags && (\n          <>\n            <View className=\"flex-row flex-wrap gap-2 px-4 py-3\">\n              {bookmark.tags.map((t) => (\n                <TagPill key={t.id} tag={t} clickable={!readOnly} />\n              ))}\n            </View>\n            {!readOnly && <RowSeparator />}\n          </>\n        )\n      )}\n      {!readOnly && (\n        <NavigationRow\n          label=\"Manage Tags\"\n          onPress={() =>\n            router.push(`/dashboard/bookmarks/${bookmark.id}/manage_tags`)\n          }\n        />\n      )}\n    </GroupedSection>\n  );\n}\n\nfunction ManageLists({ bookmark }: { bookmark: ZBookmark }) {\n  return (\n    <GroupedSection header=\"Lists\">\n      <NavigationRow\n        label=\"Manage Lists\"\n        onPress={() =>\n          router.push(`/dashboard/bookmarks/${bookmark.id}/manage_lists`)\n        }\n      />\n    </GroupedSection>\n  );\n}\n\nfunction AISummarySection({\n  bookmark,\n  readOnly,\n}: {\n  bookmark: ZBookmark;\n  readOnly: boolean;\n}) {\n  const { toast } = useToast();\n  const { colors } = useColorScheme();\n  const [isExpanded, setIsExpanded] = React.useState(false);\n\n  const { mutate: summarize, isPending: isSummarizing } = useSummarizeBookmark({\n    onSuccess: () => {\n      toast({ message: \"Summary generated!\", showProgress: false });\n    },\n    onError: () => {\n      toast({\n        message: \"Failed to generate summary\",\n        showProgress: false,\n      });\n    },\n  });\n\n  const { mutate: resummarize, isPending: isResummarizing } =\n    useSummarizeBookmark({\n      onSuccess: () => {\n        toast({ message: \"Summary regenerated!\", showProgress: false });\n      },\n      onError: () => {\n        toast({\n          message: \"Failed to regenerate summary\",\n          showProgress: false,\n        });\n      },\n    });\n\n  const { mutate: updateBookmark, isPending: isDeletingSummary } =\n    useUpdateBookmark({\n      onSuccess: () => {\n        toast({ message: \"Summary deleted!\", showProgress: false });\n      },\n      onError: () => {\n        toast({\n          message: \"Failed to delete summary\",\n          showProgress: false,\n        });\n      },\n    });\n\n  if (bookmark.content.type !== BookmarkTypes.LINK) {\n    return null;\n  }\n\n  if (bookmark.summary) {\n    return (\n      <GroupedSection header=\"AI Summary\">\n        <Pressable\n          onPress={() => setIsExpanded(!isExpanded)}\n          className=\"px-4 py-3\"\n        >\n          <View className={isExpanded ? \"\" : \"max-h-16 overflow-hidden\"}>\n            <BookmarkTextMarkdown text={bookmark.summary} />\n          </View>\n          {!isExpanded && (\n            <Text variant=\"footnote\" className=\"mt-1.5 text-primary\">\n              Show more\n            </Text>\n          )}\n        </Pressable>\n        {isExpanded && !readOnly && (\n          <>\n            <RowSeparator />\n            <View className=\"flex-row justify-end gap-1 px-2 py-2\">\n              <Pressable\n                onPress={() => resummarize({ bookmarkId: bookmark.id })}\n                disabled={isResummarizing}\n                className=\"rounded-full p-2.5 active:opacity-70\"\n              >\n                {isResummarizing ? (\n                  <ActivityIndicator size=\"small\" />\n                ) : (\n                  <RefreshCw size={18} color={colors.grey} />\n                )}\n              </Pressable>\n              <Pressable\n                onPress={() =>\n                  updateBookmark({ bookmarkId: bookmark.id, summary: null })\n                }\n                disabled={isDeletingSummary}\n                className=\"rounded-full p-2.5 active:opacity-70\"\n              >\n                {isDeletingSummary ? (\n                  <ActivityIndicator size=\"small\" />\n                ) : (\n                  <Trash2 size={18} color={colors.grey} />\n                )}\n              </Pressable>\n              <Pressable\n                onPress={() => setIsExpanded(false)}\n                className=\"rounded-full p-2.5 active:opacity-70\"\n              >\n                <ChevronUp size={18} color={colors.grey} />\n              </Pressable>\n            </View>\n          </>\n        )}\n      </GroupedSection>\n    );\n  }\n\n  if (readOnly) {\n    return null;\n  }\n\n  return (\n    <GroupedSection>\n      <Pressable\n        onPress={() => summarize({ bookmarkId: bookmark.id })}\n        disabled={isSummarizing}\n        className=\"flex-row items-center justify-center gap-2 px-4 py-3 active:opacity-70\"\n      >\n        {isSummarizing ? (\n          <>\n            <ActivityIndicator size=\"small\" color={colors.primary} />\n            <Text className=\"text-primary\">Generating...</Text>\n          </>\n        ) : (\n          <>\n            <Sparkles size={16} color={colors.primary} />\n            <Text className=\"text-primary\">Summarize with AI</Text>\n          </>\n        )}\n      </Pressable>\n    </GroupedSection>\n  );\n}\n\n// --- Main Page ---\n\nconst ViewBookmarkPage = () => {\n  const { slug } = useLocalSearchParams();\n  const { toast } = useToast();\n  const { data: currentUser } = useWhoAmI();\n  if (typeof slug !== \"string\") {\n    throw new Error(\"Unexpected param type\");\n  }\n\n  const [editedBookmark, setEditedBookmark] = React.useState<{\n    title?: string | null;\n    note?: string;\n  }>({});\n\n  const hasChanges = Object.keys(editedBookmark).length > 0;\n\n  const { mutate: editBookmark, isPending: isEditPending } = useUpdateBookmark({\n    onSuccess: () => {\n      toast({ message: \"Bookmark updated!\", showProgress: false });\n      if (router.canGoBack()) {\n        router.back();\n      } else {\n        router.replace(\"dashboard\");\n      }\n    },\n    onError: () => {\n      toast({ message: \"Failed to save changes\", showProgress: false });\n    },\n  });\n\n  const { mutate: deleteBookmark, isPending: isDeletionPending } =\n    useDeleteBookmark({\n      onSuccess: () => {\n        router.replace(\"dashboard\");\n        toast({ message: \"Bookmark deleted!\", showProgress: false });\n      },\n    });\n\n  const {\n    data: bookmark,\n    isPending,\n    refetch,\n  } = useAutoRefreshingBookmarkQuery({\n    bookmarkId: slug,\n  });\n\n  const isOwner = currentUser?.id === bookmark?.userId;\n\n  const onDone = () => {\n    const dismiss = () => {\n      if (router.canGoBack()) {\n        router.back();\n      } else {\n        router.replace(\"dashboard\");\n      }\n    };\n\n    if (hasChanges && bookmark) {\n      editBookmark({ bookmarkId: bookmark.id, ...editedBookmark });\n    } else {\n      dismiss();\n    }\n  };\n\n  if (isPending) {\n    return <FullPageSpinner />;\n  }\n\n  if (!bookmark) {\n    return (\n      <FullPageError error=\"Bookmark not found\" onRetry={() => refetch()} />\n    );\n  }\n\n  const handleDeleteBookmark = () => {\n    Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);\n    Alert.alert(\n      \"Delete Bookmark\",\n      \"Are you sure you want to delete this bookmark?\",\n      [\n        { text: \"Cancel\", style: \"cancel\" },\n        {\n          text: \"Delete\",\n          onPress: () => deleteBookmark({ bookmarkId: bookmark.id }),\n          style: \"destructive\",\n        },\n      ],\n    );\n  };\n\n  let title: string | null = null;\n  switch (bookmark.content.type) {\n    case BookmarkTypes.LINK:\n      title = bookmark.title ?? bookmark.content.title ?? null;\n      break;\n    case BookmarkTypes.TEXT:\n      title = bookmark.title ?? null;\n      break;\n    case BookmarkTypes.ASSET:\n      title = bookmark.title ?? bookmark.content.fileName ?? null;\n      break;\n  }\n\n  return (\n    <KeyboardGestureArea interpolator=\"ios\">\n      <Stack.Screen\n        options={{\n          headerShown: true,\n          headerTransparent: false,\n          headerTitle: \"Edit Bookmark\",\n          headerRight: () => (\n            <Pressable onPress={onDone} disabled={isEditPending}>\n              {isEditPending ? (\n                <ActivityIndicator size=\"small\" />\n              ) : (\n                <Text\n                  className={\n                    hasChanges ? \"font-semibold text-primary\" : \"text-primary\"\n                  }\n                >\n                  {hasChanges ? \"Save\" : \"Done\"}\n                </Text>\n              )}\n            </Pressable>\n          ),\n        }}\n      />\n      <KeyboardAwareScrollView\n        bottomOffset={8}\n        keyboardDismissMode=\"interactive\"\n        contentContainerStyle={{ padding: 16, gap: 20, paddingBottom: 40 }}\n        className=\"bg-background\"\n      >\n        <TitleEditor\n          title={title}\n          setTitle={(t) => setEditedBookmark((prev) => ({ ...prev, title: t }))}\n          isPending={isEditPending}\n          disabled={!isOwner}\n        />\n        <AISummarySection bookmark={bookmark} readOnly={!isOwner} />\n        <TagList bookmark={bookmark} readOnly={!isOwner} />\n        {isOwner && <ManageLists bookmark={bookmark} />}\n        <NotesEditor\n          notes={bookmark.note}\n          setNotes={(note) =>\n            setEditedBookmark((prev) => ({ ...prev, note: note ?? \"\" }))\n          }\n          isPending={isEditPending}\n          disabled={!isOwner}\n        />\n        {isOwner && (\n          <GroupedSection>\n            <Pressable\n              onPress={handleDeleteBookmark}\n              disabled={isDeletionPending}\n              className=\"items-center px-4 py-3 active:opacity-70\"\n            >\n              <Text className=\"text-destructive\" numberOfLines={1}>\n                {isDeletionPending ? \"Deleting...\" : \"Delete Bookmark\"}\n              </Text>\n            </Pressable>\n          </GroupedSection>\n        )}\n        <View className=\"items-center gap-1 pt-2\">\n          <Text variant=\"caption1\" color=\"tertiary\" selectable>\n            Created {bookmark.createdAt.toLocaleString()}\n          </Text>\n          {bookmark.modifiedAt &&\n            bookmark.modifiedAt.getTime() !== bookmark.createdAt.getTime() && (\n              <Text variant=\"caption1\" color=\"tertiary\" selectable>\n                Modified {bookmark.modifiedAt.toLocaleString()}\n              </Text>\n            )}\n        </View>\n      </KeyboardAwareScrollView>\n    </KeyboardGestureArea>\n  );\n};\n\nexport default ViewBookmarkPage;\n"
  },
  {
    "path": "apps/mobile/app/dashboard/bookmarks/[slug]/manage_lists.tsx",
    "content": "import React from \"react\";\nimport { ActivityIndicator, Pressable, ScrollView, View } from \"react-native\";\nimport { Stack, useLocalSearchParams } from \"expo-router\";\nimport { RowSeparator } from \"@/components/ui/GroupedList\";\nimport { Text } from \"@/components/ui/Text\";\nimport { useToast } from \"@/components/ui/Toast\";\nimport { useColorScheme } from \"@/lib/useColorScheme\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { Check } from \"lucide-react-native\";\n\nimport type { ZBookmarkList } from \"@karakeep/shared/types/lists\";\nimport {\n  useAddBookmarkToList,\n  useBookmarkLists,\n  useRemoveBookmarkFromList,\n} from \"@karakeep/shared-react/hooks/lists\";\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\nconst ListPickerPage = () => {\n  const api = useTRPC();\n  const { slug: bookmarkId } = useLocalSearchParams();\n  const { colors } = useColorScheme();\n\n  if (typeof bookmarkId !== \"string\") {\n    throw new Error(\"Unexpected param type\");\n  }\n\n  const { toast } = useToast();\n  const onError = () => {\n    toast({\n      message: \"Something went wrong\",\n      variant: \"destructive\",\n      showProgress: false,\n    });\n  };\n\n  const { data: existingLists } = useQuery(\n    api.lists.getListsOfBookmark.queryOptions(\n      { bookmarkId },\n      {\n        select: (data: { lists: ZBookmarkList[] }) =>\n          new Set(data.lists.map((l) => l.id)),\n      },\n    ),\n  );\n\n  const { data } = useBookmarkLists();\n\n  const {\n    mutate: addToList,\n    isPending: isAddingToList,\n    variables: addVariables,\n  } = useAddBookmarkToList({\n    onSuccess: () => {\n      toast({\n        message: \"Added to list!\",\n        showProgress: false,\n      });\n    },\n    onError,\n  });\n\n  const {\n    mutate: removeToList,\n    isPending: isRemovingFromList,\n    variables: removeVariables,\n  } = useRemoveBookmarkFromList({\n    onSuccess: () => {\n      toast({\n        message: \"Removed from list!\",\n        showProgress: false,\n      });\n    },\n    onError,\n  });\n\n  const toggleList = (listId: string) => {\n    if (!existingLists) return;\n    if (existingLists.has(listId)) {\n      removeToList({ bookmarkId, listId });\n    } else {\n      addToList({ bookmarkId, listId });\n    }\n  };\n\n  const isListLoading = (listId: string) => {\n    return (\n      (isAddingToList && addVariables?.listId === listId) ||\n      (isRemovingFromList && removeVariables?.listId === listId)\n    );\n  };\n\n  const { allPaths } = data ?? {};\n  const filteredPaths = allPaths\n    ?.filter((path) => path[path.length - 1].userRole !== \"viewer\")\n    .filter((path) => path[path.length - 1].type !== \"smart\");\n\n  return (\n    <>\n      <Stack.Screen\n        options={{\n          headerShown: true,\n          headerTransparent: false,\n          headerTitle: \"Manage Lists\",\n        }}\n      />\n      <ScrollView\n        contentInsetAdjustmentBehavior=\"automatic\"\n        contentContainerStyle={{ padding: 16, paddingBottom: 40 }}\n        className=\"bg-background\"\n      >\n        {filteredPaths && filteredPaths.length > 0 ? (\n          <View\n            className=\"overflow-hidden rounded-xl bg-card\"\n            style={{ borderCurve: \"continuous\" }}\n          >\n            {filteredPaths.map((path, index) => {\n              const listId = path[path.length - 1].id;\n              const isLoading = isListLoading(listId);\n              const isChecked = existingLists?.has(listId);\n\n              return (\n                <React.Fragment key={listId}>\n                  {index > 0 && <RowSeparator />}\n                  <Pressable\n                    onPress={() => !isLoading && toggleList(listId)}\n                    disabled={isLoading}\n                    className=\"flex-row items-center justify-between px-4 py-3 active:opacity-70\"\n                  >\n                    <Text className=\"flex-1 pr-3\" numberOfLines={1}>\n                      {path\n                        .map((item) => `${item.icon} ${item.name}`)\n                        .join(\" / \")}\n                    </Text>\n                    {isLoading ? (\n                      <ActivityIndicator size=\"small\" />\n                    ) : isChecked ? (\n                      <Check\n                        size={20}\n                        color={colors.primary}\n                        strokeWidth={2.5}\n                      />\n                    ) : null}\n                  </Pressable>\n                </React.Fragment>\n              );\n            })}\n          </View>\n        ) : (\n          <View className=\"items-center py-12\">\n            <Text color=\"tertiary\">No lists available</Text>\n          </View>\n        )}\n      </ScrollView>\n    </>\n  );\n};\n\nexport default ListPickerPage;\n"
  },
  {
    "path": "apps/mobile/app/dashboard/bookmarks/[slug]/manage_tags.tsx",
    "content": "import React, { useMemo } from \"react\";\nimport { Pressable, ScrollView, View } from \"react-native\";\nimport { Stack, useLocalSearchParams } from \"expo-router\";\nimport FullPageSpinner from \"@/components/ui/FullPageSpinner\";\nimport { GroupedSection, RowSeparator } from \"@/components/ui/GroupedList\";\nimport { Text } from \"@/components/ui/Text\";\nimport { useToast } from \"@/components/ui/Toast\";\nimport { useColorScheme } from \"@/lib/useColorScheme\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { Check, Plus } from \"lucide-react-native\";\n\nimport {\n  useAutoRefreshingBookmarkQuery,\n  useUpdateBookmarkTags,\n} from \"@karakeep/shared-react/hooks/bookmarks\";\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\nconst NEW_TAG_ID = \"new-tag\";\n\nconst TagPickerPage = () => {\n  const api = useTRPC();\n  const { colors } = useColorScheme();\n  const { slug: bookmarkId } = useLocalSearchParams();\n  const [search, setSearch] = React.useState(\"\");\n\n  if (typeof bookmarkId !== \"string\") {\n    throw new Error(\"Unexpected param type\");\n  }\n\n  const { toast } = useToast();\n  const onError = () => {\n    toast({\n      message: \"Something went wrong\",\n      variant: \"destructive\",\n      showProgress: false,\n    });\n  };\n\n  const { data: allTags, isPending: isAllTagsPending } = useQuery(\n    api.tags.list.queryOptions(\n      {},\n      {\n        select: React.useCallback(\n          (data: { tags: { id: string; name: string }[] }) => {\n            return data.tags\n              .map((t) => ({\n                id: t.id,\n                name: t.name,\n                lowered: t.name.toLowerCase(),\n              }))\n              .sort((a, b) => a.lowered.localeCompare(b.lowered));\n          },\n          [],\n        ),\n      },\n    ),\n  );\n\n  const { data: existingTags } = useAutoRefreshingBookmarkQuery({\n    bookmarkId,\n  });\n\n  const [optimisticTags, setOptimisticTags] = React.useState<\n    { id: string; name: string; lowered: string }[]\n  >([]);\n\n  React.useEffect(() => {\n    setOptimisticTags(\n      existingTags?.tags.map((t) => ({\n        id: t.id,\n        name: t.name,\n        lowered: t.name.toLowerCase(),\n      })) ?? [],\n    );\n  }, [existingTags]);\n\n  const { mutate: updateTags } = useUpdateBookmarkTags({\n    onMutate: (req) => {\n      req.attach.forEach((t) =>\n        setOptimisticTags((prev) => [\n          ...prev,\n          {\n            id: t.tagId!,\n            name: t.tagName!,\n            lowered: t.tagName!.toLowerCase(),\n          },\n        ]),\n      );\n      req.detach.forEach((t) =>\n        setOptimisticTags((prev) => prev.filter((p) => p.id != t.tagId!)),\n      );\n    },\n    onError,\n  });\n\n  const clearAllTags = () => {\n    if (optimisticTags.length === 0) return;\n    updateTags({\n      bookmarkId,\n      detach: optimisticTags.map((tag) => ({\n        tagId: tag.id,\n        tagName: tag.name,\n      })),\n      attach: [],\n    });\n  };\n\n  const optimisticExistingTagIds = useMemo(() => {\n    return new Set(optimisticTags?.map((t) => t.id) ?? []);\n  }, [optimisticTags]);\n\n  const { filteredAllTags, filteredOptimisticTags } = useMemo(() => {\n    const loweredSearch = search.toLowerCase();\n    let filteredAll =\n      allTags?.filter(\n        (t) =>\n          t.lowered.startsWith(loweredSearch) &&\n          !optimisticExistingTagIds.has(t.id),\n      ) ?? [];\n\n    if (allTags && search) {\n      const exactMatchExists =\n        allTags.some((t) => t.lowered == loweredSearch) ||\n        optimisticTags.some((t) => t.lowered == loweredSearch);\n      if (!exactMatchExists) {\n        filteredAll = [\n          { id: NEW_TAG_ID, name: search, lowered: loweredSearch },\n          ...filteredAll,\n        ];\n      }\n    }\n\n    const filteredExisting = optimisticTags.filter((t) =>\n      t.lowered.startsWith(loweredSearch),\n    );\n\n    return {\n      filteredAllTags: filteredAll,\n      filteredOptimisticTags: filteredExisting,\n    };\n  }, [search, allTags, optimisticTags, optimisticExistingTagIds]);\n\n  const handleTagPress = (\n    tag: { id: string; name: string },\n    action: \"attach\" | \"detach\",\n  ) => {\n    updateTags({\n      bookmarkId,\n      attach:\n        action === \"attach\"\n          ? [\n              {\n                tagId: tag.id === NEW_TAG_ID ? undefined : tag.id,\n                tagName: tag.name,\n              },\n            ]\n          : [],\n      detach:\n        action === \"detach\"\n          ? [\n              {\n                tagId: tag.id === NEW_TAG_ID ? undefined : tag.id,\n                tagName: tag.name,\n              },\n            ]\n          : [],\n    });\n  };\n\n  if (isAllTagsPending) {\n    return <FullPageSpinner />;\n  }\n\n  return (\n    <>\n      <Stack.Screen\n        options={{\n          headerSearchBarOptions: {\n            placeholder: \"Search Tags\",\n            onChangeText: (event) => setSearch(event.nativeEvent.text),\n            autoCapitalize: \"none\",\n            hideWhenScrolling: false,\n          },\n          headerRight: () => (\n            <Pressable\n              onPress={clearAllTags}\n              disabled={optimisticTags.length === 0}\n              className={optimisticTags.length === 0 ? \"opacity-50\" : \"\"}\n            >\n              <Text className=\"text-primary\">Clear</Text>\n            </Pressable>\n          ),\n        }}\n      />\n      <ScrollView\n        contentInsetAdjustmentBehavior=\"automatic\"\n        keyboardShouldPersistTaps=\"handled\"\n        contentContainerStyle={{ padding: 16, gap: 20, paddingBottom: 40 }}\n        className=\"bg-background\"\n      >\n        {filteredOptimisticTags.length > 0 && (\n          <GroupedSection header=\"Attached\">\n            {filteredOptimisticTags.map((tag, index) => (\n              <React.Fragment key={tag.id}>\n                {index > 0 && <RowSeparator />}\n                <Pressable\n                  onPress={() => handleTagPress(tag, \"detach\")}\n                  className=\"flex-row items-center justify-between px-4 py-3 active:opacity-70\"\n                >\n                  <Text className=\"flex-1 pr-3\">{tag.name}</Text>\n                  <Check size={20} color={colors.primary} strokeWidth={2.5} />\n                </Pressable>\n              </React.Fragment>\n            ))}\n          </GroupedSection>\n        )}\n        {filteredAllTags.length > 0 && (\n          <GroupedSection header=\"All Tags\">\n            {filteredAllTags.map((tag, index) => (\n              <React.Fragment key={tag.id}>\n                {index > 0 && <RowSeparator />}\n                <Pressable\n                  onPress={() => handleTagPress(tag, \"attach\")}\n                  className=\"flex-row items-center justify-between px-4 py-3 active:opacity-70\"\n                >\n                  {tag.id === NEW_TAG_ID ? (\n                    <>\n                      <Text className=\"flex-1 pr-3 text-primary\">\n                        Create &ldquo;{tag.name}&rdquo;\n                      </Text>\n                      <Plus size={20} color={colors.primary} strokeWidth={2} />\n                    </>\n                  ) : (\n                    <Text className=\"flex-1 pr-3\">{tag.name}</Text>\n                  )}\n                </Pressable>\n              </React.Fragment>\n            ))}\n          </GroupedSection>\n        )}\n        {filteredOptimisticTags.length === 0 &&\n          filteredAllTags.length === 0 && (\n            <View className=\"items-center py-12\">\n              <Text color=\"tertiary\">No tags found</Text>\n            </View>\n          )}\n      </ScrollView>\n    </>\n  );\n};\n\nexport default TagPickerPage;\n"
  },
  {
    "path": "apps/mobile/app/dashboard/bookmarks/new.tsx",
    "content": "import React, { useState } from \"react\";\nimport { View } from \"react-native\";\nimport { router } from \"expo-router\";\nimport { Button } from \"@/components/ui/Button\";\nimport { Input } from \"@/components/ui/Input\";\nimport { Text } from \"@/components/ui/Text\";\nimport { useToast } from \"@/components/ui/Toast\";\n\nimport { useCreateBookmark } from \"@karakeep/shared-react/hooks/bookmarks\";\nimport { BookmarkTypes } from \"@karakeep/shared/types/bookmarks\";\n\nconst NoteEditorPage = () => {\n  const dismiss = () => {\n    router.back();\n  };\n\n  const [text, setText] = useState(\"\");\n  const [error, setError] = useState<string | undefined>();\n  const { toast } = useToast();\n\n  const { mutate: createBookmark, isPending } = useCreateBookmark({\n    onSuccess: (resp) => {\n      if (resp.alreadyExists) {\n        toast({\n          message: \"Bookmark already exists\",\n        });\n      }\n      setText(\"\");\n      dismiss();\n    },\n    onError: (e) => {\n      let message;\n      if (e.data?.zodError) {\n        const zodError = e.data.zodError;\n        message = JSON.stringify(zodError);\n      } else {\n        message = `Something went wrong: ${e.message}`;\n      }\n      setError(message);\n    },\n  });\n\n  const onSubmit = () => {\n    const data = text.trim();\n    try {\n      const url = new URL(data);\n      if (url.protocol != \"http:\" && url.protocol != \"https:\") {\n        throw new Error(`Unsupported URL protocol: ${url.protocol}`);\n      }\n      createBookmark({ type: BookmarkTypes.LINK, url: data, source: \"mobile\" });\n    } catch {\n      createBookmark({\n        type: BookmarkTypes.TEXT,\n        text: data,\n        source: \"mobile\",\n      });\n    }\n  };\n\n  return (\n    <View className=\"flex-1 gap-2 px-4 pt-4\">\n      {error && (\n        <Text className=\"w-full text-center text-red-500\">{error}</Text>\n      )}\n      <Input\n        onChangeText={setText}\n        className=\"bg-card\"\n        multiline\n        placeholder=\"What's on your mind?\"\n        autoFocus\n        autoCapitalize={\"none\"}\n        textAlignVertical=\"top\"\n      />\n      <Button onPress={onSubmit} disabled={isPending}>\n        <Text>Save</Text>\n      </Button>\n    </View>\n  );\n};\n\nexport default NoteEditorPage;\n"
  },
  {
    "path": "apps/mobile/app/dashboard/favourites.tsx",
    "content": "import UpdatingBookmarkList from \"@/components/bookmarks/UpdatingBookmarkList\";\n\nexport default function Favourites() {\n  return (\n    <UpdatingBookmarkList\n      query={{\n        favourited: true,\n      }}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/mobile/app/dashboard/lists/[slug]/edit.tsx",
    "content": "import { useEffect, useState } from \"react\";\nimport { View } from \"react-native\";\nimport { router, useLocalSearchParams } from \"expo-router\";\nimport { Button } from \"@/components/ui/Button\";\nimport FullPageSpinner from \"@/components/ui/FullPageSpinner\";\nimport { Input } from \"@/components/ui/Input\";\nimport { Text } from \"@/components/ui/Text\";\nimport { useToast } from \"@/components/ui/Toast\";\nimport { useQuery } from \"@tanstack/react-query\";\n\nimport { useEditBookmarkList } from \"@karakeep/shared-react/hooks/lists\";\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\nconst EditListPage = () => {\n  const { slug: listId } = useLocalSearchParams<{ slug?: string | string[] }>();\n  const [text, setText] = useState(\"\");\n  const [query, setQuery] = useState(\"\");\n  const { toast } = useToast();\n  const api = useTRPC();\n  const { mutate, isPending: editIsPending } = useEditBookmarkList({\n    onSuccess: () => {\n      dismiss();\n    },\n    onError: (error) => {\n      // Extract error message from the error object\n      let errorMessage = \"Something went wrong\";\n      if (error.data?.zodError) {\n        errorMessage = Object.values(error.data.zodError.fieldErrors)\n          .flat()\n          .join(\"\\n\");\n      } else if (error.message) {\n        errorMessage = error.message;\n      }\n      toast({\n        message: errorMessage,\n        variant: \"destructive\",\n      });\n    },\n  });\n\n  if (typeof listId !== \"string\") {\n    throw new Error(\"Unexpected param type\");\n  }\n\n  const { data: list, isLoading: fetchIsPending } = useQuery(\n    api.lists.get.queryOptions({\n      listId,\n    }),\n  );\n\n  const dismiss = () => {\n    router.back();\n  };\n\n  useEffect(() => {\n    if (!list) return;\n    setText(list.name ?? \"\");\n    setQuery(list.query ?? \"\");\n  }, [list?.id, list?.query, list?.name]);\n\n  const onSubmit = () => {\n    if (!text.trim()) {\n      toast({ message: \"List name can't be empty\", variant: \"destructive\" });\n      return;\n    }\n\n    if (list?.type === \"smart\" && !query.trim()) {\n      toast({\n        message: \"Smart lists must have a search query\",\n        variant: \"destructive\",\n      });\n      return;\n    }\n\n    mutate({\n      listId,\n      name: text.trim(),\n      query: list?.type === \"smart\" ? query.trim() : undefined,\n    });\n  };\n\n  const isPending = fetchIsPending || editIsPending;\n\n  return (\n    <>\n      {isPending ? (\n        <FullPageSpinner />\n      ) : (\n        <View className=\"gap-3 px-4\">\n          {/* List Type Info - not editable */}\n          <View className=\"gap-2\">\n            <Text className=\"text-sm text-muted-foreground\">List Type</Text>\n            <View className=\"flex flex-row gap-2\">\n              <View className=\"flex-1\">\n                <Button\n                  variant={list?.type === \"manual\" ? \"primary\" : \"secondary\"}\n                  disabled\n                >\n                  <Text>Manual</Text>\n                </Button>\n              </View>\n              <View className=\"flex-1\">\n                <Button\n                  variant={list?.type === \"smart\" ? \"primary\" : \"secondary\"}\n                  disabled\n                >\n                  <Text>Smart</Text>\n                </Button>\n              </View>\n            </View>\n          </View>\n\n          {/* List Name */}\n          <View className=\"flex flex-row items-center gap-1\">\n            <Text className=\"shrink p-2\">{list?.icon || \"🚀\"}</Text>\n            <Input\n              className=\"flex-1 bg-card\"\n              onChangeText={setText}\n              value={text}\n              placeholder=\"List Name\"\n              autoFocus\n              autoCapitalize={\"none\"}\n            />\n          </View>\n\n          {/* Smart List Query Input */}\n          {list?.type === \"smart\" && (\n            <View className=\"gap-2\">\n              <Text className=\"text-sm text-muted-foreground\">\n                Search Query\n              </Text>\n              <Input\n                className=\"bg-card\"\n                onChangeText={setQuery}\n                value={query}\n                placeholder=\"e.g., #important OR list:work\"\n                autoCapitalize={\"none\"}\n              />\n              <Text className=\"text-xs italic text-muted-foreground\">\n                Smart lists automatically show bookmarks matching your search\n                query\n              </Text>\n            </View>\n          )}\n\n          <Button disabled={isPending} onPress={onSubmit}>\n            <Text>Save</Text>\n          </Button>\n        </View>\n      )}\n    </>\n  );\n};\n\nexport default EditListPage;\n"
  },
  {
    "path": "apps/mobile/app/dashboard/lists/[slug]/index.tsx",
    "content": "import { Alert, Platform, View } from \"react-native\";\nimport * as Haptics from \"expo-haptics\";\nimport { router, Stack, useLocalSearchParams } from \"expo-router\";\nimport UpdatingBookmarkList from \"@/components/bookmarks/UpdatingBookmarkList\";\nimport FullPageError from \"@/components/FullPageError\";\nimport FullPageSpinner from \"@/components/ui/FullPageSpinner\";\nimport { useArchiveFilter } from \"@/lib/hooks\";\nimport { useMenuIconColors } from \"@/lib/useMenuIconColors\";\nimport { MenuView } from \"@react-native-menu/menu\";\nimport { useMutation, useQuery } from \"@tanstack/react-query\";\nimport { Ellipsis } from \"lucide-react-native\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\nimport { ZBookmarkList } from \"@karakeep/shared/types/lists\";\n\nexport default function ListView() {\n  const { slug } = useLocalSearchParams();\n  const api = useTRPC();\n  if (typeof slug !== \"string\") {\n    throw new Error(\"Unexpected param type\");\n  }\n  const {\n    data: list,\n    error,\n    refetch,\n  } = useQuery(api.lists.get.queryOptions({ listId: slug }));\n  const { archived, isLoading: isSettingsLoading } = useArchiveFilter();\n\n  return (\n    <>\n      <Stack.Screen\n        options={{\n          headerTitle: list ? `${list.icon} ${list.name}` : \"\",\n          headerBackTitle: \"Back\",\n          headerRight: () => (\n            <ListActionsMenu listId={slug} role={list?.userRole ?? \"viewer\"} />\n          ),\n        }}\n      />\n      {error ? (\n        <FullPageError error={error.message} onRetry={() => refetch()} />\n      ) : list && !isSettingsLoading ? (\n        <UpdatingBookmarkList\n          query={{\n            listId: list.id,\n            archived,\n          }}\n        />\n      ) : (\n        <FullPageSpinner />\n      )}\n    </>\n  );\n}\n\nfunction ListActionsMenu({\n  listId,\n  role,\n}: {\n  listId: string;\n  role: ZBookmarkList[\"userRole\"];\n}) {\n  const api = useTRPC();\n  const { menuIconColor, destructiveMenuIconColor } = useMenuIconColors();\n  const { mutate: deleteList } = useMutation(\n    api.lists.delete.mutationOptions({\n      onSuccess: () => {\n        router.replace(\"/dashboard/lists\");\n      },\n    }),\n  );\n\n  const { mutate: leaveList } = useMutation(\n    api.lists.leaveList.mutationOptions({\n      onSuccess: () => {\n        router.replace(\"/dashboard/lists\");\n      },\n    }),\n  );\n\n  const handleDelete = () => {\n    Alert.alert(\"Delete List\", \"Are you sure you want to delete this list?\", [\n      { text: \"Cancel\", style: \"cancel\" },\n      {\n        text: \"Delete\",\n        onPress: () => {\n          deleteList({ listId });\n        },\n        style: \"destructive\",\n      },\n    ]);\n  };\n\n  const handleLeave = () => {\n    Alert.alert(\"Leave List\", \"Are you sure you want to leave this list?\", [\n      { text: \"Cancel\", style: \"cancel\" },\n      {\n        text: \"Leave\",\n        onPress: () => {\n          leaveList({ listId });\n        },\n        style: \"destructive\",\n      },\n    ]);\n  };\n\n  const handleEdit = () => {\n    router.push({\n      pathname: \"/dashboard/lists/[slug]/edit\",\n      params: { slug: listId },\n    });\n  };\n\n  return (\n    <MenuView\n      actions={[\n        {\n          id: \"edit\",\n          title: \"Edit List\",\n          attributes: {\n            hidden: role !== \"owner\",\n          },\n          image: Platform.select({\n            ios: \"square.and.pencil\",\n          }),\n          imageColor: Platform.select({\n            ios: menuIconColor,\n          }),\n        },\n        {\n          id: \"delete_list\",\n          title: \"Delete List\",\n          attributes: {\n            destructive: true,\n            hidden: role !== \"owner\",\n          },\n          image: Platform.select({\n            ios: \"trash\",\n          }),\n          imageColor: Platform.select({\n            ios: destructiveMenuIconColor,\n          }),\n        },\n        {\n          id: \"leave\",\n          title: \"Leave List\",\n          attributes: {\n            destructive: true,\n            hidden: role === \"owner\",\n          },\n          image: Platform.select({\n            ios: \"arrowshape.turn.up.left\",\n          }),\n          imageColor: Platform.select({\n            ios: destructiveMenuIconColor,\n          }),\n        },\n      ]}\n      onPressAction={({ nativeEvent }) => {\n        if (nativeEvent.event === \"delete_list\") {\n          handleDelete();\n        } else if (nativeEvent.event === \"leave\") {\n          handleLeave();\n        } else if (nativeEvent.event === \"edit\") {\n          handleEdit();\n        }\n      }}\n      shouldOpenOnLongPress={false}\n    >\n      <View className=\"my-auto px-4\">\n        <Ellipsis onPress={() => Haptics.selectionAsync()} color=\"gray\" />\n      </View>\n    </MenuView>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/app/dashboard/lists/new.tsx",
    "content": "import React, { useState } from \"react\";\nimport { View } from \"react-native\";\nimport { router } from \"expo-router\";\nimport { Button } from \"@/components/ui/Button\";\nimport { Input } from \"@/components/ui/Input\";\nimport { Text } from \"@/components/ui/Text\";\nimport { useToast } from \"@/components/ui/Toast\";\n\nimport { useCreateBookmarkList } from \"@karakeep/shared-react/hooks/lists\";\n\ntype ListType = \"manual\" | \"smart\";\n\nconst NewListPage = () => {\n  const dismiss = () => {\n    router.back();\n  };\n  const { toast } = useToast();\n  const [text, setText] = useState(\"\");\n  const [listType, setListType] = useState<ListType>(\"manual\");\n  const [query, setQuery] = useState(\"\");\n\n  const { mutate, isPending } = useCreateBookmarkList({\n    onSuccess: () => {\n      dismiss();\n    },\n    onError: (error) => {\n      // Extract error message from the error object\n      let errorMessage = \"Something went wrong\";\n      if (error.data?.zodError) {\n        errorMessage = Object.values(error.data.zodError.fieldErrors)\n          .flat()\n          .join(\"\\n\");\n      } else if (error.message) {\n        errorMessage = error.message;\n      }\n      toast({\n        message: errorMessage,\n        variant: \"destructive\",\n      });\n    },\n  });\n\n  const onSubmit = () => {\n    // Validate smart list has a query\n    if (listType === \"smart\" && !query.trim()) {\n      toast({\n        message: \"Smart lists must have a search query\",\n        variant: \"destructive\",\n      });\n      return;\n    }\n\n    mutate({\n      name: text,\n      icon: \"🚀\",\n      type: listType,\n      query: listType === \"smart\" ? query : undefined,\n    });\n  };\n\n  return (\n    <View className=\"gap-3 px-4\">\n      {/* List Type Selector */}\n      <View className=\"gap-2\">\n        <Text className=\"text-sm text-muted-foreground\">List Type</Text>\n        <View className=\"flex flex-row gap-2\">\n          <View className=\"flex-1\">\n            <Button\n              variant={listType === \"manual\" ? \"primary\" : \"secondary\"}\n              onPress={() => setListType(\"manual\")}\n            >\n              <Text>Manual</Text>\n            </Button>\n          </View>\n          <View className=\"flex-1\">\n            <Button\n              variant={listType === \"smart\" ? \"primary\" : \"secondary\"}\n              onPress={() => setListType(\"smart\")}\n            >\n              <Text>Smart</Text>\n            </Button>\n          </View>\n        </View>\n      </View>\n\n      {/* List Name */}\n      <View className=\"flex flex-row items-center gap-1\">\n        <Text className=\"shrink p-2\">🚀</Text>\n        <Input\n          className=\"flex-1 bg-card\"\n          onChangeText={setText}\n          placeholder=\"List Name\"\n          autoFocus\n          autoCapitalize={\"none\"}\n        />\n      </View>\n\n      {/* Smart List Query Input */}\n      {listType === \"smart\" && (\n        <View className=\"gap-2\">\n          <Text className=\"text-sm text-muted-foreground\">Search Query</Text>\n          <Input\n            className=\"bg-card\"\n            onChangeText={setQuery}\n            value={query}\n            placeholder=\"e.g., #important OR list:work\"\n            autoCapitalize={\"none\"}\n          />\n          <Text className=\"text-xs italic text-muted-foreground\">\n            Smart lists automatically show bookmarks matching your search query\n          </Text>\n        </View>\n      )}\n\n      <Button disabled={isPending} onPress={onSubmit}>\n        <Text>Save</Text>\n      </Button>\n    </View>\n  );\n};\n\nexport default NewListPage;\n"
  },
  {
    "path": "apps/mobile/app/dashboard/search.tsx",
    "content": "import { useMemo, useRef, useState } from \"react\";\nimport { FlatList, Keyboard, Pressable, TextInput, View } from \"react-native\";\nimport { router } from \"expo-router\";\nimport BookmarkList from \"@/components/bookmarks/BookmarkList\";\nimport FullPageError from \"@/components/FullPageError\";\nimport FullPageSpinner from \"@/components/ui/FullPageSpinner\";\nimport { SearchInput } from \"@/components/ui/SearchInput\";\nimport { Text } from \"@/components/ui/Text\";\nimport AsyncStorage from \"@react-native-async-storage/async-storage\";\nimport {\n  keepPreviousData,\n  useInfiniteQuery,\n  useQueryClient,\n} from \"@tanstack/react-query\";\n\nimport { useSearchHistory } from \"@karakeep/shared-react/hooks/search-history\";\nimport { useDebounce } from \"@karakeep/shared-react/hooks/use-debounce\";\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\nconst MAX_DISPLAY_SUGGESTIONS = 5;\n\nexport default function Search() {\n  const [search, setSearch] = useState(\"\");\n\n  const query = useDebounce(search, 10);\n  const inputRef = useRef<TextInput>(null);\n\n  const [isInputFocused, setIsInputFocused] = useState(true);\n  const { history, addTerm, clearHistory } = useSearchHistory({\n    getItem: (k: string) => AsyncStorage.getItem(k),\n    setItem: (k: string, v: string) => AsyncStorage.setItem(k, v),\n    removeItem: (k: string) => AsyncStorage.removeItem(k),\n  });\n\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n\n  const onRefresh = () => {\n    queryClient.invalidateQueries(api.bookmarks.searchBookmarks.pathFilter());\n  };\n\n  const { data, error, refetch, isPending, fetchNextPage, isFetchingNextPage } =\n    useInfiniteQuery(\n      api.bookmarks.searchBookmarks.infiniteQueryOptions(\n        { text: query },\n        {\n          placeholderData: keepPreviousData,\n          gcTime: 0,\n          initialCursor: null,\n          getNextPageParam: (lastPage) => lastPage.nextCursor,\n        },\n      ),\n    );\n\n  const filteredHistory = useMemo(() => {\n    if (search.trim().length === 0) {\n      // Show recent items when not typing\n      return history.slice(0, MAX_DISPLAY_SUGGESTIONS);\n    }\n    // Show filtered items when typing\n    return history\n      .filter((item) => item.toLowerCase().includes(search.toLowerCase()))\n      .slice(0, MAX_DISPLAY_SUGGESTIONS);\n  }, [search, history]);\n\n  if (error) {\n    return <FullPageError error={error.message} onRetry={() => refetch()} />;\n  }\n\n  const handleSearchSubmit = (searchTerm: string) => {\n    const term = searchTerm.trim();\n    if (term.length > 0) {\n      addTerm(term);\n      setSearch(term);\n    }\n    inputRef.current?.blur();\n    Keyboard.dismiss();\n  };\n\n  const renderHistoryItem = ({ item }: { item: string }) => (\n    <Pressable\n      onPress={() => handleSearchSubmit(item)}\n      className=\"border-b border-gray-200 p-3\"\n    >\n      <Text className=\"text-foreground\">{item}</Text>\n    </Pressable>\n  );\n\n  const handleOnFocus = () => {\n    setIsInputFocused(true);\n  };\n\n  const handleOnBlur = () => {\n    setIsInputFocused(false);\n    if (search.trim().length > 0) {\n      addTerm(search);\n    }\n  };\n\n  return (\n    <>\n      <SearchInput\n        containerClassName=\"m-3\"\n        ref={inputRef}\n        placeholder=\"Search\"\n        className=\"flex-1\"\n        value={search}\n        onChangeText={setSearch}\n        onFocus={handleOnFocus}\n        onBlur={handleOnBlur}\n        onSubmitEditing={() => handleSearchSubmit(search)}\n        returnKeyType=\"search\"\n        autoFocus\n        autoCapitalize=\"none\"\n        onCancel={router.back}\n      />\n\n      {isInputFocused && search.trim().length === 0 ? (\n        <FlatList\n          data={filteredHistory}\n          renderItem={renderHistoryItem}\n          keyExtractor={(item, index) => `${item}-${index}`}\n          ListHeaderComponent={\n            <View className=\"flex-row items-center justify-between p-3\">\n              <Text className=\"text-sm font-bold text-gray-500\">\n                Recent Searches\n              </Text>\n              {history.length > 0 && (\n                <Pressable onPress={clearHistory}>\n                  <Text className=\"text-sm text-blue-500\">Clear</Text>\n                </Pressable>\n              )}\n            </View>\n          }\n          ListEmptyComponent={\n            <Text className=\"p-3 text-center text-gray-500\">\n              No matching searches.\n            </Text>\n          }\n          keyboardShouldPersistTaps=\"handled\"\n        />\n      ) : isPending && query.length > 0 ? (\n        <FullPageSpinner />\n      ) : data && query.length > 0 ? (\n        <BookmarkList\n          bookmarks={data.pages.flatMap((p) => p.bookmarks)}\n          fetchNextPage={fetchNextPage}\n          isFetchingNextPage={isFetchingNextPage}\n          onRefresh={onRefresh}\n          isRefreshing={isPending}\n        />\n      ) : (\n        <View />\n      )}\n    </>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/app/dashboard/settings/bookmark-default-view.tsx",
    "content": "import { Pressable, ScrollView, View } from \"react-native\";\nimport { useRouter } from \"expo-router\";\nimport { Divider } from \"@/components/ui/Divider\";\nimport { Text } from \"@/components/ui/Text\";\nimport { useToast } from \"@/components/ui/Toast\";\nimport useAppSettings from \"@/lib/settings\";\nimport { Check } from \"lucide-react-native\";\n\nexport default function BookmarkDefaultViewSettings() {\n  const router = useRouter();\n  const { toast } = useToast();\n  const { settings, setSettings } = useAppSettings();\n\n  const handleUpdate = async (\n    mode: \"reader\" | \"browser\" | \"externalBrowser\",\n  ) => {\n    try {\n      await setSettings({\n        ...settings,\n        defaultBookmarkView: mode,\n      });\n      toast({\n        message: \"Default Bookmark View updated!\",\n        showProgress: false,\n      });\n      router.back();\n    } catch {\n      toast({\n        message: \"Something went wrong\",\n        variant: \"destructive\",\n        showProgress: false,\n      });\n    }\n  };\n\n  const options = ([\"reader\", \"browser\", \"externalBrowser\"] as const)\n    .map((mode) => {\n      const currentMode = settings.defaultBookmarkView;\n      const isChecked = currentMode === mode;\n      return [\n        <Pressable\n          onPress={() => handleUpdate(mode)}\n          className=\"flex flex-row items-center justify-between\"\n          key={mode}\n        >\n          <Text className=\"mr-2 flex-1\" numberOfLines={1}>\n            {\n              {\n                browser: \"Browser\",\n                reader: \"Reader\",\n                externalBrowser: \"External Browser\",\n              }[mode]\n            }\n          </Text>\n          {isChecked && <Check color=\"rgb(0, 122, 255)\" />}\n        </Pressable>,\n        <Divider\n          key={mode + \"-divider\"}\n          orientation=\"horizontal\"\n          className=\"my-3 h-0.5 w-full\"\n        />,\n      ];\n    })\n    .flat();\n  options.pop();\n\n  return (\n    <ScrollView\n      contentInsetAdjustmentBehavior=\"automatic\"\n      contentContainerClassName=\"flex w-full items-center px-4 py-2\"\n    >\n      <View className=\"w-full rounded-lg bg-card px-4 py-2\">{options}</View>\n    </ScrollView>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/app/dashboard/settings/reader-settings.tsx",
    "content": "import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { Pressable, ScrollView, View } from \"react-native\";\nimport Slider from \"@react-native-community/slider\";\nimport {\n  ReaderPreview,\n  ReaderPreviewRef,\n} from \"@/components/reader/ReaderPreview\";\nimport { Divider } from \"@/components/ui/Divider\";\nimport { Text } from \"@/components/ui/Text\";\nimport { MOBILE_FONT_FAMILIES, useReaderSettings } from \"@/lib/readerSettings\";\nimport { useColorScheme } from \"@/lib/useColorScheme\";\nimport { Check, RotateCcw } from \"lucide-react-native\";\n\nimport {\n  formatFontFamily,\n  formatFontSize,\n  formatLineHeight,\n  READER_SETTING_CONSTRAINTS,\n} from \"@karakeep/shared/types/readers\";\nimport { ZReaderFontFamily } from \"@karakeep/shared/types/users\";\n\nexport default function ReaderSettingsPage() {\n  const { isDarkColorScheme: isDark } = useColorScheme();\n\n  const {\n    settings,\n    localOverrides,\n    hasLocalOverrides,\n    hasServerDefaults,\n    updateLocal,\n    clearAllLocal,\n    saveAsDefault,\n    clearAllDefaults,\n  } = useReaderSettings();\n\n  const {\n    fontSize: effectiveFontSize,\n    lineHeight: effectiveLineHeight,\n    fontFamily: effectiveFontFamily,\n  } = settings;\n\n  // Display values for showing rounded values while dragging\n  const [displayFontSize, setDisplayFontSize] = useState(effectiveFontSize);\n  const [displayLineHeight, setDisplayLineHeight] =\n    useState(effectiveLineHeight);\n\n  // Refs to track latest display values (avoids stale closures in callbacks)\n  const displayFontSizeRef = useRef(displayFontSize);\n  displayFontSizeRef.current = displayFontSize;\n  const displayLineHeightRef = useRef(displayLineHeight);\n  displayLineHeightRef.current = displayLineHeight;\n\n  // Ref for the WebView preview component\n  const previewRef = useRef<ReaderPreviewRef>(null);\n\n  // Functions to update preview styles\n  const updatePreviewFontSize = useCallback(\n    (fontSize: number) => {\n      setDisplayFontSize(fontSize);\n      previewRef.current?.updateStyles(\n        effectiveFontFamily,\n        fontSize,\n        displayLineHeightRef.current,\n      );\n    },\n    [effectiveFontFamily],\n  );\n\n  const updatePreviewLineHeight = useCallback(\n    (lineHeight: number) => {\n      setDisplayLineHeight(lineHeight);\n      previewRef.current?.updateStyles(\n        effectiveFontFamily,\n        displayFontSizeRef.current,\n        lineHeight,\n      );\n    },\n    [effectiveFontFamily],\n  );\n\n  // Sync display values with effective settings\n  useEffect(() => {\n    setDisplayFontSize(effectiveFontSize);\n  }, [effectiveFontSize]);\n\n  useEffect(() => {\n    setDisplayLineHeight(effectiveLineHeight);\n  }, [effectiveLineHeight]);\n\n  const handleFontFamilyChange = (fontFamily: ZReaderFontFamily) => {\n    updateLocal({ fontFamily });\n    // Update preview immediately with new font family\n    previewRef.current?.updateStyles(\n      fontFamily,\n      displayFontSize,\n      displayLineHeight,\n    );\n  };\n\n  const handleFontSizeChange = (value: number) => {\n    updateLocal({ fontSize: Math.round(value) });\n  };\n\n  const handleLineHeightChange = (value: number) => {\n    updateLocal({ lineHeight: Math.round(value * 10) / 10 });\n  };\n\n  const handleSaveAsDefault = () => {\n    saveAsDefault();\n    // Note: clearAllLocal is called automatically in the shared hook's onSuccess\n  };\n\n  const handleClearLocalOverrides = () => {\n    clearAllLocal();\n  };\n\n  const handleClearServerDefaults = () => {\n    clearAllDefaults();\n  };\n\n  const fontFamilyOptions: ZReaderFontFamily[] = [\"serif\", \"sans\", \"mono\"];\n\n  return (\n    <ScrollView\n      className=\"w-full\"\n      contentContainerClassName=\"items-center gap-4 px-4 py-2\"\n      contentInsetAdjustmentBehavior=\"automatic\"\n    >\n      {/* Font Family Selection */}\n      <View className=\"w-full\">\n        <Text className=\"mb-2 px-1 text-sm font-medium text-muted-foreground\">\n          Font Family\n          {localOverrides.fontFamily !== undefined && (\n            <Text className=\"text-blue-500\"> (local)</Text>\n          )}\n        </Text>\n        <View className=\"w-full rounded-lg bg-card px-4 py-2\">\n          {fontFamilyOptions.map((fontFamily, index) => {\n            const isChecked = effectiveFontFamily === fontFamily;\n            return (\n              <View key={fontFamily}>\n                <Pressable\n                  onPress={() => handleFontFamilyChange(fontFamily)}\n                  className=\"flex flex-row items-center justify-between py-2\"\n                >\n                  <Text\n                    style={{\n                      fontFamily: MOBILE_FONT_FAMILIES[fontFamily],\n                    }}\n                  >\n                    {formatFontFamily(fontFamily)}\n                  </Text>\n                  {isChecked && <Check color=\"rgb(0, 122, 255)\" />}\n                </Pressable>\n                {index < fontFamilyOptions.length - 1 && (\n                  <Divider orientation=\"horizontal\" className=\"h-0.5\" />\n                )}\n              </View>\n            );\n          })}\n        </View>\n      </View>\n\n      {/* Font Size */}\n      <View className=\"w-full\">\n        <Text className=\"mb-2 px-1 text-sm font-medium text-muted-foreground\">\n          Font Size ({formatFontSize(displayFontSize)})\n          {localOverrides.fontSize !== undefined && (\n            <Text className=\"text-blue-500\"> (local)</Text>\n          )}\n        </Text>\n        <View className=\"flex w-full flex-row items-center gap-3 rounded-lg bg-card px-4 py-3\">\n          <Text className=\"text-muted-foreground\">\n            {READER_SETTING_CONSTRAINTS.fontSize.min}\n          </Text>\n          <Slider\n            style={{ height: 40, flex: 1 }}\n            value={displayFontSize}\n            minimumValue={READER_SETTING_CONSTRAINTS.fontSize.min}\n            maximumValue={READER_SETTING_CONSTRAINTS.fontSize.max}\n            onValueChange={(value) => updatePreviewFontSize(Math.round(value))}\n            onSlidingComplete={(value) =>\n              handleFontSizeChange(Math.round(value))\n            }\n          />\n          <Text className=\"text-muted-foreground\">\n            {READER_SETTING_CONSTRAINTS.fontSize.max}\n          </Text>\n        </View>\n      </View>\n\n      {/* Line Height */}\n      <View className=\"w-full\">\n        <Text className=\"mb-2 px-1 text-sm font-medium text-muted-foreground\">\n          Line Height ({formatLineHeight(displayLineHeight)})\n          {localOverrides.lineHeight !== undefined && (\n            <Text className=\"text-blue-500\"> (local)</Text>\n          )}\n        </Text>\n        <View className=\"flex w-full flex-row items-center gap-3 rounded-lg bg-card px-4 py-3\">\n          <Text className=\"text-muted-foreground\">\n            {READER_SETTING_CONSTRAINTS.lineHeight.min}\n          </Text>\n          <Slider\n            style={{ height: 40, flex: 1 }}\n            value={displayLineHeight}\n            minimumValue={READER_SETTING_CONSTRAINTS.lineHeight.min}\n            maximumValue={READER_SETTING_CONSTRAINTS.lineHeight.max}\n            onValueChange={(value) =>\n              updatePreviewLineHeight(Math.round(value * 10) / 10)\n            }\n            onSlidingComplete={handleLineHeightChange}\n          />\n          <Text className=\"text-muted-foreground\">\n            {READER_SETTING_CONSTRAINTS.lineHeight.max}\n          </Text>\n        </View>\n      </View>\n\n      {/* Preview */}\n      <View className=\"w-full\">\n        <Text className=\"mb-2 px-1 text-sm font-medium text-muted-foreground\">\n          Preview\n        </Text>\n        <ReaderPreview\n          ref={previewRef}\n          initialFontFamily={effectiveFontFamily}\n          initialFontSize={effectiveFontSize}\n          initialLineHeight={effectiveLineHeight}\n        />\n      </View>\n\n      <Divider orientation=\"horizontal\" className=\"my-2 w-full\" />\n\n      {/* Save as Default */}\n      <Pressable\n        onPress={handleSaveAsDefault}\n        disabled={!hasLocalOverrides}\n        className=\"w-full rounded-lg bg-card px-4 py-3\"\n      >\n        <Text\n          className={`text-center ${hasLocalOverrides ? \"text-blue-500\" : \"text-muted-foreground\"}`}\n        >\n          Save as Default (All Devices)\n        </Text>\n      </Pressable>\n\n      {/* Clear Local */}\n      {hasLocalOverrides && (\n        <Pressable\n          onPress={handleClearLocalOverrides}\n          className=\"flex w-full flex-row items-center justify-center gap-2 rounded-lg bg-card px-4 py-3\"\n        >\n          <RotateCcw size={16} color={isDark ? \"#9ca3af\" : \"#6b7280\"} />\n          <Text className=\"text-muted-foreground\">Clear Local Overrides</Text>\n        </Pressable>\n      )}\n\n      {/* Clear Server */}\n      {hasServerDefaults && (\n        <Pressable\n          onPress={handleClearServerDefaults}\n          className=\"flex w-full flex-row items-center justify-center gap-2 rounded-lg bg-card px-4 py-3\"\n        >\n          <RotateCcw size={16} color={isDark ? \"#9ca3af\" : \"#6b7280\"} />\n          <Text className=\"text-muted-foreground\">Clear Server Defaults</Text>\n        </Pressable>\n      )}\n    </ScrollView>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/app/dashboard/settings/theme.tsx",
    "content": "import { Pressable, ScrollView, View } from \"react-native\";\nimport { Divider } from \"@/components/ui/Divider\";\nimport { Text } from \"@/components/ui/Text\";\nimport useAppSettings from \"@/lib/settings\";\nimport { Check } from \"lucide-react-native\";\n\nexport default function ThemePage() {\n  const { settings, setSettings } = useAppSettings();\n\n  const options = ([\"light\", \"dark\", \"system\"] as const)\n    .map((theme) => {\n      const isChecked = settings.theme === theme;\n      return [\n        <Pressable\n          onPress={() => setSettings({ ...settings, theme })}\n          className=\"flex flex-row items-center justify-between\"\n          key={theme}\n        >\n          <Text className=\"mr-2 flex-1\" numberOfLines={1}>\n            {\n              { light: \"Light Mode\", dark: \"Dark Mode\", system: \"System\" }[\n                theme\n              ]\n            }\n          </Text>\n          {isChecked && <Check color=\"rgb(0, 122, 255)\" />}\n        </Pressable>,\n        <Divider\n          key={theme + \"-divider\"}\n          orientation=\"horizontal\"\n          className=\"my-3 h-0.5 w-full\"\n        />,\n      ];\n    })\n    .flat();\n  options.pop();\n\n  return (\n    <ScrollView\n      contentInsetAdjustmentBehavior=\"automatic\"\n      contentContainerClassName=\"flex w-full items-center px-4 py-2\"\n    >\n      <View className=\"w-full rounded-lg bg-card px-4 py-2\">{options}</View>\n    </ScrollView>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/app/dashboard/tags/[slug].tsx",
    "content": "import { Stack, useLocalSearchParams } from \"expo-router\";\nimport UpdatingBookmarkList from \"@/components/bookmarks/UpdatingBookmarkList\";\nimport FullPageError from \"@/components/FullPageError\";\nimport FullPageSpinner from \"@/components/ui/FullPageSpinner\";\nimport { useArchiveFilter } from \"@/lib/hooks\";\nimport { useQuery } from \"@tanstack/react-query\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\nexport default function TagView() {\n  const { slug } = useLocalSearchParams();\n  const api = useTRPC();\n  if (typeof slug !== \"string\") {\n    throw new Error(\"Unexpected param type\");\n  }\n\n  const {\n    data: tag,\n    error,\n    refetch,\n  } = useQuery(api.tags.get.queryOptions({ tagId: slug }));\n  const { archived, isLoading: isSettingsLoading } = useArchiveFilter();\n\n  return (\n    <>\n      <Stack.Screen\n        options={{\n          headerTitle: tag?.name ?? \"\",\n          headerBackTitle: \"Back\",\n        }}\n      />\n      {error ? (\n        <FullPageError error={error.message} onRetry={() => refetch()} />\n      ) : tag && !isSettingsLoading ? (\n        <UpdatingBookmarkList\n          query={{\n            tagId: tag.id,\n            archived,\n          }}\n        />\n      ) : (\n        <FullPageSpinner />\n      )}\n    </>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/app/error.tsx",
    "content": "import { View } from \"react-native\";\nimport { Text } from \"@/components/ui/Text\";\n\nexport default function ErrorPage() {\n  return (\n    <View className=\"flex-1 items-center justify-center gap-4\">\n      <Text variant=\"largeTitle\">Error!</Text>\n    </View>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/app/index.tsx",
    "content": "import { Redirect } from \"expo-router\";\nimport FullPageSpinner from \"@/components/ui/FullPageSpinner\";\nimport { useIsLoggedIn } from \"@/lib/session\";\n\nexport default function App() {\n  const isLoggedIn = useIsLoggedIn();\n\n  if (isLoggedIn === undefined) {\n    // Wait until it's loaded\n    return <FullPageSpinner />;\n  } else if (isLoggedIn) {\n    return <Redirect href=\"dashboard\" />;\n  } else {\n    return <Redirect href=\"signin\" />;\n  }\n}\n"
  },
  {
    "path": "apps/mobile/app/server-address.tsx",
    "content": "import { useState } from \"react\";\nimport { Pressable, View } from \"react-native\";\nimport { KeyboardAwareScrollView } from \"react-native-keyboard-controller\";\nimport { useRouter } from \"expo-router\";\nimport { Button } from \"@/components/ui/Button\";\nimport { Input } from \"@/components/ui/Input\";\nimport { Text } from \"@/components/ui/Text\";\nimport useAppSettings from \"@/lib/settings\";\nimport { Plus, Trash2 } from \"lucide-react-native\";\nimport { useColorScheme } from \"nativewind\";\n\nexport default function ServerAddress() {\n  const router = useRouter();\n  const { colorScheme } = useColorScheme();\n  const iconColor = colorScheme === \"dark\" ? \"#d1d5db\" : \"#374151\";\n  const { settings, setSettings } = useAppSettings();\n  const [address, setAddress] = useState(\n    settings.address ?? \"https://cloud.karakeep.app\",\n  );\n  const [error, setError] = useState<string | undefined>();\n\n  // Custom headers state\n  const [headers, setHeaders] = useState<{ key: string; value: string }[]>(\n    Object.entries(settings.customHeaders || {}).map(([key, value]) => ({\n      key,\n      value,\n    })),\n  );\n  const [newHeaderKey, setNewHeaderKey] = useState(\"\");\n  const [newHeaderValue, setNewHeaderValue] = useState(\"\");\n\n  const handleAddHeader = () => {\n    if (!newHeaderKey.trim() || !newHeaderValue.trim()) {\n      return;\n    }\n\n    // Check if header already exists\n    const existingIndex = headers.findIndex((h) => h.key === newHeaderKey);\n    if (existingIndex >= 0) {\n      // Update existing header\n      const updatedHeaders = [...headers];\n      updatedHeaders[existingIndex].value = newHeaderValue;\n      setHeaders(updatedHeaders);\n    } else {\n      // Add new header\n      setHeaders([...headers, { key: newHeaderKey, value: newHeaderValue }]);\n    }\n\n    setNewHeaderKey(\"\");\n    setNewHeaderValue(\"\");\n  };\n\n  const handleRemoveHeader = (index: number) => {\n    setHeaders(headers.filter((_, i) => i !== index));\n  };\n\n  const handleSave = () => {\n    // Validate the address\n    if (!address.trim()) {\n      setError(\"Server address is required\");\n      return;\n    }\n\n    if (!address.startsWith(\"http://\") && !address.startsWith(\"https://\")) {\n      setError(\"Server address must start with http:// or https://\");\n      return;\n    }\n\n    // Convert headers array to object\n    const headersObject = headers.reduce(\n      (acc, { key, value }) => {\n        if (key.trim() && value.trim()) {\n          acc[key] = value;\n        }\n        return acc;\n      },\n      {} as Record<string, string>,\n    );\n\n    // Remove trailing slash and save\n    const cleanedAddress = address.trim().replace(/\\/$/, \"\");\n    setSettings({\n      ...settings,\n      address: cleanedAddress,\n      customHeaders: headersObject,\n    });\n    router.back();\n  };\n\n  return (\n    <KeyboardAwareScrollView\n      contentContainerClassName=\"items-center gap-4 px-4 py-4\"\n      bottomOffset={20}\n      keyboardShouldPersistTaps=\"handled\"\n      contentInsetAdjustmentBehavior=\"automatic\"\n    >\n      {/* Error Message */}\n      {error && (\n        <View className=\"w-full rounded-lg bg-red-50 p-3 dark:bg-red-950\">\n          <Text className=\"text-center text-sm text-red-600 dark:text-red-400\">\n            {error}\n          </Text>\n        </View>\n      )}\n\n      {/* Server Address Section */}\n      <View className=\"w-full\">\n        <Text className=\"mb-2 px-1 text-sm font-medium text-muted-foreground\">\n          Server URL\n        </Text>\n        <View className=\"w-full gap-3 rounded-lg bg-card px-4 py-4\">\n          <Text className=\"text-sm text-muted-foreground\">\n            Enter the URL of your Karakeep server\n          </Text>\n          <Input\n            placeholder=\"https://cloud.karakeep.app\"\n            value={address}\n            onChangeText={(text) => {\n              setAddress(text);\n              setError(undefined);\n            }}\n            autoCapitalize=\"none\"\n            keyboardType=\"url\"\n            autoFocus\n            inputClasses=\"bg-background\"\n          />\n          <Text className=\"text-xs text-muted-foreground\">\n            Must start with http:// or https://\n          </Text>\n        </View>\n      </View>\n\n      {/* Custom Headers Section */}\n      <View className=\"w-full\">\n        <Text className=\"mb-2 px-1 text-sm font-medium text-muted-foreground\">\n          Custom Headers\n          {headers.length > 0 && (\n            <Text className=\"text-muted-foreground\"> ({headers.length})</Text>\n          )}\n        </Text>\n        <View className=\"w-full gap-3 rounded-lg bg-card px-4 py-4\">\n          <Text className=\"text-sm text-muted-foreground\">\n            Add custom HTTP headers for API requests\n          </Text>\n\n          {/* Existing Headers List */}\n          {headers.length === 0 ? (\n            <View className=\"py-4\">\n              <Text className=\"text-center text-sm text-muted-foreground\">\n                No custom headers configured\n              </Text>\n            </View>\n          ) : (\n            <View className=\"gap-2\">\n              {headers.map((header, index) => (\n                <View\n                  key={index}\n                  className=\"flex-row items-center gap-3 rounded-lg border border-border bg-background p-3\"\n                >\n                  <View className=\"flex-1 gap-1\">\n                    <Text className=\"text-sm font-semibold\">{header.key}</Text>\n                    <Text\n                      className=\"text-xs text-muted-foreground\"\n                      numberOfLines={1}\n                    >\n                      {header.value}\n                    </Text>\n                  </View>\n                  <Pressable\n                    onPress={() => handleRemoveHeader(index)}\n                    className=\"rounded-md p-2\"\n                    hitSlop={8}\n                  >\n                    <Trash2 size={18} color=\"#ef4444\" />\n                  </Pressable>\n                </View>\n              ))}\n            </View>\n          )}\n\n          {/* Add New Header Form */}\n          <View className=\"gap-2 border-t border-border pt-4\">\n            <Text className=\"text-sm font-medium\">Add New Header</Text>\n            <Input\n              placeholder=\"Header Name (e.g., X-Custom-Header)\"\n              value={newHeaderKey}\n              onChangeText={setNewHeaderKey}\n              autoCapitalize=\"none\"\n              inputClasses=\"bg-background\"\n            />\n            <Input\n              placeholder=\"Header Value\"\n              value={newHeaderValue}\n              onChangeText={setNewHeaderValue}\n              autoCapitalize=\"none\"\n              inputClasses=\"bg-background\"\n            />\n            <Button\n              variant=\"secondary\"\n              onPress={handleAddHeader}\n              disabled={!newHeaderKey.trim() || !newHeaderValue.trim()}\n            >\n              <Plus size={16} color={iconColor} />\n              <Text className=\"text-sm\">Add Header</Text>\n            </Button>\n          </View>\n        </View>\n      </View>\n      <Pressable onPress={handleSave} className=\"w-full items-center\">\n        <Text className=\"font-semibold text-primary\">Save</Text>\n      </Pressable>\n    </KeyboardAwareScrollView>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/app/sharing.tsx",
    "content": "import { useEffect, useRef, useState } from \"react\";\nimport { Pressable, View } from \"react-native\";\nimport Animated, { FadeIn } from \"react-native-reanimated\";\nimport { useRouter } from \"expo-router\";\nimport { useShareIntentContext } from \"expo-share-intent\";\nimport ErrorAnimation from \"@/components/sharing/ErrorAnimation\";\nimport LoadingAnimation from \"@/components/sharing/LoadingAnimation\";\nimport SuccessAnimation from \"@/components/sharing/SuccessAnimation\";\nimport { Button } from \"@/components/ui/Button\";\nimport { Text } from \"@/components/ui/Text\";\nimport useAppSettings from \"@/lib/settings\";\nimport { useUploadAsset } from \"@/lib/upload\";\nimport { useMutation, useQueryClient } from \"@tanstack/react-query\";\nimport { z } from \"zod\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\nimport { BookmarkTypes, ZBookmark } from \"@karakeep/shared/types/bookmarks\";\n\ntype Mode =\n  | { type: \"idle\" }\n  | { type: \"success\"; bookmarkId: string }\n  | { type: \"alreadyExists\"; bookmarkId: string }\n  | { type: \"error\" };\n\nfunction SaveBookmark({ setMode }: { setMode: (mode: Mode) => void }) {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n\n  const onSaved = (d: ZBookmark & { alreadyExists: boolean }) => {\n    queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter());\n    setMode({\n      type: d.alreadyExists ? \"alreadyExists\" : \"success\",\n      bookmarkId: d.id,\n    });\n  };\n\n  const { hasShareIntent, shareIntent, resetShareIntent } =\n    useShareIntentContext();\n  const { settings, isLoading } = useAppSettings();\n  const { uploadAsset } = useUploadAsset(settings, {\n    onSuccess: onSaved,\n    onError: () => {\n      setMode({ type: \"error\" });\n    },\n  });\n\n  useEffect(() => {\n    if (isLoading) {\n      return;\n    }\n    if (!isPending && shareIntent.webUrl) {\n      mutate({\n        type: BookmarkTypes.LINK,\n        url: shareIntent.webUrl,\n        source: \"mobile\",\n      });\n    } else if (!isPending && shareIntent?.text) {\n      const val = z.string().url();\n      if (val.safeParse(shareIntent.text).success) {\n        // This is a URL, else treated as text\n        mutate({\n          type: BookmarkTypes.LINK,\n          url: shareIntent.text,\n          source: \"mobile\",\n        });\n      } else {\n        mutate({\n          type: BookmarkTypes.TEXT,\n          text: shareIntent.text,\n          source: \"mobile\",\n        });\n      }\n    } else if (!isPending && shareIntent?.files) {\n      uploadAsset({\n        type: shareIntent.files[0].mimeType,\n        name: shareIntent.files[0].fileName ?? \"\",\n        uri: shareIntent.files[0].path,\n      });\n    }\n    if (hasShareIntent) {\n      resetShareIntent();\n    }\n  }, [isLoading]);\n\n  const { mutate, isPending } = useMutation(\n    api.bookmarks.createBookmark.mutationOptions({\n      onSuccess: onSaved,\n      onError: () => {\n        setMode({ type: \"error\" });\n      },\n    }),\n  );\n\n  return null;\n}\n\nexport default function Sharing() {\n  const router = useRouter();\n  const [mode, setMode] = useState<Mode>({ type: \"idle\" });\n\n  const autoCloseTimeoutId = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  // Auto dismiss the modal after saving.\n  useEffect(() => {\n    if (mode.type === \"idle\") {\n      return;\n    }\n\n    autoCloseTimeoutId.current = setTimeout(\n      () => {\n        router.replace(\"dashboard\");\n      },\n      mode.type === \"error\" ? 3000 : 2500,\n    );\n\n    return () => {\n      if (autoCloseTimeoutId.current) {\n        clearTimeout(autoCloseTimeoutId.current);\n      }\n    };\n  }, [mode.type]);\n\n  const handleManage = () => {\n    if (mode.type === \"success\" || mode.type === \"alreadyExists\") {\n      router.replace(`/dashboard/bookmarks/${mode.bookmarkId}/info`);\n      if (autoCloseTimeoutId.current) {\n        clearTimeout(autoCloseTimeoutId.current);\n      }\n    }\n  };\n\n  const handleDismiss = () => {\n    if (autoCloseTimeoutId.current) {\n      clearTimeout(autoCloseTimeoutId.current);\n    }\n    router.replace(\"dashboard\");\n  };\n\n  return (\n    <View className=\"flex-1 items-center justify-center bg-background\">\n      {/* Hidden component that handles the save logic */}\n      {mode.type === \"idle\" && <SaveBookmark setMode={setMode} />}\n\n      {/* Loading State */}\n      {mode.type === \"idle\" && <LoadingAnimation />}\n\n      {/* Success State */}\n      {(mode.type === \"success\" || mode.type === \"alreadyExists\") && (\n        <Animated.View\n          entering={FadeIn.duration(200)}\n          className=\"items-center gap-6\"\n        >\n          <SuccessAnimation isAlreadyExists={mode.type === \"alreadyExists\"} />\n\n          <Animated.View\n            entering={FadeIn.delay(400).duration(300)}\n            className=\"items-center gap-2\"\n          >\n            <Text variant=\"title1\" className=\"font-semibold text-foreground\">\n              {mode.type === \"alreadyExists\" ? \"Already Hoarded!\" : \"Hoarded!\"}\n            </Text>\n            <Text variant=\"body\" className=\"text-muted-foreground\">\n              {mode.type === \"alreadyExists\"\n                ? \"This item was saved before\"\n                : \"Saved to your collection\"}\n            </Text>\n          </Animated.View>\n\n          <Animated.View\n            entering={FadeIn.delay(600).duration(300)}\n            className=\"items-center gap-3 pt-2\"\n          >\n            <Button onPress={handleManage} variant=\"primary\" size=\"lg\">\n              <Text className=\"font-medium text-primary-foreground\">\n                Manage\n              </Text>\n            </Button>\n            <Pressable\n              onPress={handleDismiss}\n              className=\"px-4 py-2 active:opacity-60\"\n            >\n              <Text className=\"text-muted-foreground\">Dismiss</Text>\n            </Pressable>\n          </Animated.View>\n        </Animated.View>\n      )}\n\n      {/* Error State */}\n      {mode.type === \"error\" && (\n        <Animated.View\n          entering={FadeIn.duration(200)}\n          className=\"items-center gap-6\"\n        >\n          <ErrorAnimation />\n\n          <Animated.View\n            entering={FadeIn.delay(300).duration(300)}\n            className=\"items-center gap-2\"\n          >\n            <Text variant=\"title1\" className=\"font-semibold text-foreground\">\n              Oops!\n            </Text>\n            <Text variant=\"body\" className=\"text-muted-foreground\">\n              Something went wrong\n            </Text>\n          </Animated.View>\n\n          <Animated.View\n            entering={FadeIn.delay(500).duration(300)}\n            className=\"items-center gap-3 pt-2\"\n          >\n            <Pressable\n              onPress={handleDismiss}\n              className=\"px-4 py-2 active:opacity-60\"\n            >\n              <Text className=\"text-muted-foreground\">Dismiss</Text>\n            </Pressable>\n          </Animated.View>\n        </Animated.View>\n      )}\n    </View>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/app/signin.tsx",
    "content": "import { useRef, useState } from \"react\";\nimport {\n  Keyboard,\n  KeyboardAvoidingView,\n  Pressable,\n  TouchableWithoutFeedback,\n  View,\n} from \"react-native\";\nimport { Redirect, useRouter } from \"expo-router\";\nimport * as WebBrowser from \"expo-web-browser\";\nimport Logo from \"@/components/Logo\";\nimport { TailwindResolver } from \"@/components/TailwindResolver\";\nimport { Button } from \"@/components/ui/Button\";\nimport { Input } from \"@/components/ui/Input\";\nimport { Text } from \"@/components/ui/Text\";\nimport useAppSettings from \"@/lib/settings\";\nimport { useMutation } from \"@tanstack/react-query\";\nimport { Bug, Edit3 } from \"lucide-react-native\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\nenum LoginType {\n  Password,\n  ApiKey,\n}\n\nexport default function Signin() {\n  const { settings, setSettings } = useAppSettings();\n  const router = useRouter();\n  const api = useTRPC();\n  const [error, setError] = useState<string | undefined>();\n  const [loginType, setLoginType] = useState<LoginType>(LoginType.Password);\n\n  const emailRef = useRef<string>(\"\");\n  const passwordRef = useRef<string>(\"\");\n  const apiKeyRef = useRef<string>(\"\");\n\n  const toggleLoginType = () => {\n    setLoginType((prev) => {\n      if (prev === LoginType.Password) {\n        return LoginType.ApiKey;\n      } else {\n        return LoginType.Password;\n      }\n    });\n  };\n\n  const { mutate: login, isPending: userNamePasswordRequestIsPending } =\n    useMutation(\n      api.apiKeys.exchange.mutationOptions({\n        onSuccess: (resp) => {\n          setSettings({ ...settings, apiKey: resp.key, apiKeyId: resp.id });\n        },\n        onError: (e) => {\n          if (e.data?.code === \"UNAUTHORIZED\") {\n            setError(\"Wrong username or password\");\n          } else {\n            setError(`${e.message}`);\n          }\n        },\n      }),\n    );\n\n  const { mutate: validateApiKey, isPending: apiKeyValueRequestIsPending } =\n    useMutation(\n      api.apiKeys.validate.mutationOptions({\n        onSuccess: () => {\n          const apiKey = apiKeyRef.current;\n          setSettings({ ...settings, apiKey: apiKey });\n        },\n        onError: (e) => {\n          if (e.data?.code === \"UNAUTHORIZED\") {\n            setError(\"Invalid API key\");\n          } else {\n            setError(`${e.message}`);\n          }\n        },\n      }),\n    );\n\n  if (settings.apiKey) {\n    return <Redirect href=\"dashboard\" />;\n  }\n\n  const onSignUp = async () => {\n    const serverAddress = settings.address ?? \"https://cloud.karakeep.app\";\n    const signupUrl = `${serverAddress}/signup?redirectUrl=${encodeURIComponent(\"karakeep://signin\")}&skipSessionRedirect=1`;\n\n    await WebBrowser.openAuthSessionAsync(signupUrl, \"karakeep://signin\");\n  };\n\n  const onSignin = () => {\n    if (!settings.address) {\n      setError(\"Server address is required\");\n      return;\n    }\n\n    if (\n      !settings.address.startsWith(\"http://\") &&\n      !settings.address.startsWith(\"https://\")\n    ) {\n      setError(\"Server address must start with http:// or https://\");\n      return;\n    }\n\n    if (loginType === LoginType.Password) {\n      const email = emailRef.current;\n      const password = passwordRef.current;\n\n      const randStr = (Math.random() + 1).toString(36).substring(5);\n      login({\n        email: email.trim(),\n        password: password,\n        keyName: `Mobile App: (${randStr})`,\n      });\n    } else if (loginType === LoginType.ApiKey) {\n      const apiKey = apiKeyRef.current;\n      validateApiKey({ apiKey: apiKey });\n    }\n  };\n\n  return (\n    <KeyboardAvoidingView behavior=\"padding\">\n      <TouchableWithoutFeedback onPress={Keyboard.dismiss}>\n        <View className=\"flex h-full flex-col justify-center gap-2 px-4\">\n          <View className=\"items-center\">\n            <TailwindResolver\n              className=\"color-foreground\"\n              comp={(styles) => (\n                <Logo\n                  height={150}\n                  width={250}\n                  fill={styles?.color?.toString()}\n                />\n              )}\n            />\n          </View>\n          {error && (\n            <Text className=\"w-full text-center text-red-500\">{error}</Text>\n          )}\n          <View className=\"gap-2\">\n            <Text className=\"font-bold\">Server Address</Text>\n            <View className=\"flex-row items-center gap-2\">\n              <View className=\"flex-1 rounded-md border border-border bg-card px-3 py-2\">\n                <Text>{settings.address ?? \"https://cloud.karakeep.app\"}</Text>\n              </View>\n              <Button\n                size=\"icon\"\n                variant=\"secondary\"\n                onPress={() => router.push(\"/server-address\")}\n              >\n                <TailwindResolver\n                  comp={(styles) => (\n                    <Edit3 size={16} color={styles?.color?.toString()} />\n                  )}\n                  className=\"color-foreground\"\n                />\n              </Button>\n            </View>\n          </View>\n          {loginType === LoginType.Password && (\n            <>\n              <View className=\"gap-2\">\n                <Text className=\"font-bold\">Email</Text>\n                <Input\n                  className=\"w-full\"\n                  inputClasses=\"bg-card\"\n                  placeholder=\"Email\"\n                  keyboardType=\"email-address\"\n                  autoCapitalize=\"none\"\n                  defaultValue={\"\"}\n                  onChangeText={(text) => (emailRef.current = text)}\n                />\n              </View>\n              <View className=\"gap-2\">\n                <Text className=\"font-bold\">Password</Text>\n                <Input\n                  className=\"w-full\"\n                  inputClasses=\"bg-card\"\n                  placeholder=\"Password\"\n                  secureTextEntry\n                  defaultValue={\"\"}\n                  autoCapitalize=\"none\"\n                  textContentType=\"password\"\n                  onChangeText={(text) => (passwordRef.current = text)}\n                />\n              </View>\n            </>\n          )}\n\n          {loginType === LoginType.ApiKey && (\n            <View className=\"gap-2\">\n              <Text className=\"font-bold\">API Key</Text>\n              <Input\n                className=\"w-full\"\n                inputClasses=\"bg-card\"\n                placeholder=\"API Key\"\n                secureTextEntry\n                defaultValue={\"\"}\n                autoCapitalize=\"none\"\n                textContentType=\"password\"\n                onChangeText={(text) => (apiKeyRef.current = text)}\n              />\n            </View>\n          )}\n\n          <View className=\"flex flex-row items-center justify-between gap-2\">\n            <Button\n              size=\"lg\"\n              androidRootClassName=\"flex-1\"\n              onPress={onSignin}\n              disabled={\n                userNamePasswordRequestIsPending || apiKeyValueRequestIsPending\n              }\n            >\n              <Text>Sign In</Text>\n            </Button>\n            <Button\n              size=\"icon\"\n              onPress={() => router.push(\"/test-connection\")}\n              disabled={!settings.address}\n            >\n              <TailwindResolver\n                comp={(styles) => (\n                  <Bug size={20} color={styles?.color?.toString()} />\n                )}\n                className=\"text-white\"\n              />\n            </Button>\n          </View>\n          <Pressable onPress={toggleLoginType}>\n            <Text className=\"mt-2 text-center text-gray-500\">\n              {loginType === LoginType.Password\n                ? \"Use API key instead?\"\n                : \"Use password instead?\"}\n            </Text>\n          </Pressable>\n          <Pressable onPress={onSignUp}>\n            <Text className=\"mt-4 text-center text-gray-500\">\n              Don&apos;t have an account?{\" \"}\n              <Text className=\"text-foreground underline\">Sign Up</Text>\n            </Text>\n          </Pressable>\n        </View>\n      </TouchableWithoutFeedback>\n    </KeyboardAvoidingView>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/app/test-connection.tsx",
    "content": "import React from \"react\";\nimport { Platform, ScrollView, View } from \"react-native\";\nimport * as Clipboard from \"expo-clipboard\";\nimport { Button } from \"@/components/ui/Button\";\nimport { Text } from \"@/components/ui/Text\";\nimport useAppSettings from \"@/lib/settings\";\nimport { buildApiHeaders, cn } from \"@/lib/utils\";\nimport { z } from \"zod\";\n\nexport default function TestConnection() {\n  const { settings, isLoading } = useAppSettings();\n  const [text, setText] = React.useState(\"\");\n  const [randomId, setRandomId] = React.useState(Math.random());\n  const [status, setStatus] = React.useState<\"running\" | \"success\" | \"error\">(\n    \"running\",\n  );\n\n  const appendText = (text: string) => {\n    setText((prev) => prev + (prev ? \"\\n\\n\" : \"\") + text);\n  };\n\n  React.useEffect(() => {\n    if (isLoading) {\n      return;\n    }\n    setStatus(\"running\");\n    appendText(\"Running connection test ...\");\n    function runTest() {\n      const request = new XMLHttpRequest();\n      request.onreadystatechange = () => {\n        if (request.readyState !== 4) {\n          return;\n        }\n\n        if (request.status === 0) {\n          appendText(\"Network connection failed: \" + request.responseText);\n          setStatus(\"error\");\n          return;\n        }\n\n        if (request.status !== 200) {\n          appendText(\"Recieve non success error code: \" + request.status);\n          appendText(\"Got the following response:\");\n          appendText(request.responseText);\n          setStatus(\"error\");\n          return;\n        }\n        try {\n          const schema = z.object({\n            status: z.string(),\n          });\n          const data = schema.parse(JSON.parse(request.responseText));\n          if (data.status !== \"ok\") {\n            appendText(`Server is not healthy: ${data.status}`);\n            setStatus(\"error\");\n            return;\n          }\n          appendText(\"ALL GOOD\");\n          setStatus(\"success\");\n        } catch (e) {\n          appendText(`Failed to parse response as JSON: ${e}`);\n          appendText(\"Got the following response:\");\n          appendText(request.responseText);\n          setStatus(\"error\");\n          return;\n        }\n      };\n\n      appendText(\"Using address: \" + settings.address);\n      request.open(\"GET\", `${settings.address}/api/health`);\n      const headers = buildApiHeaders(settings.apiKey, settings.customHeaders);\n      Object.entries(headers).forEach(([key, value]) => {\n        request.setRequestHeader(key, value);\n      });\n      request.send();\n    }\n    runTest();\n  }, [settings.address, randomId]);\n\n  return (\n    <ScrollView\n      contentInsetAdjustmentBehavior=\"automatic\"\n      contentContainerClassName=\"m-4 flex flex-1 flex-col gap-2\"\n    >\n      <Button\n        className=\"w-full\"\n        onPress={async () => {\n          await Clipboard.setStringAsync(text);\n        }}\n      >\n        <Text>Copy Diagnostics Result</Text>\n      </Button>\n      <Button\n        className=\"w-full\"\n        variant=\"secondary\"\n        onPress={() => {\n          setText(\"\");\n          setRandomId(Math.random());\n        }}\n      >\n        <Text>Retry</Text>\n      </Button>\n      <View\n        className={cn(\n          \"w-full rounded-md p-2\",\n          status === \"running\" && \"bg-primary/50\",\n          status === \"success\" && \"bg-green-500\",\n          status === \"error\" && \"bg-red-500\",\n        )}\n      >\n        <Text\n          className={cn(\n            \"w-full text-center\",\n            status === \"running\" && \"text-primary-foreground\",\n            status === \"success\" && \"text-white\",\n            status === \"error\" && \"text-white\",\n          )}\n        >\n          {status === \"running\" && \"Running connection test ...\"}\n          {status === \"success\" && \"Connection test successful\"}\n          {status === \"error\" && \"Connection test failed\"}\n        </Text>\n      </View>\n      <ScrollView className=\"border-1 border-md h-64 flex-1 border-border bg-input p-2 leading-6\">\n        <Text\n          style={{\n            fontFamily: Platform.OS === \"ios\" ? \"Courier New\" : \"monospace\",\n          }}\n        >\n          {text}\n        </Text>\n      </ScrollView>\n    </ScrollView>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/app.config.js",
    "content": "const IS_DEV = process.env.APP_VARIANT === \"development\";\n\nexport default {\n  expo: {\n    ...(IS_DEV\n      ? {\n          name: \"Karakeep (Dev)\",\n          scheme: \"karakeep-dev\",\n        }\n      : {\n          name: \"Karakeep\",\n          scheme: \"karakeep\",\n        }),\n    slug: \"hoarder\",\n    version: \"1.9.2\",\n    orientation: \"portrait\",\n    icon: {\n      light: \"./assets/icon.png\",\n      tinted: \"./assets/icon-tinted.png\",\n    },\n    experiments: {\n      reactCanary: true,\n    },\n    userInterfaceStyle: \"automatic\",\n    assetBundlePatterns: [\"**/*\"],\n    ios: {\n      supportsTablet: true,\n      bundleIdentifier: IS_DEV\n        ? \"app.hoarder.hoardermobile.dev\"\n        : \"app.hoarder.hoardermobile\",\n      splash: {\n        image: \"./assets/splash.png\",\n        resizeMode: \"contain\",\n        backgroundColor: \"#ffffff\",\n        dark: {\n          image: \"./assets/splash-white.png\",\n          resizeMode: \"contain\",\n          backgroundColor: \"#000000\",\n        },\n      },\n      config: {\n        usesNonExemptEncryption: false,\n      },\n      infoPlist: {\n        NSAppTransportSecurity: {\n          NSAllowsArbitraryLoads: true,\n        },\n      },\n      buildNumber: \"37\",\n    },\n    android: {\n      adaptiveIcon: {\n        foregroundImage: \"./assets/adaptive-icon.png\",\n        backgroundColor: \"#000000\",\n        monochromeImage: \"./assets/adaptive-icon.png\",\n      },\n      splash: {\n        image: \"./assets/splash.png\",\n        resizeMode: \"contain\",\n        backgroundColor: \"#ffffff\",\n        dark: {\n          image: \"./assets/splash-white.png\",\n          resizeMode: \"contain\",\n          backgroundColor: \"#000000\",\n        },\n      },\n      package: IS_DEV\n        ? \"app.hoarder.hoardermobile.dev\"\n        : \"app.hoarder.hoardermobile\",\n      versionCode: 37,\n    },\n    plugins: [\n      \"./plugins/trust-local-certs.js\",\n      \"./plugins/camera-not-required.js\",\n      \"expo-router\",\n      [\n        \"expo-share-intent\",\n        {\n          iosActivationRules: {\n            NSExtensionActivationSupportsWebURLWithMaxCount: 1,\n            NSExtensionActivationSupportsWebPageWithMaxCount: 1,\n            NSExtensionActivationSupportsImageWithMaxCount: 1,\n            NSExtensionActivationSupportsMovieWithMaxCount: 0,\n            NSExtensionActivationSupportsText: true,\n            NSExtensionActivationSupportsFileWithMaxCount: 10,\n            NSExtensionActivationRule:\n              'SUBQUERY (extensionItems, $extensionItem, SUBQUERY ($extensionItem.attachments, $attachment, SUBQUERY ($attachment.registeredTypeIdentifiers, $uti, $uti UTI-CONFORMS-TO \"com.adobe.pdf\" || $uti UTI-CONFORMS-TO \"public.image\" || $uti UTI-CONFORMS-TO \"public.url\" || $uti UTI-CONFORMS-TO \"public.plain-text\").@count >= 1).@count >= 1).@count >= 1',\n          },\n          androidIntentFilters: [\"text/*\", \"image/*\", \"application/pdf\"],\n        },\n      ],\n      \"expo-secure-store\",\n      [\n        \"expo-image-picker\",\n        {\n          photosPermission:\n            \"The app access your photo gallary on your request to hoard them.\",\n        },\n      ],\n      [\n        \"expo-build-properties\",\n        {\n          android: {\n            usesCleartextTraffic: true,\n            targetSdkVersion: 35,\n            ndkVersion: \"27.1.12297006\",\n          },\n        },\n      ],\n      \"expo-web-browser\",\n      [\n        \"@sentry/react-native/expo\",\n        {\n          url: \"https://sentry.io/\",\n          project: \"react-native\",\n          organization: \"localhost-labs-ltd\",\n        },\n      ],\n    ],\n    extra: {\n      router: {\n        origin: false,\n      },\n      eas: {\n        projectId: \"d6d14643-ad43-4cd3-902a-92c5944d5e45\",\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "apps/mobile/babel.config.js",
    "content": "module.exports = function (api) {\n  api.cache(true);\n  const plugins = [];\n  plugins.push(\"react-native-reanimated/plugin\");\n  return {\n    presets: [\n      [\"babel-preset-expo\", { jsxImportSource: \"nativewind\" }],\n      \"nativewind/babel\",\n    ],\n    plugins,\n  };\n};\n"
  },
  {
    "path": "apps/mobile/components/CustomHeadersModal.tsx",
    "content": "import { useState } from \"react\";\nimport { Modal, Pressable, ScrollView, View } from \"react-native\";\nimport { KeyboardAwareScrollView } from \"react-native-keyboard-controller\";\nimport { Plus, Trash2, X } from \"lucide-react-native\";\nimport { useColorScheme } from \"nativewind\";\n\nimport { Button } from \"./ui/Button\";\nimport { Input } from \"./ui/Input\";\nimport { Text } from \"./ui/Text\";\n\ninterface CustomHeadersModalProps {\n  visible: boolean;\n  customHeaders: Record<string, string>;\n  onClose: () => void;\n  onSave: (headers: Record<string, string>) => void;\n}\n\nexport function CustomHeadersModal({\n  visible,\n  customHeaders,\n  onClose,\n  onSave,\n}: CustomHeadersModalProps) {\n  const { colorScheme } = useColorScheme();\n  const iconColor = colorScheme === \"dark\" ? \"#d1d5db\" : \"#374151\";\n\n  // Convert headers object to array of entries for easier manipulation\n  const [headers, setHeaders] = useState<{ key: string; value: string }[]>(\n    Object.entries(customHeaders).map(([key, value]) => ({ key, value })),\n  );\n  const [newHeaderKey, setNewHeaderKey] = useState(\"\");\n  const [newHeaderValue, setNewHeaderValue] = useState(\"\");\n\n  const handleAddHeader = () => {\n    if (!newHeaderKey.trim() || !newHeaderValue.trim()) {\n      return;\n    }\n\n    // Check if header already exists\n    const existingIndex = headers.findIndex((h) => h.key === newHeaderKey);\n    if (existingIndex >= 0) {\n      // Update existing header\n      const updatedHeaders = [...headers];\n      updatedHeaders[existingIndex].value = newHeaderValue;\n      setHeaders(updatedHeaders);\n    } else {\n      // Add new header\n      setHeaders([...headers, { key: newHeaderKey, value: newHeaderValue }]);\n    }\n\n    setNewHeaderKey(\"\");\n    setNewHeaderValue(\"\");\n  };\n\n  const handleRemoveHeader = (index: number) => {\n    setHeaders(headers.filter((_, i) => i !== index));\n  };\n\n  const handleSave = () => {\n    // Convert array back to object\n    const headersObject = headers.reduce(\n      (acc, { key, value }) => {\n        if (key.trim() && value.trim()) {\n          acc[key] = value;\n        }\n        return acc;\n      },\n      {} as Record<string, string>,\n    );\n\n    onSave(headersObject);\n    onClose();\n  };\n\n  const handleCancel = () => {\n    // Reset to original headers\n    setHeaders(\n      Object.entries(customHeaders).map(([key, value]) => ({ key, value })),\n    );\n    setNewHeaderKey(\"\");\n    setNewHeaderValue(\"\");\n    onClose();\n  };\n\n  return (\n    <Modal\n      visible={visible}\n      transparent\n      animationType=\"slide\"\n      onRequestClose={handleCancel}\n    >\n      <View className=\"flex-1 justify-end\">\n        <Pressable\n          className=\"absolute inset-0 bg-black/50\"\n          onPress={handleCancel}\n        />\n        <View className=\"max-h-[85%] rounded-t-3xl bg-card\">\n          <KeyboardAwareScrollView\n            contentContainerClassName=\"p-6\"\n            bottomOffset={20}\n            keyboardShouldPersistTaps=\"handled\"\n          >\n            {/* Header */}\n            <View className=\"mb-4 flex flex-row items-center justify-between\">\n              <Text className=\"text-lg font-semibold\">Custom Headers</Text>\n              <Pressable onPress={handleCancel} className=\"p-2\">\n                <X size={24} color={iconColor} />\n              </Pressable>\n            </View>\n\n            <Text className=\"mb-4 text-sm text-gray-600 dark:text-gray-400\">\n              Add custom HTTP headers that will be sent with every API request.\n            </Text>\n\n            {/* Existing Headers List */}\n            <View className=\"mb-4 max-h-64\">\n              {headers.length === 0 ? (\n                <Text className=\"py-4 text-center text-sm text-gray-500 dark:text-gray-400\">\n                  No custom headers configured\n                </Text>\n              ) : (\n                <ScrollView>\n                  {headers.map((header, index) => (\n                    <View\n                      key={index}\n                      className=\"mb-2 flex-row items-center gap-2 rounded-lg border border-border bg-background p-3\"\n                    >\n                      <View className=\"flex-1\">\n                        <Text className=\"text-sm font-semibold\">\n                          {header.key}\n                        </Text>\n                        <Text\n                          className=\"text-xs text-gray-600 dark:text-gray-400\"\n                          numberOfLines={1}\n                        >\n                          {header.value}\n                        </Text>\n                      </View>\n                      <Pressable\n                        onPress={() => handleRemoveHeader(index)}\n                        className=\"p-2\"\n                      >\n                        <Trash2 size={18} color=\"#ef4444\" />\n                      </Pressable>\n                    </View>\n                  ))}\n                </ScrollView>\n              )}\n            </View>\n\n            {/* Add New Header */}\n            <View className=\"gap-2 border-t border-border pt-4\">\n              <Text className=\"text-sm font-semibold\">Add New Header</Text>\n              <Input\n                placeholder=\"Header Name (e.g., X-Custom-Header)\"\n                value={newHeaderKey}\n                onChangeText={setNewHeaderKey}\n                autoCapitalize=\"none\"\n                inputClasses=\"bg-background\"\n              />\n              <Input\n                placeholder=\"Header Value\"\n                value={newHeaderValue}\n                onChangeText={setNewHeaderValue}\n                autoCapitalize=\"none\"\n                inputClasses=\"bg-background\"\n              />\n              <Button\n                variant=\"secondary\"\n                onPress={handleAddHeader}\n                disabled={!newHeaderKey.trim() || !newHeaderValue.trim()}\n              >\n                <Plus size={16} color={iconColor} />\n                <Text className=\"text-sm\">Add Header</Text>\n              </Button>\n            </View>\n\n            {/* Action Buttons */}\n            <View className=\"mt-4 flex flex-row gap-2 border-t border-border pt-4\">\n              <Button\n                variant=\"secondary\"\n                onPress={handleCancel}\n                androidRootClassName=\"flex-1\"\n              >\n                <Text>Cancel</Text>\n              </Button>\n              <Button\n                variant=\"primary\"\n                onPress={handleSave}\n                androidRootClassName=\"flex-1\"\n              >\n                <Text>Save</Text>\n              </Button>\n            </View>\n          </KeyboardAwareScrollView>\n        </View>\n      </View>\n    </Modal>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/components/FullPageError.tsx",
    "content": "import { View } from \"react-native\";\nimport { Text } from \"@/components/ui/Text\";\n\nimport { Button } from \"./ui/Button\";\n\nexport default function FullPageError({\n  error,\n  onRetry,\n}: {\n  error: string;\n  onRetry: () => void;\n}) {\n  return (\n    <View className=\"size-full items-center justify-center\">\n      <View className=\"h-1/4 items-center justify-between rounded-lg border border-gray-500 border-transparent bg-background px-10 py-4\">\n        <Text className=\"text-bold text-3xl text-foreground\">\n          Something Went Wrong\n        </Text>\n        <Text className=\"text-foreground\"> {error}</Text>\n        <Button onPress={onRetry}>\n          <Text>Retry</Text>\n        </Button>\n      </View>\n    </View>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/components/Logo.tsx",
    "content": "import type { SvgProps } from \"react-native-svg\";\nimport * as React from \"react\";\nimport Svg, { G, Path } from \"react-native-svg\";\n\nconst Logo = (props: SvgProps) => (\n  <Svg viewBox=\"0 0 598 166\" {...props}>\n    <Path d=\"M116.76,26.63L32.75,26.63C29.64,26.63 27.12,29.15 27.12,32.26L27.12,115.84C27.12,118.95 29.64,121.47 32.75,121.47L116.76,121.47C119.87,121.47 122.39,118.95 122.39,115.84L122.39,32.26C122.39,29.15 119.87,26.63 116.76,26.63ZM68.75,107.54C68.75,108.35 68.09,109.01 67.28,109.01L41.38,109.01C40.57,109.01 39.91,108.35 39.91,107.54L39.91,40.25C39.91,39.44 40.57,38.78 41.38,38.78L66.87,38.78C67.68,38.78 68.34,39.44 68.34,40.25L68.34,65.86C68.34,65.86 68.2,76.88 68.75,85.53L68.75,107.54ZM109.19,107.54C109.19,108.71 107.89,109.41 106.91,108.77L95.1,101.05C94.59,100.72 93.93,100.73 93.43,101.09L83.08,108.58C82.65,108.89 82.14,108.92 81.71,108.75C81.33,108.48 81.08,108.05 81.08,107.55L81.08,55.29C82.48,55.01 84.04,54.87 85.84,54.87C94.69,54.87 109.19,59.86 109.19,73.96L109.19,107.54Z\" />\n    <G transform=\"matrix(1,0,0,1,9,0)\">\n      <Path d=\"M194.31,118.15L172.6,118.15L155.16,92.58L154.84,92.58L154.84,118.15L137.19,118.15L137.19,37.28L154.84,37.28L154.84,86.49L155.16,86.49L172.17,64.88L193.46,64.88L172.92,88.95L194.31,118.15Z\" />\n      <Path d=\"M194.63,72.16C197.77,69.16 201.42,66.92 205.59,65.42C209.76,63.92 214.02,63.17 218.37,63.17C222.72,63.17 226.66,63.72 229.76,64.83C232.86,65.94 235.38,67.65 237.3,69.96C239.23,72.28 240.63,75.2 241.53,78.73C242.42,82.26 242.87,86.45 242.87,91.3L242.87,118.15L226.83,118.15L226.83,106.48L225.99,110.29C225.57,112.18 224.75,113.99 223.45,115.42C222.67,116.28 221.73,117.01 220.35,117.61C217.6,118.82 214.63,119.43 211.42,119.43C209.28,119.43 207.07,119.14 204.79,118.57C202.51,118 200.42,117.07 198.53,115.79C196.64,114.51 195.09,112.79 193.88,110.66C192.67,108.52 192.06,105.88 192.06,102.74C192.06,98.89 193.11,95.79 195.22,93.43C197.32,91.08 200.03,89.26 203.35,87.97C206.67,86.69 210.36,85.83 214.42,85.4C218.49,84.97 222.44,84.76 226.29,84.76L226.29,83.9C226.29,81.26 225.36,79.32 223.51,78.07C221.65,76.82 219.37,76.2 216.66,76.2C214.16,76.2 211.76,76.74 209.44,77.8C207.12,78.87 205.14,80.15 203.5,81.65L194.62,72.13L194.63,72.16ZM226.83,94.94L224.58,94.94C222.66,94.94 220.71,95.03 218.75,95.21C216.79,95.39 215.04,95.73 213.51,96.23C211.98,96.73 210.71,97.46 209.71,98.42C208.71,99.38 208.21,100.65 208.21,102.22C208.21,103.22 208.44,104.08 208.91,104.79C209.37,105.5 209.96,106.07 210.67,106.5C211.38,106.93 212.2,107.23 213.13,107.41C214.06,107.59 214.95,107.68 215.8,107.68C219.37,107.68 222.09,106.7 223.98,104.74C225.87,102.78 226.81,100.12 226.81,96.77L226.81,94.95L226.83,94.94Z\" />\n      <Path d=\"M284.76,79.32C283.97,79.11 284.21,78.96 283.46,78.89C282.71,78.82 281.98,78.78 281.27,78.78C278.92,78.78 276.94,79.21 275.33,80.06C273.73,80.91 272.44,81.95 271.48,83.15C270.52,84.36 269.82,85.68 269.39,87.1C268.96,88.52 268.75,89.77 268.75,90.83L268.75,118.14L251.21,118.14L251.21,64.93L268.11,64.93L268.11,71.63L268.32,76.63C269.67,73.9 271.6,67.68 274.1,65.95C276.6,64.23 279.48,63.36 282.76,63.36C283.47,63.36 284.17,63.4 284.85,63.47C285.53,63.54 285.08,63.65 285.51,63.79L284.76,79.3L284.76,79.32Z\" />\n      <Path d=\"M286.41,72.16C289.55,69.16 293.2,66.92 297.37,65.42C301.54,63.92 305.8,63.17 310.15,63.17C314.5,63.17 318.44,63.72 321.54,64.83C324.64,65.94 327.16,67.65 329.08,69.96C331.01,72.28 332.41,75.2 333.31,78.73C334.2,82.26 334.65,86.45 334.65,91.3L334.65,118.15L318.61,118.15L318.61,107.48L317.83,110.45C317.41,112.05 316.71,113.59 315.69,114.89C314.83,115.98 313.79,116.88 312.15,117.61C309.4,118.82 306.43,119.43 303.22,119.43C301.08,119.43 298.87,119.14 296.59,118.57C294.31,118 292.22,117.07 290.33,115.79C288.44,114.51 286.89,112.79 285.68,110.66C284.47,108.52 283.86,105.88 283.86,102.74C283.86,98.89 284.91,95.79 287.02,93.43C289.12,91.08 291.83,89.26 295.15,87.97C298.47,86.69 302.16,85.83 306.22,85.4C310.29,84.97 314.24,84.76 318.09,84.76L318.09,83.9C318.09,81.26 317.16,79.32 315.31,78.07C313.45,76.82 311.17,76.2 308.46,76.2C305.96,76.2 303.56,76.74 301.24,77.8C298.92,78.87 296.94,80.15 295.3,81.65L286.42,72.13L286.41,72.16ZM318.61,94.94L316.36,94.94C314.44,94.94 312.49,95.03 310.53,95.21C308.57,95.39 306.82,95.73 305.29,96.23C303.76,96.73 302.49,97.46 301.49,98.42C300.49,99.38 299.99,100.65 299.99,102.22C299.99,103.22 300.22,104.08 300.69,104.79C301.15,105.5 301.74,106.07 302.45,106.5C303.16,106.93 303.98,107.23 304.91,107.41C305.84,107.59 306.73,107.68 307.58,107.68C311.15,107.68 313.87,106.7 315.76,104.74C317.65,102.78 318.59,100.12 318.59,96.77L318.59,94.95L318.61,94.94Z\" />\n      <Path d=\"M400.22,118.15L378.51,118.15L361.07,92.58L360.75,92.58L360.75,118.15L343.1,118.15L343.1,37.28L360.75,37.28L360.75,86.49L361.07,86.49L378.08,64.88L399.37,64.88L378.83,88.95L400.22,118.15Z\" />\n      <Path d=\"M452.85,92.16L452.85,94.3C452.85,95.01 452.81,95.69 452.74,96.33L414.12,96.33C414.26,97.83 414.71,99.18 415.46,100.4C416.21,101.61 417.17,102.66 418.35,103.56C419.53,104.45 420.85,105.15 422.31,105.65C423.77,106.15 425.29,106.4 426.86,106.4C429.64,106.4 431.99,105.88 433.92,104.85C435.85,103.82 437.41,102.48 438.63,100.84L450.82,108.54C448.32,112.18 445.03,114.98 440.93,116.94C436.83,118.9 432.07,119.88 426.65,119.88C422.66,119.88 418.88,119.25 415.31,118.01C411.74,116.76 408.62,114.94 405.95,112.55C403.28,110.16 401.17,107.22 399.64,103.72C398.11,100.23 397.34,96.23 397.34,91.74C397.34,87.25 398.09,83.45 399.59,79.92C401.09,76.39 403.12,73.39 405.69,70.93C408.26,68.47 411.29,66.56 414.78,65.21C418.27,63.86 422.05,63.18 426.12,63.18C430.19,63.18 433.64,63.84 436.92,65.16C440.2,66.48 443.02,68.39 445.37,70.88C447.72,73.38 449.56,76.41 450.88,79.97C452.2,83.54 452.86,87.6 452.86,92.16L452.85,92.16ZM436.7,85.42C436.7,82.64 435.83,80.25 434.08,78.25C432.33,76.25 429.71,75.25 426.22,75.25C424.51,75.25 422.94,75.52 421.51,76.05C420.08,76.58 418.84,77.32 417.77,78.24C416.7,79.17 415.84,80.26 415.2,81.5C414.56,82.75 414.2,84.05 414.13,85.4L436.7,85.4L436.7,85.42Z\" />\n      <Path d=\"M511.9,92.16L511.9,94.3C511.9,95.01 511.86,95.69 511.79,96.33L473.17,96.33C473.31,97.83 473.76,99.18 474.51,100.4C475.26,101.61 476.22,102.66 477.4,103.56C478.58,104.45 479.9,105.15 481.36,105.65C482.82,106.15 484.34,106.4 485.91,106.4C488.69,106.4 491.04,105.88 492.97,104.85C494.9,103.82 496.46,102.48 497.68,100.84L509.87,108.54C507.37,112.18 504.08,114.98 499.98,116.94C495.88,118.9 491.12,119.88 485.7,119.88C481.71,119.88 477.93,119.25 474.36,118.01C470.79,116.76 467.67,114.94 465,112.55C462.33,110.16 460.22,107.22 458.69,103.72C457.16,100.23 456.39,96.23 456.39,91.74C456.39,87.25 457.14,83.45 458.64,79.92C460.14,76.39 462.17,73.39 464.74,70.93C467.31,68.47 470.34,66.56 473.83,65.21C477.32,63.86 481.1,63.18 485.17,63.18C489.24,63.18 492.69,63.84 495.97,65.16C499.25,66.48 502.07,68.39 504.42,70.88C506.77,73.38 508.61,76.41 509.93,79.97C511.25,83.54 511.91,87.6 511.91,92.16L511.9,92.16ZM495.75,85.42C495.75,82.64 494.88,80.25 493.13,78.25C491.38,76.25 488.76,75.25 485.27,75.25C483.56,75.25 481.99,75.52 480.56,76.05C479.13,76.58 477.89,77.32 476.82,78.24C475.75,79.17 474.89,80.26 474.25,81.5C473.61,82.75 473.25,84.05 473.18,85.4L495.75,85.4L495.75,85.42Z\" />\n      <Path d=\"M577.37,91.3C577.37,95.01 576.8,98.57 575.66,102C574.52,105.42 572.86,108.44 570.69,111.04C568.51,113.64 565.86,115.73 562.72,117.3C559.58,118.87 556.02,119.65 552.02,119.65C548.74,119.65 545.64,118.99 542.71,117.67C539.79,116.35 537.5,114.55 535.86,112.27L535.65,112.27L535.65,143.83L518.11,143.83L518.11,64.88L534.8,64.88L534.8,71.41L535.12,71.41C536.76,69.27 539.02,67.4 541.91,65.79C544.8,64.19 548.2,63.38 552.13,63.38C556.06,63.38 559.47,64.13 562.61,65.63C565.75,67.13 568.4,69.16 570.58,71.73C572.75,74.3 574.43,77.28 575.61,80.66C576.79,84.05 577.38,87.6 577.38,91.3L577.37,91.3ZM560.36,91.3C560.36,89.59 560.09,87.91 559.56,86.27C559.03,84.63 558.24,83.19 557.21,81.94C556.17,80.69 554.87,79.68 553.3,78.89C551.73,78.11 549.91,77.71 547.84,77.71C545.77,77.71 544.06,78.1 542.49,78.89C540.92,79.68 539.58,80.71 538.48,81.99C537.37,83.27 536.52,84.74 535.91,86.38C535.3,88.02 535,89.7 535,91.41C535,93.12 535.3,94.8 535.91,96.44C536.51,98.08 537.37,99.54 538.48,100.83C539.59,102.11 540.92,103.15 542.49,103.93C544.06,104.72 545.84,105.11 547.84,105.11C549.84,105.11 551.73,104.72 553.3,103.93C554.87,103.15 556.17,102.11 557.21,100.83C558.24,99.55 559.03,98.07 559.56,96.39C560.1,94.72 560.36,93.02 560.36,91.31L560.36,91.3Z\" />\n    </G>\n  </Svg>\n);\nexport default Logo;\n"
  },
  {
    "path": "apps/mobile/components/SplashScreenController.tsx",
    "content": "import { SplashScreen } from \"expo-router\";\nimport useAppSettings from \"@/lib/settings\";\n\nSplashScreen.preventAutoHideAsync();\n\nexport default function SplashScreenController() {\n  const { isLoading } = useAppSettings();\n\n  if (!isLoading) {\n    SplashScreen.hide();\n  }\n\n  return null;\n}\n"
  },
  {
    "path": "apps/mobile/components/TailwindResolver.tsx",
    "content": "import { TextStyle, ViewStyle } from \"react-native\";\nimport { cssInterop } from \"nativewind\";\n\nfunction TailwindResolverImpl({\n  comp,\n  style,\n}: {\n  comp: (style?: ViewStyle & TextStyle) => React.ReactNode;\n  style?: ViewStyle & TextStyle;\n}) {\n  return comp(style);\n}\n\nexport const TailwindResolver = cssInterop(TailwindResolverImpl, {\n  className: \"style\",\n});\n"
  },
  {
    "path": "apps/mobile/components/bookmarks/BookmarkAssetImage.tsx",
    "content": "import { View } from \"react-native\";\nimport { Image, ImageContentFit } from \"expo-image\";\nimport { useAssetUrl } from \"@/lib/hooks\";\n\nexport default function BookmarkAssetImage({\n  assetId,\n  className,\n  contentFit = \"cover\",\n}: {\n  assetId: string;\n  className: string;\n  contentFit?: ImageContentFit;\n}) {\n  const assetSource = useAssetUrl(assetId);\n\n  return (\n    <View className={className}>\n      <Image\n        source={assetSource}\n        style={{ width: \"100%\", height: \"100%\" }}\n        contentFit={contentFit}\n      />\n    </View>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/components/bookmarks/BookmarkAssetView.tsx",
    "content": "import { useState } from \"react\";\nimport { Pressable, View } from \"react-native\";\nimport ImageView from \"react-native-image-viewing\";\nimport BookmarkAssetImage from \"@/components/bookmarks/BookmarkAssetImage\";\nimport { PDFViewer } from \"@/components/bookmarks/PDFViewer\";\nimport { useAssetUrl } from \"@/lib/hooks\";\n\nimport { BookmarkTypes, ZBookmark } from \"@karakeep/shared/types/bookmarks\";\n\ninterface BookmarkAssetViewProps {\n  bookmark: ZBookmark;\n}\n\nexport default function BookmarkAssetView({\n  bookmark,\n}: BookmarkAssetViewProps) {\n  const [imageZoom, setImageZoom] = useState(false);\n\n  if (bookmark.content.type !== BookmarkTypes.ASSET) {\n    throw new Error(\"Wrong content type rendered\");\n  }\n\n  const assetSource = useAssetUrl(bookmark.content.assetId);\n\n  // Check if this is a PDF asset\n  if (bookmark.content.assetType === \"pdf\") {\n    return (\n      <View className=\"flex flex-1\">\n        <PDFViewer\n          source={assetSource.uri ?? \"\"}\n          headers={assetSource.headers}\n        />\n      </View>\n    );\n  }\n\n  // Handle image assets as before\n  return (\n    <View className=\"flex flex-1 gap-2\">\n      <ImageView\n        visible={imageZoom}\n        imageIndex={0}\n        onRequestClose={() => setImageZoom(false)}\n        doubleTapToZoomEnabled={true}\n        images={[assetSource]}\n      />\n\n      <Pressable onPress={() => setImageZoom(true)}>\n        <BookmarkAssetImage\n          assetId={bookmark.content.assetId}\n          className=\"h-56 min-h-56 w-full\"\n        />\n      </Pressable>\n    </View>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/components/bookmarks/BookmarkCard.tsx",
    "content": "import {\n  ActivityIndicator,\n  Alert,\n  Linking,\n  Platform,\n  Pressable,\n  ScrollView,\n  Share,\n  View,\n} from \"react-native\";\nimport * as Clipboard from \"expo-clipboard\";\nimport * as FileSystem from \"expo-file-system/legacy\";\nimport * as Haptics from \"expo-haptics\";\nimport { Image } from \"expo-image\";\nimport { router, useRouter } from \"expo-router\";\nimport * as Sharing from \"expo-sharing\";\nimport { Text } from \"@/components/ui/Text\";\nimport useAppSettings from \"@/lib/settings\";\nimport { useMenuIconColors } from \"@/lib/useMenuIconColors\";\nimport { buildApiHeaders } from \"@/lib/utils\";\nimport { MenuView } from \"@react-native-menu/menu\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { Ellipsis, ShareIcon, Star } from \"lucide-react-native\";\n\nimport type { ZBookmark } from \"@karakeep/shared/types/bookmarks\";\nimport {\n  useDeleteBookmark,\n  useUpdateBookmark,\n} from \"@karakeep/shared-react/hooks/bookmarks\";\nimport { useWhoAmI } from \"@karakeep/shared-react/hooks/users\";\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\nimport { BookmarkTypes } from \"@karakeep/shared/types/bookmarks\";\nimport {\n  getBookmarkLinkImageUrl,\n  getBookmarkRefreshInterval,\n  isBookmarkStillTagging,\n} from \"@karakeep/shared/utils/bookmarkUtils\";\n\nimport { Divider } from \"../ui/Divider\";\nimport { Skeleton } from \"../ui/Skeleton\";\nimport { useToast } from \"../ui/Toast\";\nimport BookmarkAssetImage from \"./BookmarkAssetImage\";\nimport BookmarkTextMarkdown from \"./BookmarkTextMarkdown\";\nimport { NotePreview } from \"./NotePreview\";\nimport TagPill from \"./TagPill\";\n\nfunction ActionBar({ bookmark }: { bookmark: ZBookmark }) {\n  const { toast } = useToast();\n  const { settings } = useAppSettings();\n  const { data: currentUser } = useWhoAmI();\n  const { menuIconColor, destructiveMenuIconColor } = useMenuIconColors();\n\n  // Check if the current user owns this bookmark\n  const isOwner = currentUser?.id === bookmark.userId;\n\n  const onError = () => {\n    toast({\n      message: \"Something went wrong\",\n      variant: \"destructive\",\n      showProgress: false,\n    });\n  };\n\n  const { mutate: deleteBookmark, isPending: isDeletionPending } =\n    useDeleteBookmark({\n      onSuccess: () => {\n        toast({\n          message: \"The bookmark has been deleted!\",\n          showProgress: false,\n        });\n      },\n      onError,\n    });\n\n  const { mutate: favouriteBookmark, variables } = useUpdateBookmark({\n    onError,\n  });\n\n  const { mutate: archiveBookmark, isPending: isArchivePending } =\n    useUpdateBookmark({\n      onSuccess: (resp) => {\n        toast({\n          message: `The bookmark has been ${resp.archived ? \"archived\" : \"un-archived\"}!`,\n          showProgress: false,\n        });\n      },\n      onError,\n    });\n\n  const deleteBookmarkAlert = () =>\n    Alert.alert(\n      \"Delete bookmark?\",\n      \"Are you sure you want to delete this bookmark?\",\n      [\n        { text: \"Cancel\", style: \"cancel\" },\n        {\n          text: \"Delete\",\n          onPress: () => deleteBookmark({ bookmarkId: bookmark.id }),\n          style: \"destructive\",\n        },\n      ],\n    );\n\n  const handleShare = async () => {\n    try {\n      switch (bookmark.content.type) {\n        case BookmarkTypes.LINK:\n          await Share.share({\n            url: bookmark.content.url,\n            message: bookmark.content.url,\n          });\n          break;\n\n        case BookmarkTypes.TEXT:\n          await Clipboard.setStringAsync(bookmark.content.text);\n          toast({\n            message: \"Text copied to clipboard\",\n            showProgress: false,\n          });\n          break;\n\n        case BookmarkTypes.ASSET:\n          if (bookmark.content.assetType === \"image\") {\n            if (await Sharing.isAvailableAsync()) {\n              const assetUrl = `${settings.address}/api/assets/${bookmark.content.assetId}`;\n              const fileUri = `${FileSystem.documentDirectory}temp_image.jpg`;\n\n              const downloadResult = await FileSystem.downloadAsync(\n                assetUrl,\n                fileUri,\n                {\n                  headers: buildApiHeaders(\n                    settings.apiKey,\n                    settings.customHeaders,\n                  ),\n                },\n              );\n\n              if (downloadResult.status === 200) {\n                await Sharing.shareAsync(downloadResult.uri);\n                // Clean up the temporary file\n                await FileSystem.deleteAsync(downloadResult.uri, {\n                  idempotent: true,\n                });\n              } else {\n                throw new Error(\"Failed to download image\");\n              }\n            }\n          } else if (bookmark.content.assetType === \"pdf\") {\n            if (await Sharing.isAvailableAsync()) {\n              const assetUrl = `${settings.address}/api/assets/${bookmark.content.assetId}`;\n              const fileName = bookmark.content.fileName || \"document.pdf\";\n              const fileUri = `${FileSystem.documentDirectory}${fileName}`;\n\n              const downloadResult = await FileSystem.downloadAsync(\n                assetUrl,\n                fileUri,\n                {\n                  headers: buildApiHeaders(\n                    settings.apiKey,\n                    settings.customHeaders,\n                  ),\n                },\n              );\n\n              if (downloadResult.status === 200) {\n                await Sharing.shareAsync(downloadResult.uri, {\n                  mimeType: \"application/pdf\",\n                  UTI: \"com.adobe.pdf\",\n                });\n                // Clean up the temporary file\n                await FileSystem.deleteAsync(downloadResult.uri, {\n                  idempotent: true,\n                });\n              } else {\n                throw new Error(\"Failed to download PDF\");\n              }\n            }\n          }\n          break;\n      }\n    } catch (error) {\n      console.error(\"Share error:\", error);\n      toast({\n        message: \"Failed to share\",\n        variant: \"destructive\",\n        showProgress: false,\n      });\n    }\n  };\n\n  // Build actions array based on ownership\n  const menuActions = [];\n  if (isOwner) {\n    menuActions.push(\n      {\n        id: \"edit\",\n        title: \"Edit\",\n        image: Platform.select({\n          ios: \"pencil\",\n        }),\n        imageColor: Platform.select({\n          ios: menuIconColor,\n        }),\n      },\n      {\n        id: \"manage_list\",\n        title: \"Manage Lists\",\n        image: Platform.select({\n          ios: \"list.bullet\",\n        }),\n        imageColor: Platform.select({\n          ios: menuIconColor,\n        }),\n      },\n      {\n        id: \"manage_tags\",\n        title: \"Manage Tags\",\n        image: Platform.select({\n          ios: \"tag\",\n        }),\n        imageColor: Platform.select({\n          ios: menuIconColor,\n        }),\n      },\n      {\n        id: \"archive\",\n        title: bookmark.archived ? \"Un-archive\" : \"Archive\",\n        image: Platform.select({\n          ios: \"folder\",\n        }),\n        imageColor: Platform.select({\n          ios: menuIconColor,\n        }),\n      },\n      {\n        id: \"delete\",\n        title: \"Delete\",\n        attributes: {\n          destructive: true,\n        },\n        image: Platform.select({\n          ios: \"trash\",\n        }),\n        imageColor: Platform.select({\n          ios: destructiveMenuIconColor,\n        }),\n      },\n    );\n  }\n\n  return (\n    <View className=\"flex flex-row gap-4\">\n      {(isArchivePending || isDeletionPending) && <ActivityIndicator />}\n      {isOwner && (\n        <Pressable\n          onPress={() => {\n            Haptics.selectionAsync();\n            favouriteBookmark({\n              bookmarkId: bookmark.id,\n              favourited: !bookmark.favourited,\n            });\n          }}\n        >\n          {(variables ? variables.favourited : bookmark.favourited) ? (\n            <Star fill=\"#ebb434\" color=\"#ebb434\" />\n          ) : (\n            <Star color=\"gray\" />\n          )}\n        </Pressable>\n      )}\n\n      <Pressable\n        onPress={() => {\n          Haptics.selectionAsync();\n          handleShare();\n        }}\n      >\n        <ShareIcon color=\"gray\" />\n      </Pressable>\n\n      {isOwner && menuActions.length > 0 && (\n        <MenuView\n          onPressAction={({ nativeEvent }) => {\n            Haptics.selectionAsync();\n            if (nativeEvent.event === \"delete\") {\n              deleteBookmarkAlert();\n            } else if (nativeEvent.event === \"archive\") {\n              archiveBookmark({\n                bookmarkId: bookmark.id,\n                archived: !bookmark.archived,\n              });\n            } else if (nativeEvent.event === \"manage_list\") {\n              router.push(`/dashboard/bookmarks/${bookmark.id}/manage_lists`);\n            } else if (nativeEvent.event === \"manage_tags\") {\n              router.push(`/dashboard/bookmarks/${bookmark.id}/manage_tags`);\n            } else if (nativeEvent.event === \"edit\") {\n              router.push(`/dashboard/bookmarks/${bookmark.id}/info`);\n            }\n          }}\n          actions={menuActions}\n          shouldOpenOnLongPress={false}\n        >\n          <Ellipsis onPress={() => Haptics.selectionAsync()} color=\"gray\" />\n        </MenuView>\n      )}\n    </View>\n  );\n}\n\nfunction TagList({ bookmark }: { bookmark: ZBookmark }) {\n  const tags = bookmark.tags;\n  const { data: currentUser } = useWhoAmI();\n  const isOwner = currentUser?.id === bookmark.userId;\n\n  if (isBookmarkStillTagging(bookmark)) {\n    return (\n      <>\n        <Skeleton className=\"h-4 w-full\" />\n        <Skeleton className=\"h-4 w-full\" />\n      </>\n    );\n  }\n\n  return (\n    <ScrollView horizontal showsHorizontalScrollIndicator={false}>\n      <View className=\"flex flex-row gap-2\">\n        {tags.map((t) => (\n          <TagPill key={t.id} tag={t} clickable={isOwner} />\n        ))}\n      </View>\n    </ScrollView>\n  );\n}\n\nfunction LinkCard({\n  bookmark,\n  onOpenBookmark,\n}: {\n  bookmark: ZBookmark;\n  onOpenBookmark: () => void;\n}) {\n  const { settings } = useAppSettings();\n  const { data: currentUser } = useWhoAmI();\n  const isOwner = currentUser?.id === bookmark.userId;\n\n  if (bookmark.content.type !== BookmarkTypes.LINK) {\n    throw new Error(\"Wrong content type rendered\");\n  }\n\n  const note = settings.showNotes ? bookmark.note?.trim() : undefined;\n  const url = bookmark.content.url;\n  const parsedUrl = new URL(url);\n\n  const imageUrl = getBookmarkLinkImageUrl(bookmark.content);\n\n  let imageComp;\n  if (imageUrl) {\n    imageComp = (\n      <View className=\"h-56 min-h-56 w-full\">\n        <Image\n          source={\n            imageUrl.localAsset\n              ? {\n                  uri: `${settings.address}${imageUrl.url}`,\n                  headers: buildApiHeaders(\n                    settings.apiKey,\n                    settings.customHeaders,\n                  ),\n                }\n              : {\n                  uri: imageUrl.url,\n                }\n          }\n          style={{ width: \"100%\", height: \"100%\" }}\n          contentFit=\"cover\"\n        />\n      </View>\n    );\n  } else {\n    imageComp = (\n      <View className=\"h-56 w-full overflow-hidden rounded-t-lg\">\n        <Image\n          // oxlint-disable-next-line no-require-imports\n          source={require(\"@/assets/blur.jpeg\")}\n          style={{ width: \"100%\", height: \"100%\" }}\n          contentFit=\"cover\"\n        />\n      </View>\n    );\n  }\n\n  return (\n    <View className=\"flex gap-2\">\n      <Pressable onPress={onOpenBookmark}>{imageComp}</Pressable>\n      <View className=\"flex gap-2 p-2\">\n        <Text\n          className=\"text-xl font-bold text-foreground\"\n          numberOfLines={2}\n          onPress={onOpenBookmark}\n        >\n          {bookmark.title ?? bookmark.content.title ?? parsedUrl.host}\n        </Text>\n        {note && (\n          <NotePreview\n            note={note}\n            bookmarkId={bookmark.id}\n            readOnly={!isOwner}\n          />\n        )}\n        <TagList bookmark={bookmark} />\n        <Divider orientation=\"vertical\" className=\"mt-2 h-0.5 w-full\" />\n        <View className=\"mt-2 flex flex-row justify-between px-2 pb-2\">\n          <Text className=\"my-auto\" numberOfLines={1}>\n            {parsedUrl.host}\n          </Text>\n          <ActionBar bookmark={bookmark} />\n        </View>\n      </View>\n    </View>\n  );\n}\n\nfunction TextCard({\n  bookmark,\n  onOpenBookmark,\n}: {\n  bookmark: ZBookmark;\n  onOpenBookmark: () => void;\n}) {\n  const { settings } = useAppSettings();\n  const { data: currentUser } = useWhoAmI();\n  const isOwner = currentUser?.id === bookmark.userId;\n\n  if (bookmark.content.type !== BookmarkTypes.TEXT) {\n    throw new Error(\"Wrong content type rendered\");\n  }\n  const note = settings.showNotes ? bookmark.note?.trim() : undefined;\n  const content = bookmark.content.text;\n  return (\n    <View className=\"flex max-h-96 gap-2 p-2\">\n      <Pressable onPress={onOpenBookmark}>\n        {bookmark.title && (\n          <Text className=\"text-xl font-bold\" numberOfLines={2}>\n            {bookmark.title}\n          </Text>\n        )}\n      </Pressable>\n      <View className=\"max-h-56 overflow-hidden p-2 text-foreground\">\n        <Pressable onPress={onOpenBookmark}>\n          <BookmarkTextMarkdown text={content} />\n        </Pressable>\n      </View>\n      {note && (\n        <NotePreview note={note} bookmarkId={bookmark.id} readOnly={!isOwner} />\n      )}\n      <TagList bookmark={bookmark} />\n      <Divider orientation=\"vertical\" className=\"mt-2 h-0.5 w-full\" />\n      <View className=\"flex flex-row justify-between p-2\">\n        <View />\n        <ActionBar bookmark={bookmark} />\n      </View>\n    </View>\n  );\n}\n\nfunction AssetCard({\n  bookmark,\n  onOpenBookmark,\n}: {\n  bookmark: ZBookmark;\n  onOpenBookmark: () => void;\n}) {\n  const { settings } = useAppSettings();\n  const { data: currentUser } = useWhoAmI();\n  const isOwner = currentUser?.id === bookmark.userId;\n\n  if (bookmark.content.type !== BookmarkTypes.ASSET) {\n    throw new Error(\"Wrong content type rendered\");\n  }\n  const note = settings.showNotes ? bookmark.note?.trim() : undefined;\n  const title = bookmark.title ?? bookmark.content.fileName;\n\n  const assetImage =\n    bookmark.assets.find((r) => r.assetType == \"assetScreenshot\")?.id ??\n    bookmark.content.assetId;\n\n  return (\n    <View className=\"flex gap-2\">\n      <Pressable onPress={onOpenBookmark}>\n        <BookmarkAssetImage\n          assetId={assetImage}\n          className=\"h-56 min-h-56 w-full\"\n        />\n      </Pressable>\n      <View className=\"flex gap-2 p-2\">\n        <Pressable onPress={onOpenBookmark}>\n          {title && (\n            <Text numberOfLines={2} className=\"text-xl font-bold\">\n              {title}\n            </Text>\n          )}\n        </Pressable>\n        {note && (\n          <NotePreview\n            note={note}\n            bookmarkId={bookmark.id}\n            readOnly={!isOwner}\n          />\n        )}\n        <TagList bookmark={bookmark} />\n        <Divider orientation=\"vertical\" className=\"mt-2 h-0.5 w-full\" />\n        <View className=\"mt-2 flex flex-row justify-between px-2 pb-2\">\n          <View />\n          <ActionBar bookmark={bookmark} />\n        </View>\n      </View>\n    </View>\n  );\n}\n\nexport default function BookmarkCard({\n  bookmark: initialData,\n}: {\n  bookmark: ZBookmark;\n}) {\n  const api = useTRPC();\n  const { data: bookmark } = useQuery(\n    api.bookmarks.getBookmark.queryOptions(\n      {\n        bookmarkId: initialData.id,\n      },\n      {\n        initialData,\n        refetchInterval: (query) => {\n          const data = query.state.data;\n          if (!data) {\n            return false;\n          }\n          return getBookmarkRefreshInterval(data);\n        },\n      },\n    ),\n  );\n\n  const router = useRouter();\n  const { settings } = useAppSettings();\n  const { toast } = useToast();\n\n  const onOpenBookmark = (bookmark: ZBookmark) => {\n    if (\n      bookmark.content.type === BookmarkTypes.LINK &&\n      settings.defaultBookmarkView === \"externalBrowser\"\n    ) {\n      void Linking.openURL(bookmark.content.url).catch(() => {\n        toast({\n          message: \"Failed to open link\",\n          variant: \"destructive\",\n          showProgress: false,\n        });\n\n        router.push(`/dashboard/bookmarks/${bookmark.id}`);\n      });\n      return;\n    }\n\n    router.push(`/dashboard/bookmarks/${bookmark.id}`);\n  };\n\n  let comp;\n  switch (bookmark.content.type) {\n    case BookmarkTypes.LINK:\n      comp = (\n        <LinkCard\n          bookmark={bookmark}\n          onOpenBookmark={() => onOpenBookmark(bookmark)}\n        />\n      );\n      break;\n    case BookmarkTypes.TEXT:\n      comp = (\n        <TextCard\n          bookmark={bookmark}\n          onOpenBookmark={() => onOpenBookmark(bookmark)}\n        />\n      );\n      break;\n    case BookmarkTypes.ASSET:\n      comp = (\n        <AssetCard\n          bookmark={bookmark}\n          onOpenBookmark={() => onOpenBookmark(bookmark)}\n        />\n      );\n      break;\n  }\n\n  return (\n    <View\n      className=\"overflow-hidden rounded-xl bg-card\"\n      style={{ borderCurve: \"continuous\" }}\n    >\n      {comp}\n    </View>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/components/bookmarks/BookmarkHtmlHighlighterDom.tsx",
    "content": "\"use dom\";\n\nimport \"@/globals.css\";\n\nimport type { Highlight } from \"@karakeep/shared-react/components/BookmarkHtmlHighlighter\";\nimport BookmarkHTMLHighlighter from \"@karakeep/shared-react/components/BookmarkHtmlHighlighter\";\nimport ScrollProgressTracker from \"@karakeep/shared-react/components/ScrollProgressTracker\";\n\nexport default function BookmarkHtmlHighlighterDom({\n  htmlContent,\n  contentStyle,\n  highlights,\n  readOnly,\n  onHighlight,\n  onUpdateHighlight,\n  onDeleteHighlight,\n  readingProgressOffset,\n  readingProgressAnchor,\n  restoreReadingPosition,\n  onSavePosition,\n  onScrollPositionChange,\n}: {\n  htmlContent: string;\n  contentStyle?: React.CSSProperties;\n  highlights?: Highlight[];\n  readOnly?: boolean;\n  onHighlight?: (highlight: Highlight) => void;\n  onUpdateHighlight?: (highlight: Highlight) => void;\n  onDeleteHighlight?: (highlight: Highlight) => void;\n  readingProgressOffset?: number | null;\n  readingProgressAnchor?: string | null;\n  restoreReadingPosition?: boolean;\n  onSavePosition?: (position: {\n    offset: number;\n    anchor: string;\n    percent: number;\n  }) => void;\n  onScrollPositionChange?: (position: {\n    offset: number;\n    anchor: string;\n    percent: number;\n  }) => void;\n  dom?: import(\"expo/dom\").DOMProps;\n}) {\n  return (\n    <div style={{ maxWidth: \"100vw\", overflowX: \"hidden\" }}>\n      <ScrollProgressTracker\n        onSavePosition={onSavePosition}\n        onScrollPositionChange={onScrollPositionChange}\n        restorePosition={restoreReadingPosition}\n        readingProgressOffset={readingProgressOffset}\n        readingProgressAnchor={readingProgressAnchor}\n        showProgressBar\n        progressBarStyle={{ position: \"fixed\" }}\n      >\n        <BookmarkHTMLHighlighter\n          htmlContent={htmlContent}\n          highlights={highlights}\n          readOnly={readOnly}\n          onHighlight={onHighlight}\n          onUpdateHighlight={onUpdateHighlight}\n          onDeleteHighlight={onDeleteHighlight}\n          style={contentStyle}\n        />\n      </ScrollProgressTracker>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/components/bookmarks/BookmarkLinkPreview.tsx",
    "content": "import { useState } from \"react\";\nimport { Pressable, TouchableOpacity, View } from \"react-native\";\nimport ImageView from \"react-native-image-viewing\";\nimport WebView from \"react-native-webview\";\nimport { WebViewSourceUri } from \"react-native-webview/lib/WebViewTypes\";\nimport { Text } from \"@/components/ui/Text\";\nimport { useAssetUrl } from \"@/lib/hooks\";\nimport { useReaderSettings, WEBVIEW_FONT_FAMILIES } from \"@/lib/readerSettings\";\nimport { useColorScheme } from \"@/lib/useColorScheme\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { BookOpen, X } from \"lucide-react-native\";\n\nimport {\n  useCreateHighlight,\n  useDeleteHighlight,\n  useUpdateHighlight,\n} from \"@karakeep/shared-react/hooks/highlights\";\nimport { useReadingProgress } from \"@karakeep/shared-react/hooks/reading-progress\";\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\nimport { BookmarkTypes, ZBookmark } from \"@karakeep/shared/types/bookmarks\";\n\nimport FullPageError from \"../FullPageError\";\nimport FullPageSpinner from \"../ui/FullPageSpinner\";\nimport BookmarkAssetImage from \"./BookmarkAssetImage\";\nimport BookmarkHtmlHighlighterDom from \"./BookmarkHtmlHighlighterDom\";\nimport { PDFViewer } from \"./PDFViewer\";\n\nexport function BookmarkLinkBrowserPreview({\n  bookmark,\n}: {\n  bookmark: ZBookmark;\n}) {\n  if (bookmark.content.type !== BookmarkTypes.LINK) {\n    throw new Error(\"Wrong content type rendered\");\n  }\n\n  return (\n    <WebView\n      startInLoadingState={true}\n      mediaPlaybackRequiresUserAction={true}\n      source={{ uri: bookmark.content.url }}\n    />\n  );\n}\n\nexport function BookmarkLinkPdfPreview({ bookmark }: { bookmark: ZBookmark }) {\n  if (bookmark.content.type !== BookmarkTypes.LINK) {\n    throw new Error(\"Wrong content type rendered\");\n  }\n\n  const asset = bookmark.assets.find((r) => r.assetType == \"pdf\");\n\n  const assetSource = useAssetUrl(asset?.id ?? \"\");\n\n  if (!asset) {\n    return (\n      <View className=\"flex-1 bg-background\">\n        <Text>Asset has no PDF</Text>\n      </View>\n    );\n  }\n\n  return (\n    <View className=\"flex flex-1\">\n      <PDFViewer source={assetSource.uri ?? \"\"} headers={assetSource.headers} />\n    </View>\n  );\n}\n\nexport function BookmarkLinkReaderPreview({\n  bookmark,\n}: {\n  bookmark: ZBookmark;\n}) {\n  const { isDarkColorScheme: isDark } = useColorScheme();\n  const { settings: readerSettings } = useReaderSettings();\n  const api = useTRPC();\n\n  const {\n    data: bookmarkWithContent,\n    error,\n    isLoading,\n    refetch,\n  } = useQuery(\n    api.bookmarks.getBookmark.queryOptions({\n      bookmarkId: bookmark.id,\n      includeContent: true,\n    }),\n  );\n\n  const { data: highlights } = useQuery(\n    api.highlights.getForBookmark.queryOptions({\n      bookmarkId: bookmark.id,\n    }),\n  );\n\n  const { mutate: createHighlight } = useCreateHighlight();\n  const { mutate: updateHighlight } = useUpdateHighlight();\n  const { mutate: deleteHighlight } = useDeleteHighlight();\n\n  const {\n    showBanner,\n    bannerPercent,\n    onContinue,\n    onDismiss,\n    restorePosition,\n    readingProgressOffset,\n    readingProgressAnchor,\n    onSavePosition,\n    onScrollPositionChange,\n  } = useReadingProgress({\n    bookmarkId: bookmark.id,\n  });\n\n  if (isLoading) {\n    return <FullPageSpinner />;\n  }\n\n  if (error) {\n    return <FullPageError error={error.message} onRetry={refetch} />;\n  }\n\n  if (bookmarkWithContent?.content.type !== BookmarkTypes.LINK) {\n    throw new Error(\"Wrong content type rendered\");\n  }\n\n  const contentStyle: React.CSSProperties = {\n    fontFamily: WEBVIEW_FONT_FAMILIES[readerSettings.fontFamily],\n    fontSize: `${readerSettings.fontSize}px`,\n    lineHeight: String(readerSettings.lineHeight),\n    color: isDark ? \"#e5e7eb\" : \"#374151\",\n    padding: \"16px\",\n    background: isDark ? \"#000000\" : \"#ffffff\",\n  };\n\n  return (\n    <View className=\"flex-1 bg-background\">\n      {showBanner && (\n        <View className=\"flex-row items-center gap-2 border-b border-border bg-background px-4 py-2\">\n          <BookOpen size={16} className=\"text-muted-foreground\" />\n          <Text className=\"flex-1 text-sm text-muted-foreground\">\n            {bannerPercent && bannerPercent > 0\n              ? `Continue where you left off (${bannerPercent}%)`\n              : \"Continue where you left off\"}\n          </Text>\n          <TouchableOpacity\n            onPress={onContinue}\n            className=\"rounded-md bg-primary px-3 py-1\"\n          >\n            <Text className=\"text-xs font-medium text-primary-foreground\">\n              Continue\n            </Text>\n          </TouchableOpacity>\n          <TouchableOpacity onPress={onDismiss} className=\"p-1\">\n            <X size={14} className=\"text-muted-foreground\" />\n          </TouchableOpacity>\n        </View>\n      )}\n      <BookmarkHtmlHighlighterDom\n        htmlContent={bookmarkWithContent.content.htmlContent ?? \"\"}\n        contentStyle={contentStyle}\n        highlights={highlights?.highlights ?? []}\n        readingProgressOffset={readingProgressOffset}\n        readingProgressAnchor={readingProgressAnchor}\n        restoreReadingPosition={restorePosition}\n        onSavePosition={onSavePosition}\n        onScrollPositionChange={onScrollPositionChange}\n        onHighlight={(h) =>\n          createHighlight({\n            startOffset: h.startOffset,\n            endOffset: h.endOffset,\n            color: h.color,\n            bookmarkId: bookmark.id,\n            text: h.text,\n            note: h.note ?? null,\n          })\n        }\n        onUpdateHighlight={(h) =>\n          updateHighlight({\n            highlightId: h.id,\n            color: h.color,\n            note: h.note,\n          })\n        }\n        onDeleteHighlight={(h) =>\n          deleteHighlight({\n            highlightId: h.id,\n          })\n        }\n        dom={{ scrollEnabled: true }}\n      />\n    </View>\n  );\n}\n\nexport function BookmarkLinkArchivePreview({\n  bookmark,\n}: {\n  bookmark: ZBookmark;\n}) {\n  const asset =\n    bookmark.assets.find((r) => r.assetType == \"precrawledArchive\") ??\n    bookmark.assets.find((r) => r.assetType == \"fullPageArchive\");\n\n  const assetSource = useAssetUrl(asset?.id ?? \"\");\n\n  if (!asset) {\n    return (\n      <View className=\"flex-1 bg-background\">\n        <Text>Asset has no offline archive</Text>\n      </View>\n    );\n  }\n\n  const webViewUri: WebViewSourceUri = {\n    uri: assetSource.uri!,\n    headers: assetSource.headers,\n  };\n  return (\n    <WebView\n      startInLoadingState={true}\n      mediaPlaybackRequiresUserAction={true}\n      source={webViewUri}\n      decelerationRate={0.998}\n    />\n  );\n}\n\nexport function BookmarkLinkScreenshotPreview({\n  bookmark,\n}: {\n  bookmark: ZBookmark;\n}) {\n  const asset = bookmark.assets.find((r) => r.assetType == \"screenshot\");\n\n  const assetSource = useAssetUrl(asset?.id ?? \"\");\n  const [imageZoom, setImageZoom] = useState(false);\n\n  if (!asset) {\n    return (\n      <View className=\"flex-1 bg-background\">\n        <Text>Asset has no screenshot</Text>\n      </View>\n    );\n  }\n\n  return (\n    <View className=\"flex flex-1 gap-2\">\n      <ImageView\n        visible={imageZoom}\n        imageIndex={0}\n        onRequestClose={() => setImageZoom(false)}\n        doubleTapToZoomEnabled={true}\n        images={[assetSource]}\n      />\n      <Pressable onPress={() => setImageZoom(true)}>\n        <BookmarkAssetImage\n          assetId={asset.id}\n          className=\"h-full w-full\"\n          contentFit=\"contain\"\n        />\n      </Pressable>\n    </View>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/components/bookmarks/BookmarkLinkTypeSelector.tsx",
    "content": "import { Platform } from \"react-native\";\nimport * as Haptics from \"expo-haptics\";\nimport { useMenuIconColors } from \"@/lib/useMenuIconColors\";\nimport { MenuView } from \"@react-native-menu/menu\";\nimport { ChevronDown } from \"lucide-react-native\";\n\nimport { BookmarkTypes, ZBookmark } from \"@karakeep/shared/types/bookmarks\";\n\nexport type BookmarkLinkType =\n  | \"browser\"\n  | \"reader\"\n  | \"screenshot\"\n  | \"archive\"\n  | \"pdf\";\n\nfunction getAvailableViewTypes(bookmark: ZBookmark): BookmarkLinkType[] {\n  if (bookmark.content.type !== BookmarkTypes.LINK) {\n    return [];\n  }\n\n  const availableTypes: BookmarkLinkType[] = [\"browser\", \"reader\"];\n\n  if (bookmark.assets.some((asset) => asset.assetType === \"screenshot\")) {\n    availableTypes.push(\"screenshot\");\n  }\n\n  if (\n    bookmark.assets.some(\n      (asset) =>\n        asset.assetType === \"precrawledArchive\" ||\n        asset.assetType === \"fullPageArchive\",\n    )\n  ) {\n    availableTypes.push(\"archive\");\n  }\n  if (bookmark.assets.some((asset) => asset.assetType === \"pdf\")) {\n    availableTypes.push(\"pdf\");\n  }\n\n  return availableTypes;\n}\n\ninterface BookmarkLinkTypeSelectorProps {\n  type: BookmarkLinkType;\n  onChange: (type: BookmarkLinkType) => void;\n  bookmark: ZBookmark;\n}\n\nexport default function BookmarkLinkTypeSelector({\n  type,\n  onChange,\n  bookmark,\n}: BookmarkLinkTypeSelectorProps) {\n  const availableTypes = getAvailableViewTypes(bookmark);\n  const { menuIconColor } = useMenuIconColors();\n\n  const viewActions = [\n    {\n      id: \"reader\" as const,\n      title: \"Reader View\",\n      state: type === \"reader\" ? (\"on\" as const) : undefined,\n      image: Platform.select({\n        ios: \"doc.text\",\n      }),\n      imageColor: Platform.select({\n        ios: menuIconColor,\n      }),\n    },\n    {\n      id: \"browser\" as const,\n      title: \"Browser\",\n      state: type === \"browser\" ? (\"on\" as const) : undefined,\n      image: Platform.select({\n        ios: \"safari\",\n      }),\n      imageColor: Platform.select({\n        ios: menuIconColor,\n      }),\n    },\n    {\n      id: \"screenshot\" as const,\n      title: \"Screenshot\",\n      state: type === \"screenshot\" ? (\"on\" as const) : undefined,\n      image: Platform.select({\n        ios: \"camera\",\n      }),\n      imageColor: Platform.select({\n        ios: menuIconColor,\n      }),\n    },\n    {\n      id: \"archive\" as const,\n      title: \"Archived Page\",\n      state: type === \"archive\" ? (\"on\" as const) : undefined,\n      image: Platform.select({\n        ios: \"tray.full\",\n      }),\n      imageColor: Platform.select({\n        ios: menuIconColor,\n      }),\n    },\n    {\n      id: \"pdf\" as const,\n      title: \"PDF\",\n      state: type === \"pdf\" ? (\"on\" as const) : undefined,\n      image: Platform.select({\n        ios: \"doc\",\n      }),\n      imageColor: Platform.select({\n        ios: menuIconColor,\n      }),\n    },\n  ];\n\n  const availableViewActions = viewActions.filter((action) =>\n    availableTypes.includes(action.id),\n  );\n\n  return (\n    <MenuView\n      onPressAction={({ nativeEvent }) => {\n        Haptics.selectionAsync();\n        onChange(nativeEvent.event as BookmarkLinkType);\n      }}\n      actions={availableViewActions}\n      shouldOpenOnLongPress={false}\n    >\n      <ChevronDown onPress={() => Haptics.selectionAsync()} color=\"gray\" />\n    </MenuView>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/components/bookmarks/BookmarkLinkView.tsx",
    "content": "import {\n  BookmarkLinkArchivePreview,\n  BookmarkLinkBrowserPreview,\n  BookmarkLinkPdfPreview,\n  BookmarkLinkReaderPreview,\n  BookmarkLinkScreenshotPreview,\n} from \"@/components/bookmarks/BookmarkLinkPreview\";\n\nimport { BookmarkTypes, ZBookmark } from \"@karakeep/shared/types/bookmarks\";\n\nimport { BookmarkLinkType } from \"./BookmarkLinkTypeSelector\";\n\ninterface BookmarkLinkViewProps {\n  bookmark: ZBookmark;\n  bookmarkPreviewType: BookmarkLinkType;\n}\n\nexport default function BookmarkLinkView({\n  bookmark,\n  bookmarkPreviewType,\n}: BookmarkLinkViewProps) {\n  if (bookmark.content.type !== BookmarkTypes.LINK) {\n    throw new Error(\"Wrong content type rendered\");\n  }\n\n  switch (bookmarkPreviewType) {\n    case \"browser\":\n      return <BookmarkLinkBrowserPreview bookmark={bookmark} />;\n    case \"reader\":\n      return <BookmarkLinkReaderPreview bookmark={bookmark} />;\n    case \"screenshot\":\n      return <BookmarkLinkScreenshotPreview bookmark={bookmark} />;\n    case \"archive\":\n      return <BookmarkLinkArchivePreview bookmark={bookmark} />;\n    case \"pdf\":\n      return <BookmarkLinkPdfPreview bookmark={bookmark} />;\n  }\n}\n"
  },
  {
    "path": "apps/mobile/components/bookmarks/BookmarkList.tsx",
    "content": "import { useRef } from \"react\";\nimport { ActivityIndicator, Keyboard, View } from \"react-native\";\nimport Animated, { LinearTransition } from \"react-native-reanimated\";\nimport EmptyState from \"@/components/ui/EmptyState\";\nimport { useScrollToTop } from \"@react-navigation/native\";\nimport { Bookmark } from \"lucide-react-native\";\n\nimport type { ZBookmark } from \"@karakeep/shared/types/bookmarks\";\n\nimport BookmarkCard from \"./BookmarkCard\";\n\nexport default function BookmarkList({\n  bookmarks,\n  header,\n  onRefresh,\n  fetchNextPage,\n  isFetchingNextPage,\n  isRefreshing,\n}: {\n  bookmarks: ZBookmark[];\n  onRefresh: () => void;\n  isRefreshing: boolean;\n  fetchNextPage?: () => void;\n  header?: React.ReactElement;\n  isFetchingNextPage?: boolean;\n}) {\n  const flatListRef = useRef(null);\n  useScrollToTop(flatListRef);\n\n  return (\n    <Animated.FlatList\n      ref={flatListRef}\n      itemLayoutAnimation={LinearTransition}\n      contentInsetAdjustmentBehavior=\"automatic\"\n      ListHeaderComponent={header}\n      contentContainerStyle={{\n        gap: 15,\n        marginHorizontal: 15,\n        marginBottom: 15,\n      }}\n      renderItem={(b) => <BookmarkCard bookmark={b.item} />}\n      ListEmptyComponent={\n        <EmptyState\n          icon={Bookmark}\n          title=\"No Bookmarks\"\n          subtitle=\"Your saved bookmarks will appear here\"\n        />\n      }\n      data={bookmarks}\n      refreshing={isRefreshing}\n      onRefresh={onRefresh}\n      onScrollBeginDrag={Keyboard.dismiss}\n      keyExtractor={(b) => b.id}\n      onEndReached={fetchNextPage}\n      ListFooterComponent={\n        isFetchingNextPage ? (\n          <View className=\"items-center\">\n            <ActivityIndicator />\n          </View>\n        ) : (\n          <View />\n        )\n      }\n    />\n  );\n}\n"
  },
  {
    "path": "apps/mobile/components/bookmarks/BookmarkTextMarkdown.tsx",
    "content": "import Markdown from \"react-native-markdown-display\";\nimport { TailwindResolver } from \"@/components/TailwindResolver\";\n\nexport default function BookmarkTextMarkdown({ text }: { text: string }) {\n  return (\n    <TailwindResolver\n      className=\"text-foreground\"\n      comp={(styles) => (\n        <Markdown\n          style={{\n            text: {\n              color: styles?.color?.toString(),\n            },\n          }}\n        >\n          {text}\n        </Markdown>\n      )}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/mobile/components/bookmarks/BookmarkTextView.tsx",
    "content": "import { useState } from \"react\";\nimport { Keyboard, Pressable, ScrollView, TextInput, View } from \"react-native\";\nimport BookmarkTextMarkdown from \"@/components/bookmarks/BookmarkTextMarkdown\";\nimport { Button } from \"@/components/ui/Button\";\nimport { Text } from \"@/components/ui/Text\";\nimport { useToast } from \"@/components/ui/Toast\";\nimport { useColorScheme } from \"nativewind\";\n\nimport { useUpdateBookmark } from \"@karakeep/shared-react/hooks/bookmarks\";\nimport { BookmarkTypes, ZBookmark } from \"@karakeep/shared/types/bookmarks\";\n\ninterface BookmarkTextViewProps {\n  bookmark: ZBookmark;\n}\n\nexport default function BookmarkTextView({ bookmark }: BookmarkTextViewProps) {\n  if (bookmark.content.type !== BookmarkTypes.TEXT) {\n    throw new Error(\"Wrong content type rendered\");\n  }\n  const { toast } = useToast();\n  const { colorScheme } = useColorScheme();\n\n  const [isEditing, setIsEditing] = useState(false);\n  const initialText = bookmark.content.text;\n  const [content, setContent] = useState(initialText);\n\n  const { mutate, isPending } = useUpdateBookmark({\n    onError: () => {\n      toast({\n        message: \"Something went wrong\",\n        variant: \"destructive\",\n      });\n    },\n    onSuccess: () => {\n      setIsEditing(false);\n      toast({\n        message: \"Text updated successfully\",\n        showProgress: false,\n      });\n    },\n  });\n\n  const handleSave = () => {\n    mutate({\n      bookmarkId: bookmark.id,\n      text: content,\n    });\n  };\n\n  const handleDiscard = () => {\n    setContent(initialText);\n    setIsEditing(false);\n    Keyboard.dismiss();\n  };\n\n  if (isEditing) {\n    return (\n      <View className=\"flex-1 p-4\">\n        <View className=\"flex-row justify-end gap-2 px-4 py-2\">\n          <Button\n            size=\"sm\"\n            onPress={handleDiscard}\n            disabled={isPending}\n            variant=\"plain\"\n          >\n            <Text>Cancel</Text>\n          </Button>\n          <Button size=\"sm\" onPress={handleSave} disabled={isPending}>\n            <Text>{isPending ? \"Saving...\" : \"Save\"}</Text>\n          </Button>\n        </View>\n\n        <TextInput\n          value={content}\n          onChangeText={setContent}\n          multiline\n          autoFocus\n          editable={!isPending}\n          placeholder=\"Enter your text here...\"\n          placeholderTextColor={colorScheme === \"dark\" ? \"#666\" : \"#999\"}\n          style={{\n            flex: 1,\n            fontSize: 16,\n            lineHeight: 24,\n            color: colorScheme === \"dark\" ? \"#fff\" : \"#000\",\n            textAlignVertical: \"top\",\n            padding: 12,\n            borderRadius: 8,\n            borderWidth: 1,\n            borderColor: colorScheme === \"dark\" ? \"#333\" : \"#ddd\",\n            backgroundColor: colorScheme === \"dark\" ? \"#111\" : \"#fff\",\n          }}\n        />\n      </View>\n    );\n  }\n\n  return (\n    <ScrollView className=\"m-4 flex-1 rounded-lg border border-border bg-card p-2\">\n      <Pressable onPress={() => setIsEditing(true)}>\n        <View className=\"min-h-[200px] rounded-xl p-4\">\n          <BookmarkTextMarkdown text={content} />\n          {content.trim() === \"\" && (\n            <Text className=\"italic text-muted-foreground\">\n              Tap to add text...\n            </Text>\n          )}\n        </View>\n      </Pressable>\n    </ScrollView>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/components/bookmarks/BottomActions.tsx",
    "content": "import { Alert, Linking, Pressable, View } from \"react-native\";\nimport { useRouter } from \"expo-router\";\nimport { TailwindResolver } from \"@/components/TailwindResolver\";\nimport { useToast } from \"@/components/ui/Toast\";\nimport { ClipboardList, Globe, Info, Tag, Trash2 } from \"lucide-react-native\";\n\nimport { useDeleteBookmark } from \"@karakeep/shared-react/hooks/bookmarks\";\nimport { useWhoAmI } from \"@karakeep/shared-react/hooks/users\";\nimport { BookmarkTypes, ZBookmark } from \"@karakeep/shared/types/bookmarks\";\n\ninterface BottomActionsProps {\n  bookmark: ZBookmark;\n}\n\nexport default function BottomActions({ bookmark }: BottomActionsProps) {\n  const { toast } = useToast();\n  const router = useRouter();\n  const { data: currentUser } = useWhoAmI();\n\n  // Check if the current user owns this bookmark\n  const isOwner = currentUser?.id === bookmark.userId;\n\n  const { mutate: deleteBookmark, isPending: isDeletionPending } =\n    useDeleteBookmark({\n      onSuccess: () => {\n        router.back();\n        toast({\n          message: \"The bookmark has been deleted!\",\n          showProgress: false,\n        });\n      },\n      onError: () => {\n        toast({\n          message: \"Something went wrong\",\n          variant: \"destructive\",\n          showProgress: false,\n        });\n      },\n    });\n\n  const deleteBookmarkAlert = () =>\n    Alert.alert(\n      \"Delete bookmark?\",\n      \"Are you sure you want to delete this bookmark?\",\n      [\n        { text: \"Cancel\", style: \"cancel\" },\n        {\n          text: \"Delete\",\n          onPress: () => deleteBookmark({ bookmarkId: bookmark.id }),\n          style: \"destructive\",\n        },\n      ],\n    );\n\n  const actions = [\n    {\n      id: \"lists\",\n      icon: (\n        <TailwindResolver\n          className=\"text-foreground\"\n          comp={(styles) => <ClipboardList color={styles?.color?.toString()} />}\n        />\n      ),\n      shouldRender: isOwner,\n      onClick: () =>\n        router.push(`/dashboard/bookmarks/${bookmark.id}/manage_lists`),\n      disabled: false,\n    },\n    {\n      id: \"tags\",\n      icon: (\n        <TailwindResolver\n          className=\"text-foreground\"\n          comp={(styles) => <Tag color={styles?.color?.toString()} />}\n        />\n      ),\n      shouldRender: isOwner,\n      onClick: () =>\n        router.push(`/dashboard/bookmarks/${bookmark.id}/manage_tags`),\n      disabled: false,\n    },\n    {\n      id: \"open\",\n      icon: (\n        <TailwindResolver\n          className=\"text-foreground\"\n          comp={(styles) => <Info color={styles?.color?.toString()} />}\n        />\n      ),\n      shouldRender: true,\n      onClick: () => router.push(`/dashboard/bookmarks/${bookmark.id}/info`),\n      disabled: false,\n    },\n    {\n      id: \"delete\",\n      icon: (\n        <TailwindResolver\n          className=\"text-foreground\"\n          comp={(styles) => <Trash2 color={styles?.color?.toString()} />}\n        />\n      ),\n      shouldRender: isOwner,\n      onClick: deleteBookmarkAlert,\n      disabled: isDeletionPending,\n    },\n    {\n      id: \"browser\",\n      icon: (\n        <TailwindResolver\n          className=\"text-foreground\"\n          comp={(styles) => <Globe color={styles?.color?.toString()} />}\n        />\n      ),\n      shouldRender: bookmark.content.type == BookmarkTypes.LINK,\n      onClick: async () => {\n        if (bookmark.content.type !== BookmarkTypes.LINK) {\n          return;\n        }\n\n        try {\n          await Linking.openURL(bookmark.content.url);\n        } catch {\n          toast({\n            message: \"Failed to open link\",\n            variant: \"destructive\",\n            showProgress: false,\n          });\n        }\n      },\n      disabled: false,\n    },\n  ];\n\n  return (\n    <View>\n      <View className=\"flex flex-row items-center justify-between px-10 pb-2 pt-4\">\n        {actions.map(\n          (a) =>\n            a.shouldRender && (\n              <Pressable\n                disabled={a.disabled}\n                key={a.id}\n                onPress={a.onClick}\n                className=\"py-auto\"\n              >\n                {a.icon}\n              </Pressable>\n            ),\n        )}\n      </View>\n    </View>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/components/bookmarks/NotePreview.tsx",
    "content": "import { useState } from \"react\";\nimport { Modal, Pressable, ScrollView, View } from \"react-native\";\nimport { router } from \"expo-router\";\nimport { ExternalLink, NotepadText, X } from \"lucide-react-native\";\nimport { useColorScheme } from \"nativewind\";\n\nimport { Button } from \"../ui/Button\";\nimport { Text } from \"../ui/Text\";\n\ninterface NotePreviewProps {\n  note: string;\n  bookmarkId: string;\n  readOnly?: boolean;\n}\n\nexport function NotePreview({\n  note,\n  bookmarkId,\n  readOnly = false,\n}: NotePreviewProps) {\n  const [isModalVisible, setIsModalVisible] = useState(false);\n  const { colorScheme } = useColorScheme();\n  const iconColor = colorScheme === \"dark\" ? \"#9ca3af\" : \"#6b7280\";\n  const modalIconColor = colorScheme === \"dark\" ? \"#d1d5db\" : \"#374151\";\n\n  if (!note?.trim()) {\n    return null;\n  }\n\n  return (\n    <>\n      <Pressable onPress={() => setIsModalVisible(true)}>\n        <View className=\"flex flex-row items-center gap-2\">\n          <NotepadText size={24} color={iconColor} />\n          <Text\n            className=\"flex-1 text-sm italic text-gray-500 dark:text-gray-400\"\n            numberOfLines={2}\n          >\n            {note}\n          </Text>\n        </View>\n      </Pressable>\n\n      <Modal\n        visible={isModalVisible}\n        transparent\n        animationType=\"slide\"\n        onRequestClose={() => setIsModalVisible(false)}\n      >\n        <View className=\"flex-1 justify-end bg-black/50\">\n          <View className=\"max-h-[80%] rounded-t-3xl bg-card p-6\">\n            {/* Header */}\n            <View className=\"mb-4 flex flex-row items-center justify-between\">\n              <Text className=\"text-lg font-semibold\">Note</Text>\n              <Pressable\n                onPress={() => setIsModalVisible(false)}\n                className=\"p-2\"\n              >\n                <X size={24} color={modalIconColor} />\n              </Pressable>\n            </View>\n\n            {/* Note Content */}\n            <ScrollView className=\"mb-4 max-h-96\">\n              <Text className=\"text-sm text-gray-700 dark:text-gray-300\">\n                {note}\n              </Text>\n            </ScrollView>\n\n            {/* Action Button */}\n            {!readOnly && (\n              <View className=\"flex flex-row justify-end border-t border-border pt-4\">\n                <Button\n                  variant=\"secondary\"\n                  onPress={() => {\n                    setIsModalVisible(false);\n                    router.push(`/dashboard/bookmarks/${bookmarkId}/info`);\n                  }}\n                >\n                  <Text className=\"text-sm\">Edit Notes</Text>\n                  <ExternalLink size={14} color={modalIconColor} />\n                </Button>\n              </View>\n            )}\n          </View>\n        </View>\n      </Modal>\n    </>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/components/bookmarks/PDFViewer.tsx",
    "content": "import React, { useEffect, useMemo, useState } from \"react\";\nimport { ActivityIndicator, StyleSheet, View } from \"react-native\";\nimport ReactNativeBlobUtil from \"react-native-blob-util\";\nimport Pdf from \"react-native-pdf\";\nimport { Text } from \"@/components/ui/Text\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { useColorScheme } from \"nativewind\";\n\ninterface PDFViewerProps {\n  source: string;\n  headers?: Record<string, string>;\n}\n\nexport function PDFViewer({ source, headers }: PDFViewerProps) {\n  const [pdfRenderError, setPdfRenderError] = useState<string | null>(null);\n  const { colorScheme } = useColorScheme();\n  const isDark = colorScheme === \"dark\";\n  const colors = {\n    background: isDark ? \"#000\" : \"#fff\",\n    foreground: isDark ? \"#fff\" : \"#000\",\n    mutedForeground: isDark ? \"#888\" : \"#666\",\n  };\n\n  const {\n    data: localPath,\n    isLoading,\n    error: downloadError,\n  } = useQuery({\n    queryKey: [\"pdf\", source],\n    queryFn: async () => {\n      // Create a temporary filename\n      const fileName = `temp_${Date.now()}.pdf`;\n      const { dirs } = ReactNativeBlobUtil.fs;\n      const path = `${dirs.DocumentDir}/${fileName}`;\n\n      const response = await ReactNativeBlobUtil.config({\n        fileCache: true,\n        path,\n      }).fetch(\"GET\", source, headers ?? {});\n      return response.path();\n    },\n    enabled: !!source,\n  });\n\n  // Merge download and render errors\n  const error = useMemo(() => {\n    if (downloadError) {\n      let errorMessage = \"Failed to download PDF\";\n      if (downloadError.message.includes(\"Network request failed\")) {\n        errorMessage = \"Network error. Please check your connection.\";\n      } else if (\n        downloadError.message.includes(\"401\") ||\n        downloadError.message.includes(\"403\")\n      ) {\n        errorMessage = \"Authentication failed. Please sign in again.\";\n      } else if (downloadError.message.includes(\"404\")) {\n        errorMessage = \"PDF not found.\";\n      }\n      return errorMessage;\n    }\n    if (pdfRenderError) {\n      return pdfRenderError;\n    }\n    return null;\n  }, [downloadError, pdfRenderError]);\n\n  // Cleanup function to remove temporary file on unmount\n  useEffect(() => {\n    return () => {\n      if (localPath) {\n        ReactNativeBlobUtil.fs.unlink(localPath).catch(() => ({}));\n      }\n    };\n  }, [source, headers]);\n\n  if (error) {\n    return (\n      <View style={[styles.container, { backgroundColor: colors.background }]}>\n        <Text style={[styles.errorText, { color: colors.foreground }]}>\n          {error}\n        </Text>\n      </View>\n    );\n  }\n\n  if (isLoading || !localPath) {\n    return (\n      <View style={[styles.container, { backgroundColor: colors.background }]}>\n        <View style={styles.loadingContainer}>\n          <ActivityIndicator size=\"large\" color={colors.foreground} />\n          <Text style={[styles.loadingText, { color: colors.mutedForeground }]}>\n            Downloading PDF...\n          </Text>\n        </View>\n      </View>\n    );\n  }\n\n  return (\n    <View style={[styles.container, { backgroundColor: colors.background }]}>\n      <Pdf\n        style={StyleSheet.absoluteFillObject}\n        source={{ uri: `file://${localPath}`, cache: true }}\n        spacing={16}\n        maxScale={3}\n        onLoadComplete={() => ({})}\n        onError={() => setPdfRenderError(\"Failed to render PDF\")}\n        trustAllCerts={false}\n        renderActivityIndicator={() => (\n          <ActivityIndicator size=\"large\" color={colors.foreground} />\n        )}\n      />\n    </View>\n  );\n}\n\nconst styles = StyleSheet.create({\n  container: {\n    flex: 1,\n  },\n  loadingContainer: {\n    ...StyleSheet.absoluteFillObject,\n    justifyContent: \"center\",\n    alignItems: \"center\",\n    zIndex: 1,\n  },\n  loadingText: {\n    marginTop: 12,\n    fontSize: 16,\n  },\n  errorText: {\n    fontSize: 16,\n    textAlign: \"center\",\n    padding: 20,\n  },\n});\n"
  },
  {
    "path": "apps/mobile/components/bookmarks/TagPill.tsx",
    "content": "import { Text, View } from \"react-native\";\nimport { Link } from \"expo-router\";\n\nimport { ZBookmarkTags } from \"@karakeep/shared/types/tags\";\n\nexport default function TagPill({\n  tag,\n  clickable = true,\n}: {\n  tag: ZBookmarkTags;\n  clickable?: boolean;\n}) {\n  return (\n    <View\n      key={tag.id}\n      className=\"rounded-full border border-input px-2.5 py-0.5 text-xs font-semibold\"\n    >\n      {clickable ? (\n        <Link\n          className=\"text-foreground\"\n          numberOfLines={1}\n          href={`dashboard/tags/${tag.id}`}\n        >\n          {tag.name}\n        </Link>\n      ) : (\n        <Text className=\"text-foreground\" numberOfLines={1}>\n          {tag.name}\n        </Text>\n      )}\n    </View>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/components/bookmarks/UpdatingBookmarkList.tsx",
    "content": "import { useInfiniteQuery, useQueryClient } from \"@tanstack/react-query\";\n\nimport type { ZGetBookmarksRequest } from \"@karakeep/shared/types/bookmarks\";\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\nimport { BookmarkTypes } from \"@karakeep/shared/types/bookmarks\";\n\nimport FullPageError from \"../FullPageError\";\nimport FullPageSpinner from \"../ui/FullPageSpinner\";\nimport BookmarkList from \"./BookmarkList\";\n\nexport default function UpdatingBookmarkList({\n  query,\n  header,\n}: {\n  query: Omit<ZGetBookmarksRequest, \"sortOrder\" | \"includeContent\">; // Sort order is not supported in mobile yet\n  header?: React.ReactElement;\n}) {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n  const {\n    data,\n    isPending,\n    isPlaceholderData,\n    error,\n    fetchNextPage,\n    isFetchingNextPage,\n    refetch,\n  } = useInfiniteQuery(\n    api.bookmarks.getBookmarks.infiniteQueryOptions(\n      { ...query, useCursorV2: true, includeContent: false },\n      {\n        initialCursor: null,\n        getNextPageParam: (lastPage) => lastPage.nextCursor,\n      },\n    ),\n  );\n\n  if (error) {\n    return <FullPageError error={error.message} onRetry={() => refetch()} />;\n  }\n\n  if (isPending || !data) {\n    return <FullPageSpinner />;\n  }\n\n  const onRefresh = () => {\n    queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter());\n    queryClient.invalidateQueries(api.bookmarks.getBookmark.pathFilter());\n  };\n\n  return (\n    <BookmarkList\n      bookmarks={data.pages\n        .flatMap((p) => p.bookmarks)\n        .filter((b) => b.content.type != BookmarkTypes.UNKNOWN)}\n      header={header}\n      onRefresh={onRefresh}\n      fetchNextPage={fetchNextPage}\n      isFetchingNextPage={isFetchingNextPage}\n      isRefreshing={isPending || isPlaceholderData}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/mobile/components/highlights/HighlightCard.tsx",
    "content": "import { ActivityIndicator, Alert, Pressable, View } from \"react-native\";\nimport * as Haptics from \"expo-haptics\";\nimport { useRouter } from \"expo-router\";\nimport { Text } from \"@/components/ui/Text\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { formatDistanceToNow } from \"date-fns\";\nimport { ExternalLink, Trash2 } from \"lucide-react-native\";\n\nimport type { ZHighlight } from \"@karakeep/shared/types/highlights\";\nimport { useDeleteHighlight } from \"@karakeep/shared-react/hooks/highlights\";\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\nimport { useToast } from \"../ui/Toast\";\n\n// Color map for highlights (mapped to Tailwind CSS classes used in NativeWind)\nconst HIGHLIGHT_COLOR_MAP = {\n  red: \"#fecaca\", // bg-red-200\n  green: \"#bbf7d0\", // bg-green-200\n  blue: \"#bfdbfe\", // bg-blue-200\n  yellow: \"#fef08a\", // bg-yellow-200\n} as const;\n\nexport default function HighlightCard({\n  highlight,\n}: {\n  highlight: ZHighlight;\n}) {\n  const { toast } = useToast();\n  const router = useRouter();\n  const api = useTRPC();\n\n  const onError = () => {\n    toast({\n      message: \"Something went wrong\",\n      variant: \"destructive\",\n      showProgress: false,\n    });\n  };\n\n  const { mutate: deleteHighlight, isPending: isDeleting } = useDeleteHighlight(\n    {\n      onSuccess: () => {\n        toast({\n          message: \"Highlight has been deleted!\",\n          showProgress: false,\n        });\n      },\n      onError,\n    },\n  );\n\n  const deleteHighlightAlert = () =>\n    Alert.alert(\n      \"Delete highlight?\",\n      \"Are you sure you want to delete this highlight?\",\n      [\n        { text: \"Cancel\", style: \"cancel\" },\n        {\n          text: \"Delete\",\n          onPress: () => deleteHighlight({ highlightId: highlight.id }),\n          style: \"destructive\",\n        },\n      ],\n    );\n\n  const { data: bookmark } = useQuery(\n    api.bookmarks.getBookmark.queryOptions(\n      {\n        bookmarkId: highlight.bookmarkId,\n      },\n      {\n        retry: false,\n      },\n    ),\n  );\n\n  const handleBookmarkPress = () => {\n    Haptics.selectionAsync();\n    router.push(`/dashboard/bookmarks/${highlight.bookmarkId}`);\n  };\n\n  return (\n    <View\n      className=\"overflow-hidden rounded-xl bg-card p-4\"\n      style={{ borderCurve: \"continuous\" }}\n    >\n      <View className=\"flex gap-3\">\n        {/* Highlight text with colored border */}\n        <View\n          className=\"rounded-r-lg border-l-4 bg-muted/30 p-3\"\n          style={{ borderLeftColor: HIGHLIGHT_COLOR_MAP[highlight.color] }}\n        >\n          <Text className=\"italic text-foreground\">\n            {highlight.text || \"No text available\"}\n          </Text>\n        </View>\n\n        {/* Note if present */}\n        {highlight.note && (\n          <View className=\"rounded-lg bg-muted/50 p-2\">\n            <Text className=\"text-sm text-muted-foreground\">\n              Note: {highlight.note}\n            </Text>\n          </View>\n        )}\n\n        {/* Footer with timestamp and actions */}\n        <View className=\"flex flex-row items-center justify-between\">\n          <View className=\"flex flex-row items-center gap-2\">\n            <Text className=\"text-xs text-muted-foreground\">\n              {formatDistanceToNow(highlight.createdAt, { addSuffix: true })}\n            </Text>\n            {bookmark && (\n              <>\n                <Text className=\"text-xs text-muted-foreground\">•</Text>\n                <Pressable\n                  onPress={handleBookmarkPress}\n                  className=\"flex flex-row items-center gap-1\"\n                >\n                  <ExternalLink size={12} color=\"gray\" />\n                  <Text className=\"text-xs text-muted-foreground\">Source</Text>\n                </Pressable>\n              </>\n            )}\n          </View>\n\n          <View className=\"flex flex-row gap-2\">\n            {isDeleting ? (\n              <ActivityIndicator size=\"small\" />\n            ) : (\n              <Pressable\n                onPress={() => {\n                  Haptics.selectionAsync();\n                  deleteHighlightAlert();\n                }}\n              >\n                <Trash2 size={18} color=\"#ef4444\" />\n              </Pressable>\n            )}\n          </View>\n        </View>\n      </View>\n    </View>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/components/highlights/HighlightList.tsx",
    "content": "import { useRef } from \"react\";\nimport { ActivityIndicator, Keyboard, View } from \"react-native\";\nimport Animated, { LinearTransition } from \"react-native-reanimated\";\nimport EmptyState from \"@/components/ui/EmptyState\";\nimport { useScrollToTop } from \"@react-navigation/native\";\nimport { Highlighter } from \"lucide-react-native\";\n\nimport type { ZHighlight } from \"@karakeep/shared/types/highlights\";\n\nimport HighlightCard from \"./HighlightCard\";\n\nexport default function HighlightList({\n  highlights,\n  header,\n  onRefresh,\n  fetchNextPage,\n  isFetchingNextPage,\n  isRefreshing,\n}: {\n  highlights: ZHighlight[];\n  onRefresh: () => void;\n  isRefreshing: boolean;\n  fetchNextPage?: () => void;\n  header?: React.ReactElement;\n  isFetchingNextPage?: boolean;\n}) {\n  const flatListRef = useRef(null);\n  useScrollToTop(flatListRef);\n\n  return (\n    <Animated.FlatList\n      ref={flatListRef}\n      itemLayoutAnimation={LinearTransition}\n      contentInsetAdjustmentBehavior=\"automatic\"\n      ListHeaderComponent={header}\n      contentContainerStyle={{\n        gap: 15,\n        marginHorizontal: 15,\n        marginBottom: 15,\n      }}\n      renderItem={(h) => <HighlightCard highlight={h.item} />}\n      ListEmptyComponent={\n        <EmptyState\n          icon={Highlighter}\n          title=\"No Highlights\"\n          subtitle=\"Highlights you create will appear here\"\n        />\n      }\n      data={highlights}\n      refreshing={isRefreshing}\n      onRefresh={onRefresh}\n      onScrollBeginDrag={Keyboard.dismiss}\n      keyExtractor={(h) => h.id}\n      onEndReached={fetchNextPage}\n      ListFooterComponent={\n        isFetchingNextPage ? (\n          <View className=\"items-center\">\n            <ActivityIndicator />\n          </View>\n        ) : (\n          <View />\n        )\n      }\n    />\n  );\n}\n"
  },
  {
    "path": "apps/mobile/components/navigation/stack.tsx",
    "content": "import { Platform, TextStyle, ViewStyle } from \"react-native\";\nimport { Stack } from \"expo-router/stack\";\nimport { cssInterop } from \"nativewind\";\n\ninterface StackProps extends React.ComponentProps<typeof Stack> {\n  contentStyle?: ViewStyle;\n  headerStyle?: TextStyle;\n}\n\nfunction StackImpl({ contentStyle, headerStyle, ...props }: StackProps) {\n  props.screenOptions = {\n    ...props.screenOptions,\n    contentStyle,\n    headerStyle: {\n      backgroundColor: headerStyle?.backgroundColor?.toString(),\n    },\n    navigationBarColor:\n      Platform.OS === \"android\"\n        ? undefined\n        : contentStyle?.backgroundColor?.toString(),\n    headerTintColor: headerStyle?.color?.toString(),\n  };\n  return <Stack {...props} />;\n}\n\n// Changing this requires reloading the app\nexport const StyledStack = cssInterop(StackImpl, {\n  contentClassName: \"contentStyle\",\n  headerClassName: \"headerStyle\",\n});\n"
  },
  {
    "path": "apps/mobile/components/reader/ReaderPreview.tsx",
    "content": "import { forwardRef, useEffect, useImperativeHandle, useRef } from \"react\";\nimport { View } from \"react-native\";\nimport WebView from \"react-native-webview\";\nimport { WEBVIEW_FONT_FAMILIES } from \"@/lib/readerSettings\";\nimport { useColorScheme } from \"@/lib/useColorScheme\";\n\nimport { ZReaderFontFamily } from \"@karakeep/shared/types/users\";\n\nconst PREVIEW_TEXT =\n  \"The quick brown fox jumps over the lazy dog. Pack my box with five dozen liquor jugs. How vexingly quick daft zebras jump!\";\n\nexport interface ReaderPreviewRef {\n  updateStyles: (\n    fontFamily: ZReaderFontFamily,\n    fontSize: number,\n    lineHeight: number,\n  ) => void;\n}\n\ninterface ReaderPreviewProps {\n  initialFontFamily: ZReaderFontFamily;\n  initialFontSize: number;\n  initialLineHeight: number;\n}\n\nexport const ReaderPreview = forwardRef<ReaderPreviewRef, ReaderPreviewProps>(\n  ({ initialFontFamily, initialFontSize, initialLineHeight }, ref) => {\n    const webViewRef = useRef<WebView>(null);\n    const { isDarkColorScheme: isDark } = useColorScheme();\n\n    const fontFamily = WEBVIEW_FONT_FAMILIES[initialFontFamily];\n    const textColor = isDark ? \"#e5e7eb\" : \"#374151\";\n    const bgColor = isDark ? \"#000000\" : \"#ffffff\";\n\n    useImperativeHandle(ref, () => ({\n      updateStyles: (\n        newFontFamily: ZReaderFontFamily,\n        newFontSize: number,\n        newLineHeight: number,\n      ) => {\n        const cssFontFamily = WEBVIEW_FONT_FAMILIES[newFontFamily];\n        webViewRef.current?.injectJavaScript(`\n          window.updateStyles(\"${cssFontFamily}\", ${newFontSize}, ${newLineHeight});\n          true;\n        `);\n      },\n    }));\n\n    // Update colors when theme changes\n    useEffect(() => {\n      webViewRef.current?.injectJavaScript(`\n        document.body.style.color = \"${textColor}\";\n        document.body.style.background = \"${bgColor}\";\n        true;\n      `);\n    }, [isDark, textColor, bgColor]);\n\n    const html = `\n      <!DOCTYPE html>\n      <html>\n        <head>\n          <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n          <style>\n            * {\n              margin: 0;\n              padding: 0;\n              box-sizing: border-box;\n            }\n            html, body {\n              height: 100%;\n              overflow: hidden;\n            }\n            body {\n              font-family: ${fontFamily};\n              font-size: ${initialFontSize}px;\n              line-height: ${initialLineHeight};\n              color: ${textColor};\n              background: ${bgColor};\n              padding: 16px;\n              word-wrap: break-word;\n              overflow-wrap: break-word;\n            }\n          </style>\n          <script>\n            window.updateStyles = function(fontFamily, fontSize, lineHeight) {\n              document.body.style.fontFamily = fontFamily;\n              document.body.style.fontSize = fontSize + 'px';\n              document.body.style.lineHeight = lineHeight;\n            };\n          </script>\n        </head>\n        <body>\n          ${PREVIEW_TEXT}\n        </body>\n      </html>\n    `;\n\n    return (\n      <View className=\"h-32 w-full overflow-hidden rounded-lg\">\n        <WebView\n          ref={webViewRef}\n          originWhitelist={[\"*\"]}\n          source={{ html }}\n          style={{\n            flex: 1,\n            backgroundColor: bgColor,\n          }}\n          scrollEnabled={false}\n          showsVerticalScrollIndicator={false}\n          showsHorizontalScrollIndicator={false}\n        />\n      </View>\n    );\n  },\n);\n\nReaderPreview.displayName = \"ReaderPreview\";\n"
  },
  {
    "path": "apps/mobile/components/settings/UserProfileHeader.tsx",
    "content": "import { View } from \"react-native\";\nimport { Avatar } from \"@/components/ui/Avatar\";\nimport { Text } from \"@/components/ui/Text\";\n\ninterface UserProfileHeaderProps {\n  image?: string | null;\n  name?: string | null;\n  email?: string | null;\n}\n\nexport function UserProfileHeader({\n  image,\n  name,\n  email,\n}: UserProfileHeaderProps) {\n  return (\n    <View className=\"w-full items-center gap-2 py-6\">\n      <Avatar image={image} name={name} size={88} />\n      <View className=\"items-center gap-1\">\n        <Text className=\"text-xl font-semibold\">{name || \"User\"}</Text>\n        {email && (\n          <Text className=\"text-sm text-muted-foreground\">{email}</Text>\n        )}\n      </View>\n    </View>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/components/sharing/ErrorAnimation.tsx",
    "content": "import { useEffect } from \"react\";\nimport { View } from \"react-native\";\nimport Animated, {\n  useAnimatedStyle,\n  useSharedValue,\n  withSequence,\n  withSpring,\n  withTiming,\n} from \"react-native-reanimated\";\nimport * as Haptics from \"expo-haptics\";\nimport { AlertCircle } from \"lucide-react-native\";\n\nexport default function ErrorAnimation() {\n  const scale = useSharedValue(0);\n  const shake = useSharedValue(0);\n\n  useEffect(() => {\n    Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);\n\n    scale.value = withSpring(1, { damping: 12, stiffness: 200 });\n    shake.value = withSequence(\n      withTiming(-10, { duration: 50 }),\n      withTiming(10, { duration: 100 }),\n      withTiming(-10, { duration: 100 }),\n      withTiming(10, { duration: 100 }),\n      withTiming(0, { duration: 50 }),\n    );\n  }, []);\n\n  const style = useAnimatedStyle(() => ({\n    transform: [{ scale: scale.value }, { translateX: shake.value }],\n  }));\n\n  return (\n    <Animated.View style={style} className=\"items-center gap-4\">\n      <View className=\"h-24 w-24 items-center justify-center rounded-full bg-destructive\">\n        <AlertCircle size={48} color=\"white\" strokeWidth={2} />\n      </View>\n    </Animated.View>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/components/sharing/LoadingAnimation.tsx",
    "content": "import { useEffect } from \"react\";\nimport { View } from \"react-native\";\nimport Animated, {\n  Easing,\n  FadeIn,\n  useAnimatedStyle,\n  useSharedValue,\n  withDelay,\n  withRepeat,\n  withSequence,\n  withTiming,\n} from \"react-native-reanimated\";\nimport { Text } from \"@/components/ui/Text\";\nimport { Archive } from \"lucide-react-native\";\n\nexport default function LoadingAnimation() {\n  const scale = useSharedValue(1);\n  const rotation = useSharedValue(0);\n  const opacity = useSharedValue(0.6);\n  const dotOpacity1 = useSharedValue(0);\n  const dotOpacity2 = useSharedValue(0);\n  const dotOpacity3 = useSharedValue(0);\n\n  useEffect(() => {\n    scale.value = withRepeat(\n      withSequence(\n        withTiming(1.1, { duration: 800, easing: Easing.inOut(Easing.ease) }),\n        withTiming(1, { duration: 800, easing: Easing.inOut(Easing.ease) }),\n      ),\n      -1,\n      false,\n    );\n\n    rotation.value = withRepeat(\n      withSequence(\n        withTiming(-5, { duration: 400, easing: Easing.inOut(Easing.ease) }),\n        withTiming(5, { duration: 800, easing: Easing.inOut(Easing.ease) }),\n        withTiming(0, { duration: 400, easing: Easing.inOut(Easing.ease) }),\n      ),\n      -1,\n      false,\n    );\n\n    opacity.value = withRepeat(\n      withSequence(\n        withTiming(1, { duration: 800 }),\n        withTiming(0.6, { duration: 800 }),\n      ),\n      -1,\n      false,\n    );\n\n    dotOpacity1.value = withRepeat(\n      withSequence(\n        withTiming(1, { duration: 300 }),\n        withDelay(900, withTiming(0, { duration: 0 })),\n      ),\n      -1,\n    );\n    dotOpacity2.value = withDelay(\n      300,\n      withRepeat(\n        withSequence(\n          withTiming(1, { duration: 300 }),\n          withDelay(600, withTiming(0, { duration: 0 })),\n        ),\n        -1,\n      ),\n    );\n    dotOpacity3.value = withDelay(\n      600,\n      withRepeat(\n        withSequence(\n          withTiming(1, { duration: 300 }),\n          withDelay(300, withTiming(0, { duration: 0 })),\n        ),\n        -1,\n      ),\n    );\n  }, []);\n\n  const iconStyle = useAnimatedStyle(() => ({\n    transform: [{ scale: scale.value }, { rotate: `${rotation.value}deg` }],\n    opacity: opacity.value,\n  }));\n\n  const dot1Style = useAnimatedStyle(() => ({ opacity: dotOpacity1.value }));\n  const dot2Style = useAnimatedStyle(() => ({ opacity: dotOpacity2.value }));\n  const dot3Style = useAnimatedStyle(() => ({ opacity: dotOpacity3.value }));\n\n  return (\n    <Animated.View\n      entering={FadeIn.duration(300)}\n      className=\"items-center gap-6\"\n    >\n      <Animated.View\n        style={iconStyle}\n        className=\"h-24 w-24 items-center justify-center rounded-full bg-primary/10\"\n      >\n        <Archive size={48} className=\"text-primary\" strokeWidth={1.5} />\n      </Animated.View>\n      <View className=\"flex-row items-baseline\">\n        <Text variant=\"title1\" className=\"font-semibold text-foreground\">\n          Hoarding\n        </Text>\n        <View className=\"w-8 flex-row\">\n          <Animated.Text style={dot1Style} className=\"text-xl text-foreground\">\n            .\n          </Animated.Text>\n          <Animated.Text style={dot2Style} className=\"text-xl text-foreground\">\n            .\n          </Animated.Text>\n          <Animated.Text style={dot3Style} className=\"text-xl text-foreground\">\n            .\n          </Animated.Text>\n        </View>\n      </View>\n    </Animated.View>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/components/sharing/SuccessAnimation.tsx",
    "content": "import { useEffect } from \"react\";\nimport { View } from \"react-native\";\nimport Animated, {\n  Easing,\n  interpolate,\n  useAnimatedStyle,\n  useSharedValue,\n  withDelay,\n  withSequence,\n  withSpring,\n  withTiming,\n} from \"react-native-reanimated\";\nimport * as Haptics from \"expo-haptics\";\nimport { Check } from \"lucide-react-native\";\n\ninterface ParticleProps {\n  angle: number;\n  delay: number;\n  color: string;\n}\n\nfunction Particle({ angle, delay, color }: ParticleProps) {\n  const progress = useSharedValue(0);\n\n  useEffect(() => {\n    progress.value = withDelay(\n      200 + delay,\n      withSequence(\n        withTiming(1, { duration: 400, easing: Easing.out(Easing.ease) }),\n        withTiming(0, { duration: 300 }),\n      ),\n    );\n  }, []);\n\n  const particleStyle = useAnimatedStyle(() => {\n    const distance = interpolate(progress.value, [0, 1], [0, 60]);\n    const opacity = interpolate(progress.value, [0, 0.5, 1], [0, 1, 0]);\n    const scale = interpolate(progress.value, [0, 0.5, 1], [0, 1, 0]);\n    const angleRad = (angle * Math.PI) / 180;\n\n    return {\n      position: \"absolute\" as const,\n      width: 8,\n      height: 8,\n      borderRadius: 4,\n      backgroundColor: color,\n      opacity,\n      transform: [\n        { translateX: Math.cos(angleRad) * distance },\n        { translateY: Math.sin(angleRad) * distance },\n        { scale },\n      ],\n    };\n  });\n\n  return <Animated.View style={particleStyle} />;\n}\n\ninterface SuccessAnimationProps {\n  isAlreadyExists: boolean;\n}\n\nexport default function SuccessAnimation({\n  isAlreadyExists,\n}: SuccessAnimationProps) {\n  const checkScale = useSharedValue(0);\n  const checkOpacity = useSharedValue(0);\n  const ringScale = useSharedValue(0.8);\n  const ringOpacity = useSharedValue(0);\n\n  const particleColor = isAlreadyExists\n    ? \"rgb(255, 180, 0)\"\n    : \"rgb(0, 200, 100)\";\n\n  useEffect(() => {\n    Haptics.notificationAsync(\n      isAlreadyExists\n        ? Haptics.NotificationFeedbackType.Warning\n        : Haptics.NotificationFeedbackType.Success,\n    );\n\n    ringScale.value = withSequence(\n      withTiming(1.2, { duration: 400, easing: Easing.out(Easing.ease) }),\n      withTiming(1, { duration: 200 }),\n    );\n    ringOpacity.value = withSequence(\n      withTiming(1, { duration: 200 }),\n      withDelay(300, withTiming(0.3, { duration: 300 })),\n    );\n\n    checkScale.value = withDelay(\n      150,\n      withSpring(1, {\n        damping: 12,\n        stiffness: 200,\n        mass: 0.8,\n      }),\n    );\n    checkOpacity.value = withDelay(150, withTiming(1, { duration: 200 }));\n  }, [isAlreadyExists]);\n\n  const ringStyle = useAnimatedStyle(() => ({\n    transform: [{ scale: ringScale.value }],\n    opacity: ringOpacity.value,\n  }));\n\n  const checkStyle = useAnimatedStyle(() => ({\n    transform: [{ scale: checkScale.value }],\n    opacity: checkOpacity.value,\n  }));\n\n  return (\n    <View className=\"items-center justify-center\">\n      {Array.from({ length: 8 }, (_, i) => (\n        <Particle\n          key={i}\n          angle={(i * 360) / 8}\n          delay={i * 50}\n          color={particleColor}\n        />\n      ))}\n\n      <Animated.View\n        style={ringStyle}\n        className={`absolute h-28 w-28 rounded-full ${\n          isAlreadyExists ? \"bg-yellow-500/20\" : \"bg-green-500/20\"\n        }`}\n      />\n\n      <Animated.View\n        style={checkStyle}\n        className={`h-24 w-24 items-center justify-center rounded-full ${\n          isAlreadyExists ? \"bg-yellow-500\" : \"bg-green-500\"\n        }`}\n      >\n        <Check size={48} color=\"white\" strokeWidth={3} />\n      </Animated.View>\n    </View>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/components/ui/ActionButton.tsx",
    "content": "import type { PressableProps } from \"react-native\";\nimport { ActivityIndicator, Pressable } from \"react-native\";\n\nexport function ActionButton({\n  children,\n  loading,\n  disabled,\n  ...props\n}: PressableProps & {\n  loading: boolean;\n}) {\n  if (disabled !== undefined) {\n    disabled ||= loading;\n  } else if (loading) {\n    disabled = true;\n  }\n  return (\n    <Pressable {...props} disabled={disabled}>\n      {loading ? <ActivityIndicator /> : children}\n    </Pressable>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/components/ui/Avatar.tsx",
    "content": "import * as React from \"react\";\nimport { View } from \"react-native\";\nimport { Image } from \"expo-image\";\nimport { Text } from \"@/components/ui/Text\";\nimport { useAssetUrl } from \"@/lib/hooks\";\nimport { cn } from \"@/lib/utils\";\n\ninterface AvatarProps {\n  image?: string | null;\n  name?: string | null;\n  size?: number;\n  className?: string;\n  fallbackClassName?: string;\n}\n\nconst AVATAR_COLORS = [\n  \"#f87171\", // red-400\n  \"#fb923c\", // orange-400\n  \"#fbbf24\", // amber-400\n  \"#a3e635\", // lime-400\n  \"#34d399\", // emerald-400\n  \"#22d3ee\", // cyan-400\n  \"#60a5fa\", // blue-400\n  \"#818cf8\", // indigo-400\n  \"#a78bfa\", // violet-400\n  \"#e879f9\", // fuchsia-400\n];\n\nfunction nameToColor(name: string | null | undefined): string {\n  if (!name) return AVATAR_COLORS[0];\n  let hash = 0;\n  for (let i = 0; i < name.length; i++) {\n    hash = name.charCodeAt(i) + ((hash << 5) - hash);\n  }\n  return AVATAR_COLORS[Math.abs(hash) % AVATAR_COLORS.length];\n}\n\nfunction isExternalUrl(url: string) {\n  return url.startsWith(\"http://\") || url.startsWith(\"https://\");\n}\n\nexport function Avatar({\n  image,\n  name,\n  size = 40,\n  className,\n  fallbackClassName,\n}: AvatarProps) {\n  const [imageError, setImageError] = React.useState(false);\n  const assetUrl = useAssetUrl(image ?? \"\");\n\n  const imageUrl = React.useMemo(() => {\n    if (!image) return null;\n    return isExternalUrl(image)\n      ? {\n          uri: image,\n        }\n      : assetUrl;\n  }, [image]);\n\n  React.useEffect(() => {\n    setImageError(false);\n  }, [image]);\n\n  const initials = React.useMemo(() => {\n    if (!name) return \"U\";\n    return name.charAt(0).toUpperCase();\n  }, [name]);\n\n  const showFallback = !imageUrl || imageError;\n  const avatarColor = nameToColor(name);\n\n  return (\n    <View\n      className={cn(\"overflow-hidden\", className)}\n      style={{\n        width: size,\n        height: size,\n        borderRadius: size / 2,\n        backgroundColor: showFallback ? avatarColor : undefined,\n      }}\n    >\n      {showFallback ? (\n        <View\n          className={cn(\n            \"flex h-full w-full items-center justify-center\",\n            fallbackClassName,\n          )}\n          style={{ backgroundColor: avatarColor }}\n        >\n          <Text\n            className=\"text-white\"\n            style={{\n              fontSize: size * 0.4,\n              lineHeight: size * 0.4,\n              textAlign: \"center\",\n            }}\n          >\n            {initials}\n          </Text>\n        </View>\n      ) : (\n        <Image\n          source={imageUrl}\n          style={{ width: \"100%\", height: \"100%\" }}\n          contentFit=\"cover\"\n          onError={() => setImageError(true)}\n        />\n      )}\n    </View>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/components/ui/Button.tsx",
    "content": "import type { VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\nimport {\n  Platform,\n  Pressable,\n  PressableProps,\n  View,\n  ViewStyle,\n} from \"react-native\";\nimport { TextClassContext } from \"@/components/ui/Text\";\nimport { useColorScheme } from \"@/lib/useColorScheme\";\nimport { cn } from \"@/lib/utils\";\nimport { COLORS } from \"@/theme/colors\";\nimport * as Slot from \"@rn-primitives/slot\";\nimport { cva } from \"class-variance-authority\";\n\nconst buttonVariants = cva(\"flex-row items-center justify-center gap-2\", {\n  variants: {\n    variant: {\n      primary: \"ios:active:opacity-80 bg-primary\",\n      secondary:\n        \"ios:border-primary ios:active:bg-primary/5 border border-foreground/40\",\n      tonal:\n        \"ios:bg-primary/10 dark:ios:bg-primary/10 ios:active:bg-primary/15 bg-primary/15 dark:bg-primary/30\",\n      plain: \"ios:active:opacity-70\",\n      destructive:\n        \"ios:bg-destructive border border-destructive/5 bg-destructive/80\",\n    },\n    size: {\n      none: \"\",\n      sm: \"rounded-full px-2.5 py-1\",\n      md: \"ios:rounded-lg ios:py-1.5 ios:px-3.5 rounded-full px-5 py-2\",\n      lg: \"ios:py-2 gap-2 rounded-xl px-5 py-2.5\",\n      icon: \"ios:rounded-lg h-10 w-10 rounded-full\",\n    },\n  },\n  defaultVariants: {\n    variant: \"primary\",\n    size: \"md\",\n  },\n});\n\nconst androidRootVariants = cva(\"overflow-hidden\", {\n  variants: {\n    size: {\n      none: \"\",\n      icon: \"rounded-full\",\n      sm: \"rounded-full\",\n      md: \"rounded-full\",\n      lg: \"rounded-xl\",\n    },\n  },\n  defaultVariants: {\n    size: \"md\",\n  },\n});\n\nconst buttonTextVariants = cva(\"font-medium\", {\n  variants: {\n    variant: {\n      primary: \"text-white\",\n      secondary: \"ios:text-primary text-foreground\",\n      tonal: \"ios:text-primary text-foreground\",\n      plain: \"text-foreground\",\n      destructive: \"text-white\",\n    },\n    size: {\n      none: \"\",\n      icon: \"\",\n      sm: \"text-[15px] leading-5\",\n      md: \"text-[17px] leading-7\",\n      lg: \"text-[17px] leading-7\",\n    },\n  },\n  defaultVariants: {\n    variant: \"primary\",\n    size: \"md\",\n  },\n});\n\nfunction convertToRGBA(rgb: string, opacity: number): string {\n  const rgbValues = rgb.match(/\\d+/g);\n  if (!rgbValues || rgbValues.length !== 3) {\n    throw new Error(\"Invalid RGB color format\");\n  }\n  const red = parseInt(rgbValues[0], 10);\n  const green = parseInt(rgbValues[1], 10);\n  const blue = parseInt(rgbValues[2], 10);\n  if (opacity < 0 || opacity > 1) {\n    throw new Error(\"Opacity must be a number between 0 and 1\");\n  }\n  return `rgba(${red},${green},${blue},${opacity})`;\n}\n\nconst ANDROID_RIPPLE = {\n  dark: {\n    primary: {\n      color: convertToRGBA(COLORS.dark.grey3, 0.4),\n      borderless: false,\n    },\n    secondary: {\n      color: convertToRGBA(COLORS.dark.grey5, 0.8),\n      borderless: false,\n    },\n    plain: { color: convertToRGBA(COLORS.dark.grey5, 0.8), borderless: false },\n    tonal: { color: convertToRGBA(COLORS.dark.grey5, 0.8), borderless: false },\n    destructive: {\n      color: convertToRGBA(COLORS.dark.destructive, 0.8),\n      borderless: false,\n    },\n  },\n  light: {\n    primary: {\n      color: convertToRGBA(COLORS.light.grey4, 0.4),\n      borderless: false,\n    },\n    secondary: {\n      color: convertToRGBA(COLORS.light.grey5, 0.4),\n      borderless: false,\n    },\n    plain: { color: convertToRGBA(COLORS.light.grey5, 0.4), borderless: false },\n    tonal: { color: convertToRGBA(COLORS.light.grey6, 0.4), borderless: false },\n    destructive: {\n      color: convertToRGBA(COLORS.light.destructive, 0.4),\n      borderless: false,\n    },\n  },\n};\n\n// Add as class when possible: https://github.com/marklawlor/nativewind/issues/522\nconst BORDER_CURVE: ViewStyle = {\n  borderCurve: \"continuous\",\n};\n\ntype ButtonVariantProps = Omit<\n  VariantProps<typeof buttonVariants>,\n  \"variant\"\n> & {\n  variant?: Exclude<VariantProps<typeof buttonVariants>[\"variant\"], null>;\n};\n\ninterface AndroidOnlyButtonProps {\n  /**\n   * ANDROID ONLY: The class name of root responsible for hidding the ripple overflow.\n   */\n  androidRootClassName?: string;\n}\n\ntype ButtonProps = PressableProps & ButtonVariantProps & AndroidOnlyButtonProps;\n\nconst Root = Platform.OS === \"android\" ? View : Slot.Pressable;\n\nconst Button = React.forwardRef<\n  React.ElementRef<typeof Pressable>,\n  ButtonProps\n>(\n  (\n    {\n      className,\n      variant = \"primary\",\n      size,\n      style = BORDER_CURVE,\n      androidRootClassName,\n      ...props\n    },\n    ref,\n  ) => {\n    const { colorScheme } = useColorScheme();\n\n    return (\n      <TextClassContext.Provider value={buttonTextVariants({ variant, size })}>\n        <Root\n          className={Platform.select({\n            ios: androidRootClassName,\n            default: androidRootVariants({\n              size,\n              className: androidRootClassName,\n            }),\n          })}\n        >\n          <Pressable\n            className={cn(\n              props.disabled && \"opacity-50\",\n              buttonVariants({ variant, size, className }),\n            )}\n            ref={ref}\n            style={style}\n            android_ripple={ANDROID_RIPPLE[colorScheme][variant]}\n            {...props}\n          />\n        </Root>\n      </TextClassContext.Provider>\n    );\n  },\n);\n\nButton.displayName = \"Button\";\n\nexport { Button, buttonTextVariants, buttonVariants };\nexport type { ButtonProps };\n"
  },
  {
    "path": "apps/mobile/components/ui/ChevronRight.tsx",
    "content": "import { useColorScheme } from \"@/lib/useColorScheme\";\nimport { ChevronRightIcon } from \"lucide-react-native\";\n\nexport default function ChevronRight({\n  color,\n  ...props\n}: React.ComponentProps<typeof ChevronRightIcon>) {\n  const { colors } = useColorScheme();\n\n  return <ChevronRightIcon color={color ?? colors.grey} {...props} />;\n}\n"
  },
  {
    "path": "apps/mobile/components/ui/Divider.tsx",
    "content": "import { View } from \"react-native\";\nimport { cn } from \"@/lib/utils\";\n\nfunction Divider({\n  className,\n  orientation,\n  ...props\n}: {\n  color?: string;\n  orientation: \"horizontal\" | \"vertical\";\n} & React.ComponentPropsWithoutRef<typeof View>) {\n  return (\n    <View\n      className={cn(\n        \"bg-slate-400/20 dark:bg-border/50\",\n        orientation === \"horizontal\" ? \"h-0.5\" : \"w-0.5\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Divider };\n"
  },
  {
    "path": "apps/mobile/components/ui/EmptyState.tsx",
    "content": "import type { LucideIcon } from \"lucide-react-native\";\nimport { View } from \"react-native\";\nimport Animated, { FadeIn } from \"react-native-reanimated\";\nimport { Text } from \"@/components/ui/Text\";\nimport { useColorScheme } from \"@/lib/useColorScheme\";\n\nexport default function EmptyState({\n  icon: Icon,\n  title,\n  subtitle,\n}: {\n  icon: LucideIcon;\n  title: string;\n  subtitle: string;\n}) {\n  const { colors } = useColorScheme();\n\n  return (\n    <Animated.View\n      entering={FadeIn.duration(400)}\n      style={{\n        width: \"100%\",\n        paddingHorizontal: 16,\n        paddingVertical: 48,\n        alignItems: \"center\",\n        justifyContent: \"center\",\n      }}\n    >\n      <View className=\"mb-4 h-20 w-20 items-center justify-center rounded-full bg-primary/10\">\n        <Icon size={36} color={colors.primary} />\n      </View>\n      <Text variant=\"title3\" style={{ textAlign: \"center\", width: \"100%\" }}>\n        {title}\n      </Text>\n      <Text\n        style={{ textAlign: \"center\", width: \"100%\", marginTop: 4 }}\n        className=\"text-muted-foreground\"\n      >\n        {subtitle}\n      </Text>\n    </Animated.View>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/components/ui/FullPageSpinner.tsx",
    "content": "import { ActivityIndicator, View } from \"react-native\";\n\nexport default function FullPageSpinner() {\n  return (\n    <View className=\"h-full w-full items-center justify-center bg-gray-100 dark:bg-background\">\n      <ActivityIndicator />\n    </View>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/components/ui/GroupedList.tsx",
    "content": "import { Pressable, StyleSheet, View } from \"react-native\";\nimport ChevronRight from \"@/components/ui/ChevronRight\";\nimport { Text } from \"@/components/ui/Text\";\n\n/**\n * iOS-style grouped table section with an optional uppercase header label\n * and a rounded card container for its children.\n */\nfunction GroupedSection({\n  children,\n  header,\n}: {\n  children: React.ReactNode;\n  header?: string;\n}) {\n  return (\n    <View style={{ gap: 6 }}>\n      {header && (\n        <Text variant=\"footnote\" color=\"tertiary\" className=\"px-5 uppercase\">\n          {header}\n        </Text>\n      )}\n      <View\n        className=\"overflow-hidden rounded-xl bg-card\"\n        style={{ borderCurve: \"continuous\" }}\n      >\n        {children}\n      </View>\n    </View>\n  );\n}\n\n/**\n * Hairline separator indented from the left, used between rows\n * within a GroupedSection.\n */\nfunction RowSeparator() {\n  return (\n    <View\n      className=\"ml-4 bg-border/30\"\n      style={{ height: StyleSheet.hairlineWidth }}\n    />\n  );\n}\n\n/**\n * A pressable row with a label and a trailing chevron,\n * used for drill-down navigation within a GroupedSection.\n */\nfunction NavigationRow({\n  label,\n  onPress,\n}: {\n  label: string;\n  onPress: () => void;\n}) {\n  return (\n    <Pressable\n      onPress={onPress}\n      className=\"flex-row items-center justify-between px-4 py-3 active:opacity-70\"\n    >\n      <Text className=\"flex-1\" numberOfLines={1}>\n        {label}\n      </Text>\n      <ChevronRight size={16} />\n    </Pressable>\n  );\n}\n\nexport { GroupedSection, NavigationRow, RowSeparator };\n"
  },
  {
    "path": "apps/mobile/components/ui/Input.tsx",
    "content": "import type { TextInputProps } from \"react-native\";\nimport { forwardRef } from \"react\";\nimport { ActivityIndicator, TextInput, View } from \"react-native\";\nimport { Text } from \"@/components/ui/Text\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface InputProps extends TextInputProps {\n  label?: string;\n  labelClasses?: string;\n  inputClasses?: string;\n  loading?: boolean;\n}\n\nexport const Input = forwardRef<TextInput, InputProps>(\n  (\n    { className, label, labelClasses, inputClasses, loading, ...props },\n    ref,\n  ) => {\n    return (\n      <View className={cn(\"flex flex-col gap-1.5\", className)}>\n        {label && (\n          <Text className={cn(\"text-base\", labelClasses)}>{label}</Text>\n        )}\n        <TextInput\n          ref={ref}\n          className={cn(\n            \"flex h-10 w-full min-w-0 flex-row items-center rounded-md border border-input text-base leading-5 text-foreground shadow-sm shadow-black/5 dark:bg-input/30 sm:h-9\",\n            \"rounded-lg border border-input px-4 py-2.5 placeholder:text-muted-foreground/50\",\n            inputClasses,\n          )}\n          {...props}\n        />\n        {loading && (\n          <ActivityIndicator className=\"absolute bottom-0 right-0 p-2\" />\n        )}\n      </View>\n    );\n  },\n);\n"
  },
  {
    "path": "apps/mobile/components/ui/SearchInput/SearchInput.ios.tsx",
    "content": "import * as React from \"react\";\nimport { Pressable, TextInput, View, ViewStyle } from \"react-native\";\nimport Animated, {\n  measure,\n  useAnimatedRef,\n  useAnimatedStyle,\n  useDerivedValue,\n  withTiming,\n} from \"react-native-reanimated\";\nimport { TailwindResolver } from \"@/components/TailwindResolver\";\nimport { Text } from \"@/components/ui/Text\";\nimport { cn } from \"@/lib/utils\";\nimport { useAugmentedRef, useControllableState } from \"@rn-primitives/hooks\";\nimport { SearchIcon } from \"lucide-react-native\";\n\nimport type { SearchInputProps } from \"./types\";\n\n// Add as class when possible: https://github.com/marklawlor/nativewind/issues/522\nconst BORDER_CURVE: ViewStyle = {\n  borderCurve: \"continuous\",\n};\n\nconst SearchInput = React.forwardRef<\n  React.ElementRef<typeof TextInput>,\n  SearchInputProps\n>(\n  (\n    {\n      value: valueProp,\n      onChangeText: onChangeTextProp,\n      onFocus: onFocusProp,\n      placeholder = \"Search...\",\n      cancelText = \"Cancel\",\n      containerClassName,\n      iconContainerClassName,\n      className,\n      iconColor: _iconColor,\n      onCancel,\n      ...props\n    },\n    ref,\n  ) => {\n    const inputRef = useAugmentedRef({ ref, methods: { focus, blur, clear } });\n    const [showCancel, setShowCancel] = React.useState(false);\n    const showCancelDerivedValue = useDerivedValue(\n      () => showCancel,\n      [showCancel],\n    );\n    const animatedRef = useAnimatedRef();\n\n    const [value = \"\", onChangeText] = useControllableState({\n      prop: valueProp,\n      defaultProp: valueProp ?? \"\",\n      onChange: onChangeTextProp,\n    });\n\n    const rootStyle = useAnimatedStyle(() => {\n      if (_WORKLET) {\n        // safely use measure\n        const measurement = measure(animatedRef);\n        return {\n          paddingRight: showCancelDerivedValue.value\n            ? withTiming(measurement?.width ?? cancelText.length * 11.2)\n            : withTiming(0),\n        };\n      }\n      return {\n        paddingRight: showCancelDerivedValue.value\n          ? withTiming(cancelText.length * 11.2)\n          : withTiming(0),\n      };\n    });\n    const buttonStyle3 = useAnimatedStyle(() => {\n      if (_WORKLET) {\n        // safely use measure\n        const measurement = measure(animatedRef);\n        return {\n          position: \"absolute\",\n          right: 0,\n          opacity: showCancelDerivedValue.value ? withTiming(1) : withTiming(0),\n          transform: [\n            {\n              translateX: showCancelDerivedValue.value\n                ? withTiming(0)\n                : measurement?.width\n                  ? withTiming(measurement.width)\n                  : cancelText.length * 11.2,\n            },\n          ],\n        };\n      }\n      return {\n        position: \"absolute\",\n        right: 0,\n        opacity: showCancelDerivedValue.value ? withTiming(1) : withTiming(0),\n        transform: [\n          {\n            translateX: showCancelDerivedValue.value\n              ? withTiming(0)\n              : withTiming(cancelText.length * 11.2),\n          },\n        ],\n      };\n    });\n\n    function focus() {\n      inputRef.current?.focus();\n    }\n\n    function blur() {\n      inputRef.current?.blur();\n    }\n\n    function clear() {\n      onChangeText(\"\");\n    }\n\n    function onFocus(e: Parameters<NonNullable<typeof onFocusProp>>[0]) {\n      setShowCancel(true);\n      onFocusProp?.(e);\n    }\n\n    return (\n      <Animated.View className=\"flex-row items-center\" style={rootStyle}>\n        <Animated.View\n          style={BORDER_CURVE}\n          className={cn(\n            \"flex-1 flex-row rounded-lg bg-card\",\n            containerClassName,\n          )}\n        >\n          <View\n            className={cn(\n              \"absolute bottom-0 left-0 top-0 z-50 justify-center pl-1.5\",\n              iconContainerClassName,\n            )}\n          >\n            <TailwindResolver\n              className=\"text-muted\"\n              comp={(styles) => (\n                <SearchIcon color={styles?.color?.toString()} size={20} />\n              )}\n            />\n          </View>\n          <TextInput\n            ref={inputRef}\n            placeholder={placeholder}\n            className={cn(\n              !showCancel && \"active:bg-muted/5 dark:active:bg-muted/20\",\n              \"flex-1 rounded-lg py-2 pl-8 pr-1 text-[17px] text-foreground\",\n              className,\n            )}\n            value={value}\n            onChangeText={onChangeText}\n            onFocus={onFocus}\n            clearButtonMode=\"while-editing\"\n            role=\"searchbox\"\n            {...props}\n          />\n        </Animated.View>\n        <Animated.View\n          ref={animatedRef}\n          style={buttonStyle3}\n          pointerEvents={!showCancel ? \"none\" : \"auto\"}\n        >\n          <Pressable\n            onPress={() => {\n              onChangeText(\"\");\n              inputRef.current?.blur();\n              setShowCancel(false);\n              onCancel?.();\n            }}\n            disabled={!showCancel}\n            pointerEvents={!showCancel ? \"none\" : \"auto\"}\n            className=\"flex-1 justify-center active:opacity-50\"\n          >\n            <Text className=\"px-2 text-primary\">{cancelText}</Text>\n          </Pressable>\n        </Animated.View>\n      </Animated.View>\n    );\n  },\n);\n\nSearchInput.displayName = \"SearchInput\";\n\nexport { SearchInput };\n"
  },
  {
    "path": "apps/mobile/components/ui/SearchInput/SearchInput.tsx",
    "content": "import * as React from \"react\";\nimport { Pressable, TextInput, View } from \"react-native\";\nimport Animated, { FadeIn, FadeOut } from \"react-native-reanimated\";\nimport { TailwindResolver } from \"@/components/TailwindResolver\";\nimport { Button } from \"@/components/ui/Button\";\nimport { useColorScheme } from \"@/lib/useColorScheme\";\nimport { cn } from \"@/lib/utils\";\nimport { useAugmentedRef, useControllableState } from \"@rn-primitives/hooks\";\nimport { SearchIcon, XIcon } from \"lucide-react-native\";\n\nimport type { SearchInputProps } from \"./types\";\n\nconst SearchInput = React.forwardRef<\n  React.ElementRef<typeof TextInput>,\n  SearchInputProps\n>(\n  (\n    {\n      value: valueProp,\n      onChangeText: onChangeTextProp,\n      placeholder = \"Search...\",\n      containerClassName,\n      iconContainerClassName,\n      className,\n      onCancel,\n      ...props\n    },\n    ref,\n  ) => {\n    const { colors } = useColorScheme();\n    const inputRef = useAugmentedRef({ ref, methods: { focus, blur, clear } });\n    const [value = \"\", onChangeText] = useControllableState({\n      prop: valueProp,\n      defaultProp: valueProp ?? \"\",\n      onChange: onChangeTextProp,\n    });\n\n    function focus() {\n      inputRef.current?.focus();\n    }\n\n    function blur() {\n      inputRef.current?.blur();\n    }\n\n    function clear() {\n      onCancel?.();\n      onChangeText(\"\");\n    }\n\n    return (\n      <Button\n        variant=\"plain\"\n        className={cn(\n          \"android:gap-0 android:h-14 flex-row items-center rounded-full bg-card px-2\",\n          containerClassName,\n        )}\n        onPress={focus}\n      >\n        <View\n          className={cn(\"p-2\", iconContainerClassName)}\n          pointerEvents=\"none\"\n        >\n          <TailwindResolver\n            className=\"text-muted\"\n            comp={(styles) => (\n              <SearchIcon color={styles?.color?.toString()} size={24} />\n            )}\n          />\n        </View>\n\n        <View className=\"flex-1\" pointerEvents=\"none\">\n          <TextInput\n            ref={inputRef}\n            placeholder={placeholder}\n            className={cn(\n              \"flex-1 rounded-r-full p-2 text-[17px] text-foreground placeholder:text-muted\",\n              className,\n            )}\n            placeholderTextColor={colors.foreground}\n            value={value}\n            onChangeText={onChangeText}\n            role=\"searchbox\"\n            {...props}\n          />\n        </View>\n        {!!value && (\n          <Animated.View entering={FadeIn} exiting={FadeOut.duration(150)}>\n            <Pressable className=\"p-2\" onPress={clear}>\n              <TailwindResolver\n                className=\"text-muted\"\n                comp={(styles) => (\n                  <XIcon size={24} color={styles?.color?.toString()} />\n                )}\n              />\n            </Pressable>\n          </Animated.View>\n        )}\n      </Button>\n    );\n  },\n);\n\nSearchInput.displayName = \"SearchInput\";\n\nexport { SearchInput };\n"
  },
  {
    "path": "apps/mobile/components/ui/SearchInput/index.ts",
    "content": "export * from \"./SearchInput\";\n"
  },
  {
    "path": "apps/mobile/components/ui/SearchInput/types.ts",
    "content": "import type { TextInput, TextInputProps } from \"react-native\";\n\ninterface SearchInputProps extends TextInputProps {\n  containerClassName?: string;\n  iconContainerClassName?: string;\n  cancelText?: string;\n  iconColor?: string;\n  onCancel?: () => void;\n}\n\ntype SearchInputRef = TextInput;\n\nexport type { SearchInputProps, SearchInputRef };\n"
  },
  {
    "path": "apps/mobile/components/ui/Skeleton.tsx",
    "content": "import type { View } from \"react-native\";\nimport { useEffect, useRef } from \"react\";\nimport { Animated } from \"react-native\";\nimport { cn } from \"@/lib/utils\";\n\nfunction Skeleton({\n  className,\n  ...props\n}: { className?: string } & React.ComponentPropsWithoutRef<typeof View>) {\n  const fadeAnim = useRef(new Animated.Value(0.5)).current;\n\n  useEffect(() => {\n    Animated.loop(\n      Animated.sequence([\n        Animated.timing(fadeAnim, {\n          toValue: 1,\n          duration: 1000,\n          useNativeDriver: true,\n        }),\n        Animated.timing(fadeAnim, {\n          toValue: 0.5,\n          duration: 1000,\n          useNativeDriver: true,\n        }),\n      ]),\n    ).start();\n  }, [fadeAnim]);\n\n  return (\n    <Animated.View\n      className={cn(\"rounded-md bg-muted\", className)}\n      style={[{ opacity: fadeAnim }]}\n      {...props}\n    />\n  );\n}\n\nexport { Skeleton };\n"
  },
  {
    "path": "apps/mobile/components/ui/Text.tsx",
    "content": "import * as React from \"react\";\nimport { Text as RNText } from \"react-native\";\nimport { cn } from \"@/lib/utils\";\nimport { cva, VariantProps } from \"class-variance-authority\";\n\nconst textVariants = cva(\"text-foreground\", {\n  variants: {\n    variant: {\n      largeTitle: \"text-4xl\",\n      title1: \"text-2xl\",\n      title2: \"text-[22px] leading-7\",\n      title3: \"text-xl\",\n      heading: \"text-[17px] font-semibold leading-6\",\n      body: \"text-[17px] leading-6\",\n      callout: \"text-base\",\n      subhead: \"text-[15px] leading-6\",\n      footnote: \"text-[13px] leading-5\",\n      caption1: \"text-xs\",\n      caption2: \"text-[11px] leading-4\",\n    },\n    color: {\n      primary: \"\",\n      secondary: \"text-secondary-foreground/90\",\n      tertiary: \"text-muted-foreground/90\",\n      quarternary: \"text-muted-foreground/50\",\n    },\n  },\n  defaultVariants: {\n    variant: \"body\",\n    color: \"primary\",\n  },\n});\n\nconst TextClassContext = React.createContext<string | undefined>(undefined);\n\nfunction Text({\n  className,\n  variant,\n  color,\n  ...props\n}: React.ComponentPropsWithoutRef<typeof RNText> &\n  VariantProps<typeof textVariants>) {\n  const textClassName = React.useContext(TextClassContext);\n  return (\n    <RNText\n      className={cn(textVariants({ variant, color }), textClassName, className)}\n      {...props}\n    />\n  );\n}\n\nexport { Text, TextClassContext, textVariants };\n"
  },
  {
    "path": "apps/mobile/components/ui/Toast.tsx",
    "content": "import { toast as sonnerToast } from \"sonner-native\";\n\nconst toastVariants = {\n  default: \"bg-foreground\",\n  destructive: \"bg-destructive\",\n  success: \"bg-green-500\",\n  info: \"bg-blue-500\",\n};\n\ntype ToastVariant = keyof typeof toastVariants;\n\n// Compatibility wrapper for sonner-native\nfunction useToast() {\n  return {\n    toast: ({\n      message,\n      variant = \"default\",\n      duration = 3000,\n    }: {\n      message: string;\n      variant?: ToastVariant;\n      duration?: number;\n      position?: \"top\" | \"bottom\";\n      showProgress?: boolean;\n    }) => {\n      // Map variants to sonner-native methods\n      switch (variant) {\n        case \"success\":\n          sonnerToast.success(message, { duration });\n          break;\n        case \"destructive\":\n          sonnerToast.error(message, { duration });\n          break;\n        case \"info\":\n          sonnerToast.info(message, { duration });\n          break;\n        default:\n          sonnerToast(message, { duration });\n      }\n    },\n    removeToast: () => {\n      // sonner-native handles dismissal automatically\n    },\n  };\n}\n\nexport { ToastVariant, toastVariants, useToast };\n"
  },
  {
    "path": "apps/mobile/eas.json",
    "content": "{\n  \"cli\": {\n    \"version\": \">= 7.5.0\",\n    \"promptToConfigurePushNotifications\": false\n  },\n  \"build\": {\n    \"development\": {\n      \"developmentClient\": true,\n      \"distribution\": \"internal\",\n      \"env\": {\n        \"APP_VARIANT\": \"development\"\n      }\n    },\n    \"preview\": {\n      \"distribution\": \"internal\"\n    },\n    \"production\": {}\n  },\n  \"submit\": {\n    \"production\": {}\n  }\n}\n"
  },
  {
    "path": "apps/mobile/globals.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n  :root {\n    --background: 242 242 247;\n    --foreground: 0 0 0;\n    --card: 255 255 255;\n    --card-foreground: 0 0 0;\n    --popover: 230 230 235;\n    --popover-foreground: 0 0 0;\n    --primary: 0 123 255;\n    --primary-foreground: 255 255 255;\n    --secondary: 45 185 227;\n    --secondary-foreground: 255 255 255;\n    --muted: 176 176 181;\n    --muted-foreground: 102 102 102;\n    --accent: 255 40 84;\n    --accent-foreground: 255 255 255;\n    --destructive: 255 56 43;\n    --destructive-foreground: 255 255 255;\n    --border: 230 230 235;\n    --input: 210 210 215;\n    --ring: 230 230 235;\n  }\n\n  @media (prefers-color-scheme: dark) {\n    :root {\n      --background: 0 0 0;\n      --foreground: 255 255 255;\n      --card: 21 21 24;\n      --card-foreground: 255 255 255;\n      --popover: 40 40 40;\n      --popover-foreground: 255 255 255;\n      --primary: 3 133 255;\n      --primary-foreground: 255 255 255;\n      --secondary: 100 211 254;\n      --secondary-foreground: 255 255 255;\n      --muted: 112 112 115;\n      --muted-foreground: 226 226 231;\n      --accent: 255 52 95;\n      --accent-foreground: 255 255 255;\n      --destructive: 254 67 54;\n      --destructive-foreground: 255 255 255;\n      --border: 40 40 40;\n      --input: 51 51 51;\n      --ring: 40 40 40;\n    }\n  }\n}\n"
  },
  {
    "path": "apps/mobile/index.ts",
    "content": "import \"expo-router/entry\";\n"
  },
  {
    "path": "apps/mobile/lib/hooks.ts",
    "content": "import { useQuery } from \"@tanstack/react-query\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\nimport useAppSettings from \"./settings\";\nimport { buildApiHeaders } from \"./utils\";\n\ninterface AssetSource {\n  uri: string;\n  headers: Record<string, string>;\n}\n\nexport function useAssetUrl(assetId: string): AssetSource {\n  const { settings } = useAppSettings();\n  return {\n    uri: `${settings.address}/api/assets/${assetId}`,\n    headers: buildApiHeaders(settings.apiKey, settings.customHeaders),\n  };\n}\n\nexport function useServerVersion() {\n  const { settings } = useAppSettings();\n\n  return useQuery({\n    queryKey: [\"serverVersion\", settings.address],\n    queryFn: async () => {\n      const response = await fetch(`${settings.address}/api/version`, {\n        headers: buildApiHeaders(settings.apiKey, settings.customHeaders),\n      });\n\n      if (!response.ok) {\n        throw new Error(`Failed to fetch server version: ${response.status}`);\n      }\n\n      const data = await response.json();\n      return data.version as string;\n    },\n    enabled: !!settings.address,\n    staleTime: 1000 * 60 * 5, // Cache for 5 minutes\n  });\n}\n\n/**\n * Hook to determine the appropriate archived filter value based on user settings.\n * Returns `false` to hide archived bookmarks, or `undefined` to show all bookmarks.\n */\nexport function useArchiveFilter(): {\n  archived: false | undefined;\n  isLoading: boolean;\n} {\n  const api = useTRPC();\n  const { data: userSettings, isLoading } = useQuery(\n    api.users.settings.queryOptions(),\n  );\n  return {\n    archived:\n      userSettings?.archiveDisplayBehaviour === \"show\" ? undefined : false,\n    isLoading,\n  };\n}\n"
  },
  {
    "path": "apps/mobile/lib/providers.tsx",
    "content": "import { useEffect } from \"react\";\nimport FullPageSpinner from \"@/components/ui/FullPageSpinner\";\nimport { Toaster } from \"sonner-native\";\n\nimport { TRPCSettingsProvider } from \"@karakeep/shared-react/providers/trpc-provider\";\n\nimport { ReaderSettingsProvider } from \"./readerSettings\";\nimport useAppSettings from \"./settings\";\n\nexport function Providers({ children }: { children: React.ReactNode }) {\n  const { settings, isLoading, load } = useAppSettings();\n\n  useEffect(() => {\n    load();\n  }, []);\n\n  if (isLoading) {\n    // Don't render anything if the settings still hasn't been loaded\n    return <FullPageSpinner />;\n  }\n\n  return (\n    <TRPCSettingsProvider settings={settings}>\n      <ReaderSettingsProvider>\n        {children}\n        <Toaster />\n      </ReaderSettingsProvider>\n    </TRPCSettingsProvider>\n  );\n}\n"
  },
  {
    "path": "apps/mobile/lib/readerSettings.tsx",
    "content": "import { ReactNode, useCallback } from \"react\";\nimport { Platform } from \"react-native\";\n\nimport {\n  ReaderSettingsProvider as BaseReaderSettingsProvider,\n  useReaderSettingsContext,\n} from \"@karakeep/shared-react/hooks/reader-settings\";\nimport { ReaderSettingsPartial } from \"@karakeep/shared/types/readers\";\nimport { ZReaderFontFamily } from \"@karakeep/shared/types/users\";\n\nimport { useSettings } from \"./settings\";\n\n// Mobile-specific font families for native Text components\n// On Android, use generic font family names: \"serif\", \"sans-serif\", \"monospace\"\n// On iOS, use specific font names like \"Georgia\" and \"Courier\"\n// Note: undefined means use the system default font\nexport const MOBILE_FONT_FAMILIES: Record<\n  ZReaderFontFamily,\n  string | undefined\n> = Platform.select({\n  android: {\n    serif: \"serif\",\n    sans: undefined,\n    mono: \"monospace\",\n  },\n  default: {\n    serif: \"Georgia\",\n    sans: undefined,\n    mono: \"Courier\",\n  },\n})!;\n\n// Font families for WebView HTML content (CSS font stacks)\nexport const WEBVIEW_FONT_FAMILIES: Record<ZReaderFontFamily, string> = {\n  serif: \"Georgia, 'Times New Roman', serif\",\n  sans: \"-apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif\",\n  mono: \"ui-monospace, Menlo, Monaco, 'Courier New', monospace\",\n} as const;\n\n/**\n * Mobile-specific provider for reader settings.\n * Wraps the shared provider with mobile storage callbacks.\n */\nexport function ReaderSettingsProvider({ children }: { children: ReactNode }) {\n  // Read from zustand store directly to keep callback stable (empty deps).\n  const getLocalOverrides = useCallback((): ReaderSettingsPartial => {\n    const currentSettings = useSettings.getState().settings.settings;\n    return {\n      fontSize: currentSettings.readerFontSize,\n      lineHeight: currentSettings.readerLineHeight,\n      fontFamily: currentSettings.readerFontFamily,\n    };\n  }, []);\n\n  const saveLocalOverrides = useCallback((overrides: ReaderSettingsPartial) => {\n    const currentSettings = useSettings.getState().settings.settings;\n    // Remove reader settings keys first, then add back only defined ones\n    const {\n      readerFontSize: _fs,\n      readerLineHeight: _lh,\n      readerFontFamily: _ff,\n      ...rest\n    } = currentSettings;\n\n    const newSettings = { ...rest };\n    if (overrides.fontSize !== undefined) {\n      (newSettings as typeof currentSettings).readerFontSize =\n        overrides.fontSize;\n    }\n    if (overrides.lineHeight !== undefined) {\n      (newSettings as typeof currentSettings).readerLineHeight =\n        overrides.lineHeight;\n    }\n    if (overrides.fontFamily !== undefined) {\n      (newSettings as typeof currentSettings).readerFontFamily =\n        overrides.fontFamily;\n    }\n\n    useSettings.getState().setSettings(newSettings);\n  }, []);\n\n  return (\n    <BaseReaderSettingsProvider\n      getLocalOverrides={getLocalOverrides}\n      saveLocalOverrides={saveLocalOverrides}\n    >\n      {children}\n    </BaseReaderSettingsProvider>\n  );\n}\n\n// Re-export the context hook as useReaderSettings for mobile consumers\nexport { useReaderSettingsContext as useReaderSettings };\n"
  },
  {
    "path": "apps/mobile/lib/session.ts",
    "content": "import { useCallback } from \"react\";\nimport { useMutation } from \"@tanstack/react-query\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\nimport useAppSettings from \"./settings\";\n\nexport function useSession() {\n  const { settings, setSettings } = useAppSettings();\n  const api = useTRPC();\n\n  const { mutate: deleteKey } = useMutation(\n    api.apiKeys.revoke.mutationOptions(),\n  );\n\n  const logout = useCallback(() => {\n    if (settings.apiKeyId) {\n      deleteKey({ id: settings.apiKeyId });\n    }\n    setSettings({ ...settings, apiKey: undefined, apiKeyId: undefined });\n  }, [settings, setSettings]);\n\n  return {\n    logout,\n  };\n}\n\nexport function useIsLoggedIn() {\n  const { settings, isLoading } = useAppSettings();\n\n  return isLoading ? undefined : !!settings.apiKey;\n}\n"
  },
  {
    "path": "apps/mobile/lib/settings.ts",
    "content": "import { useEffect } from \"react\";\nimport * as SecureStore from \"expo-secure-store\";\nimport { z } from \"zod\";\nimport { create } from \"zustand\";\n\nimport { zReaderFontFamilySchema } from \"@karakeep/shared/types/users\";\n\nconst SETTING_NAME = \"settings\";\n\nconst zSettingsSchema = z.object({\n  apiKey: z.string().optional(),\n  apiKeyId: z.string().optional(),\n  address: z.string().optional().default(\"https://cloud.karakeep.app\"),\n  imageQuality: z.number().optional().default(0.2),\n  theme: z.enum([\"light\", \"dark\", \"system\"]).optional().default(\"system\"),\n  defaultBookmarkView: z\n    .enum([\"reader\", \"browser\", \"externalBrowser\"])\n    .optional()\n    .default(\"reader\"),\n  showNotes: z.boolean().optional().default(false),\n  customHeaders: z.record(z.string(), z.string()).optional().default({}),\n  // Reader settings (local device overrides)\n  readerFontSize: z.number().int().min(12).max(24).optional(),\n  readerLineHeight: z.number().min(1.2).max(2.5).optional(),\n  readerFontFamily: zReaderFontFamilySchema.optional(),\n});\n\nexport type Settings = z.infer<typeof zSettingsSchema>;\n\ninterface AppSettingsState {\n  settings: { isLoading: boolean; settings: Settings };\n  setSettings: (settings: Settings) => Promise<void>;\n  load: () => Promise<void>;\n}\n\nconst useSettings = create<AppSettingsState>((set, get) => ({\n  settings: {\n    isLoading: true,\n    settings: {\n      address: \"https://cloud.karakeep.app\",\n      imageQuality: 0.2,\n      theme: \"system\",\n      defaultBookmarkView: \"reader\",\n      showNotes: false,\n      customHeaders: {},\n    },\n  },\n  setSettings: async (settings) => {\n    await SecureStore.setItemAsync(SETTING_NAME, JSON.stringify(settings));\n    set((_state) => ({ settings: { isLoading: false, settings } }));\n  },\n  load: async () => {\n    if (!get().settings.isLoading) {\n      return;\n    }\n    const strVal = await SecureStore.getItemAsync(SETTING_NAME);\n    if (!strVal) {\n      set((state) => ({\n        settings: { isLoading: false, settings: state.settings.settings },\n      }));\n      return;\n    }\n    const parsed = zSettingsSchema.safeParse(JSON.parse(strVal));\n    if (!parsed.success) {\n      // Wipe the state if invalid\n      set((state) => ({\n        settings: { isLoading: false, settings: state.settings.settings },\n      }));\n      return;\n    }\n\n    set((_state) => ({\n      settings: { isLoading: false, settings: parsed.data },\n    }));\n  },\n}));\n\nexport default function useAppSettings() {\n  const { settings, setSettings, load } = useSettings();\n\n  useEffect(() => {\n    if (settings.isLoading) {\n      load();\n    }\n  }, [load, settings.isLoading]);\n\n  return { ...settings, setSettings, load };\n}\n\nexport { useSettings };\n"
  },
  {
    "path": "apps/mobile/lib/upload.ts",
    "content": "import ReactNativeBlobUtil from \"react-native-blob-util\";\nimport { useMutation, useQueryClient } from \"@tanstack/react-query\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\nimport { BookmarkTypes, ZBookmark } from \"@karakeep/shared/types/bookmarks\";\nimport {\n  zUploadErrorSchema,\n  zUploadResponseSchema,\n} from \"@karakeep/shared/types/uploads\";\n\nimport type { Settings } from \"./settings\";\nimport { buildApiHeaders } from \"./utils\";\n\nexport function useUploadAsset(\n  settings: Settings,\n  options: {\n    onSuccess?: (bookmark: ZBookmark & { alreadyExists: boolean }) => void;\n    onError?: (e: string) => void;\n  },\n) {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n\n  const { mutate: createBookmark, isPending: isCreatingBookmark } = useMutation(\n    api.bookmarks.createBookmark.mutationOptions({\n      onSuccess: (d) => {\n        queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter());\n        if (options.onSuccess) {\n          options.onSuccess(d);\n        }\n      },\n      onError: (e) => {\n        if (options.onError) {\n          options.onError(e.message);\n        }\n      },\n    }),\n  );\n\n  const { mutate: uploadAsset, isPending: isUploading } = useMutation({\n    mutationFn: async (file: { type: string; name: string; uri: string }) => {\n      // There's a bug in the native FormData implementation (https://github.com/facebook/react-native/issues/44737)\n      // that will only get fixed in react native 0.77. Using the BlobUtil implementation for now.\n      const resp = await ReactNativeBlobUtil.fetch(\n        \"POST\",\n        `${settings.address}/api/assets`,\n        {\n          ...buildApiHeaders(settings.apiKey, settings.customHeaders),\n          \"Content-Type\": \"multipart/form-data\",\n        },\n        [\n          {\n            name: \"file\",\n            filename: file.name,\n            type: file.type,\n            data: ReactNativeBlobUtil.wrap(file.uri.replace(\"file://\", \"\")),\n          },\n        ],\n      );\n      return zUploadResponseSchema.parse(await resp.json());\n    },\n    onSuccess: (resp) => {\n      const assetId = resp.assetId;\n      const assetType =\n        resp.contentType === \"application/pdf\" ? \"pdf\" : \"image\";\n      createBookmark({\n        type: BookmarkTypes.ASSET,\n        assetId,\n        assetType,\n        source: \"mobile\",\n      });\n    },\n    onError: (e) => {\n      if (options.onError) {\n        const err = zUploadErrorSchema.parse(JSON.parse(e.message));\n        options.onError(err.error);\n      }\n    },\n  });\n\n  return {\n    uploadAsset,\n    isPending: isUploading || isCreatingBookmark,\n  };\n}\n"
  },
  {
    "path": "apps/mobile/lib/useColorScheme.tsx",
    "content": "import * as React from \"react\";\nimport { Platform } from \"react-native\";\nimport * as NavigationBar from \"expo-navigation-bar\";\nimport useAppSettings from \"@/lib/settings\";\nimport { COLORS } from \"@/theme/colors\";\nimport { useColorScheme as useNativewindColorScheme } from \"nativewind\";\n\nfunction useColorScheme() {\n  const { settings, isLoading } = useAppSettings();\n  const { colorScheme, setColorScheme: setNativewindColorScheme } =\n    useNativewindColorScheme();\n\n  // Sync user settings with native color scheme\n  React.useEffect(() => {\n    setNativewindColorScheme(settings.theme);\n  }, [settings.theme, isLoading]);\n\n  React.useEffect(() => {\n    if (Platform.OS === \"android\") {\n      setNavigationBar(colorScheme ?? \"light\").catch((error) => {\n        console.error('useColorScheme.tsx\", \"setColorScheme', error);\n      });\n    }\n  }, [colorScheme]);\n\n  return {\n    colorScheme: colorScheme ?? \"light\",\n    isDarkColorScheme: colorScheme === \"dark\",\n    colors: COLORS[colorScheme ?? \"light\"],\n  };\n}\n\n/**\n * Set the Android navigation bar color based on the color scheme.\n */\nfunction useInitialAndroidBarSync() {\n  const { colorScheme } = useColorScheme();\n  React.useEffect(() => {\n    if (Platform.OS !== \"android\") return;\n    setNavigationBar(colorScheme).catch((error) => {\n      console.error('useColorScheme.tsx\", \"useInitialColorScheme', error);\n    });\n  }, []);\n}\n\nexport { useColorScheme, useInitialAndroidBarSync };\n\nfunction setNavigationBar(colorScheme: \"light\" | \"dark\") {\n  return NavigationBar.setButtonStyleAsync(\n    colorScheme === \"dark\" ? \"light\" : \"dark\",\n  );\n}\n"
  },
  {
    "path": "apps/mobile/lib/useMenuIconColors.ts",
    "content": "import { useColorScheme } from \"@/lib/useColorScheme\";\n\nexport function useMenuIconColors() {\n  const { colorScheme } = useColorScheme();\n\n  const menuIconColor = colorScheme === \"dark\" ? \"#d1d5db\" : \"#4b5563\";\n  const destructiveMenuIconColor =\n    colorScheme === \"dark\" ? \"#f87171\" : \"#dc2626\";\n\n  return {\n    menuIconColor,\n    destructiveMenuIconColor,\n  };\n}\n"
  },
  {
    "path": "apps/mobile/lib/utils.ts",
    "content": "import type { ClassValue } from \"clsx\";\nimport { clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n\n/**\n * Merge props conditionally.\n *\n * @example\n * ```\n * <View {...condProps(\n *     { condition: true, props: { className: \"foo\" } },\n *     { condition: true, props: { style: { margin: \"10px\" } } },\n * )} />\n * ```\n * results in:\n * ```\n * <View className=\"foo\" style={ margin: \"10px\" } />\n * ```\n * @example\n * ```\n * <View style={condProps(\n *     { condition: true, color: \"red\" },\n *     { condition: true, fontWeight: \"bold\" }\n * )} />\n * ```\n * results in:\n * ```\n * <View style={ color: \"red\", fontWeight: \"bold\" } />\n * ```\n */\nexport function condProps(\n  ...condProps: {\n    condition: boolean;\n    props: Record<string, unknown>;\n  }[]\n): Record<string, unknown> {\n  return condProps.reduce((acc, { condition, props }) => {\n    return condition ? { ...acc, ...props } : acc;\n  }, {});\n}\n\n/**\n * Build HTTP headers for API requests, merging Authorization and custom headers.\n * This ensures all direct HTTP calls (uploads, downloads, health checks) respect\n * the user's custom header configuration.\n */\nexport function buildApiHeaders(\n  apiKey: string | undefined,\n  customHeaders: Record<string, string> = {},\n): Record<string, string> {\n  return {\n    ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),\n    ...customHeaders,\n  };\n}\n"
  },
  {
    "path": "apps/mobile/metro.config.js",
    "content": "const { FileStore } = require(\"metro-cache\");\nconst { withNativeWind } = require(\"nativewind/metro\");\nconst path = require(\"path\");\nconst { getSentryExpoConfig } = require(\"@sentry/react-native/metro\");\n\nmodule.exports = withTurborepoManagedCache(\n  withMonorepoPaths(\n    // eslint-disable-next-line no-undef\n    withNativeWind(getSentryExpoConfig(__dirname), {\n      input: \"./globals.css\",\n      configPath: \"./tailwind.config.js\",\n      inlineRem: 16,\n    }),\n  ),\n);\n\n/**\n * Add the monorepo paths to the Metro config.\n * This allows Metro to resolve modules from the monorepo.\n *\n * @see https://docs.expo.dev/guides/monorepos/#modify-the-metro-config\n * @param {import('expo/metro-config').MetroConfig} config\n * @returns {import('expo/metro-config').MetroConfig}\n */\nfunction withMonorepoPaths(config) {\n  // eslint-disable-next-line no-undef\n  const projectRoot = __dirname;\n  const workspaceRoot = path.resolve(projectRoot, \"../..\");\n\n  // #1 - Watch all files in the monorepo\n  config.watchFolders = [workspaceRoot];\n\n  // #2 - Resolve modules within the project's `node_modules` first, then all monorepo modules\n  config.resolver.nodeModulesPaths = [\n    path.resolve(projectRoot, \"node_modules\"),\n    path.resolve(workspaceRoot, \"node_modules\"),\n  ];\n\n  return config;\n}\n\n/**\n * Move the Metro cache to the `node_modules/.cache/metro` folder.\n * This repository configured Turborepo to use this cache location as well.\n * If you have any environment variables, you can configure Turborepo to invalidate it when needed.\n *\n * @see https://turbo.build/repo/docs/reference/configuration#env\n * @param {import('expo/metro-config').MetroConfig} config\n * @returns {import('expo/metro-config').MetroConfig}\n */\nfunction withTurborepoManagedCache(config) {\n  config.cacheStores = [\n    // eslint-disable-next-line no-undef\n    new FileStore({ root: path.join(__dirname, \"node_modules/.cache/metro\") }),\n  ];\n  return config;\n}\n"
  },
  {
    "path": "apps/mobile/nativewind-env.d.ts",
    "content": "/// <reference types=\"nativewind/types\" />\n"
  },
  {
    "path": "apps/mobile/package.json",
    "content": "{\n  \"name\": \"@karakeep/mobile\",\n  \"version\": \"1.0.0\",\n  \"main\": \"index.ts\",\n  \"scripts\": {\n    \"clean\": \"git clean -xdf .expo .turbo node_modules\",\n    \"clean:prebuild\": \"APP_VARIANT=development expo prebuild --no-install --clean\",\n    \"expo\": \"expo\",\n    \"start\": \"expo start\",\n    \"android\": \"APP_VARIANT=development expo run:android\",\n    \"android:preview\": \"APP_VARIANT=development pnpm run android --variant release\",\n    \"android:release\": \"pnpm run android --variant release\",\n    \"ios\": \"APP_VARIANT=development expo run:ios\",\n    \"ios:preview\": \"APP_VARIANT=development pnpm run ios --configuration Release\",\n    \"ios:release\": \"pnpm run ios --configuration Release\",\n    \"web\": \"expo start --web\",\n    \"format\": \"oxfmt --check .\",\n    \"format:fix\": \"oxfmt .\",\n    \"lint\": \"oxlint .\",\n    \"lint:fix\": \"oxlint . --fix\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@expo/metro-runtime\": \"~6.1.2\",\n    \"@expo/vector-icons\": \"^15.0.3\",\n    \"@karakeep/shared\": \"workspace:^0.1.0\",\n    \"@karakeep/shared-react\": \"workspace:^0.1.0\",\n    \"@karakeep/trpc\": \"workspace:^0.1.0\",\n    \"@react-native-async-storage/async-storage\": \"2.2.0\",\n    \"@react-native-community/slider\": \"^5.1.2\",\n    \"@react-native-menu/menu\": \"^2.0.0\",\n    \"@react-navigation/native\": \"^7.1.8\",\n    \"@rn-primitives/hooks\": \"^1.3.0\",\n    \"@rn-primitives/slot\": \"^1.2.0\",\n    \"@sentry/react-native\": \"^8.3.0\",\n    \"@shopify/flash-list\": \"2.0.2\",\n    \"@tanstack/react-query\": \"5.90.2\",\n    \"class-variance-authority\": \"^0.7.0\",\n    \"clsx\": \"^2.1.0\",\n    \"date-fns\": \"^3.6.0\",\n    \"expo\": \"~54.0.31\",\n    \"expo-build-properties\": \"~1.0.10\",\n    \"expo-checkbox\": \"~5.0.8\",\n    \"expo-clipboard\": \"~8.0.8\",\n    \"expo-constants\": \"~18.0.13\",\n    \"expo-dev-client\": \"~6.0.20\",\n    \"expo-file-system\": \"~19.0.21\",\n    \"expo-haptics\": \"~15.0.8\",\n    \"expo-image\": \"~3.0.11\",\n    \"expo-image-picker\": \"~17.0.10\",\n    \"expo-linking\": \"~8.0.11\",\n    \"expo-navigation-bar\": \"~5.0.10\",\n    \"expo-router\": \"~6.0.21\",\n    \"expo-secure-store\": \"~15.0.8\",\n    \"expo-share-intent\": \"^5.1.1\",\n    \"expo-sharing\": \"~14.0.8\",\n    \"expo-status-bar\": \"~3.0.9\",\n    \"expo-system-ui\": \"~6.0.9\",\n    \"expo-web-browser\": \"~15.0.10\",\n    \"lucide-react-native\": \"^0.513.0\",\n    \"nativewind\": \"^4.2.1\",\n    \"react\": \"^19.2.1\",\n    \"react-dom\": \"^19.2.1\",\n    \"react-native\": \"0.81.5\",\n    \"react-native-blob-util\": \"^0.21.2\",\n    \"react-native-css-interop\": \"0.2.1\",\n    \"react-native-gesture-handler\": \"~2.28.0\",\n    \"react-native-image-viewing\": \"^0.2.2\",\n    \"react-native-keyboard-controller\": \"^1.18.5\",\n    \"react-native-markdown-display\": \"^7.0.2\",\n    \"react-native-pdf\": \"7.0.3\",\n    \"react-native-reanimated\": \"~4.1.1\",\n    \"react-native-safe-area-context\": \"~5.6.0\",\n    \"react-native-screens\": \"~4.16.0\",\n    \"react-native-svg\": \"15.12.1\",\n    \"react-native-web\": \"^0.21.0\",\n    \"react-native-webview\": \"13.15.0\",\n    \"react-native-worklets\": \"0.5.1\",\n    \"sonner-native\": \"^0.22.2\",\n    \"tailwind-merge\": \"^2.2.1\",\n    \"zod\": \"^3.24.2\",\n    \"zustand\": \"^5.0.5\"\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"~7.26.0\",\n    \"@karakeep/tailwind-config\": \"workspace:^0.1.0\",\n    \"@karakeep/tsconfig\": \"workspace:^0.1.0\",\n    \"@types/react\": \"^19.2.14\",\n    \"ajv\": \"latest\",\n    \"tailwindcss\": \"^3.4.1\",\n    \"typescript\": \"^5.9\"\n  },\n  \"private\": true\n}\n"
  },
  {
    "path": "apps/mobile/plugins/camera-not-required.js",
    "content": "const { withAndroidManifest } = require(\"@expo/config-plugins\");\n\nconst withCameraNotRequired = (config) => {\n  return withAndroidManifest(config, async (config) => {\n    config.modResults = await setCustomConfigAsync(config, config.modResults);\n    return config;\n  });\n};\n\nasync function setCustomConfigAsync(_config, androidManifest) {\n  const usesFeature = androidManifest.manifest[\"uses-feature\"] ?? [];\n  usesFeature.push({\n    $: {\n      \"android:name\": \"android.hardware.camera\",\n      \"android:required\": \"false\",\n    },\n  });\n  androidManifest.manifest[\"uses-feature\"] = usesFeature;\n\n  return androidManifest;\n}\n\nmodule.exports = withCameraNotRequired;\n"
  },
  {
    "path": "apps/mobile/plugins/network_security_config.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<network-security-config>\n    <base-config cleartextTrafficPermitted=\"true\">\n        <trust-anchors>\n            <certificates src=\"system\" />\n            <certificates src=\"user\" />\n        </trust-anchors>\n    </base-config>\n</network-security-config>"
  },
  {
    "path": "apps/mobile/plugins/trust-local-certs.js",
    "content": "const { AndroidConfig, withAndroidManifest } = require(\"@expo/config-plugins\");\nconst { Paths } = require(\"@expo/config-plugins/build/android\");\nconst path = require(\"path\");\nconst fs = require(\"fs\");\nconst fsPromises = fs.promises;\n\nconst { getMainApplicationOrThrow } = AndroidConfig.Manifest;\n\nconst withTrustLocalCerts = (config) => {\n  return withAndroidManifest(config, async (config) => {\n    config.modResults = await setCustomConfigAsync(config, config.modResults);\n    return config;\n  });\n};\n\nasync function setCustomConfigAsync(config, androidManifest) {\n  const src_file_pat = path.join(__dirname, \"network_security_config.xml\");\n  const res_file_path = path.join(\n    await Paths.getResourceFolderAsync(config.modRequest.projectRoot),\n    \"xml\",\n    \"network_security_config.xml\",\n  );\n\n  const res_dir = path.resolve(res_file_path, \"..\");\n\n  if (!fs.existsSync(res_dir)) {\n    await fsPromises.mkdir(res_dir);\n  }\n\n  try {\n    await fsPromises.copyFile(src_file_pat, res_file_path);\n  } catch (e) {\n    throw e;\n  }\n\n  const mainApplication = getMainApplicationOrThrow(androidManifest);\n  mainApplication.$[\"android:networkSecurityConfig\"] =\n    \"@xml/network_security_config\";\n\n  return androidManifest;\n}\n\nmodule.exports = withTrustLocalCerts;\n"
  },
  {
    "path": "apps/mobile/tailwind.config.js",
    "content": "const { hairlineWidth } = require(\"nativewind/theme\");\n\n/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  // NOTE: Update this to include the paths to all of your component files.\n  content: [\n    \"./app/**/*.{js,jsx,ts,tsx}\",\n    \"./components/**/*.{js,jsx,ts,tsx}\",\n    \"../../packages/shared-react/**/*.{js,jsx,ts,tsx}\",\n  ],\n  presets: [require(\"nativewind/preset\")],\n  theme: {\n    extend: {\n      colors: {\n        border: withOpacity(\"border\"),\n        input: withOpacity(\"input\"),\n        ring: withOpacity(\"ring\"),\n        background: withOpacity(\"background\"),\n        foreground: withOpacity(\"foreground\"),\n        primary: {\n          DEFAULT: withOpacity(\"primary\"),\n          foreground: withOpacity(\"primary-foreground\"),\n        },\n        secondary: {\n          DEFAULT: withOpacity(\"secondary\"),\n          foreground: withOpacity(\"secondary-foreground\"),\n        },\n        destructive: {\n          DEFAULT: withOpacity(\"destructive\"),\n          foreground: withOpacity(\"destructive-foreground\"),\n        },\n        muted: {\n          DEFAULT: withOpacity(\"muted\"),\n          foreground: withOpacity(\"muted-foreground\"),\n        },\n        accent: {\n          DEFAULT: withOpacity(\"accent\"),\n          foreground: withOpacity(\"accent-foreground\"),\n        },\n        popover: {\n          DEFAULT: withOpacity(\"popover\"),\n          foreground: withOpacity(\"popover-foreground\"),\n        },\n        card: {\n          DEFAULT: withOpacity(\"card\"),\n          foreground: withOpacity(\"card-foreground\"),\n        },\n      },\n      borderWidth: {\n        hairline: hairlineWidth(),\n      },\n    },\n  },\n  plugins: [require(\"@tailwindcss/typography\")],\n};\n\nfunction withOpacity(variableName) {\n  return ({ opacityValue }) => {\n    if (opacityValue !== undefined) {\n      return `rgb(var(--${variableName}) / ${opacityValue})`;\n    }\n    return `rgb(var(--${variableName}))`;\n  };\n}\n"
  },
  {
    "path": "apps/mobile/theme/colors.ts",
    "content": "const SYSTEM_COLORS = {\n  white: \"rgb(255, 255, 255)\",\n  black: \"rgb(0, 0, 0)\",\n  light: {\n    grey6: \"rgb(242, 242, 247)\",\n    grey5: \"rgb(230, 230, 235)\",\n    grey4: \"rgb(210, 210, 215)\",\n    grey3: \"rgb(199, 199, 204)\",\n    grey2: \"rgb(176, 176, 181)\",\n    grey: \"rgb(153, 153, 158)\",\n    background: \"rgb(242, 242, 247)\",\n    foreground: \"rgb(0, 0, 0)\",\n    root: \"rgb(242, 242, 247)\",\n    card: \"rgb(242, 242, 247)\",\n    destructive: \"rgb(255, 56, 43)\",\n    primary: \"rgb(0, 123, 255)\",\n  },\n  dark: {\n    grey6: \"rgb(21, 21, 24)\",\n    grey5: \"rgb(40, 40, 40)\",\n    grey4: \"rgb(51, 51, 51)\",\n    grey3: \"rgb(70, 70, 70)\",\n    grey2: \"rgb(99, 99, 99)\",\n    grey: \"rgb(158, 158, 158)\",\n    background: \"rgb(0, 0, 0)\",\n    foreground: \"rgb(255, 255, 255)\",\n    root: \"rgb(0, 0, 0)\",\n    card: \"rgb(0, 0, 0)\",\n    destructive: \"rgb(254, 67, 54)\",\n    primary: \"rgb(3, 133, 255)\",\n  },\n} as const;\n\nconst COLORS = SYSTEM_COLORS;\n\nexport { COLORS };\n"
  },
  {
    "path": "apps/mobile/theme/index.ts",
    "content": "import { DarkTheme, DefaultTheme } from \"@react-navigation/native\";\n\nimport { COLORS } from \"./colors\";\n\nconst NAV_THEME = {\n  light: {\n    ...DefaultTheme,\n    colors: {\n      background: COLORS.light.background,\n      border: COLORS.light.grey5,\n      card: COLORS.light.card,\n      notification: COLORS.light.destructive,\n      primary: COLORS.light.primary,\n      text: COLORS.black,\n    },\n  },\n  dark: {\n    ...DarkTheme,\n    colors: {\n      background: COLORS.dark.background,\n      border: COLORS.dark.grey5,\n      card: COLORS.dark.grey6,\n      notification: COLORS.dark.destructive,\n      primary: COLORS.dark.primary,\n      text: COLORS.white,\n    },\n  },\n};\n\nexport { NAV_THEME };\n"
  },
  {
    "path": "apps/mobile/tsconfig.json",
    "content": "{\n  \"extends\": \"expo/tsconfig.base\",\n  \"compilerOptions\": {\n    \"tsBuildInfoFile\": \"node_modules/.cache/tsbuildinfo.json\",\n    \"types\": [\"nativewind/types\", \"react-native\"],\n    \"incremental\": true,\n    \"strict\": true,\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"./*\"]\n    }\n  },\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "apps/web/.oxlintrc.json",
    "content": "{\n  \"$schema\": \"../../node_modules/oxlint/configuration_schema.json\",\n  \"extends\": [\n    \"../../tooling/oxlint/oxlint-base.json\",\n    \"../../tooling/oxlint/oxlint-nextjs.json\",\n    \"../../tooling/oxlint/oxlint-react.json\"\n  ],\n  \"env\": {\n    \"builtin\": true,\n    \"commonjs\": true,\n    \"browser\": true,\n    \"es2022\": true,\n    \"node\": true\n  },\n  \"globals\": {\n    \"React\": \"writeable\"\n  },\n  \"settings\": {\n    \"react\": {\n      \"version\": \"19\"\n    }\n  },\n  \"ignorePatterns\": [\n    \"**/*.config.js\",\n    \"**/*.config.cjs\",\n    \"**/.eslintrc.cjs\",\n    \".next\",\n    \"dist\",\n    \"build\",\n    \"public/sw.js\",\n    \"public/workbox-*.js\",\n    \"pnpm-lock.yaml\"\n  ]\n}\n"
  },
  {
    "path": "apps/web/@types/i18next.d.ts",
    "content": "import \"i18next\";\n\nimport translation from \"../lib/i18n/locales/en/translation.json\";\n\ndeclare module \"i18next\" {\n  // Extend CustomTypeOptions\n  interface CustomTypeOptions {\n    defaultNS: \"translation\";\n    resources: {\n      translation: typeof translation;\n    };\n  }\n}\n"
  },
  {
    "path": "apps/web/README.md",
    "content": "This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).\n\n## Getting Started\n\nFirst, run the development server:\n\n```bash\nnpm run dev\n# or\nyarn dev\n# or\npnpm dev\n# or\nbun dev\n```\n\nOpen [http://localhost:3000](http://localhost:3000) with your browser to see the result.\n\nYou can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.\n\nThis project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.\n\n## Learn More\n\nTo learn more about Next.js, take a look at the following resources:\n\n- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.\n- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.\n\nYou can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!\n\n## Deploy on Vercel\n\nThe easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.\n\nCheck out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.\n"
  },
  {
    "path": "apps/web/app/admin/admin_tools/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport BookmarkDebugger from \"@/components/admin/BookmarkDebugger\";\nimport { useTranslation } from \"@/lib/i18n/server\";\n\nexport async function generateMetadata(): Promise<Metadata> {\n  // oxlint-disable-next-line rules-of-hooks\n  const { t } = await useTranslation();\n  return {\n    title: `${t(\"admin.admin_tools.admin_tools\")} | Karakeep`,\n  };\n}\n\nexport default function AdminToolsPage() {\n  return (\n    <div className=\"flex flex-col gap-6\">\n      <BookmarkDebugger />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/admin/background_jobs/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport BackgroundJobs from \"@/components/admin/BackgroundJobs\";\nimport { useTranslation } from \"@/lib/i18n/server\";\n\nexport async function generateMetadata(): Promise<Metadata> {\n  // oxlint-disable-next-line rules-of-hooks\n  const { t } = await useTranslation();\n  return {\n    title: `${t(\"admin.background_jobs.background_jobs\")} | Karakeep`,\n  };\n}\n\nexport default function BackgroundJobsPage() {\n  return <BackgroundJobs />;\n}\n"
  },
  {
    "path": "apps/web/app/admin/layout.tsx",
    "content": "import React from \"react\";\nimport { redirect } from \"next/navigation\";\nimport { AdminNotices } from \"@/components/admin/AdminNotices\";\nimport MobileSidebar from \"@/components/shared/sidebar/MobileSidebar\";\nimport Sidebar from \"@/components/shared/sidebar/Sidebar\";\nimport SidebarLayout from \"@/components/shared/sidebar/SidebarLayout\";\nimport { getServerAuthSession } from \"@/server/auth\";\nimport { TFunction } from \"i18next\";\nimport { Activity, ArrowLeft, Settings, Users, Wrench } from \"lucide-react\";\n\nconst adminSidebarItems = (\n  t: TFunction,\n): {\n  name: string;\n  icon: React.ReactElement;\n  path: string;\n}[] => [\n  {\n    name: t(\"settings.back_to_app\"),\n    icon: <ArrowLeft size={18} />,\n    path: \"/dashboard/bookmarks\",\n  },\n  {\n    name: t(\"admin.server_stats.server_stats\"),\n    icon: <Activity size={18} />,\n    path: \"/admin/overview\",\n  },\n  {\n    name: t(\"admin.users_list.users_list\"),\n    icon: <Users size={18} />,\n    path: \"/admin/users\",\n  },\n  {\n    name: t(\"admin.background_jobs.background_jobs\"),\n    icon: <Settings size={18} />,\n    path: \"/admin/background_jobs\",\n  },\n  {\n    name: t(\"admin.admin_tools.admin_tools\"),\n    icon: <Wrench size={18} />,\n    path: \"/admin/admin_tools\",\n  },\n];\n\nexport default async function AdminLayout({\n  children,\n}: Readonly<{\n  children: React.ReactNode;\n}>) {\n  const session = await getServerAuthSession();\n  if (!session || session.user.role !== \"admin\") {\n    redirect(\"/\");\n  }\n\n  return (\n    <SidebarLayout\n      sidebar={<Sidebar items={adminSidebarItems} />}\n      mobileSidebar={<MobileSidebar items={adminSidebarItems} />}\n    >\n      <div className=\"flex flex-col gap-1\">\n        <AdminNotices />\n        {children}\n      </div>\n    </SidebarLayout>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/admin/overview/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport BasicStats from \"@/components/admin/BasicStats\";\nimport ServiceConnections from \"@/components/admin/ServiceConnections\";\nimport { useTranslation } from \"@/lib/i18n/server\";\n\nexport async function generateMetadata(): Promise<Metadata> {\n  // oxlint-disable-next-line rules-of-hooks\n  const { t } = await useTranslation();\n  return {\n    title: `${t(\"admin.admin_settings\")} | Karakeep`,\n  };\n}\n\nexport default function AdminOverviewPage() {\n  return (\n    <div className=\"flex flex-col gap-6\">\n      <BasicStats />\n      <ServiceConnections />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/admin/page.tsx",
    "content": "import { redirect } from \"next/navigation\";\n\nexport default function AdminHomepage() {\n  redirect(\"/admin/overview\");\n  return null;\n}\n"
  },
  {
    "path": "apps/web/app/admin/users/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport { Suspense } from \"react\";\nimport InvitesList from \"@/components/admin/InvitesList\";\nimport InvitesListSkeleton from \"@/components/admin/InvitesListSkeleton\";\nimport UserList from \"@/components/admin/UserList\";\nimport UserListSkeleton from \"@/components/admin/UserListSkeleton\";\nimport { useTranslation } from \"@/lib/i18n/server\";\n\nexport async function generateMetadata(): Promise<Metadata> {\n  // oxlint-disable-next-line rules-of-hooks\n  const { t } = await useTranslation();\n  return {\n    title: `${t(\"admin.users_list.users_list\")} | Karakeep`,\n  };\n}\n\nexport default function AdminUsersPage() {\n  return (\n    <div className=\"flex flex-col gap-4\">\n      <Suspense fallback={<UserListSkeleton />}>\n        <UserList />\n      </Suspense>\n      <Suspense fallback={<InvitesListSkeleton />}>\n        <InvitesList />\n      </Suspense>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/api/[[...route]]/route.ts",
    "content": "import { createContextFromRequest } from \"@/server/api/client\";\nimport { Hono } from \"hono\";\nimport { createMiddleware } from \"hono/factory\";\nimport { handle } from \"hono/vercel\";\n\nimport allApp from \"@karakeep/api\";\nimport { Context } from \"@karakeep/trpc\";\n\nexport const runtime = \"nodejs\";\n\nexport const nextAuth = createMiddleware<{\n  Variables: {\n    ctx: Context;\n  };\n}>(async (c, next) => {\n  const ctx = await createContextFromRequest(c.req.raw);\n  c.set(\"ctx\", ctx);\n  await next();\n});\n\nconst app = new Hono().basePath(\"/api\").use(nextAuth).route(\"/\", allApp);\n\nexport const GET = handle(app);\nexport const POST = handle(app);\nexport const PATCH = handle(app);\nexport const DELETE = handle(app);\nexport const OPTIONS = handle(app);\nexport const PUT = handle(app);\n"
  },
  {
    "path": "apps/web/app/api/auth/[...nextauth]/route.tsx",
    "content": "import { authHandler } from \"@/server/auth\";\n\nexport { authHandler as GET, authHandler as POST };\n"
  },
  {
    "path": "apps/web/app/api/bookmarks/export/route.tsx",
    "content": "import { NextRequest } from \"next/server\";\nimport {\n  createContextFromRequest,\n  createTrcpClientFromCtx,\n} from \"@/server/api/client\";\nimport { and, eq, inArray } from \"drizzle-orm\";\nimport { z } from \"zod\";\n\nimport { db } from \"@karakeep/db\";\nimport {\n  bookmarksInLists,\n  bookmarks as bookmarksTable,\n} from \"@karakeep/db/schema\";\nimport {\n  toExportFormat,\n  toExportListFormat,\n  toNetscapeFormat,\n  zExportSchema,\n} from \"@karakeep/shared/import-export\";\nimport { MAX_NUM_BOOKMARKS_PER_PAGE } from \"@karakeep/shared/types/bookmarks\";\n\nexport const dynamic = \"force-dynamic\";\nexport async function GET(request: NextRequest) {\n  const ctx = await createContextFromRequest(request);\n  if (!ctx.user) {\n    return Response.json({ error: \"Unauthorized\" }, { status: 401 });\n  }\n\n  const format = request.nextUrl.searchParams.get(\"format\") ?? \"json\";\n\n  const req = {\n    limit: MAX_NUM_BOOKMARKS_PER_PAGE,\n    useCursorV2: true,\n    includeContent: true,\n  };\n\n  const caller = createTrcpClientFromCtx(() => ctx);\n\n  let resp = await caller.bookmarks.getBookmarks(req);\n  let bookmarks = resp.bookmarks;\n\n  while (resp.nextCursor) {\n    resp = await caller.bookmarks.getBookmarks({\n      ...req,\n      cursor: resp.nextCursor,\n    });\n    bookmarks = [...bookmarks, ...resp.bookmarks];\n  }\n\n  if (format === \"json\") {\n    // Fetch lists and bookmark-to-list memberships\n    const listsResp = await caller.lists.list();\n    const ownedLists = listsResp.lists.filter((l) => l.userRole === \"owner\");\n\n    const manualLists = ownedLists.filter((l) => l.type === \"manual\");\n    const manualListIds = manualLists.map((l) => l.id);\n\n    let memberships: { bookmarkId: string; listId: string }[] = [];\n    if (manualListIds.length > 0) {\n      memberships = await db\n        .select({\n          bookmarkId: bookmarksInLists.bookmarkId,\n          listId: bookmarksInLists.listId,\n        })\n        .from(bookmarksInLists)\n        .innerJoin(\n          bookmarksTable,\n          eq(bookmarksTable.id, bookmarksInLists.bookmarkId),\n        )\n        .where(\n          and(\n            inArray(bookmarksInLists.listId, manualListIds),\n            eq(bookmarksTable.userId, ctx.user.id),\n          ),\n        );\n    }\n\n    const bookmarkListMap = new Map<string, string[]>();\n    for (const m of memberships) {\n      const existing = bookmarkListMap.get(m.bookmarkId) ?? [];\n      existing.push(m.listId);\n      bookmarkListMap.set(m.bookmarkId, existing);\n    }\n\n    const exportData: z.infer<typeof zExportSchema> = {\n      bookmarks: bookmarks\n        .map((b) => toExportFormat(b, bookmarkListMap.get(b.id) ?? []))\n        .filter((b) => b.content !== null),\n      lists: ownedLists.map(toExportListFormat),\n    };\n\n    return new Response(JSON.stringify(exportData), {\n      status: 200,\n      headers: {\n        \"Content-type\": \"application/json\",\n        \"Content-disposition\": `attachment; filename=\"karakeep-export-${new Date().toISOString()}.json\"`,\n      },\n    });\n  } else if (format === \"netscape\") {\n    // Netscape format\n    const netscapeContent = toNetscapeFormat(bookmarks);\n\n    return new Response(netscapeContent, {\n      status: 200,\n      headers: {\n        \"Content-type\": \"text/html\",\n        \"Content-disposition\": `attachment; filename=\"bookmarks-${new Date().toISOString()}.html\"`,\n      },\n    });\n  } else {\n    return Response.json({ error: \"Invalid format\" }, { status: 400 });\n  }\n}\n"
  },
  {
    "path": "apps/web/app/check-email/page.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { useRouter, useSearchParams } from \"next/navigation\";\nimport { Alert, AlertDescription } from \"@/components/ui/alert\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\";\nimport { useMutation } from \"@tanstack/react-query\";\nimport { Loader2, Mail } from \"lucide-react\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\nimport { validateRedirectUrl } from \"@karakeep/shared/utils/redirectUrl\";\n\nexport default function CheckEmailPage() {\n  const api = useTRPC();\n  const searchParams = useSearchParams();\n  const router = useRouter();\n  const [message, setMessage] = useState(\"\");\n\n  const email = searchParams.get(\"email\");\n  const redirectUrl =\n    validateRedirectUrl(searchParams.get(\"redirectUrl\")) ?? \"/\";\n\n  const resendEmailMutation = useMutation(\n    api.users.resendVerificationEmail.mutationOptions({\n      onSuccess: () => {\n        setMessage(\n          \"A new verification email has been sent to your email address.\",\n        );\n      },\n      onError: (error) => {\n        setMessage(error.message || \"Failed to resend verification email.\");\n      },\n    }),\n  );\n\n  const handleResendEmail = () => {\n    if (email) {\n      resendEmailMutation.mutate({ email, redirectUrl });\n    }\n  };\n\n  const handleBackToSignIn = () => {\n    router.push(\"/signin\");\n  };\n\n  if (!email) {\n    return (\n      <div className=\"flex min-h-screen items-center justify-center bg-background px-4 py-12 sm:px-6 lg:px-8\">\n        <Card className=\"w-full max-w-md\">\n          <CardHeader className=\"text-center\">\n            <CardTitle className=\"text-2xl font-bold\">\n              Invalid Request\n            </CardTitle>\n            <CardDescription>\n              No email address provided. Please try signing up again.\n            </CardDescription>\n          </CardHeader>\n          <CardContent>\n            <Button onClick={handleBackToSignIn} className=\"w-full\">\n              Back to Sign In\n            </Button>\n          </CardContent>\n        </Card>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex min-h-screen items-center justify-center bg-background px-4 py-12 sm:px-6 lg:px-8\">\n      <Card className=\"w-full max-w-md\">\n        <CardHeader className=\"text-center\">\n          <CardTitle className=\"text-2xl font-bold\">Check Your Email</CardTitle>\n          <CardDescription>\n            We&apos;ve sent a verification link to your email address\n          </CardDescription>\n        </CardHeader>\n        <CardContent className=\"space-y-4\">\n          <div className=\"flex items-center justify-center\">\n            <Mail className=\"h-12 w-12 text-blue-600\" />\n          </div>\n\n          <div className=\"space-y-2 text-center\">\n            <p className=\"text-sm text-muted-foreground\">\n              We&apos;ve sent a verification email to:\n            </p>\n            <p className=\"font-medium text-foreground\">{email}</p>\n            <p className=\"text-sm text-muted-foreground\">\n              Click the link in the email to verify your account and complete\n              your registration.\n            </p>\n          </div>\n\n          {message && (\n            <Alert>\n              <AlertDescription className=\"text-center\">\n                {message}\n              </AlertDescription>\n            </Alert>\n          )}\n\n          <div className=\"space-y-2\">\n            <Button\n              onClick={handleResendEmail}\n              variant=\"outline\"\n              className=\"w-full\"\n              disabled={resendEmailMutation.isPending}\n            >\n              {resendEmailMutation.isPending ? (\n                <>\n                  <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n                  Sending...\n                </>\n              ) : (\n                \"Resend Verification Email\"\n              )}\n            </Button>\n            <Button\n              onClick={handleBackToSignIn}\n              variant=\"ghost\"\n              className=\"w-full\"\n            >\n              Back to Sign In\n            </Button>\n          </div>\n        </CardContent>\n      </Card>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/dashboard/@modal/(.)preview/[bookmarkId]/page.tsx",
    "content": "\"use client\";\n\nimport { use, useState } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport BookmarkPreview from \"@/components/dashboard/preview/BookmarkPreview\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\";\nimport { VisuallyHidden } from \"@radix-ui/react-visually-hidden\";\n\nexport default function BookmarkPreviewPage(props: {\n  params: Promise<{ bookmarkId: string }>;\n}) {\n  const params = use(props.params);\n  const router = useRouter();\n\n  const [open, setOpen] = useState(true);\n\n  const setOpenWithRouter = (value: boolean) => {\n    setOpen(value);\n    if (!value) {\n      router.back();\n    }\n  };\n\n  return (\n    <Dialog open={open} onOpenChange={setOpenWithRouter}>\n      <VisuallyHidden>\n        <DialogHeader>\n          <DialogTitle>Preview</DialogTitle>\n        </DialogHeader>\n      </VisuallyHidden>\n      <DialogContent\n        className=\"h-[90%] max-w-[90%] overflow-hidden rounded-xl p-0\"\n        hideCloseBtn={true}\n        onOpenAutoFocus={(e) => e.preventDefault()}\n      >\n        <BookmarkPreview\n          bookmarkId={params.bookmarkId}\n          onClose={() => setOpenWithRouter(false)}\n        />\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/dashboard/@modal/[...catchAll]/page.tsx",
    "content": "export default function CatchAll() {\n  return null;\n}\n"
  },
  {
    "path": "apps/web/app/dashboard/@modal/default.tsx",
    "content": "export default function Default() {\n  return null;\n}\n"
  },
  {
    "path": "apps/web/app/dashboard/archive/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Bookmarks from \"@/components/dashboard/bookmarks/Bookmarks\";\nimport InfoTooltip from \"@/components/ui/info-tooltip\";\nimport { useTranslation } from \"@/lib/i18n/server\";\n\nexport async function generateMetadata(): Promise<Metadata> {\n  // oxlint-disable-next-line rules-of-hooks\n  const { t } = await useTranslation();\n  return {\n    title: `${t(\"common.archive\")} | Karakeep`,\n  };\n}\n\nfunction header() {\n  return (\n    <div className=\"flex gap-2\">\n      <p className=\"text-2xl\">🗄️ Archive</p>\n      <InfoTooltip size={17} className=\"my-auto\" variant=\"explain\">\n        <p>Archived bookmarks won&apos;t appear in the homepage</p>\n      </InfoTooltip>\n    </div>\n  );\n}\n\nexport default async function ArchivedBookmarkPage() {\n  return (\n    <Bookmarks\n      header={header()}\n      query={{ archived: true }}\n      showDivider={true}\n      showEditorCard={true}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/web/app/dashboard/bookmarks/page.tsx",
    "content": "import React from \"react\";\nimport Bookmarks from \"@/components/dashboard/bookmarks/Bookmarks\";\n\nexport default async function BookmarksPage() {\n  return (\n    <div>\n      <Bookmarks query={{ archived: false }} showEditorCard={true} />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/dashboard/cleanups/page.tsx",
    "content": "import { TagDuplicationDetection } from \"@/components/dashboard/cleanups/TagDuplicationDetention\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { useTranslation } from \"@/lib/i18n/server\";\nimport { Paintbrush, Tags } from \"lucide-react\";\n\nexport default async function Cleanups() {\n  // oxlint-disable-next-line rules-of-hooks\n  const { t } = await useTranslation();\n\n  return (\n    <div className=\"flex flex-col gap-y-4 rounded-md border bg-background p-4\">\n      <span className=\"flex items-center gap-1 text-2xl\">\n        <Paintbrush />\n        {t(\"cleanups.cleanups\")}\n      </span>\n      <Separator />\n      <span className=\"flex items-center gap-1 text-xl\">\n        <Tags />\n        {t(\"cleanups.duplicate_tags.title\")}\n      </span>\n      <Separator />\n      <TagDuplicationDetection />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/dashboard/error.tsx",
    "content": "\"use client\";\n\nimport ErrorFallback from \"@/components/dashboard/ErrorFallback\";\n\nexport default function Error() {\n  return <ErrorFallback />;\n}\n"
  },
  {
    "path": "apps/web/app/dashboard/favourites/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport Bookmarks from \"@/components/dashboard/bookmarks/Bookmarks\";\nimport { useTranslation } from \"@/lib/i18n/server\";\n\nexport async function generateMetadata(): Promise<Metadata> {\n  // oxlint-disable-next-line rules-of-hooks\n  const { t } = await useTranslation();\n  return {\n    title: `${t(\"lists.favourites\")} | Karakeep`,\n  };\n}\n\nexport default async function FavouritesBookmarkPage() {\n  return (\n    <Bookmarks\n      header={\n        <div className=\"flex items-center justify-between\">\n          <p className=\"text-2xl\">⭐️ Favourites</p>\n        </div>\n      }\n      query={{ favourited: true }}\n      showDivider={true}\n      showEditorCard={true}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/web/app/dashboard/feeds/[feedId]/page.tsx",
    "content": "import { notFound } from \"next/navigation\";\nimport Bookmarks from \"@/components/dashboard/bookmarks/Bookmarks\";\nimport { api } from \"@/server/api/client\";\nimport { TRPCError } from \"@trpc/server\";\n\nexport default async function FeedPage(props: {\n  params: Promise<{ feedId: string }>;\n}) {\n  const params = await props.params;\n  let feed;\n  try {\n    feed = await api.feeds.get({ feedId: params.feedId });\n  } catch (e) {\n    if (e instanceof TRPCError) {\n      if (e.code == \"NOT_FOUND\") {\n        notFound();\n      }\n    }\n    throw e;\n  }\n\n  return (\n    <Bookmarks\n      query={{ rssFeedId: feed.id }}\n      showDivider={true}\n      showEditorCard={false}\n      header={<div className=\"text-2xl\">{feed.name}</div>}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/web/app/dashboard/highlights/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport AllHighlights from \"@/components/dashboard/highlights/AllHighlights\";\nimport { useTranslation } from \"@/lib/i18n/server\";\nimport { api } from \"@/server/api/client\";\nimport { Highlighter } from \"lucide-react\";\n\nexport async function generateMetadata(): Promise<Metadata> {\n  // oxlint-disable-next-line rules-of-hooks\n  const { t } = await useTranslation();\n  return {\n    title: `${t(\"common.highlights\")} | Karakeep`,\n  };\n}\n\nexport default async function HighlightsPage() {\n  // oxlint-disable-next-line rules-of-hooks\n  const { t } = await useTranslation();\n  const highlights = await api.highlights.getAll({});\n  return (\n    <div className=\"flex flex-col gap-4\">\n      <div className=\"flex items-center\">\n        <Highlighter className=\"mr-2\" />\n        <p className=\"text-2xl\">{t(\"common.highlights\")}</p>\n      </div>\n      <div className=\"flex flex-col gap-8 rounded-md border bg-background p-4\">\n        <AllHighlights highlights={highlights} />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/dashboard/layout.tsx",
    "content": "import { redirect } from \"next/navigation\";\nimport AllLists from \"@/components/dashboard/sidebar/AllLists\";\nimport MobileSidebar from \"@/components/shared/sidebar/MobileSidebar\";\nimport Sidebar from \"@/components/shared/sidebar/Sidebar\";\nimport SidebarLayout from \"@/components/shared/sidebar/SidebarLayout\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { ReaderSettingsProvider } from \"@/lib/readerSettings\";\nimport { UserSettingsContextProvider } from \"@/lib/userSettings\";\nimport { api } from \"@/server/api/client\";\nimport { getServerAuthSession } from \"@/server/auth\";\nimport { TRPCError } from \"@trpc/server\";\nimport { TFunction } from \"i18next\";\nimport {\n  Archive,\n  ClipboardList,\n  Highlighter,\n  Home,\n  Search,\n  Tag,\n} from \"lucide-react\";\n\nimport { PluginManager, PluginType } from \"@karakeep/shared/plugins\";\nimport { tryCatch } from \"@karakeep/shared/tryCatch\";\n\nexport default async function Dashboard({\n  children,\n  modal,\n}: Readonly<{\n  children: React.ReactNode;\n  modal: React.ReactNode;\n}>) {\n  const session = await getServerAuthSession();\n  if (!session) {\n    redirect(\"/\");\n  }\n\n  const [lists, userSettings] = await Promise.all([\n    tryCatch(api.lists.list()),\n    tryCatch(api.users.settings()),\n  ]);\n\n  if (userSettings.error) {\n    if (userSettings.error instanceof TRPCError) {\n      if (\n        userSettings.error.code === \"NOT_FOUND\" ||\n        userSettings.error.code === \"UNAUTHORIZED\"\n      ) {\n        redirect(\"/logout\");\n      }\n    }\n    throw userSettings.error;\n  }\n\n  if (lists.error) {\n    throw lists.error;\n  }\n\n  const items = (t: TFunction) =>\n    [\n      {\n        name: t(\"common.home\"),\n        icon: <Home size={18} />,\n        path: \"/dashboard/bookmarks\",\n      },\n      PluginManager.isRegistered(PluginType.Search)\n        ? [\n            {\n              name: t(\"common.search\"),\n              icon: <Search size={18} />,\n              path: \"/dashboard/search\",\n            },\n          ]\n        : [],\n      {\n        name: t(\"common.tags\"),\n        icon: <Tag size={18} />,\n        path: \"/dashboard/tags\",\n      },\n      {\n        name: t(\"common.highlights\"),\n        icon: <Highlighter size={18} />,\n        path: \"/dashboard/highlights\",\n      },\n      {\n        name: t(\"common.archive\"),\n        icon: <Archive size={18} />,\n        path: \"/dashboard/archive\",\n      },\n    ].flat();\n\n  const mobileSidebar = (t: TFunction) => [\n    ...items(t),\n    {\n      name: t(\"lists.all_lists\"),\n      icon: <ClipboardList size={18} />,\n      path: \"/dashboard/lists\",\n    },\n  ];\n\n  return (\n    <UserSettingsContextProvider userSettings={userSettings.data}>\n      <ReaderSettingsProvider>\n        <SidebarLayout\n          sidebar={\n            <Sidebar\n              items={items}\n              extraSections={\n                <>\n                  <Separator />\n                  <AllLists initialData={lists.data} />\n                </>\n              }\n            />\n          }\n          mobileSidebar={<MobileSidebar items={mobileSidebar} />}\n          modal={modal}\n        >\n          {children}\n        </SidebarLayout>\n      </ReaderSettingsProvider>\n    </UserSettingsContextProvider>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/dashboard/lists/[listId]/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport { notFound } from \"next/navigation\";\nimport Bookmarks from \"@/components/dashboard/bookmarks/Bookmarks\";\nimport ListHeader from \"@/components/dashboard/lists/ListHeader\";\nimport { api } from \"@/server/api/client\";\nimport { TRPCError } from \"@trpc/server\";\n\nimport { BookmarkListContextProvider } from \"@karakeep/shared-react/hooks/bookmark-list-context\";\n\nexport async function generateMetadata(props: {\n  params: Promise<{ listId: string }>;\n}): Promise<Metadata> {\n  const params = await props.params;\n  try {\n    const list = await api.lists.get({ listId: params.listId });\n    return {\n      title: `${list.name} | Karakeep`,\n    };\n  } catch (e) {\n    if (e instanceof TRPCError && e.code === \"NOT_FOUND\") {\n      notFound();\n    }\n    throw e;\n  }\n}\n\nexport default async function ListPage(props: {\n  params: Promise<{ listId: string }>;\n  searchParams?: Promise<{\n    includeArchived?: string;\n  }>;\n}) {\n  const searchParams = await props.searchParams;\n  const params = await props.params;\n  const userSettings = await api.users.settings();\n  let list;\n  try {\n    list = await api.lists.get({ listId: params.listId });\n  } catch (e) {\n    if (e instanceof TRPCError) {\n      if (e.code == \"NOT_FOUND\") {\n        notFound();\n      }\n    }\n    throw e;\n  }\n\n  const includeArchived =\n    searchParams?.includeArchived !== undefined\n      ? searchParams.includeArchived === \"true\"\n      : userSettings.archiveDisplayBehaviour === \"show\";\n\n  // Only show editor card if user is owner or editor (not viewer)\n  const canEdit = list.userRole === \"owner\" || list.userRole === \"editor\";\n\n  return (\n    <BookmarkListContextProvider list={list}>\n      <Bookmarks\n        query={{\n          listId: list.id,\n          archived: !includeArchived ? false : undefined,\n        }}\n        showDivider={true}\n        showEditorCard={list.type === \"manual\" && canEdit}\n        header={<ListHeader initialData={list} />}\n      />\n    </BookmarkListContextProvider>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/dashboard/lists/page.tsx",
    "content": "import AllListsView from \"@/components/dashboard/lists/AllListsView\";\nimport { EditListModal } from \"@/components/dashboard/lists/EditListModal\";\nimport { PendingInvitationsCard } from \"@/components/dashboard/lists/PendingInvitationsCard\";\nimport { Button } from \"@/components/ui/button\";\nimport { useTranslation } from \"@/lib/i18n/server\";\nimport { api } from \"@/server/api/client\";\nimport { Plus } from \"lucide-react\";\n\nexport default async function ListsPage() {\n  // oxlint-disable-next-line rules-of-hooks\n  const { t } = await useTranslation();\n  const lists = await api.lists.list();\n\n  return (\n    <div className=\"flex flex-col gap-4\">\n      <div className=\"flex items-center justify-between\">\n        <p className=\"text-2xl\">📋 {t(\"lists.all_lists\")}</p>\n        <EditListModal>\n          <Button className=\"flex items-center\">\n            <Plus className=\"mr-2 size-4\" />\n            <span>{t(\"lists.new_list\")}</span>\n          </Button>\n        </EditListModal>\n      </div>\n      <PendingInvitationsCard />\n      <div className=\"flex flex-col gap-3 rounded-md border bg-background p-4\">\n        <AllListsView initialData={lists.lists} />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/dashboard/not-found.tsx",
    "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { Button } from \"@/components/ui/button\";\nimport { ArrowLeft, FileQuestion, Home } from \"lucide-react\";\n\nexport default function NotFound() {\n  return (\n    <div className=\"flex flex-1 items-center justify-center rounded-lg bg-slate-50 p-8 shadow-sm dark:bg-slate-700/50 dark:shadow-md\">\n      <div className=\"w-full max-w-md space-y-8 text-center\">\n        {/* Not Found Icon */}\n        <div className=\"flex justify-center\">\n          <div className=\"flex h-20 w-20 items-center justify-center rounded-full bg-muted\">\n            <FileQuestion className=\"h-10 w-10 text-muted-foreground\" />\n          </div>\n        </div>\n\n        {/* Main Content */}\n        <div className=\"space-y-4\">\n          <h1 className=\"text-balance text-2xl font-semibold text-foreground\">\n            Page Not Found\n          </h1>\n          <p className=\"text-pretty leading-relaxed text-muted-foreground\">\n            We couldn&apos;t find the page you&apos;re looking for. It may have\n            been moved or doesn&apos;t exist.\n          </p>\n        </div>\n\n        {/* Action Buttons */}\n        <div className=\"space-y-3\">\n          <Button className=\"w-full\" onClick={() => window.history.back()}>\n            <ArrowLeft className=\"mr-2 h-4 w-4\" />\n            Go Back\n          </Button>\n\n          <Link href=\"/\" className=\"block\">\n            <Button variant=\"outline\" className=\"w-full\">\n              <Home className=\"mr-2 h-4 w-4\" />\n              Go Home\n            </Button>\n          </Link>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/dashboard/preview/[bookmarkId]/page.tsx",
    "content": "import { notFound } from \"next/navigation\";\nimport BookmarkPreview from \"@/components/dashboard/preview/BookmarkPreview\";\nimport { api } from \"@/server/api/client\";\nimport { TRPCError } from \"@trpc/server\";\n\nexport default async function BookmarkPreviewPage(props: {\n  params: Promise<{ bookmarkId: string }>;\n}) {\n  const params = await props.params;\n  let bookmark;\n  try {\n    bookmark = await api.bookmarks.getBookmark({\n      bookmarkId: params.bookmarkId,\n    });\n  } catch (e) {\n    if (e instanceof TRPCError) {\n      if (e.code === \"NOT_FOUND\") {\n        notFound();\n      }\n    }\n    throw e;\n  }\n\n  return (\n    <div className=\"h-[calc(100dvh-theme(spacing.16)-theme(spacing.8))] overflow-hidden\">\n      <BookmarkPreview bookmarkId={bookmark.id} initialData={bookmark} />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/dashboard/search/page.tsx",
    "content": "\"use client\";\n\nimport { Suspense, useEffect } from \"react\";\nimport BookmarksGrid from \"@/components/dashboard/bookmarks/BookmarksGrid\";\nimport BookmarksGridSkeleton from \"@/components/dashboard/bookmarks/BookmarksGridSkeleton\";\nimport { useBookmarkSearch } from \"@/lib/hooks/bookmark-search\";\nimport { useInSearchPageStore } from \"@/lib/store/useInSearchPageStore\";\nimport { useSortOrderStore } from \"@/lib/store/useSortOrderStore\";\n\nfunction SearchComp() {\n  const { data, hasNextPage, fetchNextPage, isFetchingNextPage } =\n    useBookmarkSearch();\n\n  const { setInSearchPage } = useInSearchPageStore();\n\n  const { setSortOrder } = useSortOrderStore();\n\n  useEffect(() => {\n    // also see related cleanup code in SortOrderToggle.tsx\n    setSortOrder(\"relevance\");\n  }, []);\n\n  useEffect(() => {\n    setInSearchPage(true);\n    return () => setInSearchPage(false);\n  }, [setInSearchPage]);\n\n  return (\n    <div className=\"flex flex-col gap-3\">\n      {data ? (\n        <BookmarksGrid\n          hasNextPage={hasNextPage}\n          fetchNextPage={fetchNextPage}\n          isFetchingNextPage={isFetchingNextPage}\n          bookmarks={data.pages.flatMap((b) => b.bookmarks)}\n        />\n      ) : (\n        <BookmarksGridSkeleton />\n      )}\n    </div>\n  );\n}\n\nexport default function SearchPage() {\n  return (\n    <Suspense>\n      <SearchComp />\n    </Suspense>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/dashboard/tags/[tagId]/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport { notFound } from \"next/navigation\";\nimport Bookmarks from \"@/components/dashboard/bookmarks/Bookmarks\";\nimport EditableTagName from \"@/components/dashboard/tags/EditableTagName\";\nimport { TagOptions } from \"@/components/dashboard/tags/TagOptions\";\nimport { Button } from \"@/components/ui/button\";\nimport { api } from \"@/server/api/client\";\nimport { TRPCError } from \"@trpc/server\";\nimport { MoreHorizontal } from \"lucide-react\";\n\nexport async function generateMetadata(props: {\n  params: Promise<{ tagId: string }>;\n}): Promise<Metadata> {\n  const params = await props.params;\n  try {\n    const tag = await api.tags.get({ tagId: params.tagId });\n    return {\n      title: `${tag.name} | Karakeep`,\n    };\n  } catch (e) {\n    if (e instanceof TRPCError && e.code === \"NOT_FOUND\") {\n      notFound();\n    }\n    throw e;\n  }\n}\n\nexport default async function TagPage(props: {\n  params: Promise<{ tagId: string }>;\n  searchParams?: Promise<{\n    includeArchived?: string;\n  }>;\n}) {\n  const searchParams = await props.searchParams;\n  const params = await props.params;\n  let tag;\n  try {\n    tag = await api.tags.get({ tagId: params.tagId });\n  } catch (e) {\n    if (e instanceof TRPCError) {\n      if (e.code == \"NOT_FOUND\") {\n        notFound();\n      }\n    }\n    throw e;\n  }\n  const userSettings = await api.users.settings();\n\n  const includeArchived =\n    searchParams?.includeArchived !== undefined\n      ? searchParams.includeArchived === \"true\"\n      : userSettings.archiveDisplayBehaviour === \"show\";\n\n  return (\n    <Bookmarks\n      header={\n        <div className=\"flex justify-between\">\n          <EditableTagName\n            tag={{ id: tag.id, name: tag.name }}\n            className=\"text-2xl\"\n          />\n\n          <TagOptions tag={tag}>\n            <Button variant=\"ghost\">\n              <MoreHorizontal />\n            </Button>\n          </TagOptions>\n        </div>\n      }\n      query={{\n        tagId: tag.id,\n        archived: !includeArchived ? false : undefined,\n      }}\n      showEditorCard={true}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/web/app/dashboard/tags/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport AllTagsView from \"@/components/dashboard/tags/AllTagsView\";\nimport { useTranslation } from \"@/lib/i18n/server\";\n\nexport async function generateMetadata(): Promise<Metadata> {\n  // oxlint-disable-next-line rules-of-hooks\n  const { t } = await useTranslation();\n  return {\n    title: `${t(\"tags.all_tags\")} | Karakeep`,\n  };\n}\n\nexport default async function TagsPage() {\n  return <AllTagsView />;\n}\n"
  },
  {
    "path": "apps/web/app/forgot-password/page.tsx",
    "content": "import { redirect } from \"next/navigation\";\nimport KarakeepLogo from \"@/components/KarakeepIcon\";\nimport ForgotPasswordForm from \"@/components/signin/ForgotPasswordForm\";\nimport { getServerAuthSession } from \"@/server/auth\";\n\nexport default async function ForgotPasswordPage() {\n  const session = await getServerAuthSession();\n  if (session) {\n    redirect(\"/\");\n  }\n\n  return (\n    <div className=\"flex min-h-screen items-center justify-center bg-background px-4 py-12 sm:px-6 lg:px-8\">\n      <div className=\"w-full max-w-md space-y-8\">\n        <div className=\"flex items-center justify-center\">\n          <KarakeepLogo height={80} />\n        </div>\n        <ForgotPasswordForm />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/invite/[token]/page.tsx",
    "content": "import { redirect } from \"next/navigation\";\nimport InviteAcceptForm from \"@/components/invite/InviteAcceptForm\";\nimport KarakeepLogo from \"@/components/KarakeepIcon\";\nimport { getServerAuthSession } from \"@/server/auth\";\n\ninterface InvitePageProps {\n  params: {\n    token: string;\n  };\n}\n\nexport default async function InvitePage({ params }: InvitePageProps) {\n  const session = await getServerAuthSession();\n  if (session) {\n    redirect(\"/\");\n  }\n\n  return (\n    <div className=\"flex min-h-screen items-center justify-center bg-background px-4 py-12 sm:px-6 lg:px-8\">\n      <div className=\"w-full max-w-md space-y-8\">\n        <div className=\"flex items-center justify-center\">\n          <KarakeepLogo height={80} />\n        </div>\n        <InviteAcceptForm token={params.token} />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/layout.tsx",
    "content": "import type { Metadata } from \"next\";\nimport { Inter } from \"next/font/google\";\nimport { NuqsAdapter } from \"nuqs/adapters/next/app\";\n\nimport { loadAllPlugins } from \"@karakeep/shared-server\";\n\nimport \"@karakeep/tailwind-config/globals.css\";\n\nimport type { Viewport } from \"next\";\nimport React from \"react\";\nimport Providers from \"@/lib/providers\";\nimport { getUserLocalSettings } from \"@/lib/userLocalSettings/userLocalSettings\";\nimport { getServerAuthSession } from \"@/server/auth\";\nimport { ReactQueryDevtools } from \"@tanstack/react-query-devtools\";\nimport { Toaster } from \"sonner\";\n\nimport { clientConfig } from \"@karakeep/shared/config\";\n\nawait loadAllPlugins();\n\nconst inter = Inter({\n  subsets: [\"latin\"],\n  fallback: [\"sans-serif\"],\n});\n\nexport const metadata: Metadata = {\n  title: \"Karakeep\",\n  applicationName: \"Karakeep\",\n  description:\n    \"The Bookmark Everything app. Hoard links, notes, and images and they will get automatically tagged AI.\",\n  manifest: \"/manifest.json\",\n  appleWebApp: {\n    capable: true,\n    title: \"Karakeep\",\n  },\n  formatDetection: {\n    telephone: false,\n  },\n};\n\nexport const viewport: Viewport = {\n  width: \"device-width\",\n  initialScale: 1,\n  maximumScale: 1,\n  userScalable: false,\n};\n\nexport default async function RootLayout({\n  children,\n}: Readonly<{\n  children: React.ReactNode;\n}>) {\n  const session = await getServerAuthSession();\n  const userSettings = await getUserLocalSettings();\n  const isRTL = userSettings.lang === \"ar\";\n  return (\n    <html\n      lang={userSettings.lang}\n      dir={isRTL ? \"rtl\" : \"ltr\"}\n      suppressHydrationWarning\n    >\n      <body className={inter.className}>\n        <NuqsAdapter>\n          <Providers\n            session={session}\n            clientConfig={clientConfig}\n            userLocalSettings={await getUserLocalSettings()}\n          >\n            {children}\n            <ReactQueryDevtools initialIsOpen={false} />\n          </Providers>\n          <Toaster />\n        </NuqsAdapter>\n      </body>\n    </html>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/logout/page.tsx",
    "content": "\"use client\";\n\nimport { useEffect } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport { signOut } from \"@/lib/auth/client\";\n\nimport { useSearchHistory } from \"@karakeep/shared-react/hooks/search-history\";\n\nexport default function Logout() {\n  const router = useRouter();\n  const { clearHistory } = useSearchHistory({\n    getItem: (k: string) => localStorage.getItem(k),\n    setItem: (k: string, v: string) => localStorage.setItem(k, v),\n    removeItem: (k: string) => localStorage.removeItem(k),\n  });\n  useEffect(() => {\n    signOut({\n      redirect: false,\n      callbackUrl: \"/\",\n    }).then((d) => {\n      clearHistory();\n      router.push(d.url);\n    });\n  }, []);\n  return <span />;\n}\n"
  },
  {
    "path": "apps/web/app/page.tsx",
    "content": "import { redirect } from \"next/navigation\";\nimport { getServerAuthSession } from \"@/server/auth\";\n\nexport default async function Home() {\n  const session = await getServerAuthSession();\n  if (session) {\n    redirect(\"/dashboard/bookmarks\");\n  } else {\n    redirect(\"/signin\");\n  }\n}\n"
  },
  {
    "path": "apps/web/app/public/layout.tsx",
    "content": "export default function PublicLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return (\n    <div className=\"h-screen items-center justify-center overflow-y-auto bg-muted\">\n      <main className=\"container mt-3\">{children}</main>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/public/lists/[listId]/not-found.tsx",
    "content": "import { X } from \"lucide-react\";\n\nexport default function PublicListPageNotFound() {\n  return (\n    <div className=\"mx-auto flex max-w-md flex-1 flex-col items-center justify-center px-4 py-16 text-center\">\n      <div className=\"mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-slate-100 dark:bg-slate-700\">\n        <X className=\"h-12 w-12 text-gray-300\" strokeWidth={1.5} />\n      </div>\n      <h1 className=\"mb-3 text-2xl font-semibold text-gray-800\">\n        List not found\n      </h1>\n      <p className=\"text-center text-gray-500\">\n        The list you&apos;re looking for doesn&apos;t exist or may have been\n        removed.\n      </p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/public/lists/[listId]/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport { notFound } from \"next/navigation\";\nimport NoBookmarksBanner from \"@/components/dashboard/bookmarks/NoBookmarksBanner\";\nimport PublicBookmarkGrid from \"@/components/public/lists/PublicBookmarkGrid\";\nimport PublicListHeader from \"@/components/public/lists/PublicListHeader\";\nimport { api } from \"@/server/api/client\";\nimport { TRPCError } from \"@trpc/server\";\n\nexport async function generateMetadata(props: {\n  params: Promise<{ listId: string }>;\n}): Promise<Metadata> {\n  const params = await props.params;\n  try {\n    const resp = await api.publicBookmarks.getPublicListMetadata({\n      listId: params.listId,\n    });\n    return {\n      title: `${resp.name} by ${resp.ownerName} - Karakeep`,\n      description:\n        resp.description && resp.description.length > 0\n          ? `${resp.description} by ${resp.ownerName} on Karakeep`\n          : undefined,\n      applicationName: \"Karakeep\",\n      authors: [\n        {\n          name: resp.ownerName,\n        },\n      ],\n    };\n  } catch (e) {\n    if (e instanceof TRPCError && e.code === \"NOT_FOUND\") {\n      notFound();\n    }\n  }\n  return {\n    title: \"Karakeep\",\n  };\n}\n\nexport default async function PublicListPage(props: {\n  params: Promise<{ listId: string }>;\n}) {\n  const params = await props.params;\n  try {\n    const { list, bookmarks, nextCursor } =\n      await api.publicBookmarks.getPublicBookmarksInList({\n        listId: params.listId,\n      });\n    return (\n      <div className=\"space-y-3\">\n        <PublicListHeader\n          list={{\n            id: params.listId,\n            name: list.name,\n            description: list.description,\n            icon: list.icon,\n            numItems: list.numItems,\n            ownerName: list.ownerName,\n          }}\n        />\n        {list.numItems > 0 ? (\n          <PublicBookmarkGrid\n            list={{\n              id: params.listId,\n              name: list.name,\n              description: list.description,\n              icon: list.icon,\n              numItems: list.numItems,\n              ownerName: list.ownerName,\n            }}\n            bookmarks={bookmarks}\n            nextCursor={nextCursor}\n          />\n        ) : (\n          <NoBookmarksBanner />\n        )}\n      </div>\n    );\n  } catch (e) {\n    if (e instanceof TRPCError && e.code === \"NOT_FOUND\") {\n      notFound();\n    }\n  }\n}\n"
  },
  {
    "path": "apps/web/app/reader/[bookmarkId]/page.tsx",
    "content": "\"use client\";\n\nimport { Suspense, useState } from \"react\";\nimport { useParams, useRouter } from \"next/navigation\";\nimport HighlightCard from \"@/components/dashboard/highlights/HighlightCard\";\nimport ReaderSettingsPopover from \"@/components/dashboard/preview/ReaderSettingsPopover\";\nimport ReaderView from \"@/components/dashboard/preview/ReaderView\";\nimport { Button } from \"@/components/ui/button\";\nimport { FullPageSpinner } from \"@/components/ui/full-page-spinner\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { useSession } from \"@/lib/auth/client\";\nimport { useReaderSettings } from \"@/lib/readerSettings\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { HighlighterIcon as Highlight, Printer, X } from \"lucide-react\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\nimport { BookmarkTypes } from \"@karakeep/shared/types/bookmarks\";\nimport { READER_FONT_FAMILIES } from \"@karakeep/shared/types/readers\";\nimport { getBookmarkTitle } from \"@karakeep/shared/utils/bookmarkUtils\";\n\nexport default function ReaderViewPage() {\n  const api = useTRPC();\n  const params = useParams<{ bookmarkId: string }>();\n  const bookmarkId = params.bookmarkId;\n  const { data: highlights } = useQuery(\n    api.highlights.getForBookmark.queryOptions({\n      bookmarkId,\n    }),\n  );\n  const { data: bookmark } = useQuery(\n    api.bookmarks.getBookmark.queryOptions({\n      bookmarkId,\n    }),\n  );\n\n  const { data: session } = useSession();\n  const router = useRouter();\n  const { settings } = useReaderSettings();\n  const [showHighlights, setShowHighlights] = useState(false);\n  const isOwner = session?.user?.id === bookmark?.userId;\n\n  const onClose = () => {\n    if (window.history.length > 1) {\n      router.back();\n    } else {\n      router.push(\"/dashboard\");\n    }\n  };\n\n  const handlePrint = () => {\n    window.print();\n  };\n\n  return (\n    <div className=\"min-h-screen bg-background\">\n      {/* Header */}\n      <header className=\"sticky top-0 z-40 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 print:hidden\">\n        <div className=\"flex h-14 items-center justify-between px-4\">\n          <div className=\"flex items-center gap-2\">\n            <Button variant=\"ghost\" size=\"icon\" onClick={onClose}>\n              <X className=\"h-4 w-4\" />\n            </Button>\n            <span className=\"text-sm text-muted-foreground\">Reader View</span>\n          </div>\n\n          <div className=\"flex items-center gap-2\">\n            <Button variant=\"ghost\" size=\"icon\" onClick={handlePrint}>\n              <Printer className=\"h-4 w-4\" />\n            </Button>\n\n            <ReaderSettingsPopover variant=\"ghost\" />\n\n            <Button\n              variant={showHighlights ? \"default\" : \"ghost\"}\n              size=\"icon\"\n              onClick={() => setShowHighlights(!showHighlights)}\n            >\n              <Highlight className=\"h-4 w-4\" />\n            </Button>\n          </div>\n        </div>\n      </header>\n\n      <div className=\"flex overflow-hidden\">\n        {/* Mobile backdrop */}\n        {showHighlights && (\n          <button\n            className=\"fixed inset-0 top-14 z-40 bg-black/50 lg:hidden\"\n            onClick={() => setShowHighlights(false)}\n            onKeyDown={(e) => {\n              if (e.key === \"Escape\") {\n                setShowHighlights(false);\n              }\n            }}\n            aria-label=\"Close highlights sidebar\"\n          />\n        )}\n\n        {/* Main Content */}\n        <main\n          className={`flex-1 overflow-x-hidden transition-all duration-300 ${showHighlights ? \"lg:mr-80\" : \"\"}`}\n        >\n          <article className=\"mx-auto max-w-3xl overflow-x-hidden px-4 py-8 sm:px-6\">\n            {bookmark ? (\n              <>\n                {/* Article Header */}\n                <header className=\"mb-8 space-y-4\">\n                  <h1\n                    className=\"font-bold leading-tight\"\n                    style={{\n                      fontFamily: READER_FONT_FAMILIES[settings.fontFamily],\n                      fontSize: `${settings.fontSize * 1.8}px`,\n                      lineHeight: settings.lineHeight * 0.9,\n                    }}\n                  >\n                    {getBookmarkTitle(bookmark)}\n                  </h1>\n                  <div className=\"flex items-center gap-4 text-sm text-muted-foreground\">\n                    {bookmark.content.type == BookmarkTypes.LINK && (\n                      <span>By {bookmark.content.author}</span>\n                    )}\n                    <Separator orientation=\"vertical\" className=\"h-4\" />\n                    <span>8 min</span>\n                  </div>\n                </header>\n\n                {/* Article Content */}\n                <Suspense fallback={<FullPageSpinner />}>\n                  <div className=\"overflow-x-hidden\">\n                    <ReaderView\n                      style={{\n                        fontFamily: READER_FONT_FAMILIES[settings.fontFamily],\n                        fontSize: `${settings.fontSize}px`,\n                        lineHeight: settings.lineHeight,\n                      }}\n                      bookmarkId={bookmarkId}\n                      readOnly={!isOwner}\n                      progressBarStyle={{ position: \"fixed\", top: \"3.5rem\" }}\n                    />\n                  </div>\n                </Suspense>\n              </>\n            ) : (\n              <FullPageSpinner />\n            )}\n          </article>\n        </main>\n\n        {/* Highlights Sidebar */}\n        {showHighlights && highlights && (\n          <aside className=\"fixed right-0 top-14 z-50 h-[calc(100vh-3.5rem)] w-full border-l bg-background sm:w-80 lg:z-auto lg:bg-background/95 lg:backdrop-blur lg:supports-[backdrop-filter]:bg-background/60 print:hidden\">\n            <div className=\"flex h-full flex-col\">\n              <div className=\"border-b p-4\">\n                <div className=\"flex items-center justify-between\">\n                  <h2 className=\"font-semibold\">Highlights</h2>\n                  <div className=\"flex items-center gap-2\">\n                    <span className=\"text-sm text-muted-foreground\">\n                      {highlights.highlights.length} saved\n                    </span>\n                    <Button\n                      variant=\"ghost\"\n                      size=\"icon\"\n                      className=\"h-6 w-6 lg:hidden\"\n                      onClick={() => setShowHighlights(false)}\n                    >\n                      <X className=\"h-4 w-4\" />\n                    </Button>\n                  </div>\n                </div>\n              </div>\n\n              <div className=\"flex-1 overflow-auto p-4\">\n                <div className=\"space-y-4\">\n                  {highlights.highlights.map((highlight) => (\n                    <HighlightCard\n                      key={highlight.id}\n                      highlight={highlight}\n                      clickable={true}\n                      readOnly={!isOwner}\n                    />\n                  ))}\n                </div>\n              </div>\n            </div>\n          </aside>\n        )}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/reader/layout.tsx",
    "content": "import { redirect } from \"next/navigation\";\nimport { ReaderSettingsProvider } from \"@/lib/readerSettings\";\nimport { UserSettingsContextProvider } from \"@/lib/userSettings\";\nimport { api } from \"@/server/api/client\";\nimport { getServerAuthSession } from \"@/server/auth\";\nimport { TRPCError } from \"@trpc/server\";\n\nimport { tryCatch } from \"@karakeep/shared/tryCatch\";\n\nexport default async function ReaderLayout({\n  children,\n}: Readonly<{\n  children: React.ReactNode;\n}>) {\n  const session = await getServerAuthSession();\n  if (!session) {\n    redirect(\"/\");\n  }\n\n  const userSettings = await tryCatch(api.users.settings());\n\n  if (userSettings.error) {\n    if (userSettings.error instanceof TRPCError) {\n      if (\n        userSettings.error.code === \"NOT_FOUND\" ||\n        userSettings.error.code === \"UNAUTHORIZED\"\n      ) {\n        redirect(\"/logout\");\n      }\n    }\n    throw userSettings.error;\n  }\n\n  return (\n    <UserSettingsContextProvider userSettings={userSettings.data}>\n      <ReaderSettingsProvider>{children}</ReaderSettingsProvider>\n    </UserSettingsContextProvider>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/reset-password/page.tsx",
    "content": "import { redirect } from \"next/navigation\";\nimport KarakeepLogo from \"@/components/KarakeepIcon\";\nimport ResetPasswordForm from \"@/components/signin/ResetPasswordForm\";\nimport { getServerAuthSession } from \"@/server/auth\";\n\ninterface ResetPasswordPageProps {\n  searchParams: {\n    token?: string;\n  };\n}\n\nexport default async function ResetPasswordPage({\n  searchParams,\n}: ResetPasswordPageProps) {\n  const session = await getServerAuthSession();\n  if (session) {\n    redirect(\"/\");\n  }\n\n  const { token } = searchParams;\n\n  if (!token) {\n    redirect(\"/signin\");\n  }\n\n  return (\n    <div className=\"flex min-h-screen items-center justify-center bg-background px-4 py-12 sm:px-6 lg:px-8\">\n      <div className=\"w-full max-w-md space-y-8\">\n        <div className=\"flex items-center justify-center\">\n          <KarakeepLogo height={80} />\n        </div>\n        <ResetPasswordForm token={token} />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/settings/ai/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport AISettings from \"@/components/settings/AISettings\";\nimport { useTranslation } from \"@/lib/i18n/server\";\n\nexport async function generateMetadata(): Promise<Metadata> {\n  // oxlint-disable-next-line rules-of-hooks\n  const { t } = await useTranslation();\n  return {\n    title: `${t(\"settings.ai.ai_settings\")} | Karakeep`,\n  };\n}\n\nexport default function AISettingsPage() {\n  return <AISettings />;\n}\n"
  },
  {
    "path": "apps/web/app/settings/api-keys/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport AddApiKey from \"@/components/settings/AddApiKey\";\nimport ApiKeySettings from \"@/components/settings/ApiKeySettings\";\nimport { SettingsPage } from \"@/components/settings/SettingsPage\";\nimport { useTranslation } from \"@/lib/i18n/server\";\n\nexport async function generateMetadata(): Promise<Metadata> {\n  // oxlint-disable-next-line rules-of-hooks\n  const { t } = await useTranslation();\n  return {\n    title: `${t(\"settings.api_keys.api_keys\")} | Karakeep`,\n  };\n}\n\nexport default async function ApiKeysPage() {\n  // oxlint-disable-next-line rules-of-hooks\n  const { t } = await useTranslation();\n  return (\n    <SettingsPage\n      title={t(\"settings.api_keys.api_keys\")}\n      action={<AddApiKey />}\n    >\n      <ApiKeySettings />\n    </SettingsPage>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/settings/assets/layout.tsx",
    "content": "import type { Metadata } from \"next\";\nimport { useTranslation } from \"@/lib/i18n/server\";\n\nexport async function generateMetadata(): Promise<Metadata> {\n  // oxlint-disable-next-line rules-of-hooks\n  const { t } = await useTranslation();\n  return {\n    title: `${t(\"settings.manage_assets.manage_assets\")} | Karakeep`,\n  };\n}\n\nexport default function AssetsLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return <>{children}</>;\n}\n"
  },
  {
    "path": "apps/web/app/settings/assets/page.tsx",
    "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport {\n  SettingsPage,\n  SettingsSection,\n} from \"@/components/settings/SettingsPage\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport ActionConfirmingDialog from \"@/components/ui/action-confirming-dialog\";\nimport { Button } from \"@/components/ui/button\";\nimport { FullPageSpinner } from \"@/components/ui/full-page-spinner\";\nimport { toast } from \"@/components/ui/sonner\";\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@/components/ui/table\";\nimport { ASSET_TYPE_TO_ICON } from \"@/lib/attachments\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { formatBytes } from \"@/lib/utils\";\nimport { useInfiniteQuery } from \"@tanstack/react-query\";\nimport { ExternalLink, Trash2 } from \"lucide-react\";\n\nimport { useDetachBookmarkAsset } from \"@karakeep/shared-react/hooks/assets\";\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\nimport { getAssetUrl } from \"@karakeep/shared/utils/assetUtils\";\nimport {\n  humanFriendlyNameForAssertType,\n  isAllowedToDetachAsset,\n} from \"@karakeep/trpc/lib/attachments\";\n\nexport default function AssetsSettingsPage() {\n  const api = useTRPC();\n  const { t } = useTranslation();\n  const { mutate: detachAsset, isPending: isDetaching } =\n    useDetachBookmarkAsset({\n      onSuccess: () => {\n        toast({\n          description: \"Asset has been deleted!\",\n        });\n      },\n      onError: (e) => {\n        toast({\n          description: e.message,\n          variant: \"destructive\",\n        });\n      },\n    });\n  const {\n    data,\n    isLoading: isAssetsLoading,\n    fetchNextPage,\n    hasNextPage,\n    isFetchingNextPage,\n  } = useInfiniteQuery(\n    api.assets.list.infiniteQueryOptions(\n      {\n        limit: 20,\n      },\n      {\n        getNextPageParam: (lastPage) => lastPage.nextCursor,\n      },\n    ),\n  );\n\n  const assets = data?.pages.flatMap((page) => page.assets) ?? [];\n\n  if (isAssetsLoading) {\n    return <FullPageSpinner />;\n  }\n\n  return (\n    <SettingsPage title={t(\"settings.manage_assets.manage_assets\")}>\n      <SettingsSection>\n        {assets.length === 0 && (\n          <p className=\"rounded-md bg-muted p-3 text-center text-sm text-muted-foreground\">\n            {t(\"settings.manage_assets.no_assets\")}\n          </p>\n        )}\n        {assets.length > 0 && (\n          <Table>\n            <TableHeader>\n              <TableRow>\n                <TableHead>{t(\"settings.manage_assets.asset_type\")}</TableHead>\n                <TableHead>{t(\"common.size\")}</TableHead>\n                <TableHead>{t(\"settings.manage_assets.asset_link\")}</TableHead>\n                <TableHead>\n                  {t(\"settings.manage_assets.bookmark_link\")}\n                </TableHead>\n                <TableHead>{t(\"common.actions\")}</TableHead>\n              </TableRow>\n            </TableHeader>\n            <TableBody>\n              {assets.map((asset) => (\n                <TableRow key={asset.id}>\n                  <TableCell className=\"flex items-center gap-2\">\n                    {ASSET_TYPE_TO_ICON[asset.assetType]}\n                    <span>\n                      {humanFriendlyNameForAssertType(asset.assetType)}\n                    </span>\n                  </TableCell>\n                  <TableCell>{formatBytes(asset.size)}</TableCell>\n                  <TableCell>\n                    <Link\n                      href={getAssetUrl(asset.id)}\n                      target=\"_blank\"\n                      rel=\"noopener noreferrer\"\n                      className=\"flex items-center gap-1\"\n                      prefetch={false}\n                    >\n                      <ExternalLink className=\"size-4\" />\n                      <span>View Asset</span>\n                    </Link>\n                  </TableCell>\n                  <TableCell>\n                    {asset.bookmarkId ? (\n                      <Link\n                        href={`/dashboard/preview/${asset.bookmarkId}`}\n                        className=\"flex items-center gap-1\"\n                        prefetch={false}\n                      >\n                        <ExternalLink className=\"size-4\" />\n                        <span>View Bookmark</span>\n                      </Link>\n                    ) : (\n                      <span className=\"text-muted-foreground\">No bookmark</span>\n                    )}\n                  </TableCell>\n                  <TableCell>\n                    {isAllowedToDetachAsset(asset.assetType) &&\n                      asset.bookmarkId && (\n                        <ActionConfirmingDialog\n                          title={t(\"settings.manage_assets.delete_asset\")}\n                          description={t(\n                            \"settings.manage_assets.delete_asset_confirmation\",\n                          )}\n                          actionButton={(setDialogOpen) => (\n                            <ActionButton\n                              loading={isDetaching}\n                              variant=\"destructive\"\n                              onClick={() =>\n                                detachAsset(\n                                  {\n                                    bookmarkId: asset.bookmarkId!,\n                                    assetId: asset.id,\n                                  },\n                                  { onSettled: () => setDialogOpen(false) },\n                                )\n                              }\n                            >\n                              <Trash2 className=\"mr-2 size-4\" />\n                              {t(\"actions.delete\")}\n                            </ActionButton>\n                          )}\n                        >\n                          <Button\n                            variant=\"destructive\"\n                            size=\"sm\"\n                            title=\"Delete\"\n                          >\n                            <Trash2 className=\"size-4\" />\n                          </Button>\n                        </ActionConfirmingDialog>\n                      )}\n                  </TableCell>\n                </TableRow>\n              ))}\n            </TableBody>\n          </Table>\n        )}\n        {hasNextPage && (\n          <div className=\"flex justify-center\">\n            <ActionButton\n              variant=\"secondary\"\n              onClick={() => fetchNextPage()}\n              loading={isFetchingNextPage}\n              ignoreDemoMode={true}\n            >\n              Load More\n            </ActionButton>\n          </div>\n        )}\n      </SettingsSection>\n    </SettingsPage>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/settings/backups/page.tsx",
    "content": "\"use client\";\n\nimport BackupSettings from \"@/components/settings/BackupSettings\";\nimport { SettingsPage } from \"@/components/settings/SettingsPage\";\nimport { useTranslation } from \"@/lib/i18n/client\";\n\nexport default function BackupsPage() {\n  const { t } = useTranslation();\n  return (\n    <SettingsPage\n      title={t(\"settings.backups.page_title\")}\n      description={t(\"settings.backups.page_description\")}\n    >\n      <BackupSettings />\n    </SettingsPage>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/settings/broken-links/layout.tsx",
    "content": "import type { Metadata } from \"next\";\nimport { useTranslation } from \"@/lib/i18n/server\";\n\nexport async function generateMetadata(): Promise<Metadata> {\n  // oxlint-disable-next-line rules-of-hooks\n  const { t } = await useTranslation();\n  return {\n    title: `${t(\"settings.broken_links.broken_links\")} | Karakeep`,\n  };\n}\n\nexport default function BrokenLinksLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return <>{children}</>;\n}\n"
  },
  {
    "path": "apps/web/app/settings/broken-links/page.tsx",
    "content": "\"use client\";\n\nimport {\n  SettingsPage,\n  SettingsSection,\n} from \"@/components/settings/SettingsPage\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport { FullPageSpinner } from \"@/components/ui/full-page-spinner\";\nimport { toast } from \"@/components/ui/sonner\";\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@/components/ui/table\";\nimport { useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { RefreshCw, Trash2 } from \"lucide-react\";\nimport { useTranslation } from \"react-i18next\";\n\nimport {\n  useDeleteBookmark,\n  useRecrawlBookmark,\n} from \"@karakeep/shared-react/hooks/bookmarks\";\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\nexport default function BrokenLinksPage() {\n  const api = useTRPC();\n  const { t } = useTranslation();\n\n  const queryClient = useQueryClient();\n  const { data, isPending } = useQuery(\n    api.bookmarks.getBrokenLinks.queryOptions(),\n  );\n\n  const { mutate: deleteBookmark, isPending: isDeleting } = useDeleteBookmark({\n    onSuccess: () => {\n      toast({\n        description: t(\"toasts.bookmarks.deleted\"),\n      });\n      queryClient.invalidateQueries(api.bookmarks.getBrokenLinks.pathFilter());\n    },\n    onError: () => {\n      toast({\n        description: t(\"common.something_went_wrong\"),\n        variant: \"destructive\",\n      });\n    },\n  });\n\n  const { mutate: recrawlBookmark, isPending: isRecrawling } =\n    useRecrawlBookmark({\n      onSuccess: () => {\n        toast({\n          description: t(\"toasts.bookmarks.refetch\"),\n        });\n        queryClient.invalidateQueries(\n          api.bookmarks.getBrokenLinks.pathFilter(),\n        );\n      },\n      onError: () => {\n        toast({\n          description: t(\"common.something_went_wrong\"),\n          variant: \"destructive\",\n        });\n      },\n    });\n\n  return (\n    <SettingsPage title={t(\"settings.broken_links.broken_links\")}>\n      <SettingsSection>\n        {isPending && <FullPageSpinner />}\n        {!isPending && data && data.bookmarks.length == 0 && (\n          <p className=\"rounded-md bg-muted p-3 text-center text-sm text-muted-foreground\">\n            No broken links found\n          </p>\n        )}\n        {!isPending && data && data.bookmarks.length > 0 && (\n          <Table>\n            <TableHeader>\n              <TableRow>\n                <TableHead>{t(\"common.url\")}</TableHead>\n                <TableHead>{t(\"common.created_at\")}</TableHead>\n                <TableHead>\n                  {t(\"settings.broken_links.last_crawled_at\")}\n                </TableHead>\n                <TableHead>\n                  {t(\"settings.broken_links.crawling_status\")}\n                </TableHead>\n                <TableHead>{t(\"common.action\")}</TableHead>\n              </TableRow>\n            </TableHeader>\n            <TableBody>\n              {data.bookmarks.map((b) => (\n                <TableRow key={b.id}>\n                  <TableCell>{b.url}</TableCell>\n                  <TableCell>{b.createdAt?.toLocaleString()}</TableCell>\n                  <TableCell>{b.crawledAt?.toLocaleString()}</TableCell>\n                  <TableCell>\n                    {b.isCrawlingFailure ? (\n                      <span className=\"text-red-500\">Failed</span>\n                    ) : (\n                      b.statusCode\n                    )}\n                  </TableCell>\n                  <TableCell className=\"flex gap-2\">\n                    <ActionButton\n                      variant=\"secondary\"\n                      loading={isRecrawling}\n                      onClick={() => recrawlBookmark({ bookmarkId: b.id })}\n                      className=\"flex items-center gap-2\"\n                    >\n                      <RefreshCw className=\"size-4\" />\n                      {t(\"actions.recrawl\")}\n                    </ActionButton>\n                    <ActionButton\n                      variant=\"ghostDestructive\"\n                      onClick={() => deleteBookmark({ bookmarkId: b.id })}\n                      loading={isDeleting}\n                      className=\"flex items-center gap-2\"\n                    >\n                      <Trash2 className=\"size-4\" />\n                    </ActionButton>\n                  </TableCell>\n                </TableRow>\n              ))}\n              <TableRow></TableRow>\n            </TableBody>\n          </Table>\n        )}\n      </SettingsSection>\n    </SettingsPage>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/settings/feeds/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport FeedSettings from \"@/components/settings/FeedSettings\";\nimport { useTranslation } from \"@/lib/i18n/server\";\n\nexport async function generateMetadata(): Promise<Metadata> {\n  // oxlint-disable-next-line rules-of-hooks\n  const { t } = await useTranslation();\n  return {\n    title: `${t(\"settings.feeds.rss_subscriptions\")} | Karakeep`,\n  };\n}\n\nexport default function FeedSettingsPage() {\n  return <FeedSettings />;\n}\n"
  },
  {
    "path": "apps/web/app/settings/import/[sessionId]/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport ImportSessionDetail from \"@/components/settings/ImportSessionDetail\";\nimport { useTranslation } from \"@/lib/i18n/server\";\n\nexport async function generateMetadata(): Promise<Metadata> {\n  // oxlint-disable-next-line rules-of-hooks\n  const { t } = await useTranslation();\n  return {\n    title: `${t(\"settings.import_sessions.detail.page_title\")} | Karakeep`,\n  };\n}\n\nexport default async function ImportSessionDetailPage({\n  params,\n}: {\n  params: Promise<{ sessionId: string }>;\n}) {\n  const { sessionId } = await params;\n  return <ImportSessionDetail sessionId={sessionId} />;\n}\n"
  },
  {
    "path": "apps/web/app/settings/import/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport ImportExport from \"@/components/settings/ImportExport\";\nimport { useTranslation } from \"@/lib/i18n/server\";\n\nexport async function generateMetadata(): Promise<Metadata> {\n  // oxlint-disable-next-line rules-of-hooks\n  const { t } = await useTranslation();\n  return {\n    title: `${t(\"settings.import.import_export\")} | Karakeep`,\n  };\n}\n\nexport default function ImportSettingsPage() {\n  return <ImportExport />;\n}\n"
  },
  {
    "path": "apps/web/app/settings/info/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport { ChangePassword } from \"@/components/settings/ChangePassword\";\nimport { DeleteAccount } from \"@/components/settings/DeleteAccount\";\nimport ReaderSettings from \"@/components/settings/ReaderSettings\";\nimport { SettingsPage } from \"@/components/settings/SettingsPage\";\nimport UserAvatar from \"@/components/settings/UserAvatar\";\nimport UserDetails from \"@/components/settings/UserDetails\";\nimport UserOptions from \"@/components/settings/UserOptions\";\nimport { useTranslation } from \"@/lib/i18n/server\";\n\nexport async function generateMetadata(): Promise<Metadata> {\n  // oxlint-disable-next-line rules-of-hooks\n  const { t } = await useTranslation();\n  return {\n    title: `${t(\"settings.info.user_info\")} | Karakeep`,\n  };\n}\n\nexport default async function InfoPage() {\n  // oxlint-disable-next-line rules-of-hooks\n  const { t } = await useTranslation();\n  return (\n    <SettingsPage title={t(\"settings.info.user_info\")}>\n      <UserAvatar />\n      <UserDetails />\n      <ChangePassword />\n      <UserOptions />\n      <ReaderSettings />\n      <DeleteAccount />\n    </SettingsPage>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/settings/layout.tsx",
    "content": "import { redirect } from \"next/navigation\";\nimport MobileSidebar from \"@/components/shared/sidebar/MobileSidebar\";\nimport Sidebar from \"@/components/shared/sidebar/Sidebar\";\nimport SidebarLayout from \"@/components/shared/sidebar/SidebarLayout\";\nimport { ReaderSettingsProvider } from \"@/lib/readerSettings\";\nimport { UserSettingsContextProvider } from \"@/lib/userSettings\";\nimport { api } from \"@/server/api/client\";\nimport { getServerAuthSession } from \"@/server/auth\";\nimport { TRPCError } from \"@trpc/server\";\nimport { TFunction } from \"i18next\";\nimport {\n  ArrowLeft,\n  BarChart3,\n  CloudDownload,\n  CreditCard,\n  Download,\n  GitBranch,\n  Image,\n  KeyRound,\n  Link,\n  Rss,\n  Sparkles,\n  User,\n  Webhook,\n} from \"lucide-react\";\n\nimport serverConfig from \"@karakeep/shared/config\";\nimport { tryCatch } from \"@karakeep/shared/tryCatch\";\n\nconst settingsSidebarItems = (\n  t: TFunction,\n): {\n  name: string;\n  icon: React.ReactElement;\n  path: string;\n}[] => {\n  return [\n    {\n      name: t(\"settings.back_to_app\"),\n      icon: <ArrowLeft size={18} />,\n      path: \"/dashboard/bookmarks\",\n    },\n    {\n      name: t(\"settings.info.user_info\"),\n      icon: <User size={18} />,\n      path: \"/settings/info\",\n    },\n    {\n      name: t(\"settings.stats.usage_statistics\"),\n      icon: <BarChart3 size={18} />,\n      path: \"/settings/stats\",\n    },\n    ...(serverConfig.stripe.isConfigured\n      ? [\n          {\n            name: t(\"settings.subscription.subscription\"),\n            icon: <CreditCard size={18} />,\n            path: \"/settings/subscription\",\n          },\n        ]\n      : []),\n    ...(serverConfig.inference.isConfigured\n      ? [\n          {\n            name: t(\"settings.ai.ai_settings\"),\n            icon: <Sparkles size={18} />,\n            path: \"/settings/ai\",\n          },\n        ]\n      : []),\n    {\n      name: t(\"settings.feeds.rss_subscriptions\"),\n      icon: <Rss size={18} />,\n      path: \"/settings/feeds\",\n    },\n    {\n      name: t(\"settings.backups.backups\"),\n      icon: <CloudDownload size={18} />,\n      path: \"/settings/backups\",\n    },\n    {\n      name: t(\"settings.import.import_export\"),\n      icon: <Download size={18} />,\n      path: \"/settings/import\",\n    },\n    {\n      name: t(\"settings.api_keys.api_keys\"),\n      icon: <KeyRound size={18} />,\n      path: \"/settings/api-keys\",\n    },\n    {\n      name: t(\"settings.broken_links.broken_links\"),\n      icon: <Link size={18} />,\n      path: \"/settings/broken-links\",\n    },\n    {\n      name: t(\"settings.webhooks.webhooks\"),\n      icon: <Webhook size={18} />,\n      path: \"/settings/webhooks\",\n    },\n    {\n      name: t(\"settings.rules.rules\"),\n      icon: <GitBranch size={18} />,\n      path: \"/settings/rules\",\n    },\n    {\n      name: t(\"settings.manage_assets.manage_assets\"),\n      icon: <Image size={18} />,\n      path: \"/settings/assets\",\n    },\n  ];\n};\n\nexport default async function SettingsLayout({\n  children,\n}: Readonly<{\n  children: React.ReactNode;\n}>) {\n  const session = await getServerAuthSession();\n  if (!session) {\n    redirect(\"/\");\n  }\n\n  const userSettings = await tryCatch(api.users.settings());\n\n  if (userSettings.error) {\n    if (userSettings.error instanceof TRPCError) {\n      if (\n        userSettings.error.code === \"NOT_FOUND\" ||\n        userSettings.error.code === \"UNAUTHORIZED\"\n      ) {\n        redirect(\"/logout\");\n      }\n    }\n    throw userSettings.error;\n  }\n\n  return (\n    <UserSettingsContextProvider userSettings={userSettings.data}>\n      <ReaderSettingsProvider>\n        <SidebarLayout\n          sidebar={<Sidebar items={settingsSidebarItems} />}\n          mobileSidebar={<MobileSidebar items={settingsSidebarItems} />}\n        >\n          {children}\n        </SidebarLayout>\n      </ReaderSettingsProvider>\n    </UserSettingsContextProvider>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/settings/page.tsx",
    "content": "import { redirect } from \"next/navigation\";\n\nexport default function SettingsHomepage() {\n  redirect(\"/settings/info\");\n  return null;\n}\n"
  },
  {
    "path": "apps/web/app/settings/rules/layout.tsx",
    "content": "import type { Metadata } from \"next\";\nimport { useTranslation } from \"@/lib/i18n/server\";\n\nexport async function generateMetadata(): Promise<Metadata> {\n  // oxlint-disable-next-line rules-of-hooks\n  const { t } = await useTranslation();\n  return {\n    title: `${t(\"settings.rules.rules\")} | Karakeep`,\n  };\n}\n\nexport default function RulesLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return <>{children}</>;\n}\n"
  },
  {
    "path": "apps/web/app/settings/rules/page.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { RuleEditor } from \"@/components/dashboard/rules/RuleEngineRuleEditor\";\nimport RuleList from \"@/components/dashboard/rules/RuleEngineRuleList\";\nimport {\n  SettingsPage,\n  SettingsSection,\n} from \"@/components/settings/SettingsPage\";\nimport { Button } from \"@/components/ui/button\";\nimport { FullPageSpinner } from \"@/components/ui/full-page-spinner\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { PlusCircle } from \"lucide-react\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\nimport { RuleEngineRule } from \"@karakeep/shared/types/rules\";\n\nexport default function RulesSettingsPage() {\n  const api = useTRPC();\n  const { t } = useTranslation();\n  const [editingRule, setEditingRule] = useState<\n    (Omit<RuleEngineRule, \"id\"> & { id: string | null }) | null\n  >(null);\n\n  const { data: rules, isLoading } = useQuery(\n    api.rules.list.queryOptions(undefined, {\n      refetchOnWindowFocus: true,\n      refetchOnMount: true,\n    }),\n  );\n\n  const handleCreateRule = () => {\n    const newRule = {\n      id: null,\n      name: \"New Rule\",\n      description: \"Description of the new rule\",\n      enabled: true,\n      event: { type: \"bookmarkAdded\" as const },\n      condition: { type: \"alwaysTrue\" as const },\n      actions: [{ type: \"addTag\" as const, tagId: \"\" }],\n    };\n    setEditingRule(newRule);\n  };\n\n  const handleDeleteRule = (ruleId: string) => {\n    if (editingRule?.id === ruleId) {\n      // If the rule being edited is being deleted, reset the editing rule\n      setEditingRule(null);\n    }\n  };\n\n  return (\n    <SettingsPage\n      title={t(\"settings.rules.rules\")}\n      description={t(\"settings.rules.description\")}\n      action={\n        <Button onClick={handleCreateRule} variant=\"default\">\n          <PlusCircle className=\"mr-2 h-4 w-4\" />\n          {t(\"settings.rules.ceate_rule\")}\n        </Button>\n      }\n    >\n      <SettingsSection>\n        {!rules || isLoading ? (\n          <FullPageSpinner />\n        ) : (\n          <RuleList\n            rules={rules.rules}\n            onEditRule={(r) => setEditingRule(r)}\n            onDeleteRule={handleDeleteRule}\n          />\n        )}\n        <div className=\"lg:col-span-7\">\n          {editingRule && (\n            <RuleEditor\n              rule={editingRule}\n              onCancel={() => setEditingRule(null)}\n            />\n          )}\n        </div>\n      </SettingsSection>\n    </SettingsPage>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/settings/stats/layout.tsx",
    "content": "import type { Metadata } from \"next\";\nimport { useTranslation } from \"@/lib/i18n/server\";\n\nexport async function generateMetadata(): Promise<Metadata> {\n  // oxlint-disable-next-line rules-of-hooks\n  const { t } = await useTranslation();\n  return {\n    title: `${t(\"settings.stats.usage_statistics\")} | Karakeep`,\n  };\n}\n\nexport default function StatsLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return <>{children}</>;\n}\n"
  },
  {
    "path": "apps/web/app/settings/stats/page.tsx",
    "content": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport { SettingsPage } from \"@/components/settings/SettingsPage\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { Progress } from \"@/components/ui/progress\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport {\n  Archive,\n  BarChart3,\n  BookOpen,\n  Chrome,\n  Clock,\n  Code,\n  Database,\n  FileText,\n  Globe,\n  Hash,\n  Heart,\n  HelpCircle,\n  Highlighter,\n  Image,\n  Link,\n  List,\n  Rss,\n  Smartphone,\n  TrendingUp,\n  Upload,\n  Zap,\n} from \"lucide-react\";\nimport { z } from \"zod\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\nimport { zBookmarkSourceSchema } from \"@karakeep/shared/types/bookmarks\";\n\ntype BookmarkSource = z.infer<typeof zBookmarkSourceSchema>;\n\nfunction formatBytes(bytes: number): string {\n  if (bytes === 0) return \"0 Bytes\";\n  const k = 1024;\n  const sizes = [\"Bytes\", \"KB\", \"MB\", \"GB\"];\n  const i = Math.floor(Math.log(bytes) / Math.log(k));\n  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + \" \" + sizes[i];\n}\n\nfunction formatNumber(num: number): string {\n  if (num >= 1000000) {\n    return (num / 1000000).toFixed(1) + \"M\";\n  }\n  if (num >= 1000) {\n    return (num / 1000).toFixed(1) + \"K\";\n  }\n  return num.toString();\n}\n\nconst dayNames = [\"Sun\", \"Mon\", \"Tue\", \"Wed\", \"Thu\", \"Fri\", \"Sat\"];\nconst hourLabels = Array.from({ length: 24 }, (_, i) =>\n  i === 0 ? \"12 AM\" : i < 12 ? `${i} AM` : i === 12 ? \"12 PM\" : `${i - 12} PM`,\n);\n\nfunction formatSourceName(source: BookmarkSource | null): string {\n  if (!source) return \"Unknown\";\n  const sourceMap: Record<BookmarkSource, string> = {\n    api: \"API\",\n    web: \"Web\",\n    extension: \"Browser Extension\",\n    cli: \"CLI\",\n    mobile: \"Mobile App\",\n    singlefile: \"SingleFile\",\n    rss: \"RSS Feed\",\n    import: \"Import\",\n  };\n  return sourceMap[source];\n}\n\nfunction getSourceIcon(source: BookmarkSource | null): React.ReactNode {\n  const iconProps = { className: \"h-4 w-4 text-muted-foreground\" };\n  switch (source) {\n    case \"api\":\n      return <Zap {...iconProps} />;\n    case \"web\":\n      return <Globe {...iconProps} />;\n    case \"extension\":\n      return <Chrome {...iconProps} />;\n    case \"cli\":\n      return <Code {...iconProps} />;\n    case \"mobile\":\n      return <Smartphone {...iconProps} />;\n    case \"singlefile\":\n      return <FileText {...iconProps} />;\n    case \"rss\":\n      return <Rss {...iconProps} />;\n    case \"import\":\n      return <Upload {...iconProps} />;\n    default:\n      return <HelpCircle {...iconProps} />;\n  }\n}\n\nfunction SimpleBarChart({\n  data,\n  maxValue,\n  labels,\n}: {\n  data: number[];\n  maxValue: number;\n  labels: string[];\n}) {\n  return (\n    <div className=\"space-y-2\">\n      {data.map((value, index) => (\n        <div key={index} className=\"flex items-center gap-3\">\n          <div className=\"w-12 text-right text-xs text-muted-foreground\">\n            {labels[index]}\n          </div>\n          <div className=\"relative h-2 flex-1 overflow-hidden rounded-full bg-muted\">\n            <div\n              className=\"h-full rounded-full bg-primary transition-all duration-300\"\n              style={{\n                width: `${maxValue > 0 ? (value / maxValue) * 100 : 0}%`,\n              }}\n            />\n          </div>\n          <div className=\"w-8 text-right text-xs text-muted-foreground\">\n            {value}\n          </div>\n        </div>\n      ))}\n    </div>\n  );\n}\n\nfunction StatCard({\n  title,\n  value,\n  icon,\n  description,\n}: {\n  title: string;\n  value: string | number;\n  icon: React.ReactNode;\n  description?: string;\n}) {\n  return (\n    <Card>\n      <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n        <CardTitle className=\"text-sm font-medium\">{title}</CardTitle>\n        {icon}\n      </CardHeader>\n      <CardContent>\n        <div className=\"text-2xl font-bold\">{value}</div>\n        {description && (\n          <p className=\"text-xs text-muted-foreground\">{description}</p>\n        )}\n      </CardContent>\n    </Card>\n  );\n}\n\nexport default function StatsPage() {\n  const api = useTRPC();\n  const { t } = useTranslation();\n  const { data: stats, isLoading } = useQuery(api.users.stats.queryOptions());\n  const { data: userSettings } = useQuery(api.users.settings.queryOptions());\n\n  const maxHourlyActivity = useMemo(() => {\n    if (!stats) return 0;\n    return Math.max(\n      ...stats.bookmarkingActivity.byHour.map(\n        (h: { hour: number; count: number }) => h.count,\n      ),\n    );\n  }, [stats]);\n\n  const maxDailyActivity = useMemo(() => {\n    if (!stats) return 0;\n    return Math.max(\n      ...stats.bookmarkingActivity.byDayOfWeek.map(\n        (d: { day: number; count: number }) => d.count,\n      ),\n    );\n  }, [stats]);\n\n  if (isLoading) {\n    return (\n      <SettingsPage\n        title={t(\"settings.stats.usage_statistics\")}\n        description={t(\"settings.stats.insights_description\")}\n      >\n        <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-4\">\n          {Array.from({ length: 8 }).map((_, i) => (\n            <Card key={i}>\n              <CardHeader className=\"space-y-0 pb-2\">\n                <Skeleton className=\"h-4 w-24\" />\n              </CardHeader>\n              <CardContent>\n                <Skeleton className=\"mb-2 h-8 w-16\" />\n                <Skeleton className=\"h-3 w-32\" />\n              </CardContent>\n            </Card>\n          ))}\n        </div>\n      </SettingsPage>\n    );\n  }\n\n  if (!stats) {\n    return (\n      <div className=\"flex h-64 items-center justify-center\">\n        <p className=\"text-muted-foreground\">\n          {t(\"settings.stats.failed_to_load\")}\n        </p>\n      </div>\n    );\n  }\n\n  return (\n    <SettingsPage\n      title={t(\"settings.stats.usage_statistics\")}\n      description=\"Insights into your bookmarking habits and collection\"\n    >\n      {/* Overview Stats */}\n      <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-4\">\n        <StatCard\n          title={t(\"settings.stats.overview.total_bookmarks\")}\n          value={formatNumber(stats.numBookmarks)}\n          icon={<BookOpen className=\"h-4 w-4 text-muted-foreground\" />}\n          description={t(\"settings.stats.overview.all_saved_items\")}\n        />\n        <StatCard\n          title={t(\"settings.stats.overview.favorites\")}\n          value={formatNumber(stats.numFavorites)}\n          icon={<Heart className=\"h-4 w-4 text-muted-foreground\" />}\n          description={t(\"settings.stats.overview.starred_bookmarks\")}\n        />\n        <StatCard\n          title={t(\"settings.stats.overview.archived\")}\n          value={formatNumber(stats.numArchived)}\n          icon={<Archive className=\"h-4 w-4 text-muted-foreground\" />}\n          description={t(\"settings.stats.overview.archived_items\")}\n        />\n        <StatCard\n          title={t(\"settings.stats.overview.tags\")}\n          value={formatNumber(stats.numTags)}\n          icon={<Hash className=\"h-4 w-4 text-muted-foreground\" />}\n          description={t(\"settings.stats.overview.unique_tags_created\")}\n        />\n        <StatCard\n          title={t(\"settings.stats.overview.lists\")}\n          value={formatNumber(stats.numLists)}\n          icon={<List className=\"h-4 w-4 text-muted-foreground\" />}\n          description={t(\"settings.stats.overview.bookmark_collections\")}\n        />\n        <StatCard\n          title={t(\"settings.stats.overview.highlights\")}\n          value={formatNumber(stats.numHighlights)}\n          icon={<Highlighter className=\"h-4 w-4 text-muted-foreground\" />}\n          description={t(\"settings.stats.overview.text_highlights\")}\n        />\n        <StatCard\n          title={t(\"settings.stats.overview.storage_used\")}\n          value={formatBytes(stats.totalAssetSize)}\n          icon={<Database className=\"h-4 w-4 text-muted-foreground\" />}\n          description={t(\"settings.stats.overview.total_asset_storage\")}\n        />\n        <StatCard\n          title={t(\"settings.stats.overview.this_month\")}\n          value={formatNumber(stats.bookmarkingActivity.thisMonth)}\n          icon={<TrendingUp className=\"h-4 w-4 text-muted-foreground\" />}\n          description={t(\"settings.stats.overview.bookmarks_added\")}\n        />\n      </div>\n      <div className=\"grid gap-6 md:grid-cols-2\">\n        {/* Bookmark Types */}\n        <Card>\n          <CardHeader>\n            <CardTitle className=\"flex items-center gap-2\">\n              <BarChart3 className=\"h-5 w-5\" />\n              {t(\"settings.stats.bookmark_types.title\")}\n            </CardTitle>\n          </CardHeader>\n          <CardContent className=\"space-y-4\">\n            <div className=\"space-y-3\">\n              <div className=\"flex items-center justify-between\">\n                <div className=\"flex items-center gap-2\">\n                  <Link className=\"h-4 w-4 text-blue-500\" />\n                  <span className=\"text-sm\">\n                    {t(\"settings.stats.bookmark_types.links\")}\n                  </span>\n                </div>\n                <span className=\"text-sm font-medium\">\n                  {stats.bookmarksByType.link}\n                </span>\n              </div>\n              <Progress\n                value={\n                  stats.numBookmarks > 0\n                    ? (stats.bookmarksByType.link / stats.numBookmarks) * 100\n                    : 0\n                }\n                className=\"h-2\"\n              />\n            </div>\n            <div className=\"space-y-3\">\n              <div className=\"flex items-center justify-between\">\n                <div className=\"flex items-center gap-2\">\n                  <FileText className=\"h-4 w-4 text-green-500\" />\n                  <span className=\"text-sm\">\n                    {t(\"settings.stats.bookmark_types.text_notes\")}\n                  </span>\n                </div>\n                <span className=\"text-sm font-medium\">\n                  {stats.bookmarksByType.text}\n                </span>\n              </div>\n              <Progress\n                value={\n                  stats.numBookmarks > 0\n                    ? (stats.bookmarksByType.text / stats.numBookmarks) * 100\n                    : 0\n                }\n                className=\"h-2\"\n              />\n            </div>\n            <div className=\"space-y-3\">\n              <div className=\"flex items-center justify-between\">\n                <div className=\"flex items-center gap-2\">\n                  <Image className=\"h-4 w-4 text-purple-500\" />\n                  <span className=\"text-sm\">\n                    {t(\"settings.stats.bookmark_types.assets\")}\n                  </span>\n                </div>\n                <span className=\"text-sm font-medium\">\n                  {stats.bookmarksByType.asset}\n                </span>\n              </div>\n              <Progress\n                value={\n                  stats.numBookmarks > 0\n                    ? (stats.bookmarksByType.asset / stats.numBookmarks) * 100\n                    : 0\n                }\n                className=\"h-2\"\n              />\n            </div>\n          </CardContent>\n        </Card>\n\n        {/* Recent Activity */}\n        <Card className=\"flex flex-col\">\n          <CardHeader>\n            <CardTitle className=\"flex items-center gap-2\">\n              <Clock className=\"h-5 w-5\" />\n              {t(\"settings.stats.recent_activity.title\")}\n            </CardTitle>\n          </CardHeader>\n          <CardContent className=\"flex flex-1 items-center\">\n            <div className=\"grid w-full grid-cols-3 gap-4 text-center\">\n              <div>\n                <div className=\"text-2xl font-bold text-green-600\">\n                  {stats.bookmarkingActivity.thisWeek}\n                </div>\n                <div className=\"text-xs text-muted-foreground\">\n                  {t(\"settings.stats.recent_activity.this_week\")}\n                </div>\n              </div>\n              <div>\n                <div className=\"text-2xl font-bold text-blue-600\">\n                  {stats.bookmarkingActivity.thisMonth}\n                </div>\n                <div className=\"text-xs text-muted-foreground\">\n                  {t(\"settings.stats.recent_activity.this_month\")}\n                </div>\n              </div>\n              <div>\n                <div className=\"text-2xl font-bold text-purple-600\">\n                  {stats.bookmarkingActivity.thisYear}\n                </div>\n                <div className=\"text-xs text-muted-foreground\">\n                  {t(\"settings.stats.recent_activity.this_year\")}\n                </div>\n              </div>\n            </div>\n          </CardContent>\n        </Card>\n\n        {/* Top Domains */}\n        <Card>\n          <CardHeader>\n            <CardTitle className=\"flex items-center gap-2\">\n              <Globe className=\"h-5 w-5\" />\n              {t(\"settings.stats.top_domains.title\")}\n            </CardTitle>\n          </CardHeader>\n          <CardContent>\n            {stats.topDomains.length > 0 ? (\n              <div className=\"space-y-3\">\n                {stats.topDomains\n                  .slice(0, 8)\n                  .map(\n                    (\n                      domain: { domain: string; count: number },\n                      index: number,\n                    ) => (\n                      <div\n                        key={domain.domain}\n                        className=\"flex items-center justify-between\"\n                      >\n                        <div className=\"flex items-center gap-2\">\n                          <div className=\"flex h-6 w-6 items-center justify-center rounded-full bg-muted text-xs font-medium\">\n                            {index + 1}\n                          </div>\n                          <span\n                            className=\"max-w-[200px] truncate text-sm\"\n                            title={domain.domain}\n                          >\n                            {domain.domain}\n                          </span>\n                        </div>\n                        <Badge variant=\"secondary\">{domain.count}</Badge>\n                      </div>\n                    ),\n                  )}\n              </div>\n            ) : (\n              <p className=\"text-sm text-muted-foreground\">\n                {t(\"settings.stats.top_domains.no_domains_found\")}\n              </p>\n            )}\n          </CardContent>\n        </Card>\n\n        {/* Top Tags */}\n        <Card>\n          <CardHeader>\n            <CardTitle className=\"flex items-center gap-2\">\n              <Hash className=\"h-5 w-5\" />\n              {t(\"settings.stats.most_used_tags.title\")}\n            </CardTitle>\n          </CardHeader>\n          <CardContent>\n            {stats.tagUsage.length > 0 ? (\n              <div className=\"space-y-3\">\n                {stats.tagUsage\n                  .slice(0, 8)\n                  .map(\n                    (tag: { name: string; count: number }, index: number) => (\n                      <div\n                        key={tag.name}\n                        className=\"flex items-center justify-between\"\n                      >\n                        <div className=\"flex items-center gap-2\">\n                          <div className=\"flex h-6 w-6 items-center justify-center rounded-full bg-muted text-xs font-medium\">\n                            {index + 1}\n                          </div>\n                          <span\n                            className=\"max-w-[200px] truncate text-sm\"\n                            title={tag.name}\n                          >\n                            {tag.name}\n                          </span>\n                        </div>\n                        <Badge variant=\"secondary\">{tag.count}</Badge>\n                      </div>\n                    ),\n                  )}\n              </div>\n            ) : (\n              <p className=\"text-sm text-muted-foreground\">\n                {t(\"settings.stats.most_used_tags.no_tags_found\")}\n              </p>\n            )}\n          </CardContent>\n        </Card>\n\n        {/* Bookmark Sources */}\n        <Card>\n          <CardHeader>\n            <CardTitle className=\"flex items-center gap-2\">\n              <Zap className=\"h-5 w-5\" />\n              {t(\"settings.stats.bookmark_sources.title\")}\n            </CardTitle>\n          </CardHeader>\n          <CardContent>\n            {stats.bookmarksBySource.length > 0 ? (\n              <div className=\"space-y-3\">\n                {stats.bookmarksBySource.map(\n                  (source: {\n                    source: BookmarkSource | null;\n                    count: number;\n                  }) => (\n                    <div\n                      key={source.source || \"unknown\"}\n                      className=\"flex items-center justify-between\"\n                    >\n                      <div className=\"flex items-center gap-2\">\n                        {getSourceIcon(source.source)}\n                        <span className=\"max-w-[200px] truncate text-sm\">\n                          {formatSourceName(source.source)}\n                        </span>\n                      </div>\n                      <Badge variant=\"secondary\">{source.count}</Badge>\n                    </div>\n                  ),\n                )}\n              </div>\n            ) : (\n              <p className=\"text-sm text-muted-foreground\">\n                {t(\"settings.stats.bookmark_sources.empty\")}\n              </p>\n            )}\n          </CardContent>\n        </Card>\n      </div>\n      {/* Activity Patterns */}\n      <div className=\"grid gap-6 md:grid-cols-2\">\n        {/* Hourly Activity */}\n        <Card>\n          <CardHeader>\n            <CardTitle className=\"flex items-center gap-2\">\n              <Clock className=\"h-5 w-5\" />\n              {t(\"settings.stats.activity_patterns.activity_by_hour\")}\n              {userSettings?.timezone && userSettings.timezone !== \"UTC\" && (\n                <span className=\"text-xs font-normal text-muted-foreground\">\n                  ({userSettings.timezone})\n                </span>\n              )}\n            </CardTitle>\n          </CardHeader>\n          <CardContent>\n            <SimpleBarChart\n              data={stats.bookmarkingActivity.byHour.map(\n                (h: { hour: number; count: number }) => h.count,\n              )}\n              maxValue={maxHourlyActivity}\n              labels={hourLabels}\n            />\n          </CardContent>\n        </Card>\n\n        {/* Daily Activity */}\n        <Card>\n          <CardHeader>\n            <CardTitle className=\"flex items-center gap-2\">\n              <BarChart3 className=\"h-5 w-5\" />\n              {t(\"settings.stats.activity_patterns.activity_by_day\")}\n              {userSettings?.timezone && userSettings.timezone !== \"UTC\" && (\n                <span className=\"text-xs font-normal text-muted-foreground\">\n                  ({userSettings.timezone})\n                </span>\n              )}\n            </CardTitle>\n          </CardHeader>\n          <CardContent>\n            <SimpleBarChart\n              data={stats.bookmarkingActivity.byDayOfWeek.map(\n                (d: { day: number; count: number }) => d.count,\n              )}\n              maxValue={maxDailyActivity}\n              labels={dayNames}\n            />\n          </CardContent>\n        </Card>\n      </div>\n      {/* Asset Storage */}\n      {stats.assetsByType.length > 0 && (\n        <Card>\n          <CardHeader>\n            <CardTitle className=\"flex items-center gap-2\">\n              <Database className=\"h-5 w-5\" />\n              {t(\"settings.stats.storage_breakdown.title\")}\n            </CardTitle>\n          </CardHeader>\n          <CardContent>\n            <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-3\">\n              {stats.assetsByType.map(\n                (asset: { type: string; count: number; totalSize: number }) => (\n                  <div key={asset.type} className=\"space-y-2\">\n                    <div className=\"flex items-center justify-between\">\n                      <span className=\"text-sm font-medium capitalize\">\n                        {asset.type.replace(/([A-Z])/g, \" $1\").trim()}\n                      </span>\n                      <Badge variant=\"outline\">{asset.count}</Badge>\n                    </div>\n                    <div className=\"text-xs text-muted-foreground\">\n                      {formatBytes(asset.totalSize)}\n                    </div>\n                    <Progress\n                      value={\n                        stats.totalAssetSize > 0\n                          ? (asset.totalSize / stats.totalAssetSize) * 100\n                          : 0\n                      }\n                      className=\"h-2\"\n                    />\n                  </div>\n                ),\n              )}\n            </div>\n          </CardContent>\n        </Card>\n      )}\n    </SettingsPage>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/settings/subscription/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport { redirect } from \"next/navigation\";\nimport { SettingsPage } from \"@/components/settings/SettingsPage\";\nimport SubscriptionSettings from \"@/components/settings/SubscriptionSettings\";\nimport { QuotaProgress } from \"@/components/subscription/QuotaProgress\";\nimport { useTranslation } from \"@/lib/i18n/server\";\n\nimport serverConfig from \"@karakeep/shared/config\";\n\nexport async function generateMetadata(): Promise<Metadata> {\n  // oxlint-disable-next-line rules-of-hooks\n  const { t } = await useTranslation();\n  return {\n    title: `${t(\"settings.subscription.subscription\")} | Karakeep`,\n  };\n}\n\nexport default async function SubscriptionPage() {\n  if (!serverConfig.stripe.isConfigured) {\n    redirect(\"/settings\");\n  }\n\n  // oxlint-disable-next-line rules-of-hooks\n  const { t } = await useTranslation();\n\n  return (\n    <SettingsPage title={t(\"settings.subscription.subscription\")}>\n      <SubscriptionSettings />\n      <QuotaProgress />\n    </SettingsPage>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/settings/webhooks/page.tsx",
    "content": "import type { Metadata } from \"next\";\nimport WebhookSettings from \"@/components/settings/WebhookSettings\";\nimport { useTranslation } from \"@/lib/i18n/server\";\n\nexport async function generateMetadata(): Promise<Metadata> {\n  // oxlint-disable-next-line rules-of-hooks\n  const { t } = await useTranslation();\n  return {\n    title: `${t(\"settings.webhooks.webhooks\")} | Karakeep`,\n  };\n}\n\nexport default function WebhookSettingsPage() {\n  return <WebhookSettings />;\n}\n"
  },
  {
    "path": "apps/web/app/signin/page.tsx",
    "content": "import { redirect } from \"next/dist/client/components/navigation\";\nimport KarakeepLogo from \"@/components/KarakeepIcon\";\nimport SignInForm from \"@/components/signin/SignInForm\";\nimport { getServerAuthSession } from \"@/server/auth\";\n\nexport default async function SignInPage() {\n  const session = await getServerAuthSession();\n  if (session) {\n    redirect(\"/\");\n  }\n\n  return (\n    <div className=\"flex min-h-screen items-center justify-center bg-background px-4 py-12 sm:px-6 lg:px-8\">\n      <div className=\"w-full max-w-md space-y-8\">\n        <div className=\"flex items-center justify-center\">\n          <KarakeepLogo height={80} />\n        </div>\n        <SignInForm />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/signup/page.tsx",
    "content": "import { redirect } from \"next/dist/client/components/navigation\";\nimport KarakeepLogo from \"@/components/KarakeepIcon\";\nimport SignUpForm from \"@/components/signup/SignUpForm\";\nimport { getServerAuthSession } from \"@/server/auth\";\n\nimport {\n  isMobileAppRedirect,\n  validateRedirectUrl,\n} from \"@karakeep/shared/utils/redirectUrl\";\n\nexport default async function SignUpPage({\n  searchParams,\n}: {\n  searchParams: Promise<{ redirectUrl?: string; skipSessionRedirect?: string }>;\n}) {\n  const session = await getServerAuthSession();\n  const { redirectUrl: rawRedirectUrl, skipSessionRedirect } =\n    await searchParams;\n  const redirectUrl = validateRedirectUrl(rawRedirectUrl) ?? \"/\";\n  const shouldSkipSessionRedirect =\n    isMobileAppRedirect(redirectUrl) && skipSessionRedirect === \"1\";\n\n  if (session && !shouldSkipSessionRedirect) {\n    redirect(redirectUrl);\n  }\n\n  return (\n    <div className=\"flex min-h-screen items-center justify-center bg-background px-4 py-12 sm:px-6 lg:px-8\">\n      <div className=\"w-full max-w-md space-y-8\">\n        <div className=\"flex items-center justify-center\">\n          <KarakeepLogo height={80} />\n        </div>\n        <SignUpForm redirectUrl={redirectUrl} />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/app/verify-email/page.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { useRouter, useSearchParams } from \"next/navigation\";\nimport { Alert, AlertDescription } from \"@/components/ui/alert\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\";\nimport { useMutation } from \"@tanstack/react-query\";\nimport { CheckCircle, Loader2, XCircle } from \"lucide-react\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\nimport {\n  isMobileAppRedirect,\n  validateRedirectUrl,\n} from \"@karakeep/shared/utils/redirectUrl\";\n\nexport default function VerifyEmailPage() {\n  const api = useTRPC();\n  const searchParams = useSearchParams();\n  const router = useRouter();\n  const [status, setStatus] = useState<\"loading\" | \"success\" | \"error\">(\n    \"loading\",\n  );\n  const [message, setMessage] = useState(\"\");\n\n  const token = searchParams.get(\"token\");\n  const email = searchParams.get(\"email\");\n  const redirectUrl =\n    validateRedirectUrl(searchParams.get(\"redirectUrl\")) ?? \"/\";\n\n  const verifyEmailMutation = useMutation(\n    api.users.verifyEmail.mutationOptions({\n      onSuccess: () => {\n        setStatus(\"success\");\n        if (isMobileAppRedirect(redirectUrl)) {\n          setMessage(\n            \"Your email has been successfully verified! Redirecting to the app...\",\n          );\n          // Redirect to mobile app after a brief delay\n          setTimeout(() => {\n            window.location.href = redirectUrl;\n          }, 1500);\n        } else {\n          setMessage(\n            \"Your email has been successfully verified! You can now sign in.\",\n          );\n        }\n      },\n      onError: (error) => {\n        setStatus(\"error\");\n        setMessage(\n          error.message ||\n            \"Failed to verify email. The link may be invalid or expired.\",\n        );\n      },\n    }),\n  );\n\n  const resendEmailMutation = useMutation(\n    api.users.resendVerificationEmail.mutationOptions({\n      onSuccess: () => {\n        setMessage(\n          \"A new verification email has been sent to your email address.\",\n        );\n      },\n      onError: (error) => {\n        setMessage(error.message || \"Failed to resend verification email.\");\n      },\n    }),\n  );\n\n  const isMobileRedirect = isMobileAppRedirect(redirectUrl);\n\n  useEffect(() => {\n    if (token && email) {\n      verifyEmailMutation.mutate({ token, email });\n    } else {\n      setStatus(\"error\");\n      setMessage(\"Invalid verification link. Missing token or email.\");\n    }\n  }, [token, email]);\n\n  const handleResendEmail = () => {\n    if (email) {\n      resendEmailMutation.mutate({ email, redirectUrl });\n    }\n  };\n\n  const handleSignIn = () => {\n    if (isMobileRedirect) {\n      window.location.href = redirectUrl;\n    } else if (redirectUrl !== \"/\") {\n      router.push(`/signin?redirectUrl=${encodeURIComponent(redirectUrl)}`);\n    } else {\n      router.push(\"/signin\");\n    }\n  };\n\n  return (\n    <div className=\"flex min-h-screen items-center justify-center bg-background px-4 py-12 sm:px-6 lg:px-8\">\n      <Card className=\"w-full max-w-md\">\n        <CardHeader className=\"text-center\">\n          <CardTitle className=\"text-2xl font-bold\">\n            Email Verification\n          </CardTitle>\n          <CardDescription>\n            {status === \"loading\" && \"Verifying your email address...\"}\n            {status === \"success\" && \"Email verified successfully!\"}\n            {status === \"error\" && \"Verification failed\"}\n          </CardDescription>\n        </CardHeader>\n        <CardContent className=\"space-y-4\">\n          {status === \"loading\" && (\n            <div className=\"flex items-center justify-center\">\n              <Loader2 className=\"h-8 w-8 animate-spin text-blue-600\" />\n            </div>\n          )}\n\n          {status === \"success\" && (\n            <>\n              <div className=\"flex items-center justify-center\">\n                <CheckCircle className=\"h-12 w-12 text-green-600\" />\n              </div>\n              <Alert>\n                <AlertDescription className=\"text-center\">\n                  {message}\n                </AlertDescription>\n              </Alert>\n              <Button onClick={handleSignIn} className=\"w-full\">\n                {isMobileRedirect ? \"Open App\" : \"Sign In\"}\n              </Button>\n            </>\n          )}\n\n          {status === \"error\" && (\n            <>\n              <div className=\"flex items-center justify-center\">\n                <XCircle className=\"h-12 w-12 text-red-600\" />\n              </div>\n              <Alert variant=\"destructive\">\n                <AlertDescription className=\"text-center\">\n                  {message}\n                </AlertDescription>\n              </Alert>\n              {email && (\n                <div className=\"space-y-2\">\n                  <Button\n                    onClick={handleResendEmail}\n                    variant=\"outline\"\n                    className=\"w-full\"\n                    disabled={resendEmailMutation.isPending}\n                  >\n                    {resendEmailMutation.isPending ? (\n                      <>\n                        <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n                        Sending...\n                      </>\n                    ) : (\n                      \"Resend Verification Email\"\n                    )}\n                  </Button>\n                  <Button\n                    onClick={handleSignIn}\n                    variant=\"ghost\"\n                    className=\"w-full\"\n                  >\n                    Back to Sign In\n                  </Button>\n                </div>\n              )}\n            </>\n          )}\n        </CardContent>\n      </Card>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/DemoModeBanner.tsx",
    "content": "export default function DemoModeBanner() {\n  return (\n    <div className=\"h-min w-full rounded bg-yellow-100 px-4 py-2 text-center text-black\">\n      Demo mode is on. All modifications are disabled.\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/KarakeepIcon.tsx",
    "content": "import KarakeepFull from \"@/public/icons/karakeep-full.svg\";\n\nexport default function KarakeepLogo({ height }: { height: number }) {\n  return (\n    <span className=\"flex items-center\">\n      <KarakeepFull height={height} className={`fill-foreground`} />\n    </span>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/admin/AddUserDialog.tsx",
    "content": "import { useEffect, useState } from \"react\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@/components/ui/dialog\";\nimport {\n  Form,\n  FormControl,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@/components/ui/form\";\nimport { Input } from \"@/components/ui/input\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { useMutation, useQueryClient } from \"@tanstack/react-query\";\nimport { TRPCClientError } from \"@trpc/client\";\nimport { useForm } from \"react-hook-form\";\nimport { z } from \"zod\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\nimport { zAdminCreateUserSchema } from \"@karakeep/shared/types/admin\";\n\ntype AdminCreateUserSchema = z.infer<typeof zAdminCreateUserSchema>;\n\nexport default function AddUserDialog({\n  children,\n}: {\n  children?: React.ReactNode;\n}) {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n  const [isOpen, onOpenChange] = useState(false);\n  const form = useForm<AdminCreateUserSchema>({\n    resolver: zodResolver(zAdminCreateUserSchema),\n    defaultValues: {\n      name: \"\",\n      email: \"\",\n      password: \"\",\n      confirmPassword: \"\",\n      role: \"user\",\n    },\n  });\n  const { mutate, isPending } = useMutation(\n    api.admin.createUser.mutationOptions({\n      onSuccess: () => {\n        toast({\n          description: \"User created successfully\",\n        });\n        onOpenChange(false);\n        queryClient.invalidateQueries(api.users.list.pathFilter());\n        queryClient.invalidateQueries(api.admin.userStats.pathFilter());\n      },\n      onError: (error) => {\n        if (error instanceof TRPCClientError) {\n          toast({\n            variant: \"destructive\",\n            description: error.message,\n          });\n        } else {\n          toast({\n            variant: \"destructive\",\n            description: \"Failed to create user\",\n          });\n        }\n      },\n    }),\n  );\n\n  useEffect(() => {\n    if (!isOpen) {\n      form.reset();\n    }\n  }, [isOpen, form]);\n\n  return (\n    <Dialog open={isOpen} onOpenChange={onOpenChange}>\n      <DialogTrigger asChild>{children}</DialogTrigger>\n      <DialogContent>\n        <DialogHeader>\n          <DialogTitle>Add User</DialogTitle>\n        </DialogHeader>\n        <Form {...form}>\n          <form onSubmit={form.handleSubmit((val) => mutate(val))}>\n            <div className=\"flex w-full flex-col space-y-2\">\n              <FormField\n                control={form.control}\n                name=\"name\"\n                render={({ field }) => (\n                  <FormItem>\n                    <FormLabel>Name</FormLabel>\n                    <FormControl>\n                      <Input\n                        type=\"text\"\n                        placeholder=\"Name\"\n                        {...field}\n                        className=\"w-full rounded border p-2\"\n                      />\n                    </FormControl>\n                    <FormMessage />\n                  </FormItem>\n                )}\n              />\n              <FormField\n                control={form.control}\n                name=\"email\"\n                render={({ field }) => (\n                  <FormItem>\n                    <FormLabel>Email</FormLabel>\n                    <FormControl>\n                      <Input\n                        type=\"email\"\n                        placeholder=\"Email\"\n                        {...field}\n                        className=\"w-full rounded border p-2\"\n                      />\n                    </FormControl>\n                    <FormMessage />\n                  </FormItem>\n                )}\n              />\n              <FormField\n                control={form.control}\n                name=\"password\"\n                render={({ field }) => (\n                  <FormItem>\n                    <FormLabel>Password</FormLabel>\n                    <FormControl>\n                      <Input\n                        type=\"password\"\n                        placeholder=\"Password\"\n                        {...field}\n                        className=\"w-full rounded border p-2\"\n                      />\n                    </FormControl>\n                    <FormMessage />\n                  </FormItem>\n                )}\n              />\n              <FormField\n                control={form.control}\n                name=\"confirmPassword\"\n                render={({ field }) => (\n                  <FormItem>\n                    <FormLabel>Confirm Password</FormLabel>\n                    <FormControl>\n                      <Input\n                        type=\"password\"\n                        placeholder=\"Confirm Password\"\n                        {...field}\n                        className=\"w-full rounded border p-2\"\n                      />\n                    </FormControl>\n                    <FormMessage />\n                  </FormItem>\n                )}\n              />\n              <FormField\n                control={form.control}\n                name=\"role\"\n                render={({ field }) => (\n                  <FormItem>\n                    <FormLabel>Role</FormLabel>\n                    <FormControl>\n                      <Select\n                        value={field.value}\n                        onValueChange={field.onChange}\n                      >\n                        <SelectTrigger className=\"w-full\">\n                          <SelectValue />\n                        </SelectTrigger>\n                        <SelectContent>\n                          <SelectItem value=\"user\">User</SelectItem>\n                          <SelectItem value=\"admin\">Admin</SelectItem>\n                        </SelectContent>\n                      </Select>\n                    </FormControl>\n                    <FormMessage />\n                  </FormItem>\n                )}\n              />\n              <DialogFooter className=\"sm:justify-end\">\n                <DialogClose asChild>\n                  <Button type=\"button\" variant=\"secondary\">\n                    Close\n                  </Button>\n                </DialogClose>\n                <ActionButton\n                  type=\"submit\"\n                  loading={isPending}\n                  disabled={isPending}\n                >\n                  Create\n                </ActionButton>\n              </DialogFooter>\n            </div>\n          </form>\n        </Form>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/admin/AdminCard.tsx",
    "content": "import { cn } from \"@/lib/utils\";\n\nexport function AdminCard({\n  className,\n  children,\n}: {\n  className?: string;\n  children: React.ReactNode;\n}) {\n  return (\n    <div className={cn(\"rounded-md border bg-background p-4\", className)}>\n      {children}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/admin/AdminNotices.tsx",
    "content": "\"use client\";\n\nimport { Alert, AlertDescription, AlertTitle } from \"@/components/ui/alert\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { AlertCircle } from \"lucide-react\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\nimport { AdminCard } from \"./AdminCard\";\n\ninterface AdminNotice {\n  level: \"info\" | \"warning\" | \"error\";\n  message: React.ReactNode;\n  title: string;\n}\n\nfunction useAdminNotices() {\n  const api = useTRPC();\n  const { data } = useQuery(api.admin.getAdminNoticies.queryOptions());\n  if (!data) {\n    return [];\n  }\n  const ret: AdminNotice[] = [];\n  return ret;\n}\n\nexport function AdminNotices() {\n  const notices = useAdminNotices();\n\n  if (notices.length === 0) {\n    return null;\n  }\n  return (\n    <AdminCard>\n      <div className=\"flex flex-col gap-2\">\n        {notices.map((n, i) => (\n          <Alert key={i} variant=\"destructive\">\n            <AlertCircle className=\"h-4 w-4\" />\n            <AlertTitle>{n.title}</AlertTitle>\n            <AlertDescription>{n.message}</AlertDescription>\n          </Alert>\n        ))}\n      </div>\n    </AdminCard>\n  );\n}\n\nexport function AdminNoticeBadge() {\n  const notices = useAdminNotices();\n  if (notices.length === 0) {\n    return null;\n  }\n  return <Badge variant=\"destructive\">{notices.length}</Badge>;\n}\n"
  },
  {
    "path": "apps/web/components/admin/BackgroundJobs.tsx",
    "content": "\"use client\";\n\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport ActionConfirmingDialog from \"@/components/ui/action-confirming-dialog\";\nimport { Badge } from \"@/components/ui/badge\";\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { keepPreviousData, useMutation, useQuery } from \"@tanstack/react-query\";\nimport {\n  Activity,\n  AlertTriangle,\n  Clock,\n  Database,\n  Globe,\n  HelpCircle,\n  Image,\n  RefreshCw,\n  Rss,\n  Search,\n  Sparkle,\n  Video,\n  Webhook,\n} from \"lucide-react\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\nimport { Button } from \"../ui/button\";\nimport { AdminCard } from \"./AdminCard\";\n\ninterface JobStats {\n  queued: number;\n  pending?: number;\n  failed?: number;\n}\n\ninterface JobAction {\n  label: string;\n  onClick: () => Promise<void>;\n  loading: boolean;\n}\n\nfunction JobStatusExplanation() {\n  const { t } = useTranslation();\n\n  return (\n    <Card className=\"border-blue-200 bg-blue-50/50 dark:border-blue-700 dark:bg-blue-900/10\">\n      <CardHeader className=\"pb-4\">\n        <CardTitle className=\"flex items-center gap-2 text-lg text-blue-900 dark:text-blue-200\">\n          <HelpCircle className=\"h-5 w-5 text-blue-600 dark:text-blue-400\" />\n          {t(\"admin.background_jobs.status.title\")}\n        </CardTitle>\n      </CardHeader>\n      <CardContent>\n        <div className=\"grid gap-4 md:grid-cols-3\">\n          <div className=\"flex items-start gap-3\">\n            <div className=\"flex h-8 w-8 items-center justify-center\">\n              <Clock className=\"h-4 w-4 text-blue-600 dark:text-blue-400\" />\n            </div>\n            <div>\n              <h4 className=\"font-medium text-blue-900 dark:text-blue-200\">\n                {t(\"admin.background_jobs.status.queued.title\")}\n              </h4>\n              <p className=\"text-sm text-blue-700 dark:text-blue-300\">\n                {t(\"admin.background_jobs.status.queued.description\")}\n              </p>\n            </div>\n          </div>\n          <div className=\"flex items-start gap-3\">\n            <div className=\"flex h-8 w-8 items-center justify-center\">\n              <RefreshCw className=\"h-4 w-4 text-yellow-600 dark:text-yellow-400\" />\n            </div>\n            <div>\n              <h4 className=\"font-medium text-yellow-900 dark:text-yellow-400\">\n                {t(\"admin.background_jobs.status.unprocessed.title\")}\n              </h4>\n              <p className=\"text-sm text-yellow-700 dark:text-yellow-500\">\n                {t(\"admin.background_jobs.status.unprocessed.description\")}\n              </p>\n            </div>\n          </div>\n          <div className=\"flex items-start gap-3\">\n            <div className=\"flex h-8 w-8 items-center justify-center\">\n              <AlertTriangle className=\"h-4 w-4 text-red-600 dark:text-red-500\" />\n            </div>\n            <div>\n              <h4 className=\"font-medium text-red-900 dark:text-red-500\">\n                {t(\"admin.background_jobs.status.failed.title\")}\n              </h4>\n              <p className=\"text-sm text-red-700 dark:text-red-500\">\n                {t(\"admin.background_jobs.status.failed.description\")}\n              </p>\n            </div>\n          </div>\n        </div>\n      </CardContent>\n    </Card>\n  );\n}\n\nfunction JobCardSkeleton() {\n  return (\n    <Card className=\"relative overflow-hidden\">\n      <CardHeader className=\"pb-4\">\n        <div className=\"flex items-center justify-between\">\n          <div className=\"flex items-center gap-3\">\n            <Skeleton className=\"h-5 w-5\" />\n            <Skeleton className=\"h-6 w-32\" />\n          </div>\n          <Skeleton className=\"h-6 w-16\" />\n        </div>\n        <Skeleton className=\"mt-2 h-4 w-3/4\" />\n      </CardHeader>\n      <CardContent className=\"space-y-5\">\n        <div className=\"flex flex-wrap items-center gap-4\">\n          <div className=\"flex items-center gap-2\">\n            <Skeleton className=\"h-4 w-4\" />\n            <Skeleton className=\"h-4 w-16\" />\n            <Skeleton className=\"h-6 w-8\" />\n          </div>\n          <div className=\"flex items-center gap-2\">\n            <Skeleton className=\"h-4 w-4\" />\n            <Skeleton className=\"h-4 w-20\" />\n            <Skeleton className=\"h-6 w-8\" />\n          </div>\n        </div>\n\n        <div className=\"space-y-2\">\n          <div className=\"flex justify-between\">\n            <Skeleton className=\"h-3 w-20\" />\n            <Skeleton className=\"h-3 w-16\" />\n          </div>\n          <Skeleton className=\"h-2 w-full\" />\n        </div>\n\n        <div className=\"space-y-3 border-t pt-4\">\n          <Skeleton className=\"h-4 w-32\" />\n          <div className=\"grid gap-2\">\n            <Skeleton className=\"h-9 w-full\" />\n            <Skeleton className=\"h-9 w-full\" />\n          </div>\n        </div>\n      </CardContent>\n    </Card>\n  );\n}\n\nfunction JobCard({\n  title,\n  icon: Icon,\n  stats,\n  description,\n  actions = [],\n}: {\n  title: string;\n  icon: React.ComponentType<{ className?: string }>;\n  stats: JobStats;\n  description: string;\n  actions?: JobAction[];\n}) {\n  const { t } = useTranslation();\n  const total = stats.queued + (stats.pending || 0) + (stats.failed || 0);\n  const hasActivity = total > 0;\n\n  return (\n    <Card className=\"relative overflow-hidden\">\n      <CardHeader className=\"pb-4\">\n        <div className=\"flex items-center justify-between\">\n          <div className=\"flex items-center gap-3\">\n            <Icon\n              className={`h-5 w-5 ${hasActivity ? \"text-primary\" : \"text-muted-foreground\"}`}\n            />\n            <CardTitle className=\"text-lg\">{title}</CardTitle>\n          </div>\n          {hasActivity && (\n            <Badge variant=\"secondary\">\n              <Activity className=\"mr-1 h-3 w-3\" />\n              {t(\"admin.background_jobs.active\")}\n            </Badge>\n          )}\n        </div>\n        <CardDescription className=\"mt-2 text-sm\">\n          {description}\n        </CardDescription>\n      </CardHeader>\n      <CardContent className=\"space-y-5\">\n        <div className=\"flex flex-wrap items-center gap-4 text-sm\">\n          <div className=\"flex items-center gap-2\">\n            <Clock className=\"h-4 w-4 text-blue-500\" />\n            <span className=\"font-medium\">\n              {t(\"admin.background_jobs.status.queued.title\")}\n            </span>\n            <Badge variant=\"outline\">{stats.queued}</Badge>\n          </div>\n          {stats.pending !== undefined && (\n            <div className=\"flex items-center gap-2\">\n              <RefreshCw className=\"h-4 w-4 text-yellow-500\" />\n              <span className=\"font-medium\">\n                {t(\"admin.background_jobs.status.unprocessed.title\")}\n              </span>\n              <Badge variant=\"outline\">{stats.pending}</Badge>\n            </div>\n          )}\n          {stats.failed !== undefined && stats.failed > 0 && (\n            <div className=\"flex items-center gap-2\">\n              <AlertTriangle className=\"h-4 w-4 text-red-500\" />\n              <span className=\"font-medium\">\n                {t(\"admin.background_jobs.status.failed.title\")}\n              </span>\n              <Badge variant=\"outline\">{stats.failed}</Badge>\n            </div>\n          )}\n        </div>\n\n        {actions.length > 0 && (\n          <div className=\"space-y-3 border-t pt-4\">\n            <h4 className=\"text-sm font-medium text-muted-foreground\">\n              {t(\"admin.background_jobs.available_actions\")}\n            </h4>\n            <div className=\"grid gap-2\">\n              {actions.map((action, index) => (\n                <ActionConfirmingDialog\n                  key={index}\n                  title=\"Confirm Action\"\n                  description={`Are you sure you want to ${action.label.toLowerCase()}?`}\n                  actionButton={(setDialogOpen) => (\n                    <ActionButton\n                      loading={action.loading}\n                      onClick={async () => {\n                        await action.onClick();\n                        setDialogOpen(false);\n                      }}\n                      className=\"h-auto justify-start px-3 py-2.5 text-left text-sm\"\n                    >\n                      {t(\"actions.confirm\")}\n                    </ActionButton>\n                  )}\n                >\n                  <Button variant=\"secondary\">{action.label}</Button>\n                </ActionConfirmingDialog>\n              ))}\n            </div>\n          </div>\n        )}\n      </CardContent>\n    </Card>\n  );\n}\n\nfunction useJobActions() {\n  const api = useTRPC();\n  const { t } = useTranslation();\n\n  const { mutateAsync: recrawlLinks, isPending: isRecrawlPending } =\n    useMutation(\n      api.admin.recrawlLinks.mutationOptions({\n        onSuccess: () => {\n          toast({\n            description: \"Recrawl enqueued\",\n          });\n        },\n        onError: (e) => {\n          toast({\n            variant: \"destructive\",\n            description: e.message,\n          });\n        },\n      }),\n    );\n\n  const { mutateAsync: reindexBookmarks, isPending: isReindexPending } =\n    useMutation(\n      api.admin.reindexAllBookmarks.mutationOptions({\n        onSuccess: () => {\n          toast({\n            description: \"Reindex enqueued\",\n          });\n        },\n        onError: (e) => {\n          toast({\n            variant: \"destructive\",\n            description: e.message,\n          });\n        },\n      }),\n    );\n\n  const {\n    mutateAsync: reprocessAssetsFixMode,\n    isPending: isReprocessingPending,\n  } = useMutation(\n    api.admin.reprocessAssetsFixMode.mutationOptions({\n      onSuccess: () => {\n        toast({\n          description: \"Reprocessing enqueued\",\n        });\n      },\n      onError: (e) => {\n        toast({\n          variant: \"destructive\",\n          description: e.message,\n        });\n      },\n    }),\n  );\n\n  const {\n    mutateAsync: reRunInferenceOnAllBookmarks,\n    isPending: isInferencePending,\n  } = useMutation(\n    api.admin.reRunInferenceOnAllBookmarks.mutationOptions({\n      onSuccess: () => {\n        toast({\n          description: \"Inference jobs enqueued\",\n        });\n      },\n      onError: (e) => {\n        toast({\n          variant: \"destructive\",\n          description: e.message,\n        });\n      },\n    }),\n  );\n\n  const {\n    mutateAsync: runAdminMaintenanceTask,\n    isPending: isAdminMaintenancePending,\n  } = useMutation(\n    api.admin.runAdminMaintenanceTask.mutationOptions({\n      onSuccess: () => {\n        toast({\n          description: \"Admin maintenance request has been enqueued!\",\n        });\n      },\n      onError: (e) => {\n        toast({\n          variant: \"destructive\",\n          description: e.message,\n        });\n      },\n    }),\n  );\n\n  return {\n    crawlActions: [\n      {\n        label: t(\"admin.background_jobs.actions.recrawl_pending_links_only\"),\n        onClick: () =>\n          recrawlLinks({ crawlStatus: \"pending\", runInference: true }),\n        variant: \"secondary\" as const,\n        loading: isRecrawlPending,\n      },\n      {\n        label: t(\"admin.background_jobs.actions.recrawl_failed_links_only\"),\n        onClick: () =>\n          recrawlLinks({ crawlStatus: \"failure\", runInference: true }),\n        variant: \"secondary\" as const,\n        loading: isRecrawlPending,\n      },\n      {\n        label: t(\"admin.background_jobs.actions.recrawl_all_links\"),\n        onClick: () => recrawlLinks({ crawlStatus: \"all\", runInference: true }),\n        loading: isRecrawlPending,\n      },\n      {\n        label: `${t(\"admin.background_jobs.actions.recrawl_all_links\")} (${t(\"admin.background_jobs.actions.without_inference\")})`,\n        onClick: () =>\n          recrawlLinks({ crawlStatus: \"all\", runInference: false }),\n        loading: isRecrawlPending,\n      },\n    ],\n    inferenceActions: [\n      {\n        label: t(\n          \"admin.background_jobs.actions.regenerate_ai_tags_for_pending_bookmarks_only\",\n        ),\n        onClick: () =>\n          reRunInferenceOnAllBookmarks({ type: \"tag\", status: \"pending\" }),\n        variant: \"secondary\" as const,\n        loading: isInferencePending,\n      },\n      {\n        label: t(\n          \"admin.background_jobs.actions.regenerate_ai_tags_for_failed_bookmarks_only\",\n        ),\n        onClick: () =>\n          reRunInferenceOnAllBookmarks({ type: \"tag\", status: \"failure\" }),\n        variant: \"secondary\" as const,\n        loading: isInferencePending,\n      },\n      {\n        label: t(\n          \"admin.background_jobs.actions.regenerate_ai_tags_for_all_bookmarks\",\n        ),\n        onClick: () =>\n          reRunInferenceOnAllBookmarks({ type: \"tag\", status: \"all\" }),\n        loading: isInferencePending,\n      },\n      {\n        label: t(\n          \"admin.background_jobs.actions.regenerate_ai_summaries_for_pending_bookmarks_only\",\n        ),\n        onClick: () =>\n          reRunInferenceOnAllBookmarks({\n            type: \"summarize\",\n            status: \"pending\",\n          }),\n        variant: \"secondary\" as const,\n        loading: isInferencePending,\n      },\n      {\n        label: t(\n          \"admin.background_jobs.actions.regenerate_ai_summaries_for_failed_bookmarks_only\",\n        ),\n        onClick: () =>\n          reRunInferenceOnAllBookmarks({\n            type: \"summarize\",\n            status: \"failure\",\n          }),\n        variant: \"secondary\" as const,\n        loading: isInferencePending,\n      },\n      {\n        label: t(\n          \"admin.background_jobs.actions.regenerate_ai_summaries_for_all_bookmarks\",\n        ),\n        onClick: () =>\n          reRunInferenceOnAllBookmarks({ type: \"summarize\", status: \"all\" }),\n        loading: isInferencePending,\n      },\n    ],\n    indexingActions: [\n      {\n        label: t(\"admin.background_jobs.actions.reindex_all_bookmarks\"),\n        onClick: () => reindexBookmarks(),\n        loading: isReindexPending,\n      },\n    ],\n    assetPreprocessingActions: [\n      {\n        label: t(\"admin.background_jobs.actions.reprocess_assets_fix_mode\"),\n        onClick: () => reprocessAssetsFixMode(),\n        loading: isReprocessingPending,\n      },\n    ],\n    adminMaintenanceActions: [\n      {\n        label: t(\"admin.background_jobs.actions.clean_assets\"),\n        onClick: () =>\n          runAdminMaintenanceTask({\n            type: \"tidy_assets\",\n            args: {\n              cleanDanglingAssets: true,\n              syncAssetMetadata: true,\n            },\n          }),\n        loading: isAdminMaintenancePending,\n      },\n      {\n        label: t(\n          \"admin.background_jobs.actions.migrate_large_link_html_content\",\n        ),\n        onClick: () =>\n          runAdminMaintenanceTask({ type: \"migrate_large_link_html\" }),\n        loading: isAdminMaintenancePending,\n        variant: \"secondary\" as const,\n      },\n    ],\n  };\n}\n\nexport default function BackgroundJobs() {\n  const api = useTRPC();\n  const { t } = useTranslation();\n  const { data: serverStats } = useQuery(\n    api.admin.backgroundJobsStats.queryOptions(undefined, {\n      refetchInterval: 1000,\n      placeholderData: keepPreviousData,\n    }),\n  );\n\n  const actions = useJobActions();\n\n  if (!serverStats) {\n    return (\n      <div className=\"space-y-6\">\n        <div className=\"space-y-2\">\n          <Skeleton className=\"h-8 w-64\" />\n          <Skeleton className=\"h-5 w-96\" />\n        </div>\n\n        <div className=\"grid gap-6 xl:grid-cols-2\">\n          {Array.from({ length: 8 }).map((_, index) => (\n            <JobCardSkeleton key={index} />\n          ))}\n        </div>\n      </div>\n    );\n  }\n\n  const jobs = [\n    {\n      title: t(\"admin.background_jobs.jobs.crawler.title\"),\n      icon: Globe,\n      stats: serverStats.crawlStats,\n      description: t(\"admin.background_jobs.jobs.crawler.description\"),\n      actions: actions.crawlActions,\n    },\n    {\n      title: t(\"admin.background_jobs.jobs.inference.title\"),\n      icon: Sparkle,\n      stats: serverStats.inferenceStats,\n      description: t(\"admin.background_jobs.jobs.inference.description\"),\n      actions: actions.inferenceActions,\n    },\n    {\n      title: t(\"admin.background_jobs.jobs.indexing.title\"),\n      icon: Search,\n      stats: { queued: serverStats.indexingStats.queued },\n      description: t(\"admin.background_jobs.jobs.indexing.description\"),\n      actions: actions.indexingActions,\n    },\n    {\n      title: t(\"admin.background_jobs.jobs.asset_preprocessing.title\"),\n      icon: Image,\n      stats: { queued: serverStats.assetPreprocessingStats.queued },\n      description: t(\n        \"admin.background_jobs.jobs.asset_preprocessing.description\",\n      ),\n      actions: actions.assetPreprocessingActions,\n    },\n    {\n      title: t(\"admin.background_jobs.jobs.admin_maintenance.title\"),\n      icon: Database,\n      stats: { queued: serverStats.adminMaintenanceStats.queued },\n      description: t(\n        \"admin.background_jobs.jobs.admin_maintenance.description\",\n      ),\n      actions: actions.adminMaintenanceActions,\n    },\n    {\n      title: t(\"admin.background_jobs.jobs.video.title\"),\n      icon: Video,\n      stats: { queued: serverStats.videoStats.queued },\n      description: t(\"admin.background_jobs.jobs.video.description\"),\n      actions: [],\n    },\n    {\n      title: t(\"admin.background_jobs.jobs.webhook.title\"),\n      icon: Webhook,\n      stats: { queued: serverStats.webhookStats.queued },\n      description: t(\"admin.background_jobs.jobs.webhook.description\"),\n      actions: [],\n    },\n    {\n      title: t(\"admin.background_jobs.jobs.feed.title\"),\n      icon: Rss,\n      stats: { queued: serverStats.feedStats.queued },\n      description: t(\"admin.background_jobs.jobs.feed.description\"),\n      actions: [],\n    },\n  ];\n\n  return (\n    <div className=\"space-y-6\">\n      <AdminCard className=\"space-y-6\">\n        <div className=\"space-y-2\">\n          <h2 className=\"text-2xl font-semibold tracking-tight\">\n            {t(\"admin.background_jobs.background_jobs\")}\n          </h2>\n          <p className=\"text-muted-foreground\">\n            {t(\"admin.background_jobs.monitor_and_manage\")}\n          </p>\n        </div>\n\n        <JobStatusExplanation />\n      </AdminCard>\n\n      <div className=\"grid gap-6 xl:grid-cols-2\">\n        {jobs.map((job, index) => (\n          <JobCard\n            key={index}\n            title={job.title}\n            icon={job.icon}\n            stats={job.stats}\n            description={job.description}\n            actions={job.actions}\n          />\n        ))}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/admin/BasicStats.tsx",
    "content": "\"use client\";\n\nimport { AdminCard } from \"@/components/admin/AdminCard\";\nimport { useClientConfig } from \"@/lib/clientConfig\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { useQuery } from \"@tanstack/react-query\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\nconst REPO_LATEST_RELEASE_API =\n  \"https://api.github.com/repos/karakeep-app/karakeep/releases/latest\";\nconst REPO_RELEASE_PAGE = \"https://github.com/karakeep-app/karakeep/releases\";\n\nfunction useLatestRelease() {\n  const { data } = useQuery({\n    queryKey: [\"latest-release\"],\n    queryFn: async () => {\n      const res = await fetch(REPO_LATEST_RELEASE_API);\n      if (!res.ok) {\n        return undefined;\n      }\n      const data = (await res.json()) as { name: string };\n      return data.name;\n    },\n    staleTime: 60 * 60 * 1000,\n    enabled: !useClientConfig().disableNewReleaseCheck,\n  });\n  return data;\n}\n\nfunction ReleaseInfo() {\n  const currentRelease = useClientConfig().serverVersion ?? \"NA\";\n  const latestRelease = useLatestRelease();\n\n  let newRelease;\n  if (latestRelease && currentRelease != latestRelease) {\n    newRelease = (\n      // oxlint-disable-next-line no-html-link-for-pages\n      <a\n        href={REPO_RELEASE_PAGE}\n        target=\"_blank\"\n        className=\"text-blue-500\"\n        rel=\"noreferrer\"\n        title=\"Update available\"\n      >\n        ({latestRelease}⬆️)\n      </a>\n    );\n  }\n  return (\n    <div className=\"text-nowrap\">\n      <span className=\"text-3xl font-semibold\">{currentRelease}</span>\n      <span className=\"ml-1 text-sm\">{newRelease}</span>\n    </div>\n  );\n}\n\nfunction StatsSkeleton() {\n  return (\n    <AdminCard>\n      <div className=\"mb-4 h-7 w-32 animate-pulse rounded bg-gray-200 dark:bg-gray-700\"></div>\n      <div className=\"flex flex-col gap-4 sm:flex-row\">\n        {[1, 2, 3].map((i) => (\n          <div key={i} className=\"rounded-md border bg-background p-4 sm:w-1/4\">\n            <div className=\"mb-2 h-4 w-24 animate-pulse rounded bg-gray-200 dark:bg-gray-700\"></div>\n            <div className=\"h-9 w-16 animate-pulse rounded bg-gray-200 dark:bg-gray-700\"></div>\n          </div>\n        ))}\n      </div>\n    </AdminCard>\n  );\n}\n\nexport default function BasicStats() {\n  const api = useTRPC();\n  const { t } = useTranslation();\n  const { data: serverStats } = useQuery(\n    api.admin.stats.queryOptions(undefined, {\n      refetchInterval: 5000,\n    }),\n  );\n\n  if (!serverStats) {\n    return <StatsSkeleton />;\n  }\n\n  return (\n    <AdminCard>\n      <div className=\"mb-4 text-xl font-medium\">\n        {t(\"admin.server_stats.server_stats\")}\n      </div>\n      <div className=\"flex flex-col gap-4 sm:flex-row\">\n        <div className=\"rounded-md border bg-background p-4 sm:w-1/4\">\n          <div className=\"text-sm font-medium text-gray-400\">\n            {t(\"admin.server_stats.total_users\")}\n          </div>\n          <div className=\"text-3xl font-semibold\">{serverStats.numUsers}</div>\n        </div>\n        <div className=\"rounded-md border bg-background p-4 sm:w-1/4\">\n          <div className=\"text-sm font-medium text-gray-400\">\n            {t(\"admin.server_stats.total_bookmarks\")}\n          </div>\n          <div className=\"text-3xl font-semibold\">\n            {serverStats.numBookmarks}\n          </div>\n        </div>\n        <div className=\"rounded-md border bg-background p-4 sm:w-1/4\">\n          <div className=\"text-sm font-medium text-gray-400\">\n            {t(\"admin.server_stats.server_version\")}\n          </div>\n          <ReleaseInfo />\n        </div>\n      </div>\n    </AdminCard>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/admin/BookmarkDebugger.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport Link from \"next/link\";\nimport { AdminCard } from \"@/components/admin/AdminCard\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport InfoTooltip from \"@/components/ui/info-tooltip\";\nimport { Input } from \"@/components/ui/input\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { formatBytes } from \"@/lib/utils\";\nimport { useMutation, useQuery } from \"@tanstack/react-query\";\nimport { formatDistanceToNow } from \"date-fns\";\nimport {\n  AlertCircle,\n  CheckCircle2,\n  ChevronDown,\n  ChevronRight,\n  Clock,\n  Database,\n  ExternalLink,\n  FileText,\n  FileType,\n  Image as ImageIcon,\n  Link as LinkIcon,\n  Loader2,\n  RefreshCw,\n  Search,\n  Sparkles,\n  Tag,\n  User,\n  XCircle,\n} from \"lucide-react\";\nimport { parseAsString, useQueryState } from \"nuqs\";\nimport { toast } from \"sonner\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\nimport { BookmarkTypes } from \"@karakeep/shared/types/bookmarks\";\n\nexport default function BookmarkDebugger() {\n  const api = useTRPC();\n  const { t } = useTranslation();\n  const [inputValue, setInputValue] = useState(\"\");\n  const [bookmarkId, setBookmarkId] = useQueryState(\n    \"bookmarkId\",\n    parseAsString.withDefault(\"\"),\n  );\n  const [showHtmlPreview, setShowHtmlPreview] = useState(false);\n\n  // Sync input value with URL on mount/change\n  useEffect(() => {\n    if (bookmarkId) {\n      setInputValue(bookmarkId);\n    }\n  }, [bookmarkId]);\n\n  const {\n    data: debugInfo,\n    isLoading,\n    error,\n  } = useQuery(\n    api.admin.getBookmarkDebugInfo.queryOptions(\n      { bookmarkId: bookmarkId },\n      { enabled: !!bookmarkId && bookmarkId.length > 0 },\n    ),\n  );\n\n  const handleLookup = () => {\n    if (inputValue.trim()) {\n      setBookmarkId(inputValue.trim());\n    }\n  };\n\n  const recrawlMutation = useMutation(\n    api.admin.adminRecrawlBookmark.mutationOptions({\n      onSuccess: () => {\n        toast.success(t(\"admin.admin_tools.action_success\"), {\n          description: t(\"admin.admin_tools.recrawl_queued\"),\n        });\n      },\n      onError: (error) => {\n        toast.error(t(\"admin.admin_tools.action_failed\"), {\n          description: error.message,\n        });\n      },\n    }),\n  );\n\n  const reindexMutation = useMutation(\n    api.admin.adminReindexBookmark.mutationOptions({\n      onSuccess: () => {\n        toast.success(t(\"admin.admin_tools.action_success\"), {\n          description: t(\"admin.admin_tools.reindex_queued\"),\n        });\n      },\n      onError: (error) => {\n        toast.error(t(\"admin.admin_tools.action_failed\"), {\n          description: error.message,\n        });\n      },\n    }),\n  );\n\n  const retagMutation = useMutation(\n    api.admin.adminRetagBookmark.mutationOptions({\n      onSuccess: () => {\n        toast.success(t(\"admin.admin_tools.action_success\"), {\n          description: t(\"admin.admin_tools.retag_queued\"),\n        });\n      },\n      onError: (error) => {\n        toast.error(t(\"admin.admin_tools.action_failed\"), {\n          description: error.message,\n        });\n      },\n    }),\n  );\n\n  const resummarizeMutation = useMutation(\n    api.admin.adminResummarizeBookmark.mutationOptions({\n      onSuccess: () => {\n        toast.success(t(\"admin.admin_tools.action_success\"), {\n          description: t(\"admin.admin_tools.resummarize_queued\"),\n        });\n      },\n      onError: (error) => {\n        toast.error(t(\"admin.admin_tools.action_failed\"), {\n          description: error.message,\n        });\n      },\n    }),\n  );\n\n  const handleRecrawl = () => {\n    if (bookmarkId) {\n      recrawlMutation.mutate({ bookmarkId });\n    }\n  };\n\n  const handleReindex = () => {\n    if (bookmarkId) {\n      reindexMutation.mutate({ bookmarkId });\n    }\n  };\n\n  const handleRetag = () => {\n    if (bookmarkId) {\n      retagMutation.mutate({ bookmarkId });\n    }\n  };\n\n  const handleResummarize = () => {\n    if (bookmarkId) {\n      resummarizeMutation.mutate({ bookmarkId });\n    }\n  };\n\n  const getStatusBadge = (status: \"pending\" | \"failure\" | \"success\" | null) => {\n    if (!status) return null;\n\n    const config = {\n      success: {\n        variant: \"default\" as const,\n        icon: CheckCircle2,\n      },\n      failure: {\n        variant: \"destructive\" as const,\n        icon: XCircle,\n      },\n      pending: {\n        variant: \"secondary\" as const,\n        icon: AlertCircle,\n      },\n    };\n\n    const { variant, icon: Icon } = config[status];\n\n    return (\n      <Badge variant={variant}>\n        <Icon className=\"mr-1 h-3 w-3\" />\n        {status}\n      </Badge>\n    );\n  };\n\n  return (\n    <div className=\"flex flex-col gap-4\">\n      {/* Input Section */}\n      <AdminCard>\n        <div className=\"mb-3 flex items-center gap-2\">\n          <Search className=\"h-5 w-5 text-muted-foreground\" />\n          <h2 className=\"text-lg font-semibold\">\n            {t(\"admin.admin_tools.bookmark_debugger\")}\n          </h2>\n          <InfoTooltip className=\"text-muted-foreground\" size={16}>\n            Some data will be redacted for privacy.\n          </InfoTooltip>\n        </div>\n        <div className=\"flex gap-2\">\n          <div className=\"relative max-w-md flex-1\">\n            <Database className=\"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground\" />\n            <Input\n              placeholder={t(\"admin.admin_tools.bookmark_id_placeholder\")}\n              value={inputValue}\n              onChange={(e) => setInputValue(e.target.value)}\n              onKeyDown={(e) => {\n                if (e.key === \"Enter\") {\n                  handleLookup();\n                }\n              }}\n              className=\"pl-9\"\n            />\n          </div>\n          <Button onClick={handleLookup} disabled={!inputValue.trim()}>\n            <Search className=\"mr-2 h-4 w-4\" />\n            {t(\"admin.admin_tools.lookup\")}\n          </Button>\n        </div>\n      </AdminCard>\n\n      {/* Loading State */}\n      {isLoading && (\n        <AdminCard>\n          <div className=\"flex items-center justify-center py-8\">\n            <Loader2 className=\"h-8 w-8 animate-spin text-gray-400\" />\n          </div>\n        </AdminCard>\n      )}\n\n      {/* Error State */}\n      {!isLoading && error && (\n        <AdminCard>\n          <div className=\"flex items-center gap-3 rounded-lg border border-destructive/50 bg-destructive/10 p-4\">\n            <XCircle className=\"h-5 w-5 flex-shrink-0 text-destructive\" />\n            <div className=\"flex-1\">\n              <h3 className=\"text-sm font-semibold text-destructive\">\n                {t(\"admin.admin_tools.fetch_error\")}\n              </h3>\n              <p className=\"mt-1 text-sm text-muted-foreground\">\n                {error.message}\n              </p>\n            </div>\n          </div>\n        </AdminCard>\n      )}\n\n      {/* Debug Info Display */}\n      {!isLoading && !error && debugInfo && (\n        <AdminCard>\n          <div className=\"space-y-4\">\n            {/* Basic Info & Status */}\n            <div className=\"grid gap-4 md:grid-cols-2\">\n              {/* Basic Info */}\n              <div className=\"rounded-lg border bg-muted/30 p-4\">\n                <div className=\"mb-3 flex items-center gap-2\">\n                  <Database className=\"h-4 w-4 text-muted-foreground\" />\n                  <h3 className=\"text-sm font-semibold\">\n                    {t(\"admin.admin_tools.basic_info\")}\n                  </h3>\n                </div>\n                <div className=\"space-y-2.5 text-sm\">\n                  <div className=\"flex items-center justify-between gap-2\">\n                    <span className=\"flex items-center gap-1.5 text-muted-foreground\">\n                      <Database className=\"h-3.5 w-3.5\" />\n                      {t(\"common.id\")}\n                    </span>\n                    <span className=\"font-mono text-xs\">{debugInfo.id}</span>\n                  </div>\n                  <div className=\"flex items-center justify-between gap-2\">\n                    <span className=\"flex items-center gap-1.5 text-muted-foreground\">\n                      <FileType className=\"h-3.5 w-3.5\" />\n                      {t(\"common.type\")}\n                    </span>\n                    <Badge variant=\"secondary\">{debugInfo.type}</Badge>\n                  </div>\n                  <div className=\"flex items-center justify-between gap-2\">\n                    <span className=\"flex items-center gap-1.5 text-muted-foreground\">\n                      <LinkIcon className=\"h-3.5 w-3.5\" />\n                      {t(\"common.source\")}\n                    </span>\n                    <span>{debugInfo.source || \"N/A\"}</span>\n                  </div>\n                  <div className=\"flex items-center justify-between gap-2\">\n                    <span className=\"flex items-center gap-1.5 text-muted-foreground\">\n                      <User className=\"h-3.5 w-3.5\" />\n                      {t(\"admin.admin_tools.owner_user_id\")}\n                    </span>\n                    <span className=\"font-mono text-xs\">\n                      {debugInfo.userId}\n                    </span>\n                  </div>\n                  <div className=\"flex items-center justify-between gap-2\">\n                    <span className=\"flex items-center gap-1.5 text-muted-foreground\">\n                      <Clock className=\"h-3.5 w-3.5\" />\n                      {t(\"common.created_at\")}\n                    </span>\n                    <span className=\"text-xs\">\n                      {formatDistanceToNow(new Date(debugInfo.createdAt), {\n                        addSuffix: true,\n                      })}\n                    </span>\n                  </div>\n                  {debugInfo.modifiedAt && (\n                    <div className=\"flex items-center justify-between gap-2\">\n                      <span className=\"flex items-center gap-1.5 text-muted-foreground\">\n                        <Clock className=\"h-3.5 w-3.5\" />\n                        {t(\"common.updated_at\")}\n                      </span>\n                      <span className=\"text-xs\">\n                        {formatDistanceToNow(new Date(debugInfo.modifiedAt), {\n                          addSuffix: true,\n                        })}\n                      </span>\n                    </div>\n                  )}\n                </div>\n              </div>\n\n              {/* Status */}\n              <div className=\"rounded-lg border bg-muted/30 p-4\">\n                <div className=\"mb-3 flex items-center gap-2\">\n                  <AlertCircle className=\"h-4 w-4 text-muted-foreground\" />\n                  <h3 className=\"text-sm font-semibold\">\n                    {t(\"admin.admin_tools.status\")}\n                  </h3>\n                </div>\n                <div className=\"space-y-2.5 text-sm\">\n                  <div className=\"flex items-center justify-between gap-2\">\n                    <span className=\"flex items-center gap-1.5 text-muted-foreground\">\n                      <Tag className=\"h-3.5 w-3.5\" />\n                      {t(\"admin.admin_tools.tagging_status\")}\n                    </span>\n                    {getStatusBadge(debugInfo.taggingStatus)}\n                  </div>\n                  <div className=\"flex items-center justify-between gap-2\">\n                    <span className=\"flex items-center gap-1.5 text-muted-foreground\">\n                      <Sparkles className=\"h-3.5 w-3.5\" />\n                      {t(\"admin.admin_tools.summarization_status\")}\n                    </span>\n                    {getStatusBadge(debugInfo.summarizationStatus)}\n                  </div>\n                  {debugInfo.linkInfo && (\n                    <>\n                      <div className=\"flex items-center justify-between gap-2\">\n                        <span className=\"flex items-center gap-1.5 text-muted-foreground\">\n                          <RefreshCw className=\"h-3.5 w-3.5\" />\n                          {t(\"admin.admin_tools.crawl_status\")}\n                        </span>\n                        {getStatusBadge(debugInfo.linkInfo.crawlStatus)}\n                      </div>\n                      <div className=\"flex items-center justify-between gap-2\">\n                        <span className=\"flex items-center gap-1.5 text-muted-foreground\">\n                          <LinkIcon className=\"h-3.5 w-3.5\" />\n                          {t(\"admin.admin_tools.crawl_status_code\")}\n                        </span>\n                        <Badge\n                          variant={\n                            debugInfo.linkInfo.crawlStatusCode === null ||\n                            (debugInfo.linkInfo.crawlStatusCode >= 200 &&\n                              debugInfo.linkInfo.crawlStatusCode < 300)\n                              ? \"default\"\n                              : \"destructive\"\n                          }\n                        >\n                          {debugInfo.linkInfo.crawlStatusCode}\n                        </Badge>\n                      </div>\n                      {debugInfo.linkInfo.crawledAt && (\n                        <div className=\"flex items-center justify-between gap-2\">\n                          <span className=\"flex items-center gap-1.5 text-muted-foreground\">\n                            <Clock className=\"h-3.5 w-3.5\" />\n                            {t(\"admin.admin_tools.crawled_at\")}\n                          </span>\n                          <span className=\"text-xs\">\n                            {formatDistanceToNow(\n                              new Date(debugInfo.linkInfo.crawledAt),\n                              {\n                                addSuffix: true,\n                              },\n                            )}\n                          </span>\n                        </div>\n                      )}\n                    </>\n                  )}\n                </div>\n              </div>\n            </div>\n\n            {/* Content */}\n            {(debugInfo.title ||\n              debugInfo.summary ||\n              debugInfo.linkInfo ||\n              debugInfo.textInfo?.sourceUrl ||\n              debugInfo.assetInfo) && (\n              <div className=\"rounded-lg border bg-muted/30 p-4\">\n                <div className=\"mb-3 flex items-center gap-2\">\n                  <FileText className=\"h-4 w-4 text-muted-foreground\" />\n                  <h3 className=\"text-sm font-semibold\">\n                    {t(\"admin.admin_tools.content\")}\n                  </h3>\n                </div>\n                <div className=\"space-y-3 text-sm\">\n                  {debugInfo.title && (\n                    <div>\n                      <div className=\"mb-1.5 flex items-center gap-1.5 text-xs font-medium text-muted-foreground\">\n                        <FileText className=\"h-3.5 w-3.5\" />\n                        {t(\"common.title\")}\n                      </div>\n                      <div className=\"rounded border bg-background px-3 py-2 font-medium\">\n                        {debugInfo.title}\n                      </div>\n                    </div>\n                  )}\n                  {debugInfo.summary && (\n                    <div>\n                      <div className=\"mb-1.5 flex items-center gap-1.5 text-xs font-medium text-muted-foreground\">\n                        <Sparkles className=\"h-3.5 w-3.5\" />\n                        {t(\"admin.admin_tools.summary\")}\n                      </div>\n                      <div className=\"rounded border bg-background px-3 py-2\">\n                        {debugInfo.summary}\n                      </div>\n                    </div>\n                  )}\n                  {debugInfo.linkInfo && (\n                    <div>\n                      <div className=\"mb-1.5 flex items-center gap-1.5 text-xs font-medium text-muted-foreground\">\n                        <LinkIcon className=\"h-3.5 w-3.5\" />\n                        {t(\"admin.admin_tools.url\")}\n                      </div>\n                      <Link\n                        prefetch={false}\n                        href={debugInfo.linkInfo.url}\n                        target=\"_blank\"\n                        rel=\"noopener noreferrer\"\n                        className=\"flex items-center gap-1.5 rounded border bg-background px-3 py-2 text-primary hover:underline\"\n                      >\n                        <span className=\"break-all\">\n                          {debugInfo.linkInfo.url}\n                        </span>\n                        <ExternalLink className=\"h-3.5 w-3.5 flex-shrink-0\" />\n                      </Link>\n                    </div>\n                  )}\n                  {debugInfo.textInfo?.sourceUrl && (\n                    <div>\n                      <div className=\"mb-1.5 flex items-center gap-1.5 text-xs font-medium text-muted-foreground\">\n                        <LinkIcon className=\"h-3.5 w-3.5\" />\n                        {t(\"admin.admin_tools.source_url\")}\n                      </div>\n                      <Link\n                        prefetch={false}\n                        href={debugInfo.textInfo.sourceUrl}\n                        target=\"_blank\"\n                        rel=\"noopener noreferrer\"\n                        className=\"flex items-center gap-1.5 rounded border bg-background px-3 py-2 text-primary hover:underline\"\n                      >\n                        <span className=\"break-all\">\n                          {debugInfo.textInfo.sourceUrl}\n                        </span>\n                        <ExternalLink className=\"h-3.5 w-3.5 flex-shrink-0\" />\n                      </Link>\n                    </div>\n                  )}\n                  {debugInfo.assetInfo && (\n                    <div>\n                      <div className=\"mb-1.5 flex items-center gap-1.5 text-xs font-medium text-muted-foreground\">\n                        <ImageIcon className=\"h-3.5 w-3.5\" />\n                        {t(\"admin.admin_tools.asset_type\")}\n                      </div>\n                      <div className=\"rounded border bg-background px-3 py-2\">\n                        <Badge variant=\"secondary\" className=\"mb-1\">\n                          {debugInfo.assetInfo.assetType}\n                        </Badge>\n                        {debugInfo.assetInfo.fileName && (\n                          <div className=\"mt-1 font-mono text-xs text-muted-foreground\">\n                            {debugInfo.assetInfo.fileName}\n                          </div>\n                        )}\n                      </div>\n                    </div>\n                  )}\n                </div>\n              </div>\n            )}\n\n            {/* HTML Preview */}\n            {debugInfo.linkInfo && debugInfo.linkInfo.htmlContentPreview && (\n              <div className=\"rounded-lg border bg-muted/30 p-4\">\n                <button\n                  onClick={() => setShowHtmlPreview(!showHtmlPreview)}\n                  className=\"flex w-full items-center gap-2 text-sm font-semibold hover:opacity-70\"\n                >\n                  {showHtmlPreview ? (\n                    <ChevronDown className=\"h-4 w-4 text-muted-foreground\" />\n                  ) : (\n                    <ChevronRight className=\"h-4 w-4 text-muted-foreground\" />\n                  )}\n                  <FileText className=\"h-4 w-4 text-muted-foreground\" />\n                  {t(\"admin.admin_tools.html_preview\")}\n                </button>\n                {showHtmlPreview && (\n                  <pre className=\"mt-3 max-h-60 overflow-auto rounded-md border bg-muted p-3 text-xs\">\n                    {debugInfo.linkInfo.htmlContentPreview}\n                  </pre>\n                )}\n              </div>\n            )}\n\n            {/* Tags */}\n            {debugInfo.tags.length > 0 && (\n              <div className=\"rounded-lg border bg-muted/30 p-4\">\n                <div className=\"mb-3 flex items-center gap-2\">\n                  <Tag className=\"h-4 w-4 text-muted-foreground\" />\n                  <h3 className=\"text-sm font-semibold\">\n                    {t(\"common.tags\")}{\" \"}\n                    <span className=\"text-muted-foreground\">\n                      ({debugInfo.tags.length})\n                    </span>\n                  </h3>\n                </div>\n                <div className=\"flex flex-wrap gap-2\">\n                  {debugInfo.tags.map((tag) => (\n                    <Badge\n                      key={tag.id}\n                      variant={\n                        tag.attachedBy === \"ai\" ? \"default\" : \"secondary\"\n                      }\n                      className=\"gap-1.5\"\n                    >\n                      {tag.attachedBy === \"ai\" && (\n                        <Sparkles className=\"h-3 w-3\" />\n                      )}\n                      <span>{tag.name}</span>\n                    </Badge>\n                  ))}\n                </div>\n              </div>\n            )}\n\n            {/* Assets */}\n            {debugInfo.assets.length > 0 && (\n              <div className=\"rounded-lg border bg-muted/30 p-4\">\n                <div className=\"mb-3 flex items-center gap-2\">\n                  <ImageIcon className=\"h-4 w-4 text-muted-foreground\" />\n                  <h3 className=\"text-sm font-semibold\">\n                    {t(\"common.attachments\")}{\" \"}\n                    <span className=\"text-muted-foreground\">\n                      ({debugInfo.assets.length})\n                    </span>\n                  </h3>\n                </div>\n                <div className=\"space-y-2 text-sm\">\n                  {debugInfo.assets.map((asset) => (\n                    <div\n                      key={asset.id}\n                      className=\"flex items-center justify-between rounded-md border bg-background p-3\"\n                    >\n                      <div className=\"flex items-center gap-3\">\n                        <ImageIcon className=\"h-4 w-4 text-muted-foreground\" />\n                        <div>\n                          <Badge variant=\"secondary\" className=\"text-xs\">\n                            {asset.assetType}\n                          </Badge>\n                          <div className=\"mt-1 text-xs text-muted-foreground\">\n                            {formatBytes(asset.size)}\n                          </div>\n                        </div>\n                      </div>\n                      {asset.url && (\n                        <Link\n                          prefetch={false}\n                          href={asset.url}\n                          target=\"_blank\"\n                          rel=\"noopener noreferrer\"\n                          className=\"flex items-center gap-1.5 text-primary hover:underline\"\n                        >\n                          {t(\"admin.admin_tools.view\")}\n                          <ExternalLink className=\"h-3.5 w-3.5\" />\n                        </Link>\n                      )}\n                    </div>\n                  ))}\n                </div>\n              </div>\n            )}\n\n            {/* Actions */}\n            <div className=\"rounded-lg border border-dashed bg-muted/20 p-4\">\n              <div className=\"mb-3 flex items-center gap-2\">\n                <RefreshCw className=\"h-4 w-4 text-muted-foreground\" />\n                <h3 className=\"text-sm font-semibold\">{t(\"common.actions\")}</h3>\n              </div>\n              <div className=\"flex flex-wrap gap-2\">\n                <Button\n                  onClick={handleRecrawl}\n                  disabled={\n                    debugInfo.type !== BookmarkTypes.LINK ||\n                    recrawlMutation.isPending\n                  }\n                  size=\"sm\"\n                  variant=\"outline\"\n                >\n                  {recrawlMutation.isPending ? (\n                    <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n                  ) : (\n                    <RefreshCw className=\"mr-2 h-4 w-4\" />\n                  )}\n                  {t(\"admin.admin_tools.recrawl\")}\n                </Button>\n                <Button\n                  onClick={handleReindex}\n                  disabled={reindexMutation.isPending}\n                  size=\"sm\"\n                  variant=\"outline\"\n                >\n                  {reindexMutation.isPending ? (\n                    <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n                  ) : (\n                    <Search className=\"mr-2 h-4 w-4\" />\n                  )}\n                  {t(\"admin.admin_tools.reindex\")}\n                </Button>\n                <Button\n                  onClick={handleRetag}\n                  disabled={retagMutation.isPending}\n                  size=\"sm\"\n                  variant=\"outline\"\n                >\n                  {retagMutation.isPending ? (\n                    <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n                  ) : (\n                    <Tag className=\"mr-2 h-4 w-4\" />\n                  )}\n                  {t(\"admin.admin_tools.retag\")}\n                </Button>\n                <Button\n                  onClick={handleResummarize}\n                  disabled={\n                    debugInfo.type !== BookmarkTypes.LINK ||\n                    resummarizeMutation.isPending\n                  }\n                  size=\"sm\"\n                  variant=\"outline\"\n                >\n                  {resummarizeMutation.isPending ? (\n                    <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n                  ) : (\n                    <Sparkles className=\"mr-2 h-4 w-4\" />\n                  )}\n                  {t(\"admin.admin_tools.resummarize\")}\n                </Button>\n              </div>\n            </div>\n          </div>\n        </AdminCard>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/admin/CreateInviteDialog.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@/components/ui/dialog\";\nimport {\n  Form,\n  FormControl,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@/components/ui/form\";\nimport { Input } from \"@/components/ui/input\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { useMutation, useQueryClient } from \"@tanstack/react-query\";\nimport { TRPCClientError } from \"@trpc/client\";\nimport { useForm } from \"react-hook-form\";\nimport { z } from \"zod\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\nconst createInviteSchema = z.object({\n  email: z.string().email(\"Please enter a valid email address\"),\n});\n\ninterface CreateInviteDialogProps {\n  children: React.ReactNode;\n}\n\nexport default function CreateInviteDialog({\n  children,\n}: CreateInviteDialogProps) {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n  const [open, setOpen] = useState(false);\n  const [errorMessage, setErrorMessage] = useState(\"\");\n\n  const form = useForm<z.infer<typeof createInviteSchema>>({\n    resolver: zodResolver(createInviteSchema),\n    defaultValues: {\n      email: \"\",\n    },\n  });\n\n  const createInviteMutation = useMutation(\n    api.invites.create.mutationOptions({\n      onSuccess: () => {\n        toast({\n          description: \"Invite sent successfully\",\n        });\n        queryClient.invalidateQueries(api.invites.list.pathFilter());\n        setOpen(false);\n        form.reset();\n        setErrorMessage(\"\");\n      },\n      onError: (e) => {\n        if (e instanceof TRPCClientError) {\n          setErrorMessage(e.message);\n        } else {\n          setErrorMessage(\"Failed to send invite\");\n        }\n      },\n    }),\n  );\n\n  return (\n    <Dialog open={open} onOpenChange={setOpen}>\n      <DialogTrigger asChild>{children}</DialogTrigger>\n      <DialogContent className=\"sm:max-w-md\">\n        <DialogHeader>\n          <DialogTitle>Send User Invitation</DialogTitle>\n          <DialogDescription>\n            Send an invitation to a new user to join Karakeep. They&apos;ll\n            receive an email with instructions to create their account and will\n            be assigned the &quot;user&quot; role.\n          </DialogDescription>\n        </DialogHeader>\n\n        <Form {...form}>\n          <form\n            onSubmit={form.handleSubmit(async (value) => {\n              setErrorMessage(\"\");\n              await createInviteMutation.mutateAsync(value);\n            })}\n            className=\"space-y-4\"\n          >\n            {errorMessage && (\n              <p className=\"text-sm text-destructive\">{errorMessage}</p>\n            )}\n\n            <FormField\n              control={form.control}\n              name=\"email\"\n              render={({ field }) => (\n                <FormItem>\n                  <FormLabel>Email Address</FormLabel>\n                  <FormControl>\n                    <Input\n                      type=\"email\"\n                      placeholder=\"user@example.com\"\n                      {...field}\n                    />\n                  </FormControl>\n                  <FormMessage />\n                </FormItem>\n              )}\n            />\n\n            <div className=\"flex justify-end space-x-2\">\n              <ActionButton\n                type=\"button\"\n                variant=\"outline\"\n                loading={false}\n                onClick={() => setOpen(false)}\n              >\n                Cancel\n              </ActionButton>\n              <ActionButton\n                type=\"submit\"\n                loading={createInviteMutation.isPending}\n              >\n                Send Invitation\n              </ActionButton>\n            </div>\n          </form>\n        </Form>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/admin/InvitesList.tsx",
    "content": "\"use client\";\n\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport { ButtonWithTooltip } from \"@/components/ui/button\";\nimport { toast } from \"@/components/ui/sonner\";\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@/components/ui/table\";\nimport {\n  useMutation,\n  useQueryClient,\n  useSuspenseQuery,\n} from \"@tanstack/react-query\";\nimport { formatDistanceToNow } from \"date-fns\";\nimport { Mail, MailX, UserPlus } from \"lucide-react\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\nimport ActionConfirmingDialog from \"../ui/action-confirming-dialog\";\nimport { AdminCard } from \"./AdminCard\";\nimport CreateInviteDialog from \"./CreateInviteDialog\";\n\nexport default function InvitesList() {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n  const { data: invites } = useSuspenseQuery(api.invites.list.queryOptions());\n\n  const { mutateAsync: revokeInvite, isPending: isRevokePending } = useMutation(\n    api.invites.revoke.mutationOptions({\n      onSuccess: () => {\n        toast({\n          description: \"Invite revoked successfully\",\n        });\n        queryClient.invalidateQueries(api.invites.list.pathFilter());\n      },\n      onError: (e) => {\n        toast({\n          variant: \"destructive\",\n          description: `Failed to revoke invite: ${e.message}`,\n        });\n      },\n    }),\n  );\n\n  const { mutateAsync: resendInvite, isPending: isResendPending } = useMutation(\n    api.invites.resend.mutationOptions({\n      onSuccess: () => {\n        toast({\n          description: \"Invite resent successfully\",\n        });\n        queryClient.invalidateQueries(api.invites.list.pathFilter());\n      },\n      onError: (e) => {\n        toast({\n          variant: \"destructive\",\n          description: `Failed to resend invite: ${e.message}`,\n        });\n      },\n    }),\n  );\n\n  const activeInvites = invites?.invites || [];\n\n  const InviteTable = ({\n    invites: inviteList,\n    title,\n  }: {\n    invites: NonNullable<typeof invites>[\"invites\"];\n    title: string;\n  }) => (\n    <div className=\"mb-6\">\n      {inviteList.length === 0 ? (\n        <p className=\"text-sm text-gray-500\">\n          No {title.toLowerCase()} invites\n        </p>\n      ) : (\n        <Table>\n          <TableHeader className=\"bg-gray-200\">\n            <TableHead>Email</TableHead>\n            <TableHead>Invited By</TableHead>\n            <TableHead>Created</TableHead>\n            <TableHead>Actions</TableHead>\n          </TableHeader>\n          <TableBody>\n            {inviteList.map((invite) => (\n              <TableRow key={invite.id}>\n                <TableCell className=\"py-2\">{invite.email}</TableCell>\n                <TableCell className=\"py-2\">{invite.invitedBy.name}</TableCell>\n                <TableCell className=\"py-2\">\n                  {formatDistanceToNow(new Date(invite.createdAt), {\n                    addSuffix: true,\n                  })}\n                </TableCell>\n                <TableCell className=\"flex gap-1 py-2\">\n                  {\n                    <>\n                      <ButtonWithTooltip\n                        tooltip=\"Resend Invite\"\n                        variant=\"outline\"\n                        size=\"sm\"\n                        onClick={() => resendInvite({ inviteId: invite.id })}\n                        disabled={isResendPending}\n                      >\n                        <Mail size={14} />\n                      </ButtonWithTooltip>\n                      <ActionConfirmingDialog\n                        title=\"Revoke Invite\"\n                        description={`Are you sure you want to revoke the invite for ${invite.email}? This action cannot be undone.`}\n                        actionButton={(setDialogOpen) => (\n                          <ActionButton\n                            variant=\"destructive\"\n                            loading={isRevokePending}\n                            onClick={async () => {\n                              await revokeInvite({ inviteId: invite.id });\n                              setDialogOpen(false);\n                            }}\n                          >\n                            Revoke\n                          </ActionButton>\n                        )}\n                      >\n                        <ButtonWithTooltip\n                          tooltip=\"Revoke Invite\"\n                          variant=\"outline\"\n                          size=\"sm\"\n                        >\n                          <MailX size={14} color=\"red\" />\n                        </ButtonWithTooltip>\n                      </ActionConfirmingDialog>\n                    </>\n                  }\n                </TableCell>\n              </TableRow>\n            ))}\n          </TableBody>\n        </Table>\n      )}\n    </div>\n  );\n\n  return (\n    <AdminCard>\n      <div className=\"flex flex-col gap-4\">\n        <div className=\"mb-2 flex items-center justify-between text-xl font-medium\">\n          <span>User Invitations ({activeInvites.length})</span>\n          <CreateInviteDialog>\n            <ButtonWithTooltip tooltip=\"Send Invite\" variant=\"outline\">\n              <UserPlus size={16} />\n            </ButtonWithTooltip>\n          </CreateInviteDialog>\n        </div>\n\n        <InviteTable invites={activeInvites} title=\"Invites\" />\n      </div>\n    </AdminCard>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/admin/InvitesListSkeleton.tsx",
    "content": "import { AdminCard } from \"@/components/admin/AdminCard\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@/components/ui/table\";\n\nconst headerWidths = [\"w-40\", \"w-28\", \"w-20\", \"w-20\"];\n\nexport default function InvitesListSkeleton() {\n  return (\n    <AdminCard>\n      <div className=\"flex flex-col gap-4\">\n        <div className=\"mb-2 flex items-center justify-between\">\n          <Skeleton className=\"h-6 w-48\" />\n          <Skeleton className=\"h-9 w-9\" />\n        </div>\n\n        <Table>\n          <TableHeader>\n            <TableRow>\n              {headerWidths.map((width, index) => (\n                <TableHead key={`invite-list-header-${index}`}>\n                  <Skeleton className={`h-4 ${width}`} />\n                </TableHead>\n              ))}\n            </TableRow>\n          </TableHeader>\n          <TableBody>\n            {Array.from({ length: 2 }).map((_, rowIndex) => (\n              <TableRow key={`invite-list-row-${rowIndex}`}>\n                {headerWidths.map((width, cellIndex) => (\n                  <TableCell key={`invite-list-cell-${rowIndex}-${cellIndex}`}>\n                    {cellIndex === headerWidths.length - 1 ? (\n                      <div className=\"flex gap-2\">\n                        <Skeleton className=\"h-6 w-6\" />\n                        <Skeleton className=\"h-6 w-6\" />\n                      </div>\n                    ) : (\n                      <Skeleton className={`h-4 ${width}`} />\n                    )}\n                  </TableCell>\n                ))}\n              </TableRow>\n            ))}\n          </TableBody>\n        </Table>\n      </div>\n    </AdminCard>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/admin/ResetPasswordDialog.tsx",
    "content": "import { useEffect, useState } from \"react\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@/components/ui/dialog\";\nimport {\n  Form,\n  FormControl,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@/components/ui/form\";\nimport { Input } from \"@/components/ui/input\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { useMutation } from \"@tanstack/react-query\";\nimport { TRPCClientError } from \"@trpc/client\";\nimport { useForm } from \"react-hook-form\";\nimport { z } from \"zod\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\"; // Adjust the import path as needed\n\nimport { resetPasswordSchema } from \"@karakeep/shared/types/admin\";\n\ninterface ResetPasswordDialogProps {\n  userId: string;\n  children?: React.ReactNode;\n}\n\ntype ResetPasswordSchema = z.infer<typeof resetPasswordSchema>;\n\nexport default function ResetPasswordDialog({\n  children,\n  userId,\n}: ResetPasswordDialogProps) {\n  const api = useTRPC();\n  const [isOpen, onOpenChange] = useState(false);\n  const form = useForm<ResetPasswordSchema>({\n    resolver: zodResolver(resetPasswordSchema),\n    defaultValues: {\n      userId,\n      newPassword: \"\",\n      newPasswordConfirm: \"\",\n    },\n  });\n  const { mutate, isPending } = useMutation(\n    api.admin.resetPassword.mutationOptions({\n      onSuccess: () => {\n        toast({\n          description: \"Password reset successfully\",\n        });\n        onOpenChange(false);\n      },\n      onError: (error) => {\n        if (error instanceof TRPCClientError) {\n          toast({\n            variant: \"destructive\",\n            description: error.message,\n          });\n        } else {\n          toast({\n            variant: \"destructive\",\n            description: \"Failed to reset password\",\n          });\n        }\n      },\n    }),\n  );\n\n  useEffect(() => {\n    if (isOpen) {\n      form.reset();\n    }\n  }, [isOpen, form]);\n\n  return (\n    <Dialog open={isOpen} onOpenChange={onOpenChange}>\n      <DialogTrigger asChild>{children}</DialogTrigger>\n      <DialogContent>\n        <DialogHeader>\n          <DialogTitle>Reset Password</DialogTitle>\n        </DialogHeader>\n        <Form {...form}>\n          <form onSubmit={form.handleSubmit((val) => mutate(val))}>\n            <div className=\"flex w-full flex-col space-y-2\">\n              <FormField\n                control={form.control}\n                name=\"newPassword\"\n                render={({ field }) => (\n                  <FormItem>\n                    <FormLabel>New Password</FormLabel>\n                    <FormControl>\n                      <Input\n                        type=\"password\"\n                        placeholder=\"New Password\"\n                        {...field}\n                        className=\"w-full rounded border p-2\"\n                      />\n                    </FormControl>\n                    <FormMessage />\n                  </FormItem>\n                )}\n              />\n              <FormField\n                control={form.control}\n                name=\"newPasswordConfirm\"\n                render={({ field }) => (\n                  <FormItem>\n                    <FormLabel>Confirm New Password</FormLabel>\n                    <FormControl>\n                      <Input\n                        type=\"password\"\n                        placeholder=\"Confirm New Password\"\n                        {...field}\n                        className=\"w-full rounded border p-2\"\n                      />\n                    </FormControl>\n                    <FormMessage />\n                  </FormItem>\n                )}\n              />\n              <DialogFooter className=\"sm:justify-end\">\n                <DialogClose asChild>\n                  <Button type=\"button\" variant=\"secondary\">\n                    Close\n                  </Button>\n                </DialogClose>\n                <ActionButton\n                  type=\"submit\"\n                  loading={isPending}\n                  disabled={isPending}\n                >\n                  Reset\n                </ActionButton>\n              </DialogFooter>\n            </div>\n          </form>\n        </Form>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/admin/ServiceConnections.tsx",
    "content": "\"use client\";\n\nimport { AdminCard } from \"@/components/admin/AdminCard\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { useQuery } from \"@tanstack/react-query\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\nfunction ConnectionStatus({\n  label,\n  configured,\n  connected,\n  pluginName,\n  error,\n}: {\n  label: string;\n  configured: boolean;\n  connected: boolean;\n  pluginName?: string;\n  error?: string;\n}) {\n  const { t } = useTranslation();\n\n  let statusText = t(\"admin.service_connections.status.not_configured\");\n  let badgeColor =\n    \"bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400\";\n  let iconColor = \"text-gray-400\";\n  let borderColor = \"border-gray-200 dark:border-gray-700\";\n\n  if (configured) {\n    if (connected) {\n      statusText = t(\"admin.service_connections.status.connected\");\n      badgeColor =\n        \"bg-green-50 text-green-700 dark:bg-green-900/20 dark:text-green-400\";\n      iconColor = \"text-green-500\";\n      borderColor = \"border-green-200 dark:border-green-800\";\n    } else {\n      statusText = t(\"admin.service_connections.status.disconnected\");\n      badgeColor =\n        \"bg-red-50 text-red-700 dark:bg-red-900/20 dark:text-red-400\";\n      iconColor = \"text-red-500\";\n      borderColor = \"border-red-200 dark:border-red-800\";\n    }\n  }\n\n  return (\n    <div\n      className={`rounded-lg border ${borderColor} bg-background p-5 shadow-sm transition-all sm:w-1/3`}\n    >\n      <div className=\"mb-3 flex items-center justify-between\">\n        <div>\n          <div className=\"text-base font-semibold\">{label}</div>\n          {pluginName && (\n            <div className=\"mt-1 text-xs text-muted-foreground\">\n              {pluginName}\n            </div>\n          )}\n        </div>\n        <div\n          className={`flex h-2 w-2 items-center justify-center rounded-full ${iconColor}`}\n        >\n          <div\n            className={`h-2 w-2 rounded-full ${connected && configured ? \"animate-pulse\" : \"\"} bg-current`}\n          ></div>\n        </div>\n      </div>\n      <div className=\"mb-2\">\n        <span\n          className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${badgeColor}`}\n        >\n          {statusText}\n        </span>\n      </div>\n      {error && (\n        <div className=\"mt-3 rounded-md bg-red-50 p-2 dark:bg-red-900/10\">\n          <p className=\"text-xs text-red-600 dark:text-red-400\" title={error}>\n            {error.length > 60 ? `${error.substring(0, 60)}...` : error}\n          </p>\n        </div>\n      )}\n    </div>\n  );\n}\n\nfunction ConnectionsSkeleton() {\n  return (\n    <AdminCard>\n      <div className=\"mb-4 h-7 w-40 animate-pulse rounded bg-gray-200 dark:bg-gray-700\"></div>\n      <div className=\"flex flex-col gap-4 sm:flex-row\">\n        {[1, 2, 3].map((i) => (\n          <div\n            key={i}\n            className=\"rounded-lg border border-gray-200 bg-background p-5 shadow-sm dark:border-gray-700 sm:w-1/3\"\n          >\n            <div className=\"mb-3 flex items-center justify-between\">\n              <div className=\"h-5 w-28 animate-pulse rounded bg-gray-200 dark:bg-gray-700\"></div>\n              <div className=\"h-2 w-2 animate-pulse rounded-full bg-gray-300 dark:bg-gray-600\"></div>\n            </div>\n            <div className=\"mb-2\">\n              <div className=\"h-5 w-20 animate-pulse rounded-full bg-gray-200 dark:bg-gray-700\"></div>\n            </div>\n          </div>\n        ))}\n      </div>\n    </AdminCard>\n  );\n}\n\nexport default function ServiceConnections() {\n  const api = useTRPC();\n  const { t } = useTranslation();\n  const { data: connections } = useQuery(\n    api.admin.checkConnections.queryOptions(undefined, {\n      refetchInterval: 10000,\n    }),\n  );\n\n  if (!connections) {\n    return <ConnectionsSkeleton />;\n  }\n\n  return (\n    <AdminCard>\n      <div className=\"mb-2 text-xl font-medium\">\n        {t(\"admin.service_connections.title\")}\n      </div>\n      <p className=\"mb-4 text-sm text-muted-foreground\">\n        {t(\"admin.service_connections.description\")}\n      </p>\n      <div className=\"flex flex-col gap-4 sm:flex-row\">\n        <ConnectionStatus\n          label={t(\"admin.service_connections.search_engine\")}\n          configured={connections.searchEngine.configured}\n          connected={connections.searchEngine.connected}\n          pluginName={connections.searchEngine.pluginName}\n          error={connections.searchEngine.error}\n        />\n        <ConnectionStatus\n          label={t(\"admin.service_connections.browser\")}\n          configured={connections.browser.configured}\n          connected={connections.browser.connected}\n          pluginName={connections.browser.pluginName}\n          error={connections.browser.error}\n        />\n        <ConnectionStatus\n          label={t(\"admin.service_connections.queue_system\")}\n          configured={connections.queue.configured}\n          connected={connections.queue.connected}\n          pluginName={connections.queue.pluginName}\n          error={connections.queue.error}\n        />\n      </div>\n    </AdminCard>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/admin/UpdateUserDialog.tsx",
    "content": "import { useEffect, useState } from \"react\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@/components/ui/dialog\";\nimport {\n  Form,\n  FormControl,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@/components/ui/form\";\nimport { Input } from \"@/components/ui/input\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { useMutation, useQueryClient } from \"@tanstack/react-query\";\nimport { TRPCClientError } from \"@trpc/client\";\nimport { useForm } from \"react-hook-form\";\nimport { z } from \"zod\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\nimport { updateUserSchema } from \"@karakeep/shared/types/admin\";\n\ntype UpdateUserSchema = z.infer<typeof updateUserSchema>;\n\ninterface UpdateUserDialogProps {\n  userId: string;\n  currentRole: \"user\" | \"admin\";\n  currentQuota: number | null;\n  currentStorageQuota: number | null;\n  children?: React.ReactNode;\n}\nexport default function UpdateUserDialog({\n  userId,\n  currentRole,\n  currentQuota,\n  currentStorageQuota,\n  children,\n}: UpdateUserDialogProps) {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n  const [isOpen, onOpenChange] = useState(false);\n  const defaultValues = {\n    userId,\n    role: currentRole,\n    bookmarkQuota: currentQuota,\n    storageQuota: currentStorageQuota,\n  };\n  const form = useForm<UpdateUserSchema>({\n    resolver: zodResolver(updateUserSchema),\n    defaultValues,\n  });\n  const { mutate, isPending } = useMutation(\n    api.admin.updateUser.mutationOptions({\n      onSuccess: () => {\n        toast({\n          description: \"User updated successfully\",\n        });\n        queryClient.invalidateQueries(api.users.list.pathFilter());\n        onOpenChange(false);\n      },\n      onError: (error) => {\n        if (error instanceof TRPCClientError) {\n          toast({\n            variant: \"destructive\",\n            description: error.message,\n          });\n        } else {\n          toast({\n            variant: \"destructive\",\n            description: \"Failed to update user\",\n          });\n        }\n      },\n    }),\n  );\n\n  useEffect(() => {\n    if (isOpen) {\n      form.reset(defaultValues);\n    }\n  }, [isOpen, form]);\n\n  return (\n    <Dialog open={isOpen} onOpenChange={onOpenChange}>\n      <DialogTrigger asChild>{children}</DialogTrigger>\n      <DialogContent>\n        <DialogTrigger asChild></DialogTrigger>\n        <DialogHeader>\n          <DialogTitle>Edit User</DialogTitle>\n        </DialogHeader>\n        <Form {...form}>\n          <form onSubmit={form.handleSubmit((val) => mutate(val))}>\n            <div className=\"flex w-full flex-col space-y-4\">\n              <FormField\n                control={form.control}\n                name=\"role\"\n                render={({ field }) => (\n                  <FormItem>\n                    <FormLabel>Role</FormLabel>\n                    <FormControl>\n                      <Select\n                        onValueChange={field.onChange}\n                        defaultValue={field.value}\n                      >\n                        <SelectTrigger className=\"w-full\">\n                          <SelectValue />\n                        </SelectTrigger>\n                        <SelectContent>\n                          <SelectItem value=\"user\">User</SelectItem>\n                          <SelectItem value=\"admin\">Admin</SelectItem>\n                        </SelectContent>\n                      </Select>\n                    </FormControl>\n                    <FormMessage />\n                  </FormItem>\n                )}\n              />\n              <FormField\n                control={form.control}\n                name=\"bookmarkQuota\"\n                render={({ field }) => (\n                  <FormItem>\n                    <FormLabel>Bookmark Quota</FormLabel>\n                    <FormControl>\n                      <Input\n                        type=\"number\"\n                        min=\"0\"\n                        placeholder=\"Unlimited (leave empty)\"\n                        {...field}\n                        value={field.value ?? \"\"}\n                        onChange={(e) => {\n                          const value = e.target.value;\n                          field.onChange(value === \"\" ? null : parseInt(value));\n                        }}\n                      />\n                    </FormControl>\n                    <FormMessage />\n                  </FormItem>\n                )}\n              />\n              <FormField\n                control={form.control}\n                name=\"storageQuota\"\n                render={({ field }) => (\n                  <FormItem>\n                    <FormLabel>Storage Quota (bytes)</FormLabel>\n                    <FormControl>\n                      <Input\n                        type=\"number\"\n                        min=\"0\"\n                        placeholder=\"Unlimited (leave empty)\"\n                        {...field}\n                        value={field.value ?? \"\"}\n                        onChange={(e) => {\n                          const value = e.target.value;\n                          field.onChange(value === \"\" ? null : parseInt(value));\n                        }}\n                      />\n                    </FormControl>\n                    <FormMessage />\n                  </FormItem>\n                )}\n              />\n              <FormField\n                control={form.control}\n                name=\"userId\"\n                render={({ field }) => (\n                  <FormItem>\n                    <FormControl>\n                      <input type=\"hidden\" {...field} />\n                    </FormControl>\n                  </FormItem>\n                )}\n              />\n              <DialogFooter className=\"sm:justify-end\">\n                <DialogClose asChild>\n                  <Button type=\"button\" variant=\"secondary\">\n                    Close\n                  </Button>\n                </DialogClose>\n                <ActionButton\n                  type=\"submit\"\n                  loading={isPending}\n                  disabled={isPending}\n                >\n                  Update\n                </ActionButton>\n              </DialogFooter>\n            </div>\n          </form>\n        </Form>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/admin/UserList.tsx",
    "content": "\"use client\";\n\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport { ButtonWithTooltip } from \"@/components/ui/button\";\nimport { toast } from \"@/components/ui/sonner\";\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@/components/ui/table\";\nimport { useSession } from \"@/lib/auth/client\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport {\n  useMutation,\n  useQueryClient,\n  useSuspenseQuery,\n} from \"@tanstack/react-query\";\nimport { Check, KeyRound, Pencil, Trash, UserPlus, X } from \"lucide-react\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\nimport ActionConfirmingDialog from \"../ui/action-confirming-dialog\";\nimport AddUserDialog from \"./AddUserDialog\";\nimport { AdminCard } from \"./AdminCard\";\nimport ResetPasswordDialog from \"./ResetPasswordDialog\";\nimport UpdateUserDialog from \"./UpdateUserDialog\";\n\nfunction toHumanReadableSize(size: number) {\n  const sizes = [\"Bytes\", \"KB\", \"MB\", \"GB\", \"TB\"];\n  if (size === 0) return \"0 Bytes\";\n  const i = Math.floor(Math.log(size) / Math.log(1024));\n  return (size / Math.pow(1024, i)).toFixed(2) + \" \" + sizes[i];\n}\n\nexport default function UsersSection() {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n  const { t } = useTranslation();\n  const { data: session } = useSession();\n  const {\n    data: { users },\n  } = useSuspenseQuery(api.users.list.queryOptions());\n  const { data: userStats } = useSuspenseQuery(\n    api.admin.userStats.queryOptions(),\n  );\n  const { mutateAsync: deleteUser, isPending: isDeletionPending } = useMutation(\n    api.users.delete.mutationOptions({\n      onSuccess: () => {\n        toast({\n          description: \"User deleted\",\n        });\n        queryClient.invalidateQueries(api.users.list.pathFilter());\n      },\n      onError: (e) => {\n        toast({\n          variant: \"destructive\",\n          description: `Something went wrong: ${e.message}`,\n        });\n      },\n    }),\n  );\n\n  return (\n    <AdminCard>\n      <div className=\"flex flex-col gap-4\">\n        <div className=\"mb-2 flex items-center justify-between text-xl font-medium\">\n          <span>{t(\"admin.users_list.users_list\")}</span>\n          <AddUserDialog>\n            <ButtonWithTooltip tooltip=\"Create User\" variant=\"outline\">\n              <UserPlus size={16} />\n            </ButtonWithTooltip>\n          </AddUserDialog>\n        </div>\n\n        <Table>\n          <TableHeader className=\"bg-gray-200\">\n            <TableRow>\n              <TableHead>{t(\"common.name\")}</TableHead>\n              <TableHead>{t(\"common.email\")}</TableHead>\n              <TableHead>{t(\"admin.users_list.num_bookmarks\")}</TableHead>\n              <TableHead>{t(\"admin.users_list.asset_sizes\")}</TableHead>\n              <TableHead>{t(\"common.role\")}</TableHead>\n              <TableHead>{t(\"admin.users_list.local_user\")}</TableHead>\n              <TableHead>{t(\"common.actions\")}</TableHead>\n            </TableRow>\n          </TableHeader>\n          <TableBody>\n            {users.map((u) => (\n              <TableRow key={u.id}>\n                <TableCell className=\"py-1\">{u.name}</TableCell>\n                <TableCell className=\"py-1\">{u.email}</TableCell>\n                <TableCell className=\"py-1\">\n                  {userStats[u.id].numBookmarks} /{\" \"}\n                  {u.bookmarkQuota ?? t(\"admin.users_list.unlimited\")}\n                </TableCell>\n                <TableCell className=\"py-1\">\n                  {toHumanReadableSize(userStats[u.id].assetSizes)} /{\" \"}\n                  {u.storageQuota\n                    ? toHumanReadableSize(u.storageQuota)\n                    : t(\"admin.users_list.unlimited\")}\n                </TableCell>\n                <TableCell className=\"py-1\">\n                  {u.role && t(`common.roles.${u.role}`)}\n                </TableCell>\n                <TableCell className=\"py-1\">\n                  {u.localUser ? <Check /> : <X />}\n                </TableCell>\n                <TableCell className=\"flex gap-1 py-1\">\n                  <ActionConfirmingDialog\n                    title={t(\"admin.users_list.delete_user\")}\n                    description={t(\n                      \"admin.users_list.delete_user_confirm_description\",\n                      {\n                        name: u.name ?? \"this user\",\n                      },\n                    )}\n                    actionButton={(setDialogOpen) => (\n                      <ActionButton\n                        variant=\"destructive\"\n                        loading={isDeletionPending}\n                        onClick={async () => {\n                          await deleteUser({ userId: u.id });\n                          setDialogOpen(false);\n                        }}\n                      >\n                        Delete\n                      </ActionButton>\n                    )}\n                  >\n                    <ButtonWithTooltip\n                      tooltip={t(\"admin.users_list.delete_user\")}\n                      variant=\"outline\"\n                      disabled={session!.user.id == u.id}\n                    >\n                      <Trash size={16} color=\"red\" />\n                    </ButtonWithTooltip>\n                  </ActionConfirmingDialog>\n                  <ResetPasswordDialog userId={u.id}>\n                    <ButtonWithTooltip\n                      tooltip={t(\"admin.users_list.reset_password\")}\n                      variant=\"outline\"\n                      disabled={session!.user.id == u.id || !u.localUser}\n                    >\n                      <KeyRound size={16} color=\"red\" />\n                    </ButtonWithTooltip>\n                  </ResetPasswordDialog>\n                  <UpdateUserDialog\n                    userId={u.id}\n                    currentRole={u.role!}\n                    currentQuota={u.bookmarkQuota}\n                    currentStorageQuota={u.storageQuota}\n                  >\n                    <ButtonWithTooltip\n                      tooltip=\"Edit User\"\n                      variant=\"outline\"\n                      disabled={session!.user.id == u.id}\n                    >\n                      <Pencil size={16} color=\"red\" />\n                    </ButtonWithTooltip>\n                  </UpdateUserDialog>\n                </TableCell>\n              </TableRow>\n            ))}\n          </TableBody>\n        </Table>\n      </div>\n    </AdminCard>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/admin/UserListSkeleton.tsx",
    "content": "import { AdminCard } from \"@/components/admin/AdminCard\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@/components/ui/table\";\n\nconst headerWidths = [\"w-24\", \"w-32\", \"w-28\", \"w-28\", \"w-20\", \"w-16\", \"w-24\"];\n\nexport default function UserListSkeleton() {\n  return (\n    <AdminCard>\n      <div className=\"flex flex-col gap-4\">\n        <div className=\"mb-2 flex items-center justify-between\">\n          <Skeleton className=\"h-6 w-40\" />\n          <Skeleton className=\"h-9 w-9\" />\n        </div>\n\n        <Table>\n          <TableHeader>\n            <TableRow>\n              {headerWidths.map((width, index) => (\n                <TableHead key={`user-list-header-${index}`}>\n                  <Skeleton className={`h-4 ${width}`} />\n                </TableHead>\n              ))}\n            </TableRow>\n          </TableHeader>\n          <TableBody>\n            {Array.from({ length: 4 }).map((_, rowIndex) => (\n              <TableRow key={`user-list-row-${rowIndex}`}>\n                {headerWidths.map((width, cellIndex) => (\n                  <TableCell key={`user-list-cell-${rowIndex}-${cellIndex}`}>\n                    {cellIndex === headerWidths.length - 1 ? (\n                      <div className=\"flex gap-2\">\n                        <Skeleton className=\"h-6 w-6\" />\n                        <Skeleton className=\"h-6 w-6\" />\n                        <Skeleton className=\"h-6 w-6\" />\n                      </div>\n                    ) : (\n                      <Skeleton className={`h-4 ${width}`} />\n                    )}\n                  </TableCell>\n                ))}\n              </TableRow>\n            ))}\n          </TableBody>\n        </Table>\n      </div>\n    </AdminCard>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/BulkBookmarksAction.tsx",
    "content": "\"use client\";\n\nimport React, { useEffect, useState } from \"react\";\nimport { usePathname } from \"next/navigation\";\nimport {\n  ActionButton,\n  ActionButtonWithTooltip,\n} from \"@/components/ui/action-button\";\nimport ActionConfirmingDialog from \"@/components/ui/action-confirming-dialog\";\nimport { toast } from \"@/components/ui/sonner\";\nimport useBulkActionsStore from \"@/lib/bulkActions\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport {\n  CheckCheck,\n  FileDown,\n  Hash,\n  Link,\n  List,\n  ListMinus,\n  Pencil,\n  RotateCw,\n  Trash2,\n  X,\n} from \"lucide-react\";\n\nimport {\n  useDeleteBookmark,\n  useRecrawlBookmark,\n  useUpdateBookmark,\n} from \"@karakeep/shared-react/hooks/bookmarks\";\nimport { useRemoveBookmarkFromList } from \"@karakeep/shared-react/hooks/lists\";\nimport { limitConcurrency } from \"@karakeep/shared/concurrency\";\nimport { BookmarkTypes } from \"@karakeep/shared/types/bookmarks\";\n\nimport BulkManageListsModal from \"./bookmarks/BulkManageListsModal\";\nimport BulkTagModal from \"./bookmarks/BulkTagModal\";\nimport { ArchivedActionIcon, FavouritedActionIcon } from \"./bookmarks/icons\";\n\nconst MAX_CONCURRENT_BULK_ACTIONS = 50;\n\nexport default function BulkBookmarksAction() {\n  const { t } = useTranslation();\n  const {\n    selectedBookmarks,\n    isBulkEditEnabled,\n    listContext: withinListContext,\n  } = useBulkActionsStore();\n  const setIsBulkEditEnabled = useBulkActionsStore(\n    (state) => state.setIsBulkEditEnabled,\n  );\n  const selectAllBookmarks = useBulkActionsStore((state) => state.selectAll);\n  const unSelectAllBookmarks = useBulkActionsStore(\n    (state) => state.unSelectAll,\n  );\n  const isEverythingSelected = useBulkActionsStore(\n    (state) => state.isEverythingSelected,\n  );\n  const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);\n  const [isRemoveFromListDialogOpen, setIsRemoveFromListDialogOpen] =\n    useState(false);\n  const [manageListsModal, setManageListsModalOpen] = useState(false);\n  const [bulkTagModal, setBulkTagModalOpen] = useState(false);\n  const pathname = usePathname();\n  const [currentPathname, setCurrentPathname] = useState(\"\");\n\n  // Reset bulk edit state when the route changes\n  useEffect(() => {\n    if (pathname !== currentPathname) {\n      setCurrentPathname(pathname);\n      setIsBulkEditEnabled(false);\n    }\n  }, [pathname, currentPathname]);\n\n  const onError = () => {\n    toast({\n      variant: \"destructive\",\n      title: \"Something went wrong\",\n      description: \"There was a problem with your request.\",\n    });\n  };\n\n  const deleteBookmarkMutator = useDeleteBookmark({\n    onSuccess: () => {\n      setIsBulkEditEnabled(false);\n    },\n    onError,\n  });\n\n  const updateBookmarkMutator = useUpdateBookmark({\n    onSuccess: () => {\n      setIsBulkEditEnabled(false);\n    },\n    onError,\n  });\n\n  const recrawlBookmarkMutator = useRecrawlBookmark({\n    onSuccess: () => {\n      setIsBulkEditEnabled(false);\n    },\n    onError,\n  });\n\n  const removeBookmarkFromListMutator = useRemoveBookmarkFromList({\n    onSuccess: () => {\n      setIsBulkEditEnabled(false);\n    },\n    onError,\n  });\n\n  interface UpdateBookmarkProps {\n    favourited?: boolean;\n    archived?: boolean;\n  }\n\n  const recrawlBookmarks = async (archiveFullPage: boolean) => {\n    const links = selectedBookmarks.filter(\n      (item) => item.content.type === BookmarkTypes.LINK,\n    );\n    await Promise.all(\n      limitConcurrency(\n        links.map(\n          (item) => () =>\n            recrawlBookmarkMutator.mutateAsync({\n              bookmarkId: item.id,\n              archiveFullPage,\n            }),\n        ),\n        MAX_CONCURRENT_BULK_ACTIONS,\n      ),\n    );\n    toast({\n      description: `${links.length} bookmarks will be ${archiveFullPage ? \"re-crawled and archived!\" : \"refreshed!\"}`,\n    });\n  };\n\n  function isClipboardAvailable() {\n    if (typeof window === \"undefined\") {\n      return false;\n    }\n    return window && window.navigator && window.navigator.clipboard;\n  }\n\n  const copyLinks = async () => {\n    if (!isClipboardAvailable()) {\n      toast({\n        description: `Copying is only available over https`,\n      });\n      return;\n    }\n    const copyString = selectedBookmarks\n      .map((item) => {\n        return item.content.type === BookmarkTypes.LINK && item.content.url;\n      })\n      .filter(Boolean)\n      .join(\"\\n\");\n\n    await navigator.clipboard.writeText(copyString);\n\n    toast({\n      description: `Added ${selectedBookmarks.length} bookmark links into the clipboard!`,\n    });\n  };\n\n  const updateBookmarks = async ({\n    favourited,\n    archived,\n  }: UpdateBookmarkProps) => {\n    await Promise.all(\n      limitConcurrency(\n        selectedBookmarks.map(\n          (item) => () =>\n            updateBookmarkMutator.mutateAsync({\n              bookmarkId: item.id,\n              favourited,\n              archived,\n            }),\n        ),\n        MAX_CONCURRENT_BULK_ACTIONS,\n      ),\n    );\n    toast({\n      description: `${selectedBookmarks.length} bookmarks have been updated!`,\n    });\n  };\n\n  const deleteBookmarks = async () => {\n    await Promise.all(\n      limitConcurrency(\n        selectedBookmarks.map(\n          (item) => () =>\n            deleteBookmarkMutator.mutateAsync({ bookmarkId: item.id }),\n        ),\n        MAX_CONCURRENT_BULK_ACTIONS,\n      ),\n    );\n    toast({\n      description: `${selectedBookmarks.length} bookmarks have been deleted!`,\n    });\n    setIsDeleteDialogOpen(false);\n  };\n\n  const removeBookmarksFromList = async () => {\n    if (!withinListContext) return;\n\n    const results = await Promise.allSettled(\n      limitConcurrency(\n        selectedBookmarks.map(\n          (item) => () =>\n            removeBookmarkFromListMutator.mutateAsync({\n              bookmarkId: item.id,\n              listId: withinListContext.id,\n            }),\n        ),\n        MAX_CONCURRENT_BULK_ACTIONS,\n      ),\n    );\n\n    const successes = results.filter((r) => r.status === \"fulfilled\").length;\n    if (successes > 0) {\n      toast({\n        description: `${successes} bookmarks have been removed from the list!`,\n      });\n    }\n    setIsRemoveFromListDialogOpen(false);\n  };\n\n  const alreadyFavourited =\n    selectedBookmarks.length &&\n    selectedBookmarks.every((item) => item.favourited === true);\n\n  const alreadyArchived =\n    selectedBookmarks.length &&\n    selectedBookmarks.every((item) => item.archived === true);\n\n  const actionList = [\n    {\n      name: isClipboardAvailable()\n        ? t(\"actions.copy_link\")\n        : \"Copying is only available over https\",\n      icon: <Link size={18} />,\n      action: () => copyLinks(),\n      isPending: false,\n      hidden: !isBulkEditEnabled,\n    },\n    {\n      name: t(\"actions.remove_from_list\"),\n      icon: <ListMinus size={18} />,\n      action: () => setIsRemoveFromListDialogOpen(true),\n      isPending: removeBookmarkFromListMutator.isPending,\n      hidden:\n        !isBulkEditEnabled ||\n        !withinListContext ||\n        withinListContext.type !== \"manual\" ||\n        (withinListContext.userRole !== \"editor\" &&\n          withinListContext.userRole !== \"owner\"),\n    },\n    {\n      name: t(\"actions.add_to_list\"),\n      icon: <List size={18} />,\n      action: () => setManageListsModalOpen(true),\n      isPending: false,\n      hidden: !isBulkEditEnabled,\n    },\n    {\n      name: t(\"actions.edit_tags\"),\n      icon: <Hash size={18} />,\n      action: () => setBulkTagModalOpen(true),\n      isPending: false,\n      hidden: !isBulkEditEnabled,\n    },\n    {\n      name: alreadyFavourited ? t(\"actions.unfavorite\") : t(\"actions.favorite\"),\n      icon: <FavouritedActionIcon favourited={!!alreadyFavourited} size={18} />,\n      action: () => updateBookmarks({ favourited: !alreadyFavourited }),\n      isPending: updateBookmarkMutator.isPending,\n      hidden: !isBulkEditEnabled,\n    },\n    {\n      name: alreadyArchived ? t(\"actions.unarchive\") : t(\"actions.archive\"),\n      icon: <ArchivedActionIcon size={18} archived={!!alreadyArchived} />,\n      action: () => updateBookmarks({ archived: !alreadyArchived }),\n      isPending: updateBookmarkMutator.isPending,\n      hidden: !isBulkEditEnabled,\n    },\n    {\n      name: t(\"actions.preserve_offline_archive\"),\n      icon: <FileDown size={18} />,\n      action: () => recrawlBookmarks(true),\n      isPending: recrawlBookmarkMutator.isPending,\n      hidden: !isBulkEditEnabled,\n    },\n    {\n      name: t(\"actions.refresh\"),\n      icon: <RotateCw size={18} />,\n      action: () => recrawlBookmarks(false),\n      isPending: recrawlBookmarkMutator.isPending,\n      hidden: !isBulkEditEnabled,\n    },\n    {\n      name: t(\"actions.delete\"),\n      icon: <Trash2 size={18} color=\"red\" />,\n      action: () => setIsDeleteDialogOpen(true),\n      hidden: !isBulkEditEnabled,\n    },\n    {\n      name: isEverythingSelected()\n        ? t(\"actions.unselect_all\")\n        : t(\"actions.select_all\"),\n      icon: (\n        <p className=\"flex items-center gap-2\">\n          ( <CheckCheck size={18} /> {selectedBookmarks.length} )\n        </p>\n      ),\n      action: () =>\n        isEverythingSelected() ? unSelectAllBookmarks() : selectAllBookmarks(),\n      alwaysEnable: true,\n      hidden: !isBulkEditEnabled,\n    },\n    {\n      name: t(\"actions.close_bulk_edit\"),\n      icon: <X size={18} />,\n      action: () => setIsBulkEditEnabled(false),\n      alwaysEnable: true,\n      hidden: !isBulkEditEnabled,\n    },\n    {\n      name: t(\"actions.bulk_edit\"),\n      icon: <Pencil size={18} />,\n      action: () => setIsBulkEditEnabled(true),\n      alwaysEnable: true,\n      hidden: isBulkEditEnabled,\n    },\n  ];\n\n  return (\n    <div>\n      <ActionConfirmingDialog\n        open={isDeleteDialogOpen}\n        setOpen={setIsDeleteDialogOpen}\n        title={\"Delete Bookmarks\"}\n        description={<p>Are you sure you want to delete these bookmarks?</p>}\n        actionButton={() => (\n          <ActionButton\n            type=\"button\"\n            variant=\"destructive\"\n            loading={deleteBookmarkMutator.isPending}\n            onClick={() => deleteBookmarks()}\n          >\n            {t(\"actions.delete\")}\n          </ActionButton>\n        )}\n      />\n      <ActionConfirmingDialog\n        open={isRemoveFromListDialogOpen}\n        setOpen={setIsRemoveFromListDialogOpen}\n        title={\"Remove Bookmarks from List\"}\n        description={\n          <p>\n            Are you sure you want to remove {selectedBookmarks.length} bookmarks\n            from this list?\n          </p>\n        }\n        actionButton={() => (\n          <ActionButton\n            type=\"button\"\n            variant=\"destructive\"\n            loading={removeBookmarkFromListMutator.isPending}\n            onClick={() => removeBookmarksFromList()}\n          >\n            {t(\"actions.remove\")}\n          </ActionButton>\n        )}\n      />\n      <BulkManageListsModal\n        bookmarkIds={selectedBookmarks.map((b) => b.id)}\n        open={manageListsModal}\n        setOpen={setManageListsModalOpen}\n      />\n      <BulkTagModal\n        bookmarkIds={selectedBookmarks.map((b) => b.id)}\n        open={bulkTagModal}\n        setOpen={setBulkTagModalOpen}\n      />\n      <div className=\"flex items-center\">\n        {actionList.map(\n          ({ name, icon: Icon, action, isPending, hidden, alwaysEnable }) => (\n            <ActionButtonWithTooltip\n              className={hidden ? \"hidden\" : \"block\"}\n              tooltip={name}\n              disabled={!selectedBookmarks.length && !alwaysEnable}\n              delayDuration={100}\n              loading={!!isPending}\n              variant=\"ghost\"\n              key={name}\n              onClick={action}\n            >\n              {Icon}\n            </ActionButtonWithTooltip>\n          ),\n        )}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/EditableText.tsx",
    "content": "import { useEffect, useRef, useState } from \"react\";\nimport { ActionButtonWithTooltip } from \"@/components/ui/action-button\";\nimport { ButtonWithTooltip } from \"@/components/ui/button\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipPortal,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { Check, Pencil, X } from \"lucide-react\";\n\ninterface Props {\n  viewClassName?: string;\n  untitledClassName?: string;\n  editClassName?: string;\n  onSave: (title: string | null) => void;\n  isSaving: boolean;\n  originalText: string | null;\n  setEditable: (editable: boolean) => void;\n}\n\nfunction EditMode({\n  onSave: onSaveCB,\n  editClassName: className,\n  isSaving,\n  originalText,\n  setEditable,\n}: Props) {\n  const { t } = useTranslation();\n  const ref = useRef<HTMLDivElement>(null);\n\n  useEffect(() => {\n    if (ref.current) {\n      ref.current.focus();\n      ref.current.textContent = originalText;\n    }\n  }, [ref]);\n\n  const onSave = () => {\n    let toSave: string | null = ref.current?.textContent ?? null;\n    if (originalText == toSave) {\n      // Nothing to do here\n      return;\n    }\n    if (toSave == \"\") {\n      toSave = null;\n    }\n    onSaveCB(toSave);\n    setEditable(false);\n  };\n\n  return (\n    <div className=\"flex gap-3\">\n      <div\n        ref={ref}\n        role=\"presentation\"\n        className={className}\n        contentEditable={true}\n        onKeyDown={(e) => {\n          if (e.key === \"Enter\") {\n            e.preventDefault();\n            onSave();\n          }\n        }}\n      />\n      <ActionButtonWithTooltip\n        tooltip={t(\"actions.save\")}\n        delayDuration={500}\n        size=\"none\"\n        variant=\"ghost\"\n        className=\"align-middle text-gray-400\"\n        loading={isSaving}\n        onClick={() => onSave()}\n      >\n        <Check className=\"size-4\" />\n      </ActionButtonWithTooltip>\n      <ButtonWithTooltip\n        tooltip={t(\"actions.cancel\")}\n        delayDuration={500}\n        size=\"none\"\n        variant=\"ghost\"\n        className=\"align-middle text-gray-400\"\n        onClick={() => {\n          setEditable(false);\n        }}\n      >\n        <X className=\"size-4\" />\n      </ButtonWithTooltip>\n    </div>\n  );\n}\n\nfunction ViewMode({\n  originalText,\n  setEditable,\n  viewClassName,\n  untitledClassName,\n}: Props) {\n  const { t } = useTranslation();\n  return (\n    <Tooltip delayDuration={500}>\n      <div className=\"flex max-w-full items-center gap-3\">\n        <TooltipTrigger asChild>\n          {originalText ? (\n            <p className={viewClassName}>{originalText}</p>\n          ) : (\n            <p className={untitledClassName}>Untitled</p>\n          )}\n        </TooltipTrigger>\n        <ButtonWithTooltip\n          delayDuration={500}\n          tooltip={t(\"actions.edit_title\")}\n          size=\"none\"\n          variant=\"ghost\"\n          className=\"align-middle text-gray-400\"\n          onClick={() => {\n            setEditable(true);\n          }}\n        >\n          <Pencil className=\"size-4\" />\n        </ButtonWithTooltip>\n      </div>\n      <TooltipPortal>\n        {originalText && (\n          <TooltipContent side=\"bottom\" className=\"max-w-[40ch] break-words\">\n            {originalText}\n          </TooltipContent>\n        )}\n      </TooltipPortal>\n    </Tooltip>\n  );\n}\n\nexport function EditableText(props: {\n  viewClassName?: string;\n  untitledClassName?: string;\n  editClassName?: string;\n  originalText: string | null;\n  onSave: (title: string | null) => void;\n  isSaving: boolean;\n}) {\n  const [editable, setEditable] = useState(false);\n\n  return editable ? (\n    <EditMode setEditable={setEditable} {...props} />\n  ) : (\n    <ViewMode setEditable={setEditable} {...props} />\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/ErrorFallback.tsx",
    "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { Button } from \"@/components/ui/button\";\nimport { AlertTriangle, Home, RefreshCw } from \"lucide-react\";\n\nexport default function ErrorFallback() {\n  return (\n    <div className=\"flex flex-1 items-center justify-center rounded-lg bg-slate-50 p-8 shadow-sm dark:bg-slate-700/50 dark:shadow-md\">\n      <div className=\"w-full max-w-md space-y-8 text-center\">\n        <div className=\"flex justify-center\">\n          <div className=\"flex h-20 w-20 items-center justify-center rounded-full bg-muted\">\n            <AlertTriangle className=\"h-10 w-10 text-muted-foreground\" />\n          </div>\n        </div>\n\n        <div className=\"space-y-4\">\n          <h1 className=\"text-balance text-2xl font-semibold text-foreground\">\n            Oops! Something went wrong\n          </h1>\n          <p className=\"text-pretty leading-relaxed text-muted-foreground\">\n            We&apos;re sorry, but an unexpected error occurred. Please try again\n            or contact support if the issue persists.\n          </p>\n        </div>\n\n        <div className=\"space-y-3\">\n          <Button className=\"w-full\" onClick={() => window.location.reload()}>\n            <RefreshCw className=\"mr-2 h-4 w-4\" />\n            Try Again\n          </Button>\n\n          <Link href=\"/\" className=\"block\">\n            <Button variant=\"outline\" className=\"w-full\">\n              <Home className=\"mr-2 h-4 w-4\" />\n              Go Home\n            </Button>\n          </Link>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/GlobalActions.tsx",
    "content": "\"use client\";\n\nimport BulkBookmarksAction from \"@/components/dashboard/BulkBookmarksAction\";\nimport SortOrderToggle from \"@/components/dashboard/SortOrderToggle\";\nimport ViewOptions from \"@/components/dashboard/ViewOptions\";\nimport { useInBookmarkGridStore } from \"@/lib/store/useInBookmarkGridStore\";\n\nexport default function GlobalActions() {\n  const inBookmarkGrid = useInBookmarkGridStore(\n    (state) => state.inBookmarkGrid,\n  );\n  return (\n    <div className=\"flex min-w-max flex-wrap overflow-hidden\">\n      {inBookmarkGrid && <ViewOptions />}\n      {inBookmarkGrid && <BulkBookmarksAction />}\n      {inBookmarkGrid && <SortOrderToggle />}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/SortOrderToggle.tsx",
    "content": "\"use client\";\n\nimport { useEffect } from \"react\";\nimport { ButtonWithTooltip } from \"@/components/ui/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { useInSearchPageStore } from \"@/lib/store/useInSearchPageStore\";\nimport { useSortOrderStore } from \"@/lib/store/useSortOrderStore\";\nimport { Check, ListFilter, SortAsc, SortDesc } from \"lucide-react\";\n\nexport default function SortOrderToggle() {\n  const { t } = useTranslation();\n  const isInSearchPage = useInSearchPageStore();\n\n  const { sortOrder: currentSort, setSortOrder } = useSortOrderStore();\n\n  // also see related on page enter sortOrder.relevance init\n  // in apps/web/app/dashboard/search/page.tsx\n  useEffect(() => {\n    if (!isInSearchPage && currentSort === \"relevance\") {\n      // reset to default sort order\n      setSortOrder(\"desc\");\n    }\n  }, [isInSearchPage, currentSort]);\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <ButtonWithTooltip\n          tooltip={t(\"actions.sort.title\")}\n          delayDuration={100}\n          variant=\"ghost\"\n        >\n          {currentSort === \"relevance\" && <ListFilter size={18} />}\n          {currentSort === \"asc\" && <SortAsc size={18} />}\n          {currentSort === \"desc\" && <SortDesc size={18} />}\n        </ButtonWithTooltip>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent className=\"w-fit\">\n        {isInSearchPage && (\n          <DropdownMenuItem\n            className=\"cursor-pointer justify-between\"\n            onClick={() => setSortOrder(\"relevance\")}\n          >\n            <div className=\"flex items-center\">\n              <ListFilter size={16} className=\"mr-2\" />\n              <span>{t(\"actions.sort.relevant_first\")}</span>\n            </div>\n            {currentSort === \"relevance\" && <Check className=\"ml-2 h-4 w-4\" />}\n          </DropdownMenuItem>\n        )}\n        <DropdownMenuItem\n          className=\"cursor-pointer justify-between\"\n          onClick={() => setSortOrder(\"desc\")}\n        >\n          <div className=\"flex items-center\">\n            <SortDesc size={16} className=\"mr-2\" />\n            <span>{t(\"actions.sort.newest_first\")}</span>\n          </div>\n          {currentSort === \"desc\" && <Check className=\"ml-2 h-4 w-4\" />}\n        </DropdownMenuItem>\n        <DropdownMenuItem\n          className=\"cursor-pointer justify-between\"\n          onClick={() => setSortOrder(\"asc\")}\n        >\n          <div className=\"flex items-center\">\n            <SortAsc size={16} className=\"mr-2\" />\n            <span>{t(\"actions.sort.oldest_first\")}</span>\n          </div>\n          {currentSort === \"asc\" && <Check className=\"ml-2 h-4 w-4\" />}\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/UploadDropzone.tsx",
    "content": "\"use client\";\n\nimport React, { useCallback, useState } from \"react\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { BOOKMARK_DRAG_MIME } from \"@/lib/bookmark-drag\";\nimport useUpload from \"@/lib/hooks/upload-file\";\nimport { cn } from \"@/lib/utils\";\nimport { TRPCClientError } from \"@trpc/client\";\nimport DropZone from \"react-dropzone\";\n\nimport { useCreateBookmarkWithPostHook } from \"@karakeep/shared-react/hooks/bookmarks\";\nimport { BookmarkTypes } from \"@karakeep/shared/types/bookmarks\";\n\nimport LoadingSpinner from \"../ui/spinner\";\nimport BookmarkAlreadyExistsToast from \"../utils/BookmarkAlreadyExistsToast\";\n\nexport function useUploadAsset() {\n  const { mutateAsync: createBookmark } = useCreateBookmarkWithPostHook({\n    onSuccess: (resp) => {\n      if (resp.alreadyExists) {\n        toast({\n          description: <BookmarkAlreadyExistsToast bookmarkId={resp.id} />,\n          variant: \"default\",\n        });\n      } else {\n        toast({ description: \"Bookmark uploaded\" });\n      }\n    },\n    onError: () => {\n      toast({ description: \"Something went wrong\", variant: \"destructive\" });\n    },\n  });\n\n  const { mutateAsync: runUploadAsset } = useUpload({\n    onSuccess: async (resp) => {\n      const assetType =\n        resp.contentType === \"application/pdf\" ? \"pdf\" : \"image\";\n      await createBookmark({\n        ...resp,\n        type: BookmarkTypes.ASSET,\n        assetType,\n        source: \"web\",\n      });\n    },\n    onError: (err, req) => {\n      toast({\n        description: `${req.name}: ${err.error}`,\n        variant: \"destructive\",\n      });\n    },\n  });\n\n  return useCallback(\n    async (file: File) => {\n      // Handle markdown files as text bookmarks\n      if (file.type === \"text/markdown\" || file.name.endsWith(\".md\")) {\n        try {\n          const content = await file.text();\n          await createBookmark({\n            type: BookmarkTypes.TEXT,\n            text: content,\n            title: file.name.replace(/\\.md$/i, \"\"), // Remove .md extension from title\n            source: \"web\",\n          });\n        } catch {\n          toast({\n            description: `${file.name}: Failed to read markdown file`,\n            variant: \"destructive\",\n          });\n        }\n      } else {\n        return runUploadAsset(file);\n      }\n    },\n    [runUploadAsset],\n  );\n}\n\nfunction useUploadAssets({\n  onFileUpload,\n  onFileError,\n  onAllUploaded,\n}: {\n  onFileUpload: () => void;\n  onFileError: (name: string, e: Error) => void;\n  onAllUploaded: () => void;\n}) {\n  const runUpload = useUploadAsset();\n\n  return async (files: File[]) => {\n    if (files.length == 0) {\n      return;\n    }\n    for (const file of files) {\n      try {\n        await runUpload(file);\n        onFileUpload();\n      } catch (e) {\n        if (e instanceof TRPCClientError || e instanceof Error) {\n          onFileError(file.name, e);\n        }\n      }\n    }\n    onAllUploaded();\n  };\n}\n\nexport default function UploadDropzone({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  const [numUploading, setNumUploading] = useState(0);\n  const [numUploaded, setNumUploaded] = useState(0);\n  const uploadAssets = useUploadAssets({\n    onFileUpload: () => {\n      setNumUploaded((c) => c + 1);\n    },\n    onFileError: () => {\n      setNumUploaded((c) => c + 1);\n    },\n    onAllUploaded: () => {\n      setNumUploading(0);\n      setNumUploaded(0);\n      return;\n    },\n  });\n\n  const [isDragging, setDragging] = useState(false);\n  const onDrop = (acceptedFiles: File[]) => {\n    uploadAssets(acceptedFiles);\n    setNumUploading(acceptedFiles.length);\n    setDragging(false);\n  };\n\n  return (\n    <DropZone\n      noClick\n      onDrop={onDrop}\n      onDragEnter={(e) => {\n        // Don't show overlay for internal bookmark card drags\n        if (!e.dataTransfer.types.includes(BOOKMARK_DRAG_MIME)) {\n          setDragging(true);\n        }\n      }}\n      onDragLeave={() => setDragging(false)}\n    >\n      {({ getRootProps, getInputProps }) => (\n        <div {...getRootProps()}>\n          <input {...getInputProps()} hidden />\n          <div\n            className={cn(\n              \"fixed inset-0 z-50 flex h-full w-full items-center justify-center bg-gray-200 opacity-90\",\n              isDragging || numUploading > 0 ? undefined : \"hidden\",\n            )}\n          >\n            {numUploading > 0 ? (\n              <div className=\"flex items-center justify-center gap-2\">\n                <p className=\"text-2xl font-bold text-gray-700\">\n                  Uploading {numUploaded} / {numUploading}\n                </p>\n                <LoadingSpinner />\n              </div>\n            ) : (\n              <p className=\"text-2xl font-bold text-gray-700\">\n                Drop Your Image / PDF / Markdown file\n              </p>\n            )}\n          </div>\n          {children}\n        </div>\n      )}\n    </DropZone>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/ViewOptions.tsx",
    "content": "\"use client\";\n\nimport {\n  createElement,\n  useEffect,\n  useOptimistic,\n  useState,\n  useTransition,\n} from \"react\";\nimport { ButtonWithTooltip } from \"@/components/ui/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSeparator,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { Label } from \"@/components/ui/label\";\nimport { Slider } from \"@/components/ui/slider\";\nimport { Switch } from \"@/components/ui/switch\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport {\n  useBookmarkDisplaySettings,\n  useBookmarkLayout,\n  useGridColumns,\n} from \"@/lib/userLocalSettings/bookmarksLayout\";\nimport {\n  updateBookmarksLayout,\n  updateGridColumns,\n  updateImageFit,\n  updateShowNotes,\n  updateShowTags,\n  updateShowTitle,\n} from \"@/lib/userLocalSettings/userLocalSettings\";\nimport {\n  Check,\n  Heading,\n  Image,\n  LayoutDashboard,\n  LayoutGrid,\n  LayoutList,\n  List,\n  LucideIcon,\n  NotepadText,\n  Settings,\n  Tag,\n} from \"lucide-react\";\n\ntype LayoutType = \"masonry\" | \"grid\" | \"list\" | \"compact\";\n\nconst iconMap: Record<LayoutType, LucideIcon> = {\n  masonry: LayoutDashboard,\n  grid: LayoutGrid,\n  list: LayoutList,\n  compact: List,\n};\n\nexport default function ViewOptions() {\n  const { t } = useTranslation();\n  const layout = useBookmarkLayout();\n  const gridColumns = useGridColumns();\n  const actualDisplaySettings = useBookmarkDisplaySettings();\n  const [tempColumns, setTempColumns] = useState(gridColumns);\n  const [, startTransition] = useTransition();\n\n  // Optimistic state for all toggles\n  const [optimisticDisplaySettings, setOptimisticDisplaySettings] =\n    useOptimistic(actualDisplaySettings);\n\n  const [optimisticLayout, setOptimisticLayout] = useOptimistic(layout);\n\n  const [optimisticImageFit, setOptimisticImageFit] = useOptimistic(\n    actualDisplaySettings.imageFit,\n  );\n\n  const showColumnSlider =\n    optimisticLayout === \"grid\" || optimisticLayout === \"masonry\";\n\n  // Update temp value when actual value changes\n  useEffect(() => {\n    setTempColumns(gridColumns);\n  }, [gridColumns]);\n\n  // Handlers with optimistic updates\n  const handleLayoutChange = (newLayout: LayoutType) => {\n    startTransition(async () => {\n      setOptimisticLayout(newLayout);\n      await updateBookmarksLayout(newLayout);\n    });\n  };\n\n  const handleShowNotesChange = (checked: boolean) => {\n    startTransition(async () => {\n      setOptimisticDisplaySettings({\n        ...optimisticDisplaySettings,\n        showNotes: checked,\n      });\n      await updateShowNotes(checked);\n    });\n  };\n\n  const handleShowTagsChange = (checked: boolean) => {\n    startTransition(async () => {\n      setOptimisticDisplaySettings({\n        ...optimisticDisplaySettings,\n        showTags: checked,\n      });\n      await updateShowTags(checked);\n    });\n  };\n\n  const handleShowTitleChange = (checked: boolean) => {\n    startTransition(async () => {\n      setOptimisticDisplaySettings({\n        ...optimisticDisplaySettings,\n        showTitle: checked,\n      });\n      await updateShowTitle(checked);\n    });\n  };\n\n  const handleImageFitChange = (fit: \"cover\" | \"contain\") => {\n    startTransition(async () => {\n      setOptimisticImageFit(fit);\n      await updateImageFit(fit);\n    });\n  };\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <ButtonWithTooltip\n          tooltip={t(\"view_options.title\")}\n          delayDuration={100}\n          variant=\"ghost\"\n        >\n          <Settings size={18} />\n        </ButtonWithTooltip>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent className=\"w-56\">\n        <div className=\"px-2 py-1.5 text-sm font-semibold\">\n          {t(\"view_options.layout\")}\n        </div>\n        {(Object.keys(iconMap) as LayoutType[]).map((key) => (\n          <DropdownMenuItem\n            key={key}\n            className=\"cursor-pointer justify-between\"\n            onSelect={(e) => {\n              e.preventDefault();\n              handleLayoutChange(key);\n            }}\n          >\n            <div className=\"flex items-center gap-2\">\n              {createElement(iconMap[key as LayoutType], { size: 18 })}\n              <span>{t(`layouts.${key}`)}</span>\n            </div>\n            {optimisticLayout === key && <Check className=\"ml-2 size-4\" />}\n          </DropdownMenuItem>\n        ))}\n\n        {showColumnSlider && (\n          <>\n            <DropdownMenuSeparator />\n            <div className=\"px-2 py-3\">\n              <div className=\"mb-2 flex items-center justify-between\">\n                <span className=\"text-sm font-semibold\">\n                  {t(\"view_options.columns\")}\n                </span>\n                <span className=\"text-sm text-muted-foreground\">\n                  {tempColumns}\n                </span>\n              </div>\n              <Slider\n                value={[tempColumns]}\n                onValueChange={([value]) => setTempColumns(value)}\n                onValueCommit={([value]) => updateGridColumns(value)}\n                min={1}\n                max={6}\n                step={1}\n                className=\"w-full\"\n              />\n              <div className=\"mt-1 flex justify-between text-xs text-muted-foreground\">\n                <span>1</span>\n                <span>6</span>\n              </div>\n            </div>\n          </>\n        )}\n\n        <DropdownMenuSeparator />\n        <div className=\"px-2 py-1.5 text-sm font-semibold\">\n          {t(\"view_options.display_options\")}\n        </div>\n\n        <div className=\"space-y-3 px-2 py-2\">\n          <div className=\"flex items-center justify-between\">\n            <Label\n              htmlFor=\"show-notes\"\n              className=\"flex cursor-pointer items-center gap-2 text-sm\"\n            >\n              <NotepadText size={16} />\n              <span>{t(\"view_options.show_note_previews\")}</span>\n            </Label>\n            <Switch\n              id=\"show-notes\"\n              checked={optimisticDisplaySettings.showNotes}\n              onCheckedChange={handleShowNotesChange}\n            />\n          </div>\n\n          <div className=\"flex items-center justify-between\">\n            <Label\n              htmlFor=\"show-tags\"\n              className=\"flex cursor-pointer items-center gap-2 text-sm\"\n            >\n              <Tag size={16} />\n              <span>{t(\"view_options.show_tags\")}</span>\n            </Label>\n            <Switch\n              id=\"show-tags\"\n              checked={optimisticDisplaySettings.showTags}\n              onCheckedChange={handleShowTagsChange}\n            />\n          </div>\n\n          <div className=\"flex items-center justify-between\">\n            <Label\n              htmlFor=\"show-title\"\n              className=\"flex cursor-pointer items-center gap-2 text-sm\"\n            >\n              <Heading size={16} />\n              <span>{t(\"view_options.show_title\")}</span>\n            </Label>\n            <Switch\n              id=\"show-title\"\n              checked={optimisticDisplaySettings.showTitle}\n              onCheckedChange={handleShowTitleChange}\n            />\n          </div>\n        </div>\n\n        <DropdownMenuSeparator />\n        <div className=\"px-2 py-1.5 text-sm font-semibold\">\n          {t(\"view_options.image_options\")}\n        </div>\n\n        <div className=\"space-y-1 px-2 py-2\">\n          <DropdownMenuItem\n            className=\"cursor-pointer justify-between\"\n            onSelect={(e) => {\n              e.preventDefault();\n              handleImageFitChange(\"cover\");\n            }}\n          >\n            <div className=\"flex items-center gap-2\">\n              <Image size={16} />\n              <span>{t(\"view_options.image_fit_cover\")}</span>\n            </div>\n            {optimisticImageFit === \"cover\" && (\n              <Check className=\"ml-2 size-4\" />\n            )}\n          </DropdownMenuItem>\n          <DropdownMenuItem\n            className=\"cursor-pointer justify-between\"\n            onSelect={(e) => {\n              e.preventDefault();\n              handleImageFitChange(\"contain\");\n            }}\n          >\n            <div className=\"flex items-center gap-2\">\n              <Image size={16} />\n              <span>{t(\"view_options.image_fit_contain\")}</span>\n            </div>\n            {optimisticImageFit === \"contain\" && (\n              <Check className=\"ml-2 size-4\" />\n            )}\n          </DropdownMenuItem>\n        </div>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/bookmarks/AssetCard.tsx",
    "content": "\"use client\";\n\nimport Image from \"next/image\";\nimport Link from \"next/link\";\nimport { cn } from \"@/lib/utils\";\nimport { FileText } from \"lucide-react\";\n\nimport type { ZBookmarkTypeAsset } from \"@karakeep/shared/types/bookmarks\";\nimport { getAssetUrl } from \"@karakeep/shared/utils/assetUtils\";\nimport { getSourceUrl } from \"@karakeep/shared/utils/bookmarkUtils\";\n\nimport { BookmarkLayoutAdaptingCard } from \"./BookmarkLayoutAdaptingCard\";\nimport FooterLinkURL from \"./FooterLinkURL\";\n\nfunction AssetImage({\n  bookmark,\n  className,\n}: {\n  bookmark: ZBookmarkTypeAsset;\n  className?: string;\n}) {\n  const bookmarkedAsset = bookmark.content;\n  switch (bookmarkedAsset.assetType) {\n    case \"image\": {\n      return (\n        <Link href={`/dashboard/preview/${bookmark.id}`}>\n          <Image\n            alt=\"asset\"\n            src={getAssetUrl(bookmarkedAsset.assetId)}\n            fill={true}\n            className={className}\n          />\n        </Link>\n      );\n    }\n    case \"pdf\": {\n      const screenshotAssetId = bookmark.assets.find(\n        (r) => r.assetType === \"assetScreenshot\",\n      )?.id;\n      if (!screenshotAssetId) {\n        return (\n          <div\n            className={cn(className, \"flex items-center justify-center\")}\n            title=\"PDF screenshot not available. Run asset preprocessing job to generate one screenshot\"\n          >\n            <FileText size={80} />\n          </div>\n        );\n      }\n      return (\n        <Link href={`/dashboard/preview/${bookmark.id}`}>\n          <Image\n            alt=\"asset\"\n            src={getAssetUrl(screenshotAssetId)}\n            fill={true}\n            className={className}\n          />\n        </Link>\n      );\n    }\n    default: {\n      const _exhaustiveCheck: never = bookmarkedAsset.assetType;\n      return <span />;\n    }\n  }\n}\n\nexport default function AssetCard({\n  bookmark: bookmarkedAsset,\n  className,\n}: {\n  bookmark: ZBookmarkTypeAsset;\n  className?: string;\n}) {\n  return (\n    <BookmarkLayoutAdaptingCard\n      title={bookmarkedAsset.title ?? bookmarkedAsset.content.fileName}\n      footer={\n        getSourceUrl(bookmarkedAsset) && (\n          <FooterLinkURL url={getSourceUrl(bookmarkedAsset)} />\n        )\n      }\n      bookmark={bookmarkedAsset}\n      className={className}\n      wrapTags={true}\n      image={(_layout, className) => (\n        <div className=\"relative size-full flex-1\">\n          <AssetImage bookmark={bookmarkedAsset} className={className} />\n        </div>\n      )}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/bookmarks/BookmarkActionBar.tsx",
    "content": "import Link from \"next/link\";\nimport { buttonVariants } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport { Maximize2 } from \"lucide-react\";\n\nimport type { ZBookmark } from \"@karakeep/shared/types/bookmarks\";\n\nimport BookmarkOptions from \"./BookmarkOptions\";\nimport { FavouritedActionIcon } from \"./icons\";\n\nexport default function BookmarkActionBar({\n  bookmark,\n}: {\n  bookmark: ZBookmark;\n}) {\n  return (\n    <div className=\"flex text-gray-500\">\n      {bookmark.favourited && (\n        <FavouritedActionIcon className=\"m-1 size-8 rounded p-1\" favourited />\n      )}\n      <Link\n        href={`/dashboard/preview/${bookmark.id}`}\n        className={cn(buttonVariants({ variant: \"ghost\" }), \"px-2\")}\n      >\n        <Maximize2 size={16} />\n      </Link>\n      <BookmarkOptions bookmark={bookmark} />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/bookmarks/BookmarkCard.tsx",
    "content": "import { useQuery } from \"@tanstack/react-query\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\nimport { BookmarkTypes, ZBookmark } from \"@karakeep/shared/types/bookmarks\";\nimport { getBookmarkRefreshInterval } from \"@karakeep/shared/utils/bookmarkUtils\";\n\nimport AssetCard from \"./AssetCard\";\nimport LinkCard from \"./LinkCard\";\nimport TextCard from \"./TextCard\";\nimport UnknownCard from \"./UnknownCard\";\n\nexport default function BookmarkCard({\n  bookmark: initialData,\n  className,\n}: {\n  bookmark: ZBookmark;\n  className?: string;\n}) {\n  const api = useTRPC();\n  const { data: bookmark } = useQuery(\n    api.bookmarks.getBookmark.queryOptions(\n      {\n        bookmarkId: initialData.id,\n      },\n      {\n        initialData,\n        refetchInterval: (query) => {\n          const data = query.state.data;\n          if (!data) {\n            return false;\n          }\n          return getBookmarkRefreshInterval(data);\n        },\n      },\n    ),\n  );\n\n  switch (bookmark.content.type) {\n    case BookmarkTypes.LINK:\n      return (\n        <LinkCard\n          className={className}\n          bookmark={{ ...bookmark, content: bookmark.content }}\n        />\n      );\n    case BookmarkTypes.TEXT:\n      return (\n        <TextCard\n          className={className}\n          bookmark={{ ...bookmark, content: bookmark.content }}\n        />\n      );\n    case BookmarkTypes.ASSET:\n      return (\n        <AssetCard\n          className={className}\n          bookmark={{ ...bookmark, content: bookmark.content }}\n        />\n      );\n    case BookmarkTypes.UNKNOWN:\n      return <UnknownCard className={className} bookmark={bookmark} />;\n  }\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/bookmarks/BookmarkFormattedCreatedAt.tsx",
    "content": "import { format, isAfter, subYears } from \"date-fns\";\n\nexport default function BookmarkFormattedCreatedAt(prop: { createdAt: Date }) {\n  const createdAt = prop.createdAt;\n  const oneYearAgo = subYears(new Date(), 1);\n  const formatString = isAfter(createdAt, oneYearAgo) ? \"MMM d\" : \"MMM d, yyyy\";\n  return format(createdAt, formatString);\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx",
    "content": "\"use client\";\n\nimport type { BookmarksLayoutTypes } from \"@/lib/userLocalSettings/types\";\nimport type { ReactNode } from \"react\";\nimport { useCallback, useEffect, useState } from \"react\";\nimport Image from \"next/image\";\nimport Link from \"next/link\";\nimport { useSession } from \"@/lib/auth/client\";\nimport { BOOKMARK_DRAG_MIME } from \"@/lib/bookmark-drag\";\nimport useBulkActionsStore from \"@/lib/bulkActions\";\nimport { useClientConfig } from \"@/lib/clientConfig\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport {\n  bookmarkLayoutSwitch,\n  useBookmarkDisplaySettings,\n  useBookmarkLayout,\n} from \"@/lib/userLocalSettings/bookmarksLayout\";\nimport { cn } from \"@/lib/utils\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport {\n  Check,\n  GripVertical,\n  Image as ImageIcon,\n  NotebookPen,\n} from \"lucide-react\";\nimport { useTheme } from \"next-themes\";\nimport { toast } from \"sonner\";\n\nimport type { ZBookmark } from \"@karakeep/shared/types/bookmarks\";\nimport { useBookmarkListContext } from \"@karakeep/shared-react/hooks/bookmark-list-context\";\nimport { useUpdateBookmark } from \"@karakeep/shared-react/hooks/bookmarks\";\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\nimport { BookmarkTypes } from \"@karakeep/shared/types/bookmarks\";\nimport {\n  getBookmarkTitle,\n  isBookmarkStillTagging,\n} from \"@karakeep/shared/utils/bookmarkUtils\";\nimport { switchCase } from \"@karakeep/shared/utils/switch\";\n\nimport BookmarkActionBar from \"./BookmarkActionBar\";\nimport BookmarkFormattedCreatedAt from \"./BookmarkFormattedCreatedAt\";\nimport BookmarkOwnerIcon from \"./BookmarkOwnerIcon\";\nimport { ArchivedActionIcon, FavouritedActionIcon } from \"./icons\";\nimport { NotePreview } from \"./NotePreview\";\nimport TagList from \"./TagList\";\n\ninterface Props {\n  bookmark: ZBookmark;\n  image: (layout: BookmarksLayoutTypes, className: string) => ReactNode;\n  title?: ReactNode;\n  content?: ReactNode;\n  footer?: ReactNode;\n  className?: string;\n  fitHeight?: boolean;\n  wrapTags: boolean;\n}\n\nfunction BottomRow({\n  footer,\n  bookmark,\n}: {\n  footer?: ReactNode;\n  bookmark: ZBookmark;\n}) {\n  return (\n    <div className=\"justify flex w-full shrink-0 justify-between text-gray-500\">\n      <div className=\"flex items-center gap-2 overflow-hidden text-nowrap font-light\">\n        {footer && <>{footer}•</>}\n        <Link\n          href={`/dashboard/preview/${bookmark.id}`}\n          suppressHydrationWarning\n        >\n          <BookmarkFormattedCreatedAt createdAt={bookmark.createdAt} />\n        </Link>\n      </div>\n      <BookmarkActionBar bookmark={bookmark} />\n    </div>\n  );\n}\n\nfunction OwnerIndicator({ bookmark }: { bookmark: ZBookmark }) {\n  const api = useTRPC();\n  const listContext = useBookmarkListContext();\n  const collaborators = useQuery(\n    api.lists.getCollaborators.queryOptions(\n      {\n        listId: listContext?.id ?? \"\",\n      },\n      {\n        refetchOnWindowFocus: false,\n        enabled: !!listContext?.hasCollaborators,\n      },\n    ),\n  );\n\n  if (!listContext || listContext.userRole === \"owner\" || !collaborators.data) {\n    return null;\n  }\n\n  let owner = undefined;\n  if (bookmark.userId === collaborators.data.owner?.id) {\n    owner = collaborators.data.owner;\n  } else {\n    owner = collaborators.data.collaborators.find(\n      (c) => c.userId === bookmark.userId,\n    )?.user;\n  }\n\n  if (!owner) return null;\n\n  return (\n    <div className=\"absolute right-2 top-2 z-40 opacity-0 transition-opacity duration-200 group-hover:opacity-100\">\n      <BookmarkOwnerIcon ownerName={owner.name} ownerAvatar={owner.image} />\n    </div>\n  );\n}\n\nfunction MultiBookmarkSelector({ bookmark }: { bookmark: ZBookmark }) {\n  const { selectedBookmarks, isBulkEditEnabled } = useBulkActionsStore();\n  const toggleBookmark = useBulkActionsStore((state) => state.toggleBookmark);\n  const [isSelected, setIsSelected] = useState(false);\n  const { theme } = useTheme();\n  const { data: session } = useSession();\n\n  useEffect(() => {\n    setIsSelected(selectedBookmarks.some((item) => item.id === bookmark.id));\n  }, [selectedBookmarks]);\n\n  // Don't show selector for non-owned bookmarks or when bulk edit is disabled\n  const isOwner = session?.user?.id === bookmark.userId;\n  if (!isBulkEditEnabled || !isOwner) return null;\n\n  const getIconColor = () => {\n    if (theme === \"dark\") {\n      return isSelected ? \"black\" : \"white\";\n    }\n    return isSelected ? \"white\" : \"black\";\n  };\n\n  const getIconBackgroundColor = () => {\n    if (theme === \"dark\") {\n      return isSelected ? \"bg-white\" : \"bg-white bg-opacity-10\";\n    }\n    return isSelected ? \"bg-black\" : \"bg-white bg-opacity-40\";\n  };\n\n  return (\n    <button\n      className={cn(\n        \"absolute left-0 top-0 z-50 h-full w-full bg-opacity-0\",\n        {\n          \"bg-opacity-10\": isSelected,\n        },\n        theme === \"dark\" ? \"bg-white\" : \"bg-black\",\n      )}\n      onClick={() => toggleBookmark(bookmark)}\n    >\n      <div className=\"absolute right-2 top-2 z-50 opacity-100\">\n        <div\n          className={cn(\n            \"flex h-4 w-4 items-center justify-center rounded-full border border-gray-600\",\n            getIconBackgroundColor(),\n          )}\n        >\n          <Check size={12} color={getIconColor()} />\n        </div>\n      </div>\n    </button>\n  );\n}\n\nfunction DragHandle({\n  bookmark,\n  className,\n}: {\n  bookmark: ZBookmark;\n  className?: string;\n}) {\n  const { isBulkEditEnabled } = useBulkActionsStore();\n  const handleDragStart = useCallback(\n    (e: React.DragEvent) => {\n      e.stopPropagation();\n      e.dataTransfer.setData(BOOKMARK_DRAG_MIME, bookmark.id);\n      e.dataTransfer.effectAllowed = \"copy\";\n\n      // Create a small pill element as the drag preview\n      const pill = document.createElement(\"div\");\n      const title = getBookmarkTitle(bookmark) ?? \"Untitled\";\n      pill.textContent =\n        title.length > 40 ? title.substring(0, 40) + \"\\u2026\" : title;\n      Object.assign(pill.style, {\n        position: \"fixed\",\n        left: \"-9999px\",\n        top: \"-9999px\",\n        padding: \"6px 12px\",\n        borderRadius: \"8px\",\n        backgroundColor: \"hsl(var(--card))\",\n        border: \"1px solid hsl(var(--border))\",\n        boxShadow: \"0 4px 12px rgba(0,0,0,0.15)\",\n        fontSize: \"13px\",\n        fontFamily: \"inherit\",\n        color: \"hsl(var(--foreground))\",\n        maxWidth: \"240px\",\n        whiteSpace: \"nowrap\",\n        overflow: \"hidden\",\n        textOverflow: \"ellipsis\",\n      });\n      document.body.appendChild(pill);\n      e.dataTransfer.setDragImage(pill, 0, 0);\n      requestAnimationFrame(() => pill.remove());\n    },\n    [bookmark],\n  );\n\n  if (isBulkEditEnabled) return null;\n\n  return (\n    <div\n      draggable\n      onDragStart={handleDragStart}\n      className={cn(\n        \"absolute z-40 hidden cursor-grab rounded bg-background/70 p-0.5 opacity-0 shadow-sm transition-opacity duration-200 group-hover:opacity-100 [@media(pointer:fine)]:block\",\n        className,\n      )}\n    >\n      <GripVertical className=\"size-4 text-muted-foreground\" />\n    </div>\n  );\n}\n\nfunction HoverActionBar({ bookmark }: { bookmark: ZBookmark }) {\n  const { t } = useTranslation();\n  const { isBulkEditEnabled } = useBulkActionsStore();\n  const { data: session } = useSession();\n  const demoMode = !!useClientConfig().demoMode;\n  const updateBookmarkMutator = useUpdateBookmark({\n    onSuccess: () => {\n      toast.success(t(\"toasts.bookmarks.updated\"));\n    },\n    onError: () => {\n      toast.error(t(\"common.something_went_wrong\"));\n    },\n  });\n\n  const isOwner = session?.user?.id === bookmark.userId;\n  if (!isOwner || isBulkEditEnabled || demoMode) return null;\n\n  return (\n    <div className=\"pointer-events-none absolute right-2 top-2 z-30 hidden gap-1 rounded bg-white/50 p-1 opacity-0 backdrop-blur-sm transition-opacity duration-200 group-hover:opacity-100 dark:bg-black/50 [@media(pointer:fine)]:pointer-events-auto [@media(pointer:fine)]:flex\">\n      <button\n        title={\n          bookmark.favourited ? t(\"actions.unfavorite\") : t(\"actions.favorite\")\n        }\n        className=\"rounded p-0.5 hover:bg-background/50\"\n        onClick={(e) => {\n          e.preventDefault();\n          e.stopPropagation();\n          updateBookmarkMutator.mutate({\n            bookmarkId: bookmark.id,\n            favourited: !bookmark.favourited,\n          });\n        }}\n      >\n        <FavouritedActionIcon favourited={bookmark.favourited} size={16} />\n      </button>\n      <button\n        title={\n          bookmark.archived ? t(\"actions.unarchive\") : t(\"actions.archive\")\n        }\n        className=\"rounded p-0.5 hover:bg-background/50\"\n        onClick={(e) => {\n          e.preventDefault();\n          e.stopPropagation();\n          updateBookmarkMutator.mutate({\n            bookmarkId: bookmark.id,\n            archived: !bookmark.archived,\n          });\n        }}\n      >\n        <ArchivedActionIcon archived={bookmark.archived} size={16} />\n      </button>\n    </div>\n  );\n}\n\nfunction ListView({\n  bookmark,\n  image,\n  title,\n  content,\n  footer,\n  className,\n}: Props) {\n  const { showNotes, showTags, showTitle, imageFit } =\n    useBookmarkDisplaySettings();\n  const imgFitClass = switchCase(imageFit, {\n    cover: \"object-cover\",\n    contain: \"object-contain\",\n  });\n  const note = showNotes ? bookmark.note?.trim() : undefined;\n\n  return (\n    <div\n      className={cn(\n        \"group relative flex max-h-96 gap-4 overflow-hidden rounded-lg p-2\",\n        className,\n      )}\n    >\n      <MultiBookmarkSelector bookmark={bookmark} />\n      <OwnerIndicator bookmark={bookmark} />\n      <DragHandle\n        bookmark={bookmark}\n        className=\"left-1 top-1/2 -translate-y-1/2\"\n      />\n      <HoverActionBar bookmark={bookmark} />\n      <div className=\"flex size-32 items-center justify-center overflow-hidden\">\n        {image(\"list\", cn(\"size-32 rounded-lg\", imgFitClass))}\n      </div>\n      <div className=\"flex h-full flex-1 flex-col justify-between gap-2 overflow-hidden\">\n        <div className=\"flex flex-col gap-2 overflow-hidden\">\n          {showTitle && title && (\n            <div className=\"line-clamp-2 flex-none shrink-0 overflow-hidden text-ellipsis break-words text-lg\">\n              {title}\n            </div>\n          )}\n          {content && <div className=\"shrink-1 overflow-hidden\">{content}</div>}\n          {note && <NotePreview note={note} bookmarkId={bookmark.id} />}\n          {showTags && (\n            <div className=\"flex shrink-0 flex-wrap gap-1 overflow-hidden\">\n              <TagList\n                bookmark={bookmark}\n                loading={isBookmarkStillTagging(bookmark)}\n              />\n            </div>\n          )}\n        </div>\n        <BottomRow footer={footer} bookmark={bookmark} />\n      </div>\n    </div>\n  );\n}\n\nfunction GridView({\n  bookmark,\n  image,\n  title,\n  content,\n  footer,\n  className,\n  wrapTags,\n  layout,\n  fitHeight = false,\n}: Props & { layout: BookmarksLayoutTypes }) {\n  const { showNotes, showTags, showTitle, imageFit } =\n    useBookmarkDisplaySettings();\n  const imgFitClass = switchCase(imageFit, {\n    cover: \"object-cover\",\n    contain: \"object-contain\",\n  });\n  const note = showNotes ? bookmark.note?.trim() : undefined;\n  const img = image(\n    \"grid\",\n    cn(\"h-56 min-h-56 w-full rounded-t-lg\", imgFitClass),\n  );\n\n  return (\n    <div\n      className={cn(\n        \"group relative flex flex-col overflow-hidden rounded-lg\",\n        className,\n        fitHeight && layout != \"grid\" ? \"max-h-96\" : \"h-96\",\n      )}\n    >\n      <MultiBookmarkSelector bookmark={bookmark} />\n      <OwnerIndicator bookmark={bookmark} />\n      <DragHandle bookmark={bookmark} className=\"left-2 top-2\" />\n      <HoverActionBar bookmark={bookmark} />\n      {img && <div className=\"h-56 w-full shrink-0 overflow-hidden\">{img}</div>}\n      <div className=\"flex h-full flex-col justify-between gap-2 overflow-hidden p-2\">\n        <div className=\"grow-1 flex flex-col gap-2 overflow-hidden\">\n          {showTitle && title && (\n            <div className=\"line-clamp-2 flex-none shrink-0 overflow-hidden text-ellipsis break-words text-lg\">\n              {title}\n            </div>\n          )}\n          {content && <div className=\"shrink-1 overflow-hidden\">{content}</div>}\n          {note && <NotePreview note={note} bookmarkId={bookmark.id} />}\n          {showTags && (\n            <div className=\"flex shrink-0 flex-wrap gap-1 overflow-hidden\">\n              <TagList\n                className={wrapTags ? undefined : \"h-full\"}\n                bookmark={bookmark}\n                loading={isBookmarkStillTagging(bookmark)}\n              />\n            </div>\n          )}\n        </div>\n        <BottomRow footer={footer} bookmark={bookmark} />\n      </div>\n    </div>\n  );\n}\n\nfunction CompactView({ bookmark, title, footer, className }: Props) {\n  const { showTitle } = useBookmarkDisplaySettings();\n  return (\n    <div\n      className={cn(\n        \"group relative flex flex-col overflow-hidden rounded-lg\",\n        className,\n        \"max-h-96\",\n      )}\n    >\n      <MultiBookmarkSelector bookmark={bookmark} />\n      <OwnerIndicator bookmark={bookmark} />\n      <div className=\"flex h-full justify-between gap-2 overflow-hidden p-2\">\n        <div className=\"flex items-center gap-2\">\n          {bookmark.content.type === BookmarkTypes.LINK &&\n            bookmark.content.favicon && (\n              <Image\n                src={bookmark.content.favicon}\n                alt=\"favicon\"\n                width={5}\n                unoptimized\n                height={5}\n                className=\"size-5\"\n              />\n            )}\n          {bookmark.content.type === BookmarkTypes.TEXT && (\n            <NotebookPen className=\"size-5\" />\n          )}\n          {bookmark.content.type === BookmarkTypes.ASSET && (\n            <ImageIcon className=\"size-5\" />\n          )}\n          {showTitle && (\n            <div className=\"shrink-1 text-md line-clamp-1 overflow-hidden text-ellipsis break-words\">\n              {title ?? \"Untitled\"}\n            </div>\n          )}\n          {footer && (\n            <p className=\"flex shrink-0 gap-2 text-gray-500\">•{footer}</p>\n          )}\n          <p className=\"text-gray-500\">•</p>\n          <Link\n            href={`/dashboard/preview/${bookmark.id}`}\n            suppressHydrationWarning\n            className=\"shrink-0 gap-2 text-gray-500\"\n          >\n            <BookmarkFormattedCreatedAt createdAt={bookmark.createdAt} />\n          </Link>\n        </div>\n        <BookmarkActionBar bookmark={bookmark} />\n      </div>\n    </div>\n  );\n}\n\nexport function BookmarkLayoutAdaptingCard(props: Props) {\n  const layout = useBookmarkLayout();\n\n  return bookmarkLayoutSwitch(layout, {\n    masonry: <GridView layout={layout} {...props} />,\n    grid: <GridView layout={layout} {...props} />,\n    list: <ListView {...props} />,\n    compact: <CompactView {...props} />,\n  });\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/bookmarks/BookmarkMarkdownComponent.tsx",
    "content": "import MarkdownEditor from \"@/components/ui/markdown/markdown-editor\";\nimport { MarkdownReadonly } from \"@/components/ui/markdown/markdown-readonly\";\nimport { toast } from \"@/components/ui/sonner\";\n\nimport { useUpdateBookmark } from \"@karakeep/shared-react/hooks/bookmarks\";\n\nexport function BookmarkMarkdownComponent({\n  children: bookmark,\n  readOnly = true,\n}: {\n  children: {\n    id: string;\n    content: {\n      text: string;\n    };\n  };\n  readOnly?: boolean;\n}) {\n  const { mutate: updateBookmarkMutator, isPending } = useUpdateBookmark({\n    onSuccess: () => {\n      toast({\n        description: \"Note updated!\",\n      });\n    },\n    onError: () => {\n      toast({ description: \"Something went wrong\", variant: \"destructive\" });\n    },\n  });\n\n  const onSave = (text: string) => {\n    updateBookmarkMutator({\n      bookmarkId: bookmark.id,\n      text,\n    });\n  };\n\n  return (\n    <div className=\"h-full\">\n      {readOnly ? (\n        <MarkdownReadonly onSave={onSave}>\n          {bookmark.content.text}\n        </MarkdownReadonly>\n      ) : (\n        <MarkdownEditor onSave={onSave} isSaving={isPending}>\n          {bookmark.content.text}\n        </MarkdownEditor>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx",
    "content": "\"use client\";\n\nimport { ChangeEvent, useEffect, useRef, useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuSub,\n  DropdownMenuSubContent,\n  DropdownMenuSubTrigger,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { useSession } from \"@/lib/auth/client\";\nimport { useClientConfig } from \"@/lib/clientConfig\";\nimport useUpload from \"@/lib/hooks/upload-file\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport {\n  Archive,\n  Download,\n  FileDown,\n  FileText,\n  ImagePlus,\n  Link,\n  List,\n  ListX,\n  MoreHorizontal,\n  Pencil,\n  RotateCw,\n  SquarePen,\n  Trash2,\n} from \"lucide-react\";\nimport { toast } from \"sonner\";\n\nimport type {\n  ZBookmark,\n  ZBookmarkedAsset,\n  ZBookmarkedLink,\n} from \"@karakeep/shared/types/bookmarks\";\nimport {\n  useAttachBookmarkAsset,\n  useReplaceBookmarkAsset,\n} from \"@karakeep/shared-react/hooks/assets\";\nimport { useBookmarkGridContext } from \"@karakeep/shared-react/hooks/bookmark-grid-context\";\nimport { useBookmarkListContext } from \"@karakeep/shared-react/hooks/bookmark-list-context\";\nimport {\n  useRecrawlBookmark,\n  useUpdateBookmark,\n} from \"@karakeep/shared-react/hooks/bookmarks\";\nimport { useRemoveBookmarkFromList } from \"@karakeep/shared-react/hooks/lists\";\nimport { BookmarkTypes } from \"@karakeep/shared/types/bookmarks\";\nimport { getAssetUrl } from \"@karakeep/shared/utils/assetUtils\";\n\nimport { BookmarkedTextEditor } from \"./BookmarkedTextEditor\";\nimport DeleteBookmarkConfirmationDialog from \"./DeleteBookmarkConfirmationDialog\";\nimport { EditBookmarkDialog } from \"./EditBookmarkDialog\";\nimport { ArchivedActionIcon, FavouritedActionIcon } from \"./icons\";\nimport { useManageListsModal } from \"./ManageListsModal\";\n\ninterface ActionItem {\n  id: string;\n  title: string;\n  icon: React.ReactNode;\n  visible: boolean;\n  disabled: boolean;\n  className?: string;\n  onClick: () => void;\n}\n\ninterface SubsectionItem {\n  id: string;\n  title: string;\n  icon: React.ReactNode;\n  visible: boolean;\n  items: ActionItem[];\n}\n\nconst getBannerSonnerId = (bookmarkId: string) =>\n  `replace-banner-${bookmarkId}`;\n\ntype ActionItemType = ActionItem | SubsectionItem;\n\nfunction isSubsectionItem(item: ActionItemType): item is SubsectionItem {\n  return \"items\" in item;\n}\n\nexport default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) {\n  const { t } = useTranslation();\n  const linkId = bookmark.id;\n  const { data: session } = useSession();\n\n  const demoMode = !!useClientConfig().demoMode;\n\n  // Check if the current user owns this bookmark\n  const isOwner = session?.user?.id === bookmark.userId;\n\n  const [isClipboardAvailable, setIsClipboardAvailable] = useState(false);\n\n  useEffect(() => {\n    // This code only runs in the browser\n    setIsClipboardAvailable(\n      typeof window !== \"undefined\" &&\n        window.navigator &&\n        !!window.navigator.clipboard,\n    );\n  }, []);\n\n  const { setOpen: setManageListsModalOpen, content: manageListsModal } =\n    useManageListsModal(bookmark.id);\n\n  const [deleteBookmarkDialogOpen, setDeleteBookmarkDialogOpen] =\n    useState(false);\n  const [isTextEditorOpen, setTextEditorOpen] = useState(false);\n  const [isEditBookmarkDialogOpen, setEditBookmarkDialogOpen] = useState(false);\n\n  const bannerFileInputRef = useRef<HTMLInputElement>(null);\n\n  const { mutate: uploadBannerAsset } = useUpload({\n    onError: (e) => {\n      toast.error(e.error, { id: getBannerSonnerId(bookmark.id) });\n    },\n  });\n\n  const { mutate: attachAsset, isPending: isAttaching } =\n    useAttachBookmarkAsset({\n      onSuccess: () => {\n        toast.success(t(\"toasts.bookmarks.update_banner\"), {\n          id: getBannerSonnerId(bookmark.id),\n        });\n      },\n      onError: (e) => {\n        toast.error(e.message, { id: getBannerSonnerId(bookmark.id) });\n      },\n    });\n\n  const { mutate: replaceAsset, isPending: isReplacing } =\n    useReplaceBookmarkAsset({\n      onSuccess: () => {\n        toast.success(t(\"toasts.bookmarks.update_banner\"), {\n          id: getBannerSonnerId(bookmark.id),\n        });\n      },\n      onError: (e) => {\n        toast.error(e.message, { id: getBannerSonnerId(bookmark.id) });\n      },\n    });\n\n  const { listId } = useBookmarkGridContext() ?? {};\n  const withinListContext = useBookmarkListContext();\n\n  const onError = () => {\n    toast.error(t(\"common.something_went_wrong\"));\n  };\n\n  const updateBookmarkMutator = useUpdateBookmark({\n    onSuccess: () => {\n      toast.success(t(\"toasts.bookmarks.updated\"));\n    },\n    onError,\n  });\n\n  const crawlBookmarkMutator = useRecrawlBookmark({\n    onSuccess: () => {\n      toast.success(t(\"toasts.bookmarks.refetch\"));\n    },\n    onError,\n  });\n\n  const fullPageArchiveBookmarkMutator = useRecrawlBookmark({\n    onSuccess: () => {\n      toast.success(t(\"toasts.bookmarks.full_page_archive\"));\n    },\n    onError,\n  });\n\n  const preservePdfMutator = useRecrawlBookmark({\n    onSuccess: () => {\n      toast.success(t(\"toasts.bookmarks.preserve_pdf\"));\n    },\n    onError,\n  });\n\n  const removeFromListMutator = useRemoveBookmarkFromList({\n    onSuccess: () => {\n      toast.success(t(\"toasts.bookmarks.delete_from_list\"));\n    },\n    onError,\n  });\n\n  const handleBannerFileChange = (event: ChangeEvent<HTMLInputElement>) => {\n    const files = event.target.files;\n    if (files && files.length > 0) {\n      const file = files[0];\n      const existingBanner = bookmark.assets.find(\n        (asset) => asset.assetType === \"bannerImage\",\n      );\n\n      if (existingBanner) {\n        toast.loading(t(\"toasts.bookmarks.uploading_banner\"), {\n          id: getBannerSonnerId(bookmark.id),\n        });\n        uploadBannerAsset(file, {\n          onSuccess: (resp) => {\n            replaceAsset({\n              bookmarkId: bookmark.id,\n              oldAssetId: existingBanner.id,\n              newAssetId: resp.assetId,\n            });\n          },\n        });\n      } else {\n        toast.loading(t(\"toasts.bookmarks.uploading_banner\"), {\n          id: getBannerSonnerId(bookmark.id),\n        });\n        uploadBannerAsset(file, {\n          onSuccess: (resp) => {\n            attachAsset({\n              bookmarkId: bookmark.id,\n              asset: {\n                id: resp.assetId,\n                assetType: \"bannerImage\",\n              },\n            });\n          },\n        });\n      }\n    }\n  };\n\n  // Define action items array\n  const actionItems: ActionItemType[] = [\n    {\n      id: \"edit\",\n      title: t(\"actions.edit\"),\n      icon: <Pencil className=\"mr-2 size-4\" />,\n      visible: isOwner,\n      disabled: false,\n      onClick: () => setEditBookmarkDialogOpen(true),\n    },\n    {\n      id: \"open-editor\",\n      title: t(\"actions.open_editor\"),\n      icon: <SquarePen className=\"mr-2 size-4\" />,\n      visible: isOwner && bookmark.content.type === BookmarkTypes.TEXT,\n      disabled: false,\n      onClick: () => setTextEditorOpen(true),\n    },\n    {\n      id: \"favorite\",\n      title: bookmark.favourited\n        ? t(\"actions.unfavorite\")\n        : t(\"actions.favorite\"),\n      icon: (\n        <FavouritedActionIcon\n          className=\"mr-2 size-4\"\n          favourited={bookmark.favourited}\n        />\n      ),\n      visible: isOwner,\n      disabled: demoMode,\n      onClick: () =>\n        updateBookmarkMutator.mutate({\n          bookmarkId: linkId,\n          favourited: !bookmark.favourited,\n        }),\n    },\n    {\n      id: \"archive\",\n      title: bookmark.archived ? t(\"actions.unarchive\") : t(\"actions.archive\"),\n      icon: (\n        <ArchivedActionIcon\n          className=\"mr-2 size-4\"\n          archived={bookmark.archived}\n        />\n      ),\n      visible: isOwner,\n      disabled: demoMode,\n      onClick: () =>\n        updateBookmarkMutator.mutate({\n          bookmarkId: linkId,\n          archived: !bookmark.archived,\n        }),\n    },\n    {\n      id: \"copy-link\",\n      title: t(\"actions.copy_link\"),\n      icon: <Link className=\"mr-2 size-4\" />,\n      visible: bookmark.content.type === BookmarkTypes.LINK,\n      disabled: !isClipboardAvailable,\n      onClick: () => {\n        navigator.clipboard.writeText(\n          (bookmark.content as ZBookmarkedLink).url,\n        );\n        toast.success(t(\"toasts.bookmarks.clipboard_copied\"));\n      },\n    },\n    {\n      id: \"manage-lists\",\n      title: t(\"actions.manage_lists\"),\n      icon: <List className=\"mr-2 size-4\" />,\n      visible: isOwner,\n      disabled: false,\n      onClick: () => setManageListsModalOpen(true),\n    },\n    {\n      id: \"remove-from-list\",\n      title: t(\"actions.remove_from_list\"),\n      icon: <ListX className=\"mr-2 size-4\" />,\n      visible: Boolean(\n        (isOwner ||\n          (withinListContext &&\n            (withinListContext.userRole === \"editor\" ||\n              withinListContext.userRole === \"owner\"))) &&\n        !!listId &&\n        !!withinListContext &&\n        withinListContext.type === \"manual\",\n      ),\n      disabled: demoMode,\n      onClick: () =>\n        removeFromListMutator.mutate({\n          listId: listId!,\n          bookmarkId: bookmark.id,\n        }),\n    },\n    {\n      id: \"offline-copies\",\n      title: t(\"actions.offline_copies\"),\n      icon: <Archive className=\"mr-2 size-4\" />,\n      visible: isOwner && bookmark.content.type === BookmarkTypes.LINK,\n      items: [\n        {\n          id: \"download-full-page\",\n          title: t(\"actions.preserve_offline_archive\"),\n          icon: <FileDown className=\"mr-2 size-4\" />,\n          visible: true,\n          disabled: demoMode,\n          onClick: () => {\n            fullPageArchiveBookmarkMutator.mutate({\n              bookmarkId: bookmark.id,\n              archiveFullPage: true,\n            });\n          },\n        },\n        {\n          id: \"preserve-pdf\",\n          title: t(\"actions.preserve_as_pdf\"),\n          icon: <FileText className=\"mr-2 size-4\" />,\n          visible: true,\n          disabled: demoMode,\n          onClick: () => {\n            preservePdfMutator.mutate({\n              bookmarkId: bookmark.id,\n              storePdf: true,\n            });\n          },\n        },\n        {\n          id: \"download-full-page-archive\",\n          title: t(\"actions.download_full_page_archive_file\"),\n          icon: <Download className=\"mr-2 size-4\" />,\n          visible:\n            bookmark.content.type === BookmarkTypes.LINK &&\n            !!(\n              bookmark.content.fullPageArchiveAssetId ||\n              bookmark.content.precrawledArchiveAssetId\n            ),\n          disabled: false,\n          onClick: () => {\n            const link = bookmark.content as ZBookmarkedLink;\n            const archiveAssetId =\n              link.fullPageArchiveAssetId ?? link.precrawledArchiveAssetId;\n            if (archiveAssetId) {\n              window.open(getAssetUrl(archiveAssetId), \"_blank\");\n            }\n          },\n        },\n        {\n          id: \"download-pdf\",\n          title: t(\"actions.download_pdf_file\"),\n          icon: <Download className=\"mr-2 size-4\" />,\n          visible: !!(bookmark.content as ZBookmarkedLink).pdfAssetId,\n          disabled: false,\n          onClick: () => {\n            const link = bookmark.content as ZBookmarkedLink;\n            if (link.pdfAssetId) {\n              window.open(getAssetUrl(link.pdfAssetId), \"_blank\");\n            }\n          },\n        },\n      ],\n    },\n    {\n      id: \"more\",\n      title: t(\"actions.more\"),\n      icon: <MoreHorizontal className=\"mr-2 size-4\" />,\n      visible: isOwner,\n      items: [\n        {\n          id: \"refresh\",\n          title: t(\"actions.refresh\"),\n          icon: <RotateCw className=\"mr-2 size-4\" />,\n          visible: bookmark.content.type === BookmarkTypes.LINK,\n          disabled: demoMode,\n          onClick: () =>\n            crawlBookmarkMutator.mutate({ bookmarkId: bookmark.id }),\n        },\n        {\n          id: \"download-asset\",\n          title: t(\"actions.download\"),\n          icon: <Download className=\"mr-2 size-4\" />,\n          visible: bookmark.content.type === BookmarkTypes.ASSET,\n          disabled: false,\n          onClick: () => {\n            const asset = bookmark.content as ZBookmarkedAsset;\n            window.open(getAssetUrl(asset.assetId), \"_blank\");\n          },\n        },\n        {\n          id: \"replace-banner\",\n          title: bookmark.assets.find((a) => a.assetType === \"bannerImage\")\n            ? t(\"actions.replace_banner\")\n            : t(\"actions.add_banner\"),\n          icon: <ImagePlus className=\"mr-2 size-4\" />,\n          visible: true,\n          disabled: demoMode || isAttaching || isReplacing,\n          onClick: () => bannerFileInputRef.current?.click(),\n        },\n      ],\n    },\n    {\n      id: \"delete\",\n      title: t(\"actions.delete\"),\n      icon: <Trash2 className=\"mr-2 size-4\" />,\n      visible: isOwner,\n      disabled: demoMode,\n      className: \"text-destructive\",\n      onClick: () => setDeleteBookmarkDialogOpen(true),\n    },\n  ];\n\n  // Filter visible items\n  const visibleItems: ActionItemType[] = actionItems.filter((item) => {\n    if (isSubsectionItem(item)) {\n      return item.visible && item.items.some((subItem) => subItem.visible);\n    }\n    return item.visible;\n  });\n\n  // If no items are visible, don't render the dropdown\n  if (visibleItems.length === 0) {\n    return null;\n  }\n\n  return (\n    <>\n      {manageListsModal}\n      <EditBookmarkDialog\n        bookmark={bookmark}\n        open={isEditBookmarkDialogOpen}\n        setOpen={setEditBookmarkDialogOpen}\n      />\n      <DeleteBookmarkConfirmationDialog\n        bookmark={bookmark}\n        open={deleteBookmarkDialogOpen}\n        setOpen={setDeleteBookmarkDialogOpen}\n      />\n      <BookmarkedTextEditor\n        bookmark={bookmark}\n        open={isTextEditorOpen}\n        setOpen={setTextEditorOpen}\n      />\n      <DropdownMenu>\n        <DropdownMenuTrigger asChild>\n          <Button\n            variant=\"ghost\"\n            className=\"px-1 focus-visible:ring-0 focus-visible:ring-offset-0\"\n          >\n            <MoreHorizontal />\n          </Button>\n        </DropdownMenuTrigger>\n        <DropdownMenuContent className=\"w-fit\">\n          {visibleItems.map((item) => {\n            if (isSubsectionItem(item)) {\n              const visibleSubItems = item.items.filter(\n                (subItem) => subItem.visible,\n              );\n              if (visibleSubItems.length === 0) {\n                return null;\n              }\n              return (\n                <DropdownMenuSub key={item.id}>\n                  <DropdownMenuSubTrigger>\n                    {item.icon}\n                    <span>{item.title}</span>\n                  </DropdownMenuSubTrigger>\n                  <DropdownMenuSubContent>\n                    {visibleSubItems.map((subItem) => (\n                      <DropdownMenuItem\n                        key={subItem.id}\n                        disabled={subItem.disabled}\n                        onClick={subItem.onClick}\n                      >\n                        {subItem.icon}\n                        <span>{subItem.title}</span>\n                      </DropdownMenuItem>\n                    ))}\n                  </DropdownMenuSubContent>\n                </DropdownMenuSub>\n              );\n            }\n            return (\n              <DropdownMenuItem\n                key={item.id}\n                disabled={item.disabled}\n                className={item.className}\n                onClick={item.onClick}\n              >\n                {item.icon}\n                <span>{item.title}</span>\n              </DropdownMenuItem>\n            );\n          })}\n        </DropdownMenuContent>\n      </DropdownMenu>\n      <input\n        type=\"file\"\n        ref={bannerFileInputRef}\n        onChange={handleBannerFileChange}\n        className=\"hidden\"\n        accept=\".jpg,.jpeg,.png,.webp\"\n      />\n    </>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/bookmarks/BookmarkOwnerIcon.tsx",
    "content": "import {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { UserAvatar } from \"@/components/ui/user-avatar\";\n\ninterface BookmarkOwnerIconProps {\n  ownerName: string;\n  ownerAvatar: string | null;\n}\n\nexport default function BookmarkOwnerIcon({\n  ownerName,\n  ownerAvatar,\n}: BookmarkOwnerIconProps) {\n  return (\n    <Tooltip>\n      <TooltipTrigger>\n        <UserAvatar\n          name={ownerName}\n          image={ownerAvatar}\n          className=\"size-5 shrink-0 rounded-full ring-1 ring-border\"\n        />\n      </TooltipTrigger>\n      <TooltipContent className=\"font-sm\">\n        <p className=\"font-medium\">{ownerName}</p>\n      </TooltipContent>\n    </Tooltip>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/bookmarks/BookmarkTagsEditor.tsx",
    "content": "import { toast } from \"@/components/ui/sonner\";\n\nimport type { ZBookmark } from \"@karakeep/shared/types/bookmarks\";\nimport { useUpdateBookmarkTags } from \"@karakeep/shared-react/hooks/bookmarks\";\n\nimport { TagsEditor } from \"./TagsEditor\";\n\nexport function BookmarkTagsEditor({\n  bookmark,\n  disabled,\n}: {\n  bookmark: ZBookmark;\n  disabled?: boolean;\n}) {\n  const { mutate } = useUpdateBookmarkTags({\n    onSuccess: () => {\n      toast({\n        description: \"Tags has been updated!\",\n      });\n    },\n    onError: () => {\n      toast({\n        variant: \"destructive\",\n        title: \"Something went wrong\",\n        description: \"There was a problem with your request.\",\n      });\n    },\n  });\n\n  return (\n    <TagsEditor\n      tags={bookmark.tags}\n      disabled={disabled}\n      onAttach={({ tagName, tagId }) => {\n        mutate({\n          bookmarkId: bookmark.id,\n          attach: [\n            {\n              tagName,\n              tagId,\n            },\n          ],\n          detach: [],\n        });\n      }}\n      onDetach={({ tagId }) => {\n        mutate({\n          bookmarkId: bookmark.id,\n          attach: [],\n          detach: [{ tagId }],\n        });\n      }}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/bookmarks/BookmarkedTextEditor.tsx",
    "content": "import { BookmarkMarkdownComponent } from \"@/components/dashboard/bookmarks/BookmarkMarkdownComponent\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\";\n\nimport { ZBookmark, ZBookmarkTypeText } from \"@karakeep/shared/types/bookmarks\";\n\nexport function BookmarkedTextEditor({\n  bookmark,\n  open,\n  setOpen,\n}: {\n  bookmark: ZBookmark;\n  open: boolean;\n  setOpen: (open: boolean) => void;\n}) {\n  const isNewBookmark = bookmark === undefined;\n\n  return (\n    <Dialog open={open} onOpenChange={setOpen}>\n      <DialogContent className=\"max-w-[80%]\">\n        <DialogHeader className=\"flex\">\n          <DialogTitle className=\"w-fit\">\n            {isNewBookmark ? \"New Note\" : \"Edit Note\"}\n          </DialogTitle>\n        </DialogHeader>\n        <div className=\"h-[80vh]\">\n          <BookmarkMarkdownComponent readOnly={false}>\n            {bookmark as ZBookmarkTypeText}\n          </BookmarkMarkdownComponent>\n        </div>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/bookmarks/Bookmarks.tsx",
    "content": "import { redirect } from \"next/navigation\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { api } from \"@/server/api/client\";\nimport { getServerAuthSession } from \"@/server/auth\";\n\nimport type { ZGetBookmarksRequest } from \"@karakeep/shared/types/bookmarks\";\n\nimport UpdatableBookmarksGrid from \"./UpdatableBookmarksGrid\";\n\nexport default async function Bookmarks({\n  query,\n  header,\n  showDivider,\n  showEditorCard = false,\n}: {\n  query: Omit<ZGetBookmarksRequest, \"sortOrder\" | \"includeContent\">; // Sort order is handled by the store\n  header?: React.ReactNode;\n  showDivider?: boolean;\n  showEditorCard?: boolean;\n}) {\n  const session = await getServerAuthSession();\n  if (!session) {\n    redirect(\"/\");\n  }\n\n  const bookmarks = await api.bookmarks.getBookmarks({\n    ...query,\n  });\n\n  return (\n    <div className=\"flex flex-col gap-3\">\n      {header}\n      {showDivider && <Separator />}\n      <UpdatableBookmarksGrid\n        query={query}\n        bookmarks={bookmarks}\n        showEditorCard={showEditorCard}\n      />\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx",
    "content": "import { useEffect, useMemo } from \"react\";\nimport NoBookmarksBanner from \"@/components/dashboard/bookmarks/NoBookmarksBanner\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport useBulkActionsStore from \"@/lib/bulkActions\";\nimport { useInBookmarkGridStore } from \"@/lib/store/useInBookmarkGridStore\";\nimport {\n  bookmarkLayoutSwitch,\n  useBookmarkLayout,\n  useGridColumns,\n} from \"@/lib/userLocalSettings/bookmarksLayout\";\nimport tailwindConfig from \"@/tailwind.config\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { ErrorBoundary } from \"react-error-boundary\";\nimport { useInView } from \"react-intersection-observer\";\nimport Masonry from \"react-masonry-css\";\nimport resolveConfig from \"tailwindcss/resolveConfig\";\n\nimport type { ZBookmark } from \"@karakeep/shared/types/bookmarks\";\nimport { useBookmarkListContext } from \"@karakeep/shared-react/hooks/bookmark-list-context\";\n\nimport BookmarkCard from \"./BookmarkCard\";\nimport EditorCard from \"./EditorCard\";\nimport UnknownCard from \"./UnknownCard\";\n\nfunction StyledBookmarkCard({ children }: { children: React.ReactNode }) {\n  return (\n    <Slot className=\"mb-4 border border-border bg-card duration-300 ease-in hover:shadow-lg hover:transition-all\">\n      {children}\n    </Slot>\n  );\n}\n\nfunction getBreakpointConfig(userColumns: number) {\n  const fullConfig = resolveConfig(tailwindConfig);\n\n  const breakpointColumnsObj: { [key: number]: number; default: number } = {\n    default: userColumns,\n  };\n\n  // Responsive behavior: reduce columns on smaller screens\n  const lgColumns = Math.max(1, Math.min(userColumns, userColumns - 1));\n  const mdColumns = Math.max(1, Math.min(userColumns, 2));\n  const smColumns = 1;\n\n  breakpointColumnsObj[parseInt(fullConfig.theme.screens.lg)] = lgColumns;\n  breakpointColumnsObj[parseInt(fullConfig.theme.screens.md)] = mdColumns;\n  breakpointColumnsObj[parseInt(fullConfig.theme.screens.sm)] = smColumns;\n  return breakpointColumnsObj;\n}\n\nexport default function BookmarksGrid({\n  bookmarks,\n  hasNextPage = false,\n  fetchNextPage = () => ({}),\n  isFetchingNextPage = false,\n  showEditorCard = false,\n}: {\n  bookmarks: ZBookmark[];\n  showEditorCard?: boolean;\n  hasNextPage?: boolean;\n  isFetchingNextPage?: boolean;\n  fetchNextPage?: () => void;\n}) {\n  const layout = useBookmarkLayout();\n  const gridColumns = useGridColumns();\n  const bulkActionsStore = useBulkActionsStore();\n  const inBookmarkGrid = useInBookmarkGridStore();\n  const withinListContext = useBookmarkListContext();\n  const breakpointConfig = useMemo(\n    () => getBreakpointConfig(gridColumns),\n    [gridColumns],\n  );\n  const { ref: loadMoreRef, inView: loadMoreButtonInView } = useInView();\n\n  useEffect(() => {\n    bulkActionsStore.setVisibleBookmarks(bookmarks);\n    bulkActionsStore.setListContext(withinListContext);\n\n    return () => {\n      bulkActionsStore.setVisibleBookmarks([]);\n      bulkActionsStore.setListContext(undefined);\n    };\n  }, [bookmarks, withinListContext?.id]);\n\n  useEffect(() => {\n    inBookmarkGrid.setInBookmarkGrid(true);\n    return () => {\n      inBookmarkGrid.setInBookmarkGrid(false);\n    };\n  }, []);\n\n  useEffect(() => {\n    if (loadMoreButtonInView && hasNextPage && !isFetchingNextPage) {\n      fetchNextPage();\n    }\n  }, [loadMoreButtonInView]);\n\n  if (bookmarks.length == 0 && !showEditorCard) {\n    return <NoBookmarksBanner />;\n  }\n\n  const children = [\n    showEditorCard && (\n      <StyledBookmarkCard key={\"editor\"}>\n        <EditorCard />\n      </StyledBookmarkCard>\n    ),\n    ...bookmarks.map((b) => (\n      <ErrorBoundary key={b.id} fallback={<UnknownCard bookmark={b} />}>\n        <StyledBookmarkCard>\n          <BookmarkCard bookmark={b} />\n        </StyledBookmarkCard>\n      </ErrorBoundary>\n    )),\n  ];\n  return (\n    <>\n      {bookmarkLayoutSwitch(layout, {\n        masonry: (\n          <Masonry\n            className=\"-ml-4 flex w-auto\"\n            columnClassName=\"pl-4\"\n            breakpointCols={breakpointConfig}\n          >\n            {children}\n          </Masonry>\n        ),\n        grid: (\n          <Masonry\n            className=\"-ml-4 flex w-auto\"\n            columnClassName=\"pl-4\"\n            breakpointCols={breakpointConfig}\n          >\n            {children}\n          </Masonry>\n        ),\n        list: <div className=\"grid grid-cols-1\">{children}</div>,\n        compact: <div className=\"grid grid-cols-1\">{children}</div>,\n      })}\n      {hasNextPage && (\n        <div className=\"flex justify-center\">\n          <ActionButton\n            ref={loadMoreRef}\n            ignoreDemoMode={true}\n            loading={isFetchingNextPage}\n            onClick={() => fetchNextPage()}\n            variant=\"ghost\"\n          >\n            Load More\n          </ActionButton>\n        </div>\n      )}\n    </>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/bookmarks/BookmarksGridSkeleton.tsx",
    "content": "// TODO: Refactor the bookmark layout grid to be generic and allow to pass the bookmark component generically.\n// This removes the need for handling the layout in this component.\nimport { useMemo } from \"react\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport {\n  bookmarkLayoutSwitch,\n  useBookmarkLayout,\n  useGridColumns,\n} from \"@/lib/userLocalSettings/bookmarksLayout\";\nimport tailwindConfig from \"@/tailwind.config\";\nimport Masonry from \"react-masonry-css\";\nimport resolveConfig from \"tailwindcss/resolveConfig\";\n\nfunction getBreakpointConfig(userColumns: number) {\n  const fullConfig = resolveConfig(tailwindConfig);\n\n  const breakpointColumnsObj: { [key: number]: number; default: number } = {\n    default: userColumns,\n  };\n\n  const lgColumns = Math.max(1, Math.min(userColumns, userColumns - 1));\n  const mdColumns = Math.max(1, Math.min(userColumns, 2));\n  const smColumns = 1;\n\n  breakpointColumnsObj[parseInt(fullConfig.theme.screens.lg)] = lgColumns;\n  breakpointColumnsObj[parseInt(fullConfig.theme.screens.md)] = mdColumns;\n  breakpointColumnsObj[parseInt(fullConfig.theme.screens.sm)] = smColumns;\n  return breakpointColumnsObj;\n}\n\nfunction BookmarkCardSkeleton({ height }: { height: string }) {\n  return (\n    <div className=\"mb-4 border border-border bg-card p-4\">\n      <div className=\"space-y-3\">\n        <Skeleton className={`w-full ${height}`} />\n        <div className=\"flex items-center space-x-2\">\n          <Skeleton className=\"h-4 w-4 rounded-full\" />\n          <Skeleton className=\"h-3 w-24\" />\n        </div>\n        <Skeleton className=\"h-4 w-3/4\" />\n      </div>\n    </div>\n  );\n}\n\nexport default function BookmarksGridSkeleton({\n  count = 12,\n}: {\n  count?: number;\n}) {\n  const layout = useBookmarkLayout();\n  const gridColumns = useGridColumns();\n  const breakpointConfig = useMemo(\n    () => getBreakpointConfig(gridColumns),\n    [gridColumns],\n  );\n\n  const children = Array.from({ length: count }, (_, i) => (\n    <BookmarkCardSkeleton\n      key={i}\n      height={bookmarkLayoutSwitch(layout, {\n        masonry: \"h-48\",\n        grid: \"h-48\",\n        list: \"h-32\",\n        compact: \"h-4\",\n      })}\n    />\n  ));\n\n  return bookmarkLayoutSwitch(layout, {\n    masonry: (\n      <Masonry\n        className=\"-ml-4 flex w-auto\"\n        columnClassName=\"pl-4\"\n        breakpointCols={breakpointConfig}\n      >\n        {children}\n      </Masonry>\n    ),\n    grid: (\n      <Masonry\n        className=\"-ml-4 flex w-auto\"\n        columnClassName=\"pl-4\"\n        breakpointCols={breakpointConfig}\n      >\n        {children}\n      </Masonry>\n    ),\n    list: <div className=\"grid grid-cols-1\">{children}</div>,\n    compact: <div className=\"grid grid-cols-1\">{children}</div>,\n  });\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/bookmarks/BulkManageListsModal.tsx",
    "content": "import { ActionButton } from \"@/components/ui/action-button\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\";\nimport {\n  Form,\n  FormControl,\n  FormField,\n  FormItem,\n  FormMessage,\n} from \"@/components/ui/form\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { useForm } from \"react-hook-form\";\nimport { z } from \"zod\";\n\nimport { useAddBookmarkToList } from \"@karakeep/shared-react/hooks/lists\";\nimport { limitConcurrency } from \"@karakeep/shared/concurrency\";\n\nimport { BookmarkListSelector } from \"../lists/BookmarkListSelector\";\n\nexport default function BulkManageListsModal({\n  bookmarkIds,\n  open,\n  setOpen,\n}: {\n  bookmarkIds: string[];\n  open: boolean;\n  setOpen: (open: boolean) => void;\n}) {\n  const formSchema = z.object({\n    listId: z.string({\n      required_error: \"Please select a list\",\n    }),\n  });\n  const form = useForm<z.infer<typeof formSchema>>({\n    resolver: zodResolver(formSchema),\n    defaultValues: {\n      listId: undefined,\n    },\n  });\n\n  const { mutateAsync: addToList, isPending: isAddingToListPending } =\n    useAddBookmarkToList({\n      onSettled: () => {\n        form.resetField(\"listId\");\n      },\n      onError: (e) => {\n        if (e.data?.code == \"BAD_REQUEST\") {\n          toast({\n            variant: \"destructive\",\n            description: e.message,\n          });\n        } else {\n          toast({\n            variant: \"destructive\",\n            title: \"Something went wrong\",\n          });\n        }\n      },\n    });\n\n  const onSubmit = async (value: z.infer<typeof formSchema>) => {\n    const results = await Promise.allSettled(\n      limitConcurrency(\n        bookmarkIds.map(\n          (bookmarkId) => () =>\n            addToList({\n              bookmarkId,\n              listId: value.listId,\n            }),\n        ),\n        50,\n      ),\n    );\n\n    const successes = results.filter((r) => r.status == \"fulfilled\").length;\n    if (successes > 0) {\n      toast({\n        description: `${successes} bookmarks have been added to the list!`,\n      });\n    }\n\n    setOpen(false);\n  };\n\n  return (\n    <Dialog open={open} onOpenChange={setOpen}>\n      <DialogContent>\n        <Form {...form}>\n          <form\n            className=\"flex w-full flex-col gap-4\"\n            onSubmit={form.handleSubmit(onSubmit)}\n          >\n            <DialogHeader>\n              <DialogTitle>\n                Add {bookmarkIds.length} bookmarks to List\n              </DialogTitle>\n            </DialogHeader>\n\n            <FormField\n              control={form.control}\n              name=\"listId\"\n              render={({ field }) => {\n                return (\n                  <FormItem>\n                    <FormControl>\n                      <BookmarkListSelector\n                        value={field.value}\n                        onChange={field.onChange}\n                        listTypes={[\"manual\"]}\n                      />\n                    </FormControl>\n                    <FormMessage />\n                  </FormItem>\n                );\n              }}\n            />\n            <DialogFooter className=\"sm:justify-end\">\n              <DialogClose asChild>\n                <Button type=\"button\" variant=\"secondary\">\n                  Close\n                </Button>\n              </DialogClose>\n              <ActionButton\n                type=\"submit\"\n                loading={isAddingToListPending}\n                disabled={isAddingToListPending}\n              >\n                Add\n              </ActionButton>\n            </DialogFooter>\n          </form>\n        </Form>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/bookmarks/BulkTagModal.tsx",
    "content": "import { Button } from \"@/components/ui/button\";\nimport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { useQueries } from \"@tanstack/react-query\";\n\nimport { useUpdateBookmarkTags } from \"@karakeep/shared-react/hooks/bookmarks\";\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\nimport { limitConcurrency } from \"@karakeep/shared/concurrency\";\nimport { ZBookmark } from \"@karakeep/shared/types/bookmarks\";\n\nimport { TagsEditor } from \"./TagsEditor\";\n\nexport default function BulkTagModal({\n  bookmarkIds,\n  open,\n  setOpen,\n}: {\n  bookmarkIds: string[];\n  open: boolean;\n  setOpen: (open: boolean) => void;\n}) {\n  const api = useTRPC();\n  const results = useQueries({\n    queries: bookmarkIds.map((id) =>\n      api.bookmarks.getBookmark.queryOptions({ bookmarkId: id }),\n    ),\n  });\n\n  const bookmarks = results\n    .map((r) => r.data)\n    .filter((b): b is ZBookmark => !!b);\n\n  const { mutateAsync } = useUpdateBookmarkTags({\n    onError: (err) => {\n      if (err.data?.code == \"BAD_REQUEST\") {\n        if (err.data.zodError) {\n          toast({\n            variant: \"destructive\",\n            description: Object.values(err.data.zodError.fieldErrors)\n              .flat()\n              .join(\"\\n\"),\n          });\n        } else {\n          toast({\n            variant: \"destructive\",\n            description: err.message,\n          });\n        }\n      } else {\n        toast({\n          variant: \"destructive\",\n          title: \"Something went wrong\",\n        });\n      }\n    },\n  });\n\n  const onAttach = async (tag: { tagName: string; tagId?: string }) => {\n    const results = await Promise.allSettled(\n      limitConcurrency(\n        bookmarkIds.map(\n          (id) => () =>\n            mutateAsync({\n              bookmarkId: id,\n              attach: [tag],\n              detach: [],\n            }),\n        ),\n        50,\n      ),\n    );\n    const successes = results.filter((r) => r.status == \"fulfilled\").length;\n    toast({\n      description: `Tag \"${tag.tagName}\" has been added to ${successes} bookmarks!`,\n    });\n  };\n\n  const onDetach = async ({\n    tagId,\n    tagName,\n  }: {\n    tagId: string;\n    tagName: string;\n  }) => {\n    const results = await Promise.allSettled(\n      limitConcurrency(\n        bookmarkIds.map(\n          (id) => () =>\n            mutateAsync({\n              bookmarkId: id,\n              attach: [],\n              detach: [{ tagId }],\n            }),\n        ),\n        50,\n      ),\n    );\n    const successes = results.filter((r) => r.status == \"fulfilled\").length;\n    toast({\n      description: `Tag \"${tagName}\" has been removed from ${successes} bookmarks!`,\n    });\n  };\n\n  // Get all the tags that are attached to all the bookmarks\n  let tags = bookmarks\n    .flatMap((b) => b.tags)\n    .filter((tag) =>\n      bookmarks.every((b) => b.tags.some((t) => tag.id == t.id)),\n    );\n  // Filter duplicates\n  tags = tags.filter(\n    (tag, index, self) => index === self.findIndex((t) => t.id == tag.id),\n  );\n\n  return (\n    <Dialog open={open} onOpenChange={setOpen}>\n      <DialogContent>\n        <DialogHeader>\n          <DialogTitle>Edit Tags of {bookmarks.length} Bookmarks</DialogTitle>\n        </DialogHeader>\n        <TagsEditor tags={tags} onAttach={onAttach} onDetach={onDetach} />\n        <DialogFooter className=\"sm:justify-end\">\n          <DialogClose asChild>\n            <Button type=\"button\" variant=\"secondary\">\n              Close\n            </Button>\n          </DialogClose>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/bookmarks/DeleteBookmarkConfirmationDialog.tsx",
    "content": "import { usePathname, useRouter } from \"next/navigation\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport ActionConfirmingDialog from \"@/components/ui/action-confirming-dialog\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { useTranslation } from \"@/lib/i18n/client\";\n\nimport { useDeleteBookmark } from \"@karakeep/shared-react/hooks//bookmarks\";\nimport { ZBookmark } from \"@karakeep/shared/types/bookmarks\";\n\nexport default function DeleteBookmarkConfirmationDialog({\n  bookmark,\n  children,\n  open,\n  setOpen,\n}: {\n  bookmark: ZBookmark;\n  children?: React.ReactNode;\n  open: boolean;\n  setOpen: (v: boolean) => void;\n}) {\n  const { t } = useTranslation();\n  const currentPath = usePathname();\n  const router = useRouter();\n\n  const { mutate: deleteBoomark, isPending } = useDeleteBookmark({\n    onSuccess: () => {\n      toast({\n        description: t(\"toasts.bookmarks.deleted\"),\n      });\n      setOpen(false);\n      if (currentPath.includes(bookmark.id)) {\n        router.push(\"/dashboard/bookmarks\");\n      }\n    },\n    onError: () => {\n      toast({\n        variant: \"destructive\",\n        description: `Something went wrong`,\n      });\n    },\n  });\n\n  return (\n    <ActionConfirmingDialog\n      open={open}\n      setOpen={setOpen}\n      title={t(\"dialogs.bookmarks.delete_confirmation_title\")}\n      description={t(\"dialogs.bookmarks.delete_confirmation_description\")}\n      actionButton={() => (\n        <ActionButton\n          type=\"button\"\n          variant=\"destructive\"\n          loading={isPending}\n          onClick={() => deleteBoomark({ bookmarkId: bookmark.id })}\n        >\n          Delete\n        </ActionButton>\n      )}\n    >\n      {children}\n    </ActionConfirmingDialog>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/bookmarks/EditBookmarkDialog.tsx",
    "content": "import * as React from \"react\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport { Button } from \"@/components/ui/button\";\nimport { Calendar } from \"@/components/ui/calendar\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@/components/ui/dialog\";\nimport {\n  Form,\n  FormControl,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@/components/ui/form\";\nimport { Input } from \"@/components/ui/input\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { useDialogFormReset } from \"@/lib/hooks/useDialogFormReset\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { cn } from \"@/lib/utils\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { format } from \"date-fns\";\nimport { CalendarIcon } from \"lucide-react\";\nimport { useForm } from \"react-hook-form\";\n\nimport { useUpdateBookmark } from \"@karakeep/shared-react/hooks/bookmarks\";\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\nimport {\n  BookmarkTypes,\n  ZBookmark,\n  ZUpdateBookmarksRequest,\n  zUpdateBookmarksRequestSchema,\n} from \"@karakeep/shared/types/bookmarks\";\nimport { getBookmarkTitle } from \"@karakeep/shared/utils/bookmarkUtils\";\n\nimport { BookmarkTagsEditor } from \"./BookmarkTagsEditor\";\n\nconst formSchema = zUpdateBookmarksRequestSchema;\n\nexport function EditBookmarkDialog({\n  open,\n  setOpen,\n  bookmark,\n  children,\n}: {\n  bookmark: ZBookmark;\n  children?: React.ReactNode;\n  open: boolean;\n  setOpen: (v: boolean) => void;\n}) {\n  const api = useTRPC();\n  const { t } = useTranslation();\n\n  const { data: assetContent, isLoading: isAssetContentLoading } = useQuery(\n    api.bookmarks.getBookmark.queryOptions(\n      {\n        bookmarkId: bookmark.id,\n        includeContent: true,\n      },\n      {\n        enabled: open && bookmark.content.type == BookmarkTypes.ASSET,\n        select: (b) =>\n          b.content.type == BookmarkTypes.ASSET ? b.content.content : null,\n      },\n    ),\n  );\n\n  const bookmarkToDefault = (bookmark: ZBookmark) => ({\n    bookmarkId: bookmark.id,\n    summary: bookmark.summary,\n    note: bookmark.note === null ? undefined : bookmark.note,\n    title: getBookmarkTitle(bookmark),\n    createdAt: bookmark.createdAt ?? new Date(),\n    // Link specific defaults (only if bookmark is a link)\n    url:\n      bookmark.content.type === BookmarkTypes.LINK\n        ? bookmark.content.url\n        : undefined,\n    description:\n      bookmark.content.type === BookmarkTypes.LINK\n        ? (bookmark.content.description ?? \"\")\n        : undefined,\n    author:\n      bookmark.content.type === BookmarkTypes.LINK\n        ? (bookmark.content.author ?? \"\")\n        : undefined,\n    publisher:\n      bookmark.content.type === BookmarkTypes.LINK\n        ? (bookmark.content.publisher ?? \"\")\n        : undefined,\n    datePublished:\n      bookmark.content.type === BookmarkTypes.LINK\n        ? bookmark.content.datePublished\n        : undefined,\n    // Asset specific fields\n    assetContent: assetContent ?? undefined,\n  });\n\n  const form = useForm<ZUpdateBookmarksRequest>({\n    resolver: zodResolver(formSchema),\n    defaultValues: bookmarkToDefault(bookmark),\n  });\n\n  const { mutate: updateBookmarkMutate, isPending: isUpdatingBookmark } =\n    useUpdateBookmark({\n      onSuccess: (updatedBookmark) => {\n        toast({ description: \"Bookmark details updated successfully!\" });\n        // Close the dialog after successful detail update\n        setOpen(false);\n        // Reset form with potentially updated data\n        form.reset(bookmarkToDefault(updatedBookmark));\n      },\n      onError: (error) => {\n        toast({\n          variant: \"destructive\",\n          title: \"Failed to update bookmark\",\n          description: error.message,\n        });\n      },\n    });\n\n  function onSubmit(values: ZUpdateBookmarksRequest) {\n    // Ensure optional fields that are empty strings are sent as null/undefined if appropriate\n    const payload = {\n      ...values,\n      title: values.title ?? null,\n    };\n    updateBookmarkMutate(payload);\n  }\n\n  // Reset form only when dialog is initially opened to preserve unsaved changes\n  // This prevents losing unsaved title edits when tags are updated, which would\n  // cause the bookmark prop to change and trigger a form reset\n  useDialogFormReset(open, form, bookmarkToDefault(bookmark));\n\n  // Update assetContent field when it's loaded\n  React.useEffect(() => {\n    if (assetContent && bookmark.content.type === BookmarkTypes.ASSET) {\n      form.setValue(\"assetContent\", assetContent);\n    }\n  }, [assetContent, bookmark.content.type, form]);\n\n  const isLink = bookmark.content.type === BookmarkTypes.LINK;\n  const isAsset = bookmark.content.type === BookmarkTypes.ASSET;\n\n  return (\n    <Dialog open={open} onOpenChange={setOpen}>\n      {children && <DialogTrigger asChild>{children}</DialogTrigger>}\n      <DialogContent className=\"max-h-[90vh] overflow-y-auto sm:max-w-xl\">\n        <DialogHeader>\n          <DialogTitle>{t(\"bookmark_editor.title\")}</DialogTitle>\n          <DialogDescription>{t(\"bookmark_editor.subtitle\")}</DialogDescription>\n        </DialogHeader>\n        <Form {...form}>\n          <form onSubmit={form.handleSubmit(onSubmit)} className=\"space-y-4\">\n            <FormField\n              control={form.control}\n              name=\"title\"\n              render={({ field }) => (\n                <FormItem>\n                  <FormLabel>{t(\"common.title\")}</FormLabel>\n                  <FormControl>\n                    <Input\n                      placeholder=\"Bookmark title\"\n                      {...field}\n                      value={field.value ?? \"\"}\n                    />\n                  </FormControl>\n                  <FormMessage />\n                </FormItem>\n              )}\n            />\n\n            {isLink && (\n              <FormField\n                control={form.control}\n                name=\"url\"\n                render={({ field }) => (\n                  <FormItem>\n                    <FormLabel>{t(\"common.url\")}</FormLabel>\n                    <FormControl>\n                      <Input placeholder=\"https://example.com\" {...field} />\n                    </FormControl>\n                    <FormMessage />\n                  </FormItem>\n                )}\n              />\n            )}\n\n            {\n              <FormField\n                control={form.control}\n                name=\"note\"\n                render={({ field }) => (\n                  <FormItem>\n                    <FormLabel>{t(\"common.note\")}</FormLabel>\n                    <FormControl>\n                      <Textarea\n                        placeholder=\"Bookmark notes\"\n                        {...field}\n                        value={field.value ?? \"\"}\n                      />\n                    </FormControl>\n                    <FormMessage />\n                  </FormItem>\n                )}\n              />\n            }\n\n            {isLink && (\n              <FormField\n                control={form.control}\n                name=\"description\"\n                render={({ field }) => (\n                  <FormItem>\n                    <FormLabel>{t(\"common.description\")}</FormLabel>\n                    <FormControl>\n                      <Textarea\n                        placeholder=\"Bookmark description\"\n                        {...field}\n                        value={field.value ?? \"\"}\n                      />\n                    </FormControl>\n                    <FormMessage />\n                  </FormItem>\n                )}\n              />\n            )}\n\n            {isLink && (\n              <FormField\n                control={form.control}\n                name=\"summary\"\n                render={({ field }) => (\n                  <FormItem>\n                    <FormLabel>{t(\"common.summary\")}</FormLabel>\n                    <FormControl>\n                      <Textarea\n                        placeholder=\"Bookmark summary\"\n                        {...field}\n                        value={field.value ?? \"\"}\n                      />\n                    </FormControl>\n                    <FormMessage />\n                  </FormItem>\n                )}\n              />\n            )}\n\n            {isAsset && (\n              <FormField\n                control={form.control}\n                name=\"assetContent\"\n                render={({ field }) => (\n                  <FormItem>\n                    <FormLabel>\n                      {t(\"bookmark_editor.extracted_content\")}\n                    </FormLabel>\n                    <FormControl>\n                      <Textarea\n                        disabled={isAssetContentLoading}\n                        placeholder=\"Extracted Content\"\n                        {...field}\n                        value={field.value ?? \"\"}\n                      />\n                    </FormControl>\n                    <FormMessage />\n                  </FormItem>\n                )}\n              />\n            )}\n\n            {isLink && (\n              <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n                <FormField\n                  control={form.control}\n                  name=\"author\"\n                  render={({ field }) => (\n                    <FormItem>\n                      <FormLabel>{t(\"bookmark_editor.author\")}</FormLabel>\n                      <FormControl>\n                        <Input\n                          placeholder=\"Author name\"\n                          {...field}\n                          value={field.value ?? \"\"}\n                        />\n                      </FormControl>\n                      <FormMessage />\n                    </FormItem>\n                  )}\n                />\n                <FormField\n                  control={form.control}\n                  name=\"publisher\"\n                  render={({ field }) => (\n                    <FormItem>\n                      <FormLabel>{t(\"bookmark_editor.publisher\")}</FormLabel>\n                      <FormControl>\n                        <Input\n                          placeholder=\"Publisher name\"\n                          {...field}\n                          value={field.value ?? \"\"}\n                        />\n                      </FormControl>\n                      <FormMessage />\n                    </FormItem>\n                  )}\n                />\n              </div>\n            )}\n\n            <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n              <FormField\n                control={form.control}\n                name=\"createdAt\"\n                render={({ field }) => (\n                  <FormItem className=\"flex flex-col\">\n                    <FormLabel>{t(\"common.created_at\")}</FormLabel>\n                    <Popover>\n                      <PopoverTrigger asChild>\n                        <FormControl>\n                          <Button\n                            variant={\"outline\"}\n                            className={cn(\n                              \"pl-3 text-left font-normal\",\n                              !field.value && \"text-muted-foreground\",\n                            )}\n                          >\n                            {field.value ? (\n                              format(field.value, \"PPP\")\n                            ) : (\n                              <span>{t(\"bookmark_editor.pick_a_date\")}</span>\n                            )}\n                            <CalendarIcon className=\"ml-auto h-4 w-4 opacity-50\" />\n                          </Button>\n                        </FormControl>\n                      </PopoverTrigger>\n                      <PopoverContent className=\"w-auto p-0\" align=\"start\">\n                        <Calendar\n                          mode=\"single\"\n                          selected={field.value}\n                          onSelect={field.onChange}\n                          disabled={(date) =>\n                            date > new Date() || date < new Date(\"1900-01-01\")\n                          }\n                        />\n                      </PopoverContent>\n                    </Popover>\n                    <FormMessage />\n                  </FormItem>\n                )}\n              />\n\n              {isLink && (\n                <FormField\n                  control={form.control}\n                  name=\"datePublished\"\n                  render={({ field }) => (\n                    <FormItem className=\"flex flex-col\">\n                      <FormLabel>\n                        {t(\"bookmark_editor.date_published\")}\n                      </FormLabel>\n                      <Popover>\n                        <PopoverTrigger asChild>\n                          <FormControl>\n                            <Button\n                              variant={\"outline\"}\n                              className={cn(\n                                \"pl-3 text-left font-normal\",\n                                !field.value && \"text-muted-foreground\",\n                              )}\n                            >\n                              {field.value ? (\n                                format(field.value, \"PPP\")\n                              ) : (\n                                <span>{t(\"bookmark_editor.pick_a_date\")}</span>\n                              )}\n                              <CalendarIcon className=\"ml-auto h-4 w-4 opacity-50\" />\n                            </Button>\n                          </FormControl>\n                        </PopoverTrigger>\n                        <PopoverContent className=\"w-auto p-0\" align=\"start\">\n                          <Calendar\n                            mode=\"single\"\n                            selected={field.value ?? undefined} // Calendar expects Date | undefined\n                            onSelect={(date) => field.onChange(date ?? null)} // Handle undefined -> null\n                            disabled={(date) =>\n                              date > new Date() || date < new Date(\"1900-01-01\")\n                            }\n                          />\n                        </PopoverContent>\n                      </Popover>\n                      <FormMessage />\n                    </FormItem>\n                  )}\n                />\n              )}\n            </div>\n\n            <FormItem>\n              <FormLabel>{t(\"common.tags\")}</FormLabel>\n              <FormControl>\n                <BookmarkTagsEditor bookmark={bookmark} />\n              </FormControl>\n              <FormMessage />\n            </FormItem>\n\n            <DialogFooter>\n              <Button\n                type=\"button\"\n                variant=\"outline\"\n                onClick={() => setOpen(false)}\n                disabled={isUpdatingBookmark}\n              >\n                {t(\"actions.cancel\")}\n              </Button>\n              <ActionButton type=\"submit\" loading={isUpdatingBookmark}>\n                {t(\"bookmark_editor.save_changes\")}\n              </ActionButton>\n            </DialogFooter>\n          </form>\n        </Form>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/bookmarks/EditorCard.tsx",
    "content": "import type { SubmitErrorHandler, SubmitHandler } from \"react-hook-form\";\nimport React, { useImperativeHandle, useRef } from \"react\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport { Form, FormControl, FormItem } from \"@/components/ui/form\";\nimport { Kbd } from \"@/components/ui/kbd\";\nimport MultipleChoiceDialog from \"@/components/ui/multiple-choice-dialog\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport BookmarkAlreadyExistsToast from \"@/components/utils/BookmarkAlreadyExistsToast\";\nimport { useClientConfig } from \"@/lib/clientConfig\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport {\n  useBookmarkLayout,\n  useBookmarkLayoutSwitch,\n} from \"@/lib/userLocalSettings/bookmarksLayout\";\nimport { cn, getOS } from \"@/lib/utils\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { useForm } from \"react-hook-form\";\nimport { useHotkeys } from \"react-hotkeys-hook\";\nimport { z } from \"zod\";\n\nimport { useCreateBookmarkWithPostHook } from \"@karakeep/shared-react/hooks/bookmarks\";\nimport { BookmarkTypes } from \"@karakeep/shared/types/bookmarks\";\n\nimport { useUploadAsset } from \"../UploadDropzone\";\n\ninterface MultiUrlImportState {\n  urls: URL[];\n  text: string;\n}\n\nexport default function EditorCard({ className }: { className?: string }) {\n  const { t } = useTranslation();\n  const inputRef = useRef<HTMLTextAreaElement>(null);\n\n  const [multiUrlImportState, setMultiUrlImportState] =\n    React.useState<MultiUrlImportState | null>(null);\n\n  const demoMode = !!useClientConfig().demoMode;\n  const bookmarkLayout = useBookmarkLayout();\n  const formSchema = z.object({\n    text: z.string(),\n  });\n  const form = useForm<z.infer<typeof formSchema>>({\n    resolver: zodResolver(formSchema),\n    defaultValues: {\n      text: \"\",\n    },\n  });\n  const { ref, ...textFieldProps } = form.register(\"text\");\n  useImperativeHandle(ref, () => inputRef.current);\n  useHotkeys(\"mod+e\", () => {\n    inputRef.current?.focus();\n  });\n\n  const { mutate, isPending } = useCreateBookmarkWithPostHook({\n    onSuccess: (resp) => {\n      if (resp.alreadyExists) {\n        toast({\n          description: <BookmarkAlreadyExistsToast bookmarkId={resp.id} />,\n          variant: \"default\",\n        });\n      }\n      form.reset();\n      // if the list layout is used, we reset the size of the editor card to the original size after submitting\n      if (bookmarkLayout === \"list\" && inputRef?.current?.style) {\n        inputRef.current.style.height = \"auto\";\n      }\n    },\n    onError: (e) => {\n      toast({ description: e.message, variant: \"destructive\" });\n    },\n  });\n\n  const uploadAsset = useUploadAsset();\n\n  function tryToImportUrls(text: string): void {\n    const lines = text.split(\"\\n\");\n    const urls: URL[] = [];\n    for (const line of lines) {\n      // parsing can also throw an exception, but will be caught outside\n      const url = new URL(line);\n      if (url.protocol != \"http:\" && url.protocol != \"https:\") {\n        throw new Error(\"Invalid URL\");\n      }\n      urls.push(url);\n    }\n\n    if (urls.length === 1) {\n      // Only 1 url in the textfield --> simply import it\n      mutate({ type: BookmarkTypes.LINK, url: text });\n      return;\n    }\n    // multiple urls found --> ask the user if it should be imported as multiple URLs or as a text bookmark\n    setMultiUrlImportState({ urls, text });\n  }\n\n  const onInput = (e: React.FormEvent<HTMLTextAreaElement>) => {\n    // Expand the textarea to a max of half the screen size in the list layout only\n    if (bookmarkLayout === \"list\") {\n      const target = e.target as HTMLTextAreaElement;\n      const maxHeight = window.innerHeight * 0.5;\n      target.style.height = \"auto\";\n\n      if (target.scrollHeight <= maxHeight) {\n        target.style.height = `${target.scrollHeight}px`;\n      } else {\n        target.style.height = `${maxHeight}px`;\n      }\n    }\n  };\n\n  const onSubmit: SubmitHandler<z.infer<typeof formSchema>> = (data) => {\n    const text = data.text.trim();\n    if (!text.length) return;\n    try {\n      tryToImportUrls(text);\n    } catch {\n      // Not a URL\n      mutate({ type: BookmarkTypes.TEXT, text });\n    }\n  };\n\n  const onError: SubmitErrorHandler<z.infer<typeof formSchema>> = (errors) => {\n    toast({\n      description: Object.values(errors)\n        .map((v) => v.message)\n        .join(\"\\n\"),\n      variant: \"destructive\",\n    });\n  };\n  const cardHeight = useBookmarkLayoutSwitch({\n    grid: \"h-96\",\n    masonry: \"h-48\",\n    list: undefined,\n    compact: undefined,\n  });\n\n  const handlePaste = async (\n    event: React.ClipboardEvent<HTMLTextAreaElement>,\n  ) => {\n    if (event?.clipboardData?.items) {\n      await Promise.all(\n        Array.from(event.clipboardData.items)\n          .filter((item) => item?.type?.startsWith(\"image\"))\n          .map((item) => {\n            const blob = item.getAsFile();\n            if (blob) {\n              return uploadAsset(blob);\n            }\n          }),\n      );\n    }\n  };\n\n  /**\n   * Methods that triggers when \"enter\" is pressed (without ctrl)\n   * It checks if the current line is a todo\n   * if it is it automatically appends a todo a the start of the new line\n   */\n  const handleNewTodo = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {\n    const todoMarkup = \"- [ ] \";\n    const textarea = inputRef.current;\n    if (!textarea) return;\n    const start = textarea.selectionStart;\n    const end = textarea.selectionEnd;\n    const textBefore = textarea.value.slice(0, start);\n    const lines = textBefore.split(\"\\n\");\n    const currentLine = lines[lines.length - 1];\n    const currentLineIsTodo = currentLine.startsWith(todoMarkup);\n    if (!currentLineIsTodo) return;\n    e.preventDefault();\n    const newValue =\n      textarea.value.slice(0, start) +\n      \"\\n\" +\n      todoMarkup +\n      textarea.value.slice(end);\n    form.setValue(\"text\", newValue, { shouldDirty: true, shouldTouch: true });\n    textarea.value = newValue;\n    textarea.selectionStart = start + todoMarkup.length + 1;\n    textarea.selectionEnd = start + todoMarkup.length + 1;\n    textarea.dispatchEvent(new Event(\"input\", { bubbles: true }));\n  };\n\n  const OS = getOS();\n\n  return (\n    <Form {...form}>\n      <form\n        className={cn(\n          className,\n          \"relative flex flex-col gap-2 rounded-xl bg-card p-4\",\n          cardHeight,\n        )}\n        onSubmit={form.handleSubmit(onSubmit, onError)}\n      >\n        <div className=\"flex justify-between\">\n          <p className=\"text-sm\">{t(\"editor.new_item\")}</p>\n          <Kbd>⌘ + E</Kbd>\n        </div>\n        <Separator />\n        <FormItem className=\"flex-1\">\n          <FormControl>\n            <Textarea\n              ref={inputRef}\n              disabled={isPending}\n              className={cn(\n                \"text-md h-full w-full border-none p-0 font-light focus-visible:ring-0\",\n                { \"resize-none\": bookmarkLayout !== \"list\" },\n              )}\n              placeholder={t(\"editor.placeholder_v2\")}\n              onKeyDown={(e) => {\n                if (demoMode) {\n                  return;\n                }\n                if (\n                  e.key === \"Enter\" &&\n                  !(e.metaKey || e.ctrlKey || e.shiftKey)\n                ) {\n                  handleNewTodo(e);\n                }\n                if (e.key === \"Enter\" && (e.metaKey || e.ctrlKey)) {\n                  form.handleSubmit(onSubmit, onError)();\n                }\n              }}\n              onPaste={(e) => {\n                if (demoMode) {\n                  return;\n                }\n                handlePaste(e);\n              }}\n              onInput={onInput}\n              {...textFieldProps}\n            />\n          </FormControl>\n        </FormItem>\n        <ActionButton\n          disabled={!form.formState.dirtyFields.text}\n          loading={isPending}\n          type=\"submit\"\n          variant=\"secondary\"\n        >\n          {form.formState.dirtyFields.text\n            ? demoMode\n              ? t(\"editor.disabled_submissions\")\n              : `${t(\"actions.save\")} (${OS === \"macos\" ? \"⌘\" : \"Ctrl\"} + Enter)`\n            : t(\"actions.save\")}\n        </ActionButton>\n\n        {multiUrlImportState && (\n          <MultipleChoiceDialog\n            open={true}\n            title={t(\"editor.multiple_urls_dialog_title\")}\n            description={t(\"editor.multiple_urls_dialog_desc\")}\n            onOpenChange={(open) => {\n              if (!open) {\n                setMultiUrlImportState(null);\n              }\n            }}\n            actionButtons={[\n              () => (\n                <ActionButton\n                  type=\"button\"\n                  variant=\"secondary\"\n                  loading={isPending}\n                  onClick={() => {\n                    mutate({\n                      type: BookmarkTypes.TEXT,\n                      text: multiUrlImportState.text,\n                    });\n                    setMultiUrlImportState(null);\n                  }}\n                >\n                  {t(\"editor.import_as_text\")}\n                </ActionButton>\n              ),\n              () => (\n                <ActionButton\n                  type=\"button\"\n                  variant=\"destructive\"\n                  loading={isPending}\n                  onClick={() => {\n                    multiUrlImportState.urls.forEach((url) =>\n                      mutate({ type: BookmarkTypes.LINK, url: url.toString() }),\n                    );\n                    setMultiUrlImportState(null);\n                  }}\n                >\n                  {t(\"editor.import_as_separate_bookmarks\")}\n                </ActionButton>\n              ),\n            ]}\n          ></MultipleChoiceDialog>\n        )}\n      </form>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/bookmarks/FooterLinkURL.tsx",
    "content": "import Link from \"next/link\";\n\nexport default function FooterLinkURL({ url }: { url: string | null }) {\n  if (!url) {\n    return null;\n  }\n  const parsedUrl = new URL(url);\n  return (\n    <Link\n      className=\"line-clamp-1 hover:text-foreground\"\n      href={url}\n      target=\"_blank\"\n      rel=\"noreferrer\"\n    >\n      {parsedUrl.host}\n    </Link>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/bookmarks/LinkCard.tsx",
    "content": "\"use client\";\n\nimport Image from \"next/image\";\nimport Link from \"next/link\";\nimport { useUserSettings } from \"@/lib/userSettings\";\n\nimport type { ZBookmarkTypeLink } from \"@karakeep/shared/types/bookmarks\";\nimport {\n  getBookmarkLinkImageUrl,\n  getBookmarkTitle,\n  getSourceUrl,\n  isBookmarkStillCrawling,\n} from \"@karakeep/shared/utils/bookmarkUtils\";\n\nimport { BookmarkLayoutAdaptingCard } from \"./BookmarkLayoutAdaptingCard\";\nimport FooterLinkURL from \"./FooterLinkURL\";\n\nconst useOnClickUrl = (bookmark: ZBookmarkTypeLink) => {\n  const userSettings = useUserSettings();\n  return {\n    urlTarget:\n      userSettings.bookmarkClickAction === \"open_original_link\"\n        ? (\"_blank\" as const)\n        : (\"_self\" as const),\n    onClickUrl:\n      userSettings.bookmarkClickAction === \"expand_bookmark_preview\"\n        ? `/dashboard/preview/${bookmark.id}`\n        : bookmark.content.url,\n  };\n};\n\nfunction LinkTitle({ bookmark }: { bookmark: ZBookmarkTypeLink }) {\n  const { onClickUrl, urlTarget } = useOnClickUrl(bookmark);\n  const parsedUrl = new URL(bookmark.content.url);\n  return (\n    <Link href={onClickUrl} target={urlTarget} rel=\"noreferrer\">\n      {getBookmarkTitle(bookmark) ?? parsedUrl.host}\n    </Link>\n  );\n}\n\nfunction LinkImage({\n  bookmark,\n  className,\n}: {\n  bookmark: ZBookmarkTypeLink;\n  className?: string;\n}) {\n  const { onClickUrl, urlTarget } = useOnClickUrl(bookmark);\n  const link = bookmark.content;\n\n  const imgComponent = (url: string, unoptimized: boolean) => (\n    <Image\n      unoptimized={unoptimized}\n      className={className}\n      alt=\"card banner\"\n      fill={true}\n      src={url}\n    />\n  );\n\n  const imageDetails = getBookmarkLinkImageUrl(link);\n\n  let img: React.ReactNode;\n  if (isBookmarkStillCrawling(bookmark)) {\n    img = imgComponent(\"/blur.avif\", false);\n  } else if (imageDetails) {\n    img = imgComponent(imageDetails.url, !imageDetails.localAsset);\n  } else {\n    // No image found\n    // A dummy white pixel for when there's no image.\n    img = imgComponent(\n      \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdj+P///38ACfsD/QVDRcoAAAAASUVORK5CYII=\",\n      true,\n    );\n  }\n\n  return (\n    <Link\n      href={onClickUrl}\n      target={urlTarget}\n      rel=\"noreferrer\"\n      className={className}\n    >\n      <div className=\"relative size-full flex-1\">{img}</div>\n    </Link>\n  );\n}\n\nexport default function LinkCard({\n  bookmark: bookmarkLink,\n  className,\n}: {\n  bookmark: ZBookmarkTypeLink;\n  className?: string;\n}) {\n  return (\n    <BookmarkLayoutAdaptingCard\n      title={<LinkTitle bookmark={bookmarkLink} />}\n      footer={<FooterLinkURL url={getSourceUrl(bookmarkLink)} />}\n      bookmark={bookmarkLink}\n      wrapTags={false}\n      image={(_layout, className) => (\n        <LinkImage className={className} bookmark={bookmarkLink} />\n      )}\n      className={className}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/bookmarks/ManageListsModal.tsx",
    "content": "import { useState } from \"react\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\";\nimport {\n  Form,\n  FormControl,\n  FormField,\n  FormItem,\n  FormMessage,\n} from \"@/components/ui/form\";\nimport { toast } from \"@/components/ui/sonner\";\nimport LoadingSpinner from \"@/components/ui/spinner\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { Archive, X } from \"lucide-react\";\nimport { useForm } from \"react-hook-form\";\nimport { z } from \"zod\";\n\nimport {\n  useAddBookmarkToList,\n  useBookmarkLists,\n  useRemoveBookmarkFromList,\n} from \"@karakeep/shared-react/hooks/lists\";\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\nimport { BookmarkListSelector } from \"../lists/BookmarkListSelector\";\nimport ArchiveBookmarkButton from \"./action-buttons/ArchiveBookmarkButton\";\n\nexport default function ManageListsModal({\n  bookmarkId,\n  open,\n  setOpen,\n}: {\n  bookmarkId: string;\n  open: boolean;\n  setOpen: (open: boolean) => void;\n}) {\n  const api = useTRPC();\n  const { t } = useTranslation();\n  const formSchema = z.object({\n    listId: z.string({\n      required_error: \"Please select a list\",\n    }),\n  });\n  const form = useForm<z.infer<typeof formSchema>>({\n    resolver: zodResolver(formSchema),\n    defaultValues: {\n      listId: undefined,\n    },\n  });\n\n  const { data: allLists, isPending: isAllListsPending } = useBookmarkLists(\n    undefined,\n    { enabled: open },\n  );\n\n  const { data: alreadyInList, isPending: isAlreadyInListPending } = useQuery(\n    api.lists.getListsOfBookmark.queryOptions(\n      {\n        bookmarkId,\n      },\n      { enabled: open },\n    ),\n  );\n\n  const isLoading = isAllListsPending || isAlreadyInListPending;\n\n  const { mutate: addToList, isPending: isAddingToListPending } =\n    useAddBookmarkToList({\n      onSuccess: () => {\n        toast({\n          description: t(\"toasts.lists.updated\"),\n        });\n        form.resetField(\"listId\");\n      },\n      onError: (e) => {\n        if (e.data?.code == \"BAD_REQUEST\") {\n          toast({\n            variant: \"destructive\",\n            description: e.message,\n          });\n        } else {\n          toast({\n            variant: \"destructive\",\n            title: t(\"common.something_went_wrong\"),\n          });\n        }\n      },\n    });\n\n  const { mutate: deleteFromList, isPending: isDeleteFromListPending } =\n    useRemoveBookmarkFromList({\n      onSuccess: () => {\n        toast({\n          description: t(\"toasts.lists.updated\"),\n        });\n        form.resetField(\"listId\");\n      },\n      onError: (e) => {\n        if (e.data?.code == \"BAD_REQUEST\") {\n          toast({\n            variant: \"destructive\",\n            description: e.message,\n          });\n        } else {\n          toast({\n            variant: \"destructive\",\n            title: t(\"common.something_went_wrong\"),\n          });\n        }\n      },\n    });\n\n  return (\n    <Dialog open={open} onOpenChange={setOpen}>\n      <DialogContent>\n        <Form {...form}>\n          <form\n            onSubmit={form.handleSubmit((value) => {\n              addToList({\n                bookmarkId: bookmarkId,\n                listId: value.listId,\n              });\n            })}\n          >\n            <DialogHeader>\n              <DialogTitle>Manage Lists</DialogTitle>\n            </DialogHeader>\n            {isLoading ? (\n              <LoadingSpinner className=\"my-4\" />\n            ) : (\n              allLists && (\n                <ul className=\"flex flex-col gap-2 pb-2 pt-4\">\n                  {alreadyInList?.lists.map((list) => (\n                    <li\n                      key={list.id}\n                      className=\"flex items-center justify-between rounded-lg border border-border bg-background px-2 py-1 text-foreground\"\n                    >\n                      <p>\n                        {allLists\n                          .getPathById(list.id)!\n                          .map((l) => `${l.icon} ${l.name}`)\n                          .join(\" / \")}\n                      </p>\n                      <ActionButton\n                        type=\"button\"\n                        variant=\"ghost\"\n                        size=\"sm\"\n                        loading={isDeleteFromListPending}\n                        onClick={() =>\n                          deleteFromList({ bookmarkId, listId: list.id })\n                        }\n                      >\n                        <X className=\"size-4\" />\n                      </ActionButton>\n                    </li>\n                  ))}\n                </ul>\n              )\n            )}\n\n            <div className=\"pb-4\">\n              <FormField\n                control={form.control}\n                name=\"listId\"\n                render={({ field }) => {\n                  return (\n                    <FormItem>\n                      <FormControl>\n                        <BookmarkListSelector\n                          value={field.value}\n                          hideBookmarkIds={alreadyInList?.lists.map(\n                            (l) => l.id,\n                          )}\n                          onChange={field.onChange}\n                          listTypes={[\"manual\"]}\n                        />\n                      </FormControl>\n                      <FormMessage />\n                    </FormItem>\n                  );\n                }}\n              />\n            </div>\n            <DialogFooter className=\"sm:justify-end\">\n              <DialogClose asChild>\n                <Button type=\"button\" variant=\"secondary\">\n                  {t(\"actions.close\")}\n                </Button>\n              </DialogClose>\n              <ArchiveBookmarkButton\n                type=\"button\"\n                bookmarkId={bookmarkId}\n                onDone={() => setOpen(false)}\n              >\n                <Archive className=\"mr-2 size-4\" /> {t(\"actions.archive\")}\n              </ArchiveBookmarkButton>\n              <ActionButton\n                type=\"submit\"\n                loading={isAddingToListPending}\n                disabled={isAddingToListPending}\n              >\n                {t(\"actions.add\")}\n              </ActionButton>\n            </DialogFooter>\n          </form>\n        </Form>\n      </DialogContent>\n    </Dialog>\n  );\n}\n\nexport function useManageListsModal(bookmarkId: string) {\n  const [open, setOpen] = useState(false);\n\n  return {\n    open,\n    setOpen,\n    content: open && (\n      <ManageListsModal bookmarkId={bookmarkId} open={open} setOpen={setOpen} />\n    ),\n  };\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/bookmarks/NoBookmarksBanner.tsx",
    "content": "\"use client\";\n\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { Bookmark } from \"lucide-react\";\n\nexport default function NoBookmarksBanner() {\n  const { t } = useTranslation();\n  return (\n    <div className=\"flex flex-col items-center justify-center rounded-lg bg-slate-50 p-10 text-center shadow-sm dark:bg-slate-700/50 dark:shadow-md\">\n      <div className=\"mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-slate-100 dark:bg-slate-700\">\n        <Bookmark className=\"h-8 w-8 text-slate-400 dark:text-slate-300\" />\n      </div>\n      <h3 className=\"mb-2 text-xl font-medium text-slate-700 dark:text-slate-100\">\n        {t(\"banners.no_bookmarks.title\")}\n      </h3>\n      <p className=\"mb-6 max-w-md text-slate-500 dark:text-slate-400\">\n        {t(\"banners.no_bookmarks.description\")}\n      </p>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/bookmarks/NotePreview.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport Link from \"next/link\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { cn } from \"@/lib/utils\";\nimport { ExternalLink, NotepadText } from \"lucide-react\";\n\ninterface NotePreviewProps {\n  note: string;\n  bookmarkId: string;\n  className?: string;\n}\n\nexport function NotePreview({ note, bookmarkId, className }: NotePreviewProps) {\n  const { t } = useTranslation();\n  const [isOpen, setIsOpen] = useState(false);\n\n  if (!note?.trim()) {\n    return null;\n  }\n\n  return (\n    <Popover open={isOpen} onOpenChange={setIsOpen}>\n      <PopoverTrigger asChild>\n        <div\n          className={cn(\n            \"flex cursor-pointer items-center gap-1.5 text-sm font-light italic text-gray-500 dark:text-gray-400\",\n            className,\n          )}\n        >\n          <NotepadText className=\"size-5 shrink-0\" />\n          <div className=\"line-clamp-2 min-w-0 flex-1 overflow-hidden text-wrap break-words\">\n            {note}\n          </div>\n        </div>\n      </PopoverTrigger>\n      <PopoverContent className=\"w-96 max-w-[calc(100vw-2rem)]\" align=\"start\">\n        <div className=\"space-y-3\">\n          <div className=\"max-h-60 overflow-y-auto whitespace-pre-wrap break-words text-sm text-gray-700 dark:text-gray-300\">\n            {note}\n          </div>\n          <div className=\"flex justify-end\">\n            <Link href={`/dashboard/preview/${bookmarkId}`}>\n              <Button\n                variant=\"outline\"\n                size=\"sm\"\n                className=\"gap-2\"\n                onClick={() => setIsOpen(false)}\n              >\n                {t(\"actions.edit_notes\")}\n                <ExternalLink className=\"size-4\" />\n              </Button>\n            </Link>\n          </div>\n        </div>\n      </PopoverContent>\n    </Popover>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/bookmarks/SummarizeBookmarkArea.tsx",
    "content": "import React from \"react\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport { MarkdownReadonly } from \"@/components/ui/markdown/markdown-readonly\";\nimport { toast } from \"@/components/ui/sonner\";\nimport LoadingSpinner from \"@/components/ui/spinner\";\nimport { useClientConfig } from \"@/lib/clientConfig\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { cn } from \"@/lib/utils\";\nimport { ChevronUp, RefreshCw, Sparkles, Trash2 } from \"lucide-react\";\n\nimport {\n  useSummarizeBookmark,\n  useUpdateBookmark,\n} from \"@karakeep/shared-react/hooks/bookmarks\";\nimport { BookmarkTypes, ZBookmark } from \"@karakeep/shared/types/bookmarks\";\n\nfunction AISummary({\n  bookmarkId,\n  summary,\n  readOnly = false,\n}: {\n  bookmarkId: string;\n  summary: string;\n  readOnly?: boolean;\n}) {\n  const [isExpanded, setIsExpanded] = React.useState(false);\n  const { mutate: resummarize, isPending: isResummarizing } =\n    useSummarizeBookmark({\n      onError: () => {\n        toast({\n          description: \"Something went wrong\",\n          variant: \"destructive\",\n        });\n      },\n    });\n  const { mutate: updateBookmark, isPending: isUpdatingBookmark } =\n    useUpdateBookmark({\n      onError: () => {\n        toast({\n          description: \"Something went wrong\",\n          variant: \"destructive\",\n        });\n      },\n    });\n  return (\n    <div className=\"w-full p-1\">\n      {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}\n      <div\n        className={`relative overflow-hidden rounded-lg p-4 transition-all duration-300 ease-in-out ${isExpanded ? \"h-auto\" : \"cursor-pointer\"} border border-muted-foreground/20 p-[2px]`}\n        onClick={() => !isExpanded && setIsExpanded(true)}\n      >\n        <div className=\"h-full rounded-lg bg-accent p-2\">\n          <MarkdownReadonly\n            className={`text-sm ${!isExpanded && \"line-clamp-3\"}`}\n          >\n            {summary}\n          </MarkdownReadonly>\n          {isExpanded && (\n            <span className=\"flex justify-end gap-2 pt-2\">\n              {!readOnly && (\n                <>\n                  <ActionButton\n                    variant=\"none\"\n                    size=\"none\"\n                    spinner={<LoadingSpinner className=\"size-4\" />}\n                    className=\"rounded-full bg-gray-200 p-1 text-gray-600 dark:bg-gray-700 dark:text-gray-400\"\n                    aria-label={isExpanded ? \"Collapse\" : \"Expand\"}\n                    loading={isResummarizing}\n                    onClick={() => resummarize({ bookmarkId })}\n                  >\n                    <RefreshCw size={16} />\n                  </ActionButton>\n                  <ActionButton\n                    size=\"none\"\n                    variant=\"none\"\n                    spinner={<LoadingSpinner className=\"size-4\" />}\n                    className=\"rounded-full bg-gray-200 p-1 text-gray-600 dark:bg-gray-700 dark:text-gray-400\"\n                    aria-label={isExpanded ? \"Collapse\" : \"Expand\"}\n                    loading={isUpdatingBookmark}\n                    onClick={() =>\n                      updateBookmark({ bookmarkId, summary: null })\n                    }\n                  >\n                    <Trash2 size={16} />\n                  </ActionButton>\n                </>\n              )}\n              <button\n                className=\"rounded-full bg-gray-200 p-1 text-gray-600 dark:bg-gray-700 dark:text-gray-400\"\n                aria-label=\"Collapse\"\n                onClick={() => setIsExpanded(false)}\n              >\n                <ChevronUp size={16} />\n              </button>\n            </span>\n          )}\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport default function SummarizeBookmarkArea({\n  bookmark,\n  readOnly = false,\n}: {\n  bookmark: ZBookmark;\n  readOnly?: boolean;\n}) {\n  const { t } = useTranslation();\n  const { mutate, isPending } = useSummarizeBookmark({\n    onError: () => {\n      toast({\n        description: \"Something went wrong\",\n        variant: \"destructive\",\n      });\n    },\n  });\n\n  const clientConfig = useClientConfig();\n  if (bookmark.content.type !== BookmarkTypes.LINK) {\n    return null;\n  }\n\n  if (bookmark.summary) {\n    return (\n      <AISummary\n        bookmarkId={bookmark.id}\n        summary={bookmark.summary}\n        readOnly={readOnly}\n      />\n    );\n  } else if (!clientConfig.inference.isConfigured || readOnly) {\n    return null;\n  } else {\n    return (\n      <div className=\"flex w-full items-center gap-4\">\n        <ActionButton\n          onClick={() => mutate({ bookmarkId: bookmark.id })}\n          variant=\"secondary\"\n          className={cn(\n            `w-full text-muted-foreground transition-all duration-300 hover:text-foreground`,\n          )}\n          loading={isPending}\n        >\n          <span className=\"flex items-center gap-1.5\">\n            {t(\"actions.summarize_with_ai\")}\n            <Sparkles className=\"size-4\" />\n          </span>\n        </ActionButton>\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/bookmarks/TagList.tsx",
    "content": "import Link from \"next/link\";\nimport { badgeVariants } from \"@/components/ui/badge\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport { useSession } from \"@/lib/auth/client\";\nimport { cn } from \"@/lib/utils\";\n\nimport type { ZBookmark } from \"@karakeep/shared/types/bookmarks\";\n\nexport default function TagList({\n  bookmark,\n  loading,\n  className,\n}: {\n  bookmark: ZBookmark;\n  loading?: boolean;\n  className?: string;\n}) {\n  const { data: session } = useSession();\n  const isOwner = session?.user?.id === bookmark.userId;\n\n  if (loading) {\n    return (\n      <div className=\"flex w-full flex-col justify-end space-y-2 p-2\">\n        <Skeleton className=\"h-4 w-full\" />\n        <Skeleton className=\"h-4 w-full\" />\n      </div>\n    );\n  }\n  return (\n    <>\n      {bookmark.tags.map((t) => (\n        <div key={t.id} className={className}>\n          {isOwner ? (\n            <Link\n              key={t.id}\n              className={cn(\n                badgeVariants({ variant: \"secondary\" }),\n                \"text-nowrap font-light text-gray-700 hover:bg-foreground hover:text-secondary dark:text-gray-400\",\n              )}\n              href={`/dashboard/tags/${t.id}`}\n            >\n              {t.name}\n            </Link>\n          ) : (\n            <span\n              key={t.id}\n              className={cn(\n                badgeVariants({ variant: \"secondary\" }),\n                \"text-nowrap font-light text-gray-700 dark:text-gray-400\",\n              )}\n            >\n              {t.name}\n            </span>\n          )}\n        </div>\n      ))}\n    </>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/bookmarks/TagModal.tsx",
    "content": "import { useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\";\nimport { useTranslation } from \"@/lib/i18n/client\";\n\nimport type { ZBookmark } from \"@karakeep/shared/types/bookmarks\";\n\nimport { BookmarkTagsEditor } from \"./BookmarkTagsEditor\";\n\nexport default function TagModal({\n  bookmark,\n  open,\n  setOpen,\n}: {\n  bookmark: ZBookmark;\n  open: boolean;\n  setOpen: (open: boolean) => void;\n}) {\n  const { t } = useTranslation();\n  return (\n    <Dialog open={open} onOpenChange={setOpen}>\n      <DialogContent>\n        <DialogHeader>\n          <DialogTitle>{t(\"actions.edit_tags\")}</DialogTitle>\n        </DialogHeader>\n        <BookmarkTagsEditor bookmark={bookmark} />\n        <DialogFooter className=\"sm:justify-end\">\n          <DialogClose asChild>\n            <Button type=\"button\" variant=\"secondary\">\n              {t(\"actions.close\")}\n            </Button>\n          </DialogClose>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n\nexport function useTagModel(bookmark: ZBookmark) {\n  const [open, setOpen] = useState(false);\n\n  return {\n    open,\n    setOpen,\n    content: (\n      <TagModal\n        key={bookmark.id}\n        bookmark={bookmark}\n        open={open}\n        setOpen={setOpen}\n      />\n    ),\n  };\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/bookmarks/TagsEditor.tsx",
    "content": "import React, { useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandItem,\n  CommandList,\n} from \"@/components/ui/command\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { useClientConfig } from \"@/lib/clientConfig\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { cn } from \"@/lib/utils\";\nimport { keepPreviousData, useQuery } from \"@tanstack/react-query\";\nimport { Command as CommandPrimitive } from \"cmdk\";\nimport { Check, Loader2, Plus, Sparkles, X } from \"lucide-react\";\n\nimport type { ZBookmarkTags } from \"@karakeep/shared/types/tags\";\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\nexport function TagsEditor({\n  tags: _tags,\n  onAttach,\n  onDetach,\n  disabled,\n  allowCreation = true,\n  placeholder,\n}: {\n  tags: ZBookmarkTags[];\n  onAttach: (tag: { tagName: string; tagId?: string }) => void;\n  onDetach: (tag: { tagName: string; tagId: string }) => void;\n  disabled?: boolean;\n  allowCreation?: boolean;\n  placeholder?: string;\n}) {\n  const api = useTRPC();\n  const { t } = useTranslation();\n  const demoMode = !!useClientConfig().demoMode;\n  const isDisabled = demoMode || disabled;\n  const inputRef = React.useRef<HTMLInputElement>(null);\n  const containerRef = React.useRef<HTMLDivElement>(null);\n  const [open, setOpen] = React.useState(false);\n  const [inputValue, setInputValue] = React.useState(\"\");\n  const [optimisticTags, setOptimisticTags] = useState<ZBookmarkTags[]>(_tags);\n  const tempIdCounter = React.useRef(0);\n  const hasInitializedRef = React.useRef(_tags.length > 0);\n\n  const generateTempId = React.useCallback(() => {\n    tempIdCounter.current += 1;\n    if (\n      typeof crypto !== \"undefined\" &&\n      typeof crypto.randomUUID === \"function\"\n    ) {\n      return `temp-${crypto.randomUUID()}`;\n    }\n\n    return `temp-${Date.now()}-${tempIdCounter.current}`;\n  }, []);\n\n  React.useEffect(() => {\n    // When allowCreation is false, only sync on initial load\n    // After that, rely on optimistic updates to avoid re-ordering\n    if (!allowCreation) {\n      if (!hasInitializedRef.current && _tags.length > 0) {\n        hasInitializedRef.current = true;\n        setOptimisticTags(_tags);\n      }\n      return;\n    }\n\n    // For allowCreation mode, sync server state with optimistic state\n    setOptimisticTags((prev) => {\n      // Start with a copy to avoid mutating the previous state\n      const results = [...prev];\n      let changed = false;\n\n      for (const tag of _tags) {\n        const idx = results.findIndex((t) => t.name === tag.name);\n        if (idx == -1) {\n          results.push(tag);\n          changed = true;\n          continue;\n        }\n        if (results[idx].id.startsWith(\"temp-\")) {\n          results[idx] = tag;\n          changed = true;\n          continue;\n        }\n      }\n\n      return changed ? results : prev;\n    });\n  }, [_tags, allowCreation]);\n\n  const { data: filteredOptions, isLoading: isExistingTagsLoading } = useQuery(\n    api.tags.list.queryOptions(\n      {\n        nameContains: inputValue,\n        limit: 50,\n        sortBy: inputValue.length > 0 ? \"relevance\" : \"usage\",\n      },\n      {\n        select: (data) =>\n          data.tags.map((t) => ({\n            id: t.id,\n            name: t.name,\n            attachedBy:\n              (t.numBookmarksByAttachedType.human ?? 0) > 0\n                ? (\"human\" as const)\n                : (\"ai\" as const),\n          })),\n        placeholderData: keepPreviousData,\n        gcTime: inputValue.length > 0 ? 60_000 : 3_600_000,\n      },\n    ),\n  );\n\n  const selectedValues = optimisticTags.map((tag) => tag.id);\n\n  // Add \"create new\" option if input doesn't match any existing option\n  const trimmedInputValue = inputValue.trim();\n\n  interface DisplayOption {\n    id: string;\n    name: string;\n    label: string;\n    attachedBy: \"human\" | \"ai\";\n    isCreateOption?: boolean;\n  }\n\n  const displayedOptions = React.useMemo<DisplayOption[]>(() => {\n    if (!filteredOptions) return [];\n\n    const baseOptions = filteredOptions.map((option) => ({\n      ...option,\n      label: option.name,\n    }));\n\n    if (!trimmedInputValue) {\n      return baseOptions;\n    }\n\n    const exactMatch = baseOptions.some(\n      (opt) => opt.name.toLowerCase() === trimmedInputValue.toLowerCase(),\n    );\n\n    if (!exactMatch && allowCreation) {\n      return [\n        {\n          id: \"create-new\",\n          name: trimmedInputValue,\n          label: `Create \"${trimmedInputValue}\"`,\n          attachedBy: \"human\" as const,\n          isCreateOption: true,\n        },\n        ...baseOptions,\n      ];\n    }\n\n    return baseOptions;\n  }, [filteredOptions, trimmedInputValue, allowCreation]);\n\n  const onChange = (\n    actionMeta:\n      | { action: \"create-option\"; name: string }\n      | { action: \"select-option\"; id: string; name: string }\n      | {\n          action: \"remove-value\";\n          id: string;\n          name: string;\n        },\n  ) => {\n    switch (actionMeta.action) {\n      case \"remove-value\": {\n        setOptimisticTags((prev) => prev.filter((t) => t.id != actionMeta.id));\n        onDetach({\n          tagId: actionMeta.id,\n          tagName: actionMeta.name,\n        });\n        break;\n      }\n      case \"create-option\": {\n        const tempId = generateTempId();\n        setOptimisticTags((prev) => [\n          ...prev,\n          {\n            id: tempId,\n            name: actionMeta.name,\n            attachedBy: \"human\" as const,\n          },\n        ]);\n        onAttach({ tagName: actionMeta.name });\n        break;\n      }\n      case \"select-option\": {\n        setOptimisticTags((prev) => {\n          if (prev.some((tag) => tag.id === actionMeta.id)) {\n            return prev;\n          }\n\n          return [\n            ...prev,\n            {\n              id: actionMeta.id,\n              name: actionMeta.name,\n              attachedBy: \"human\" as const,\n            },\n          ];\n        });\n        onAttach({\n          tagName: actionMeta.name,\n          tagId: actionMeta.id,\n        });\n        break;\n      }\n    }\n  };\n\n  const createTag = () => {\n    if (!inputValue.trim()) return;\n    onChange({ action: \"create-option\", name: inputValue.trim() });\n    setInputValue(\"\");\n  };\n\n  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n    if (e.key === \"Escape\") {\n      setOpen(false);\n    } else if (\n      e.key === \"Backspace\" &&\n      !inputValue &&\n      optimisticTags.length > 0\n    ) {\n      const lastTag = optimisticTags.slice(-1)[0];\n      onChange({\n        action: \"remove-value\",\n        id: lastTag.id,\n        name: lastTag.name,\n      });\n    }\n  };\n\n  const handleSelect = (option: DisplayOption) => {\n    if (option.isCreateOption) {\n      onChange({ action: \"create-option\", name: option.name });\n      setInputValue(\"\");\n      inputRef.current?.focus();\n      return;\n    }\n\n    // If already selected, remove it\n    if (selectedValues.includes(option.id)) {\n      onChange({\n        action: \"remove-value\",\n        id: option.id,\n        name: option.name,\n      });\n    } else {\n      // Add the new tag\n      onChange({\n        action: \"select-option\",\n        id: option.id,\n        name: option.name,\n      });\n    }\n\n    // Reset input and keep focus\n    setInputValue(\"\");\n    inputRef.current?.focus();\n  };\n\n  const handleOpenChange = (open: boolean) => {\n    setOpen(open);\n    if (open) {\n      // Focus the input\n      setTimeout(() => {\n        inputRef.current?.focus();\n      }, 0);\n    }\n  };\n\n  const inputPlaceholder =\n    placeholder ??\n    (allowCreation\n      ? t(\"tags.search_or_create_placeholder\", {\n          defaultValue: \"Search or create tags...\",\n        })\n      : t(\"tags.search_placeholder\", {\n          defaultValue: \"Search tags...\",\n        }));\n  const visiblePlaceholder =\n    optimisticTags.length === 0 ? inputPlaceholder : undefined;\n  const inputWidth = Math.max(\n    inputValue.length > 0\n      ? inputValue.length\n      : Math.min(visiblePlaceholder?.length ?? 1, 24),\n    1,\n  );\n\n  return (\n    <div ref={containerRef} className=\"w-full\">\n      <Popover open={open && !isDisabled} onOpenChange={handleOpenChange}>\n        <Command shouldFilter={false}>\n          <PopoverTrigger asChild>\n            <div\n              className={cn(\n                \"relative flex min-h-10 w-full flex-wrap items-center gap-2 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2\",\n                isDisabled && \"cursor-not-allowed opacity-50\",\n              )}\n            >\n              {optimisticTags.length > 0 && (\n                <>\n                  {optimisticTags.map((tag) => (\n                    <div\n                      key={tag.id}\n                      className={cn(\n                        \"flex min-h-8 space-x-1 rounded px-2\",\n                        tag.attachedBy == \"ai\"\n                          ? \"bg-purple-500 text-white\"\n                          : \"bg-accent\",\n                      )}\n                    >\n                      <div className=\"m-auto flex gap-2\">\n                        {tag.attachedBy === \"ai\" && (\n                          <Sparkles className=\"m-auto size-4\" />\n                        )}\n                        {tag.name}\n                        {!isDisabled && (\n                          <button\n                            type=\"button\"\n                            className=\"rounded-full outline-none ring-offset-background focus:ring-1 focus:ring-ring focus:ring-offset-2\"\n                            onClick={(e) => {\n                              e.stopPropagation();\n                              onChange({\n                                action: \"remove-value\",\n                                id: tag.id,\n                                name: tag.name,\n                              });\n                            }}\n                          >\n                            <X className=\"h-3 w-3\" />\n                            <span className=\"sr-only\">Remove {tag.name}</span>\n                          </button>\n                        )}\n                      </div>\n                    </div>\n                  ))}\n                </>\n              )}\n              <CommandPrimitive.Input\n                ref={inputRef}\n                value={inputValue}\n                onKeyDown={handleKeyDown}\n                onValueChange={(v) => setInputValue(v)}\n                placeholder={visiblePlaceholder}\n                className=\"bg-transparent outline-none placeholder:text-muted-foreground\"\n                style={{ width: `${inputWidth}ch` }}\n                disabled={isDisabled}\n              />\n              {isExistingTagsLoading && (\n                <div className=\"absolute bottom-2 right-2\">\n                  <Loader2 className=\"h-4 w-4 animate-spin opacity-50\" />\n                </div>\n              )}\n            </div>\n          </PopoverTrigger>\n          <PopoverContent\n            className=\"w-[--radix-popover-trigger-width] p-0\"\n            align=\"start\"\n            onWheel={(e) => e.stopPropagation()}\n          >\n            <CommandList className=\"max-h-64\">\n              {displayedOptions.length === 0 ? (\n                <CommandEmpty>\n                  {trimmedInputValue && allowCreation ? (\n                    <div className=\"flex items-center justify-between px-2 py-1.5\">\n                      <span>Create &quot;{trimmedInputValue}&quot;</span>\n                      <Button\n                        variant=\"ghost\"\n                        size=\"sm\"\n                        onClick={createTag}\n                        className=\"h-auto p-1\"\n                      >\n                        <Plus className=\"h-4 w-4\" />\n                      </Button>\n                    </div>\n                  ) : (\n                    \"No tags found.\"\n                  )}\n                </CommandEmpty>\n              ) : (\n                <CommandGroup>\n                  {displayedOptions.map((option) => {\n                    const isSelected = selectedValues.includes(option.id);\n                    return (\n                      <CommandItem\n                        key={\n                          option.isCreateOption\n                            ? `create-${option.name}`\n                            : option.id\n                        }\n                        value={option.label}\n                        onSelect={() => handleSelect(option)}\n                      >\n                        <div className=\"flex w-full items-center gap-2\">\n                          {option.isCreateOption ? (\n                            <Plus className=\"h-4 w-4\" />\n                          ) : (\n                            <Check\n                              className={cn(\n                                \"h-4 w-4\",\n                                isSelected ? \"opacity-100\" : \"opacity-0\",\n                              )}\n                            />\n                          )}\n                          <span>{option.name}</span>\n                        </div>\n                      </CommandItem>\n                    );\n                  })}\n                </CommandGroup>\n              )}\n            </CommandList>\n          </PopoverContent>\n        </Command>\n      </Popover>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/bookmarks/TextCard.tsx",
    "content": "\"use client\";\n\nimport Image from \"next/image\";\nimport Link from \"next/link\";\nimport { BookmarkMarkdownComponent } from \"@/components/dashboard/bookmarks/BookmarkMarkdownComponent\";\nimport { bookmarkLayoutSwitch } from \"@/lib/userLocalSettings/bookmarksLayout\";\nimport { cn } from \"@/lib/utils\";\n\nimport type { ZBookmarkTypeText } from \"@karakeep/shared/types/bookmarks\";\nimport { getAssetUrl } from \"@karakeep/shared/utils/assetUtils\";\nimport { getSourceUrl } from \"@karakeep/shared/utils/bookmarkUtils\";\n\nimport { BookmarkLayoutAdaptingCard } from \"./BookmarkLayoutAdaptingCard\";\nimport FooterLinkURL from \"./FooterLinkURL\";\n\nexport default function TextCard({\n  bookmark,\n  className,\n}: {\n  bookmark: ZBookmarkTypeText;\n  className?: string;\n}) {\n  const banner = bookmark.assets.find((a) => a.assetType == \"bannerImage\");\n  return (\n    <>\n      <BookmarkLayoutAdaptingCard\n        title={bookmark.title}\n        content={\n          <BookmarkMarkdownComponent readOnly={true}>\n            {bookmark}\n          </BookmarkMarkdownComponent>\n        }\n        footer={\n          getSourceUrl(bookmark) && (\n            <FooterLinkURL url={getSourceUrl(bookmark)} />\n          )\n        }\n        wrapTags={true}\n        bookmark={bookmark}\n        className={className}\n        fitHeight={true}\n        image={(layout, className) =>\n          bookmarkLayoutSwitch(layout, {\n            grid: null,\n            masonry: null,\n            compact: null,\n            list: banner ? (\n              <div className=\"relative size-full flex-1\">\n                <Link href={`/dashboard/preview/${bookmark.id}`}>\n                  <Image\n                    alt=\"card banner\"\n                    fill={true}\n                    className={cn(\"flex-1\", className)}\n                    src={getAssetUrl(banner.id)}\n                  />\n                </Link>\n              </div>\n            ) : (\n              <div\n                className={cn(\n                  \"flex size-full items-center justify-center bg-accent text-center\",\n                  className,\n                )}\n              >\n                Note\n              </div>\n            ),\n          })\n        }\n      />\n    </>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/bookmarks/UnknownCard.tsx",
    "content": "\"use client\";\n\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { AlertCircle } from \"lucide-react\";\n\nimport type { ZBookmark } from \"@karakeep/shared/types/bookmarks\";\n\nimport { BookmarkLayoutAdaptingCard } from \"./BookmarkLayoutAdaptingCard\";\n\nexport default function UnknownCard({\n  bookmark,\n  className,\n}: {\n  bookmark: ZBookmark;\n  className?: string;\n}) {\n  const { t } = useTranslation();\n  return (\n    <BookmarkLayoutAdaptingCard\n      title={bookmark.title}\n      bookmark={bookmark}\n      className={className}\n      wrapTags={false}\n      image={(_layout) => (\n        <div className=\"flex size-full flex-1 flex-col items-center justify-center bg-red-50 dark:bg-red-950/10\">\n          <AlertCircle className=\"mb-3 h-10 w-10 text-red-500\" />\n          <h3 className=\"font-medium text-red-700 dark:text-red-400\">\n            {t(\"common.something_went_wrong\")}\n          </h3>\n        </div>\n      )}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/bookmarks/UpdatableBookmarksGrid.tsx",
    "content": "\"use client\";\n\nimport { useEffect } from \"react\";\nimport UploadDropzone from \"@/components/dashboard/UploadDropzone\";\nimport { useSortOrderStore } from \"@/lib/store/useSortOrderStore\";\nimport { useInfiniteQuery } from \"@tanstack/react-query\";\n\nimport type {\n  ZGetBookmarksRequest,\n  ZGetBookmarksResponse,\n} from \"@karakeep/shared/types/bookmarks\";\nimport { BookmarkGridContextProvider } from \"@karakeep/shared-react/hooks/bookmark-grid-context\";\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\nimport BookmarksGrid from \"./BookmarksGrid\";\n\nexport default function UpdatableBookmarksGrid({\n  query,\n  bookmarks: initialBookmarks,\n  showEditorCard = false,\n}: {\n  query: Omit<ZGetBookmarksRequest, \"sortOrder\" | \"includeContent\">; // Sort order is handled by the store\n  bookmarks: ZGetBookmarksResponse;\n  showEditorCard?: boolean;\n  itemsPerPage?: number;\n}) {\n  const api = useTRPC();\n  let sortOrder = useSortOrderStore((state) => state.sortOrder);\n  if (sortOrder === \"relevance\") {\n    // Relevance is not supported in the `getBookmarks` endpoint.\n    sortOrder = \"desc\";\n  }\n\n  const finalQuery = { ...query, sortOrder, includeContent: false };\n\n  const { data, fetchNextPage, hasNextPage, isFetchingNextPage, refetch } =\n    useInfiniteQuery(\n      api.bookmarks.getBookmarks.infiniteQueryOptions(\n        { ...finalQuery, useCursorV2: true },\n        {\n          initialData: () => ({\n            pages: [initialBookmarks],\n            pageParams: [query.cursor ?? null],\n          }),\n          initialCursor: null,\n          getNextPageParam: (lastPage) => lastPage.nextCursor,\n          refetchOnMount: true,\n        },\n      ),\n    );\n\n  useEffect(() => {\n    refetch();\n  }, [sortOrder, refetch]);\n\n  const grid = (\n    <BookmarksGrid\n      bookmarks={data.pages.flatMap((b) => b.bookmarks)}\n      hasNextPage={hasNextPage}\n      fetchNextPage={fetchNextPage}\n      isFetchingNextPage={isFetchingNextPage}\n      showEditorCard={showEditorCard}\n    />\n  );\n\n  return (\n    <BookmarkGridContextProvider query={finalQuery}>\n      {showEditorCard ? <UploadDropzone>{grid}</UploadDropzone> : grid}\n    </BookmarkGridContextProvider>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/bookmarks/action-buttons/ArchiveBookmarkButton.tsx",
    "content": "import React from \"react\";\nimport { ActionButton, ActionButtonProps } from \"@/components/ui/action-button\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { useQuery } from \"@tanstack/react-query\";\n\nimport { useUpdateBookmark } from \"@karakeep/shared-react/hooks/bookmarks\";\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\ninterface ArchiveBookmarkButtonProps extends Omit<\n  ActionButtonProps,\n  \"loading\" | \"disabled\"\n> {\n  bookmarkId: string;\n  onDone?: () => void;\n}\n\nconst ArchiveBookmarkButton = React.forwardRef<\n  HTMLButtonElement,\n  ArchiveBookmarkButtonProps\n>(({ bookmarkId, onDone, ...props }, ref) => {\n  const api = useTRPC();\n  const { data } = useQuery(\n    api.bookmarks.getBookmark.queryOptions(\n      { bookmarkId },\n      {\n        select: (data) => ({\n          archived: data.archived,\n        }),\n      },\n    ),\n  );\n\n  const { mutate: updateBookmark, isPending: isArchivingBookmark } =\n    useUpdateBookmark({\n      onSuccess: () => {\n        toast({\n          description: \"Bookmark has been archived!\",\n        });\n        onDone?.();\n      },\n      onError: (e) => {\n        if (e.data?.code == \"BAD_REQUEST\") {\n          toast({\n            variant: \"destructive\",\n            description: e.message,\n          });\n        } else {\n          toast({\n            variant: \"destructive\",\n            title: \"Something went wrong\",\n          });\n        }\n      },\n    });\n\n  if (!data) {\n    return <span />;\n  }\n\n  return (\n    <ActionButton\n      ref={ref}\n      loading={isArchivingBookmark}\n      disabled={data.archived}\n      onClick={() =>\n        updateBookmark({\n          bookmarkId,\n          archived: !data.archived,\n        })\n      }\n      {...props}\n    />\n  );\n});\n\nArchiveBookmarkButton.displayName = \"ArchiveBookmarkButton\";\nexport default ArchiveBookmarkButton;\n"
  },
  {
    "path": "apps/web/components/dashboard/bookmarks/icons.tsx",
    "content": "import { Archive, ArchiveRestore, Star } from \"lucide-react\";\n\nexport function FavouritedActionIcon({\n  favourited,\n  className,\n  size,\n  strokeWidth,\n}: {\n  favourited: boolean;\n  className?: string;\n  size?: number;\n  strokeWidth?: number;\n}) {\n  return favourited ? (\n    <Star\n      size={size}\n      strokeWidth={strokeWidth}\n      className={className}\n      color=\"#ebb434\"\n      fill=\"#ebb434\"\n    />\n  ) : (\n    <Star size={size} strokeWidth={strokeWidth} className={className} />\n  );\n}\n\nexport function ArchivedActionIcon({\n  archived,\n  className,\n  size,\n  strokeWidth,\n}: {\n  archived: boolean;\n  className?: string;\n  size?: number;\n  strokeWidth?: number;\n}) {\n  return archived ? (\n    <ArchiveRestore\n      size={size}\n      strokeWidth={strokeWidth}\n      className={className}\n    />\n  ) : (\n    <Archive size={size} strokeWidth={strokeWidth} className={className} />\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/cleanups/TagDuplicationDetention.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport Link from \"next/link\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport ActionConfirmingDialog from \"@/components/ui/action-confirming-dialog\";\nimport { badgeVariants } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Collapsible,\n  CollapsibleContent,\n  CollapsibleTrigger,\n} from \"@/components/ui/collapsible\";\nimport { toast } from \"@/components/ui/sonner\";\nimport LoadingSpinner from \"@/components/ui/spinner\";\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@/components/ui/table\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { cn } from \"@/lib/utils\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { distance } from \"fastest-levenshtein\";\nimport { Check, Combine, X } from \"lucide-react\";\n\nimport { useMergeTag } from \"@karakeep/shared-react/hooks/tags\";\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\ninterface Suggestion {\n  mergeIntoId: string;\n  tags: { id: string; name: string }[];\n}\n\nfunction normalizeTag(tag: string) {\n  return tag.toLocaleLowerCase().replace(/[ -_]/g, \"\");\n}\n\nconst useSuggestions = () => {\n  const [suggestions, setSuggestions] = useState<Suggestion[]>([]);\n\n  function updateMergeInto(suggestion: Suggestion, newMergeIntoId: string) {\n    setSuggestions((prev) =>\n      prev.map((s) =>\n        s === suggestion ? { ...s, mergeIntoId: newMergeIntoId } : s,\n      ),\n    );\n  }\n\n  function deleteSuggestion(suggestion: Suggestion) {\n    setSuggestions((prev) => prev.filter((s) => s !== suggestion));\n  }\n\n  return { suggestions, updateMergeInto, deleteSuggestion, setSuggestions };\n};\n\nfunction ApplyAllButton({ suggestions }: { suggestions: Suggestion[] }) {\n  const { t } = useTranslation();\n  const [applying, setApplying] = useState(false);\n  const { mutateAsync } = useMergeTag({\n    onError: (e) => {\n      toast({\n        description: e.message,\n        variant: \"destructive\",\n      });\n    },\n  });\n\n  const applyAll = async (setDialogOpen: (open: boolean) => void) => {\n    const promises = suggestions.map((suggestion) =>\n      mutateAsync({\n        intoTagId: suggestion.mergeIntoId,\n        fromTagIds: suggestion.tags\n          .filter((t) => t.id != suggestion.mergeIntoId)\n          .map((t) => t.id),\n      }),\n    );\n    setApplying(true);\n    await Promise.all(promises)\n      .then(() => {\n        toast({\n          description: \"All suggestions has been applied!\",\n        });\n      })\n      .catch(() => ({}))\n      .finally(() => {\n        setApplying(false);\n        setDialogOpen(false);\n      });\n  };\n\n  return (\n    <ActionConfirmingDialog\n      title={t(\"cleanups.duplicate_tags.merge_all_suggestions\")}\n      description={`Are you sure you want to apply all ${suggestions.length} suggestions?`}\n      actionButton={(setDialogOpen) => (\n        <ActionButton\n          loading={applying}\n          variant=\"destructive\"\n          onClick={() => applyAll(setDialogOpen)}\n        >\n          <Check className=\"mr-2 size-4\" />\n          {t(\"actions.apply_all\")}\n        </ActionButton>\n      )}\n    >\n      <Button variant=\"destructive\">\n        <Check className=\"mr-2 size-4\" />\n        {t(\"actions.apply_all\")}\n      </Button>\n    </ActionConfirmingDialog>\n  );\n}\n\nfunction SuggestionRow({\n  suggestion,\n  updateMergeInto,\n  deleteSuggestion,\n}: {\n  suggestion: Suggestion;\n  updateMergeInto: (suggestion: Suggestion, newMergeIntoId: string) => void;\n  deleteSuggestion: (suggestion: Suggestion) => void;\n}) {\n  const { t } = useTranslation();\n  const { mutate, isPending } = useMergeTag({\n    onSuccess: () => {\n      toast({\n        description: \"Tags have been merged!\",\n      });\n    },\n    onError: (e) => {\n      toast({\n        description: e.message,\n        variant: \"destructive\",\n      });\n    },\n  });\n  return (\n    <TableRow key={suggestion.mergeIntoId}>\n      <TableCell className=\"flex flex-wrap gap-1\">\n        {suggestion.tags.map((tag, idx) => {\n          const selected = suggestion.mergeIntoId == tag.id;\n          return (\n            <div key={idx} className=\"group relative\">\n              <Link\n                href={`/dashboard/tags/${tag.id}`}\n                className={cn(\n                  badgeVariants({ variant: \"outline\" }),\n                  \"text-sm\",\n                  selected\n                    ? \"border border-blue-500 dark:border-blue-900\"\n                    : null,\n                )}\n              >\n                {tag.name}\n              </Link>\n              <Button\n                size=\"none\"\n                className={cn(\n                  \"-translate-1/2 absolute -right-1.5 -top-1.5 rounded-full p-0.5\",\n                  selected ? null : \"hidden group-hover:block\",\n                )}\n                onClick={() => updateMergeInto(suggestion, tag.id)}\n              >\n                <Check className=\"size-3\" />\n              </Button>\n            </div>\n          );\n        })}\n      </TableCell>\n      <TableCell className=\"space-x-1 space-y-1 text-center\">\n        <ActionButton\n          loading={isPending}\n          onClick={() =>\n            mutate({\n              intoTagId: suggestion.mergeIntoId,\n              fromTagIds: suggestion.tags\n                .filter((t) => t.id != suggestion.mergeIntoId)\n                .map((t) => t.id),\n            })\n          }\n        >\n          <Combine className=\"mr-2 size-4\" />\n          {t(\"actions.merge\")}\n        </ActionButton>\n\n        <Button\n          variant={\"secondary\"}\n          onClick={() => deleteSuggestion(suggestion)}\n        >\n          <X className=\"mr-2 size-4\" />\n          {t(\"actions.ignore\")}\n        </Button>\n      </TableCell>\n    </TableRow>\n  );\n}\n\nexport function TagDuplicationDetection() {\n  const api = useTRPC();\n  const [expanded, setExpanded] = useState(false);\n  let { data: allTags } = useQuery(\n    api.tags.list.queryOptions(\n      {},\n      {\n        refetchOnWindowFocus: false,\n      },\n    ),\n  );\n\n  const { suggestions, updateMergeInto, setSuggestions, deleteSuggestion } =\n    useSuggestions();\n\n  useEffect(() => {\n    allTags = allTags ?? { tags: [], nextCursor: null };\n    const sortedTags = allTags.tags.sort((a, b) =>\n      normalizeTag(a.name).localeCompare(normalizeTag(b.name)),\n    );\n\n    const initialSuggestions: Suggestion[] = [];\n    for (let i = 0; i < sortedTags.length; i++) {\n      const currentName = normalizeTag(sortedTags[i].name);\n      const suggestion = [sortedTags[i]];\n      for (let j = i + 1; j < sortedTags.length; j++) {\n        const nextName = normalizeTag(sortedTags[j].name);\n        if (distance(currentName, nextName) <= 1) {\n          suggestion.push(sortedTags[j]);\n        } else {\n          break;\n        }\n      }\n      if (suggestion.length > 1) {\n        initialSuggestions.push({\n          mergeIntoId: suggestion[0].id,\n          tags: suggestion,\n        });\n        i += suggestion.length - 1;\n      }\n    }\n    setSuggestions(initialSuggestions);\n  }, [allTags]);\n\n  if (!allTags) {\n    return <LoadingSpinner />;\n  }\n\n  return (\n    <Collapsible open={expanded} onOpenChange={setExpanded}>\n      You have {suggestions.length} suggestions for tag merging.\n      {suggestions.length > 0 && (\n        <CollapsibleTrigger asChild>\n          <Button variant=\"link\" size=\"sm\">\n            {expanded ? \"Hide All\" : \"Show All\"}\n          </Button>\n        </CollapsibleTrigger>\n      )}\n      <CollapsibleContent>\n        <p className=\"text-sm italic text-muted-foreground\">\n          For every suggestion, select the tag that you want to keep and other\n          tags will be merged into it.\n        </p>\n        {suggestions.length > 0 && (\n          <Table>\n            <TableHeader>\n              <TableRow>\n                <TableHead>Tags</TableHead>\n                <TableHead className=\"text-center\">\n                  <ApplyAllButton suggestions={suggestions} />\n                </TableHead>\n              </TableRow>\n            </TableHeader>\n            <TableBody>\n              {suggestions.map((suggestion) => (\n                <SuggestionRow\n                  key={suggestion.mergeIntoId}\n                  suggestion={suggestion}\n                  updateMergeInto={updateMergeInto}\n                  deleteSuggestion={deleteSuggestion}\n                />\n              ))}\n            </TableBody>\n          </Table>\n        )}\n      </CollapsibleContent>\n    </Collapsible>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/feeds/FeedSelector.tsx",
    "content": "import {\n  Select,\n  SelectContent,\n  SelectGroup,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\";\nimport LoadingSpinner from \"@/components/ui/spinner\";\nimport { cn } from \"@/lib/utils\";\nimport { useQuery } from \"@tanstack/react-query\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\nexport function FeedSelector({\n  value,\n  onChange,\n  placeholder = \"Select a feed\",\n  className,\n}: {\n  className?: string;\n  value?: string | null;\n  onChange: (value: string) => void;\n  placeholder?: string;\n}) {\n  const api = useTRPC();\n  const { data, isPending } = useQuery(\n    api.feeds.list.queryOptions(undefined, {\n      select: (data) => data.feeds,\n    }),\n  );\n\n  if (isPending) {\n    return <LoadingSpinner />;\n  }\n\n  return (\n    <Select onValueChange={onChange} value={value ?? \"\"}>\n      <SelectTrigger className={cn(\"w-full\", className)}>\n        <SelectValue placeholder={placeholder} />\n      </SelectTrigger>\n      <SelectContent>\n        <SelectGroup>\n          {data?.map((f) => (\n            <SelectItem key={f.id} value={f.id}>\n              {f.name}\n            </SelectItem>\n          ))}\n          {(data ?? []).length == 0 && (\n            <SelectItem value=\"nofeed\" disabled>\n              You don&apos;t currently have any feeds.\n            </SelectItem>\n          )}\n        </SelectGroup>\n      </SelectContent>\n    </Select>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/header/Header.tsx",
    "content": "import Link from \"next/link\";\nimport { redirect } from \"next/navigation\";\nimport GlobalActions from \"@/components/dashboard/GlobalActions\";\nimport ProfileOptions from \"@/components/dashboard/header/ProfileOptions\";\nimport { SearchInput } from \"@/components/dashboard/search/SearchInput\";\nimport KarakeepLogo from \"@/components/KarakeepIcon\";\nimport { getServerAuthSession } from \"@/server/auth\";\n\nexport default async function Header() {\n  const session = await getServerAuthSession();\n  if (!session) {\n    redirect(\"/\");\n  }\n\n  return (\n    <header className=\"sticky left-0 right-0 top-0 z-50 flex h-16 items-center justify-between overflow-x-auto overflow-y-hidden bg-background p-4 shadow\">\n      <div className=\"hidden items-center sm:flex\">\n        <Link href={\"/dashboard/bookmarks\"} className=\"w-56\">\n          <KarakeepLogo height={38} />\n        </Link>\n      </div>\n      <div className=\"flex flex-1 gap-2\">\n        <SearchInput className=\"rounded-md bg-muted\" />\n        <GlobalActions />\n      </div>\n      <div className=\"flex items-center\">\n        <ProfileOptions />\n      </div>\n    </header>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/header/ProfileOptions.tsx",
    "content": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport Link from \"next/link\";\nimport { redirect, useRouter } from \"next/navigation\";\nimport { useToggleTheme } from \"@/components/theme-provider\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { UserAvatar } from \"@/components/ui/user-avatar\";\nimport { useSession } from \"@/lib/auth/client\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport {\n  BookOpen,\n  LogOut,\n  Moon,\n  Paintbrush,\n  Puzzle,\n  Settings,\n  Shield,\n  Sun,\n  Twitter,\n} from \"lucide-react\";\nimport { useTheme } from \"next-themes\";\n\nimport { useWhoAmI } from \"@karakeep/shared-react/hooks/users\";\n\nimport { AdminNoticeBadge } from \"../../admin/AdminNotices\";\n\nfunction DarkModeToggle() {\n  const { t } = useTranslation();\n  const { theme } = useTheme();\n\n  if (theme == \"dark\") {\n    return (\n      <>\n        <Sun className=\"mr-2 size-4\" />\n        <span>{t(\"options.light_mode\")}</span>\n      </>\n    );\n  } else {\n    return (\n      <>\n        <Moon className=\"mr-2 size-4\" />\n        <span>{t(\"options.dark_mode\")}</span>\n      </>\n    );\n  }\n}\n\nexport default function SidebarProfileOptions() {\n  const { t } = useTranslation();\n  const toggleTheme = useToggleTheme();\n  const { data: session } = useSession();\n  const { data: whoami } = useWhoAmI();\n  const router = useRouter();\n\n  const avatarImage = whoami?.image ?? null;\n  const avatarUrl = useMemo(() => avatarImage ?? null, [avatarImage]);\n\n  if (!session) return redirect(\"/\");\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button\n          className=\"border-new-gray-200 aspect-square rounded-full border-4 bg-black p-0 text-white\"\n          variant=\"ghost\"\n        >\n          <UserAvatar\n            image={avatarUrl}\n            name={session.user.name}\n            className=\"h-full w-full rounded-full\"\n          />\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent className=\"mr-2 min-w-64 p-2\">\n        <div className=\"flex gap-2\">\n          <div className=\"border-new-gray-200 flex aspect-square size-11 items-center justify-center overflow-hidden rounded-full border-4 bg-black p-0 text-white\">\n            <UserAvatar\n              image={avatarUrl}\n              name={session.user.name}\n              className=\"h-full w-full\"\n            />\n          </div>\n          <div className=\"flex flex-col\">\n            <p>{session.user.name}</p>\n            <p className=\"text-sm text-gray-400\">{session.user.email}</p>\n          </div>\n        </div>\n        <Separator className=\"my-2\" />\n        <DropdownMenuItem asChild>\n          <Link href=\"/settings\">\n            <Settings className=\"mr-2 size-4\" />\n            {t(\"settings.user_settings\")}\n          </Link>\n        </DropdownMenuItem>\n        {session.user.role == \"admin\" && (\n          <DropdownMenuItem asChild>\n            <Link href=\"/admin\" className=\"flex justify-between\">\n              <div className=\"items-cente flex gap-2\">\n                <Shield className=\"size-4\" />\n                {t(\"admin.admin_settings\")}\n              </div>\n              <AdminNoticeBadge />\n            </Link>\n          </DropdownMenuItem>\n        )}\n        <Separator className=\"my-2\" />\n        <DropdownMenuItem asChild>\n          <Link href=\"/dashboard/cleanups\">\n            <Paintbrush className=\"mr-2 size-4\" />\n            {t(\"cleanups.cleanups\")}\n          </Link>\n        </DropdownMenuItem>\n        <DropdownMenuItem onClick={toggleTheme}>\n          <DarkModeToggle />\n        </DropdownMenuItem>\n        <Separator className=\"my-2\" />\n        <DropdownMenuItem asChild>\n          <a href=\"https://karakeep.app/apps\" target=\"_blank\" rel=\"noreferrer\">\n            <Puzzle className=\"mr-2 size-4\" />\n            {t(\"options.apps_extensions\")}\n          </a>\n        </DropdownMenuItem>\n        <DropdownMenuItem asChild>\n          <a href=\"https://docs.karakeep.app\" target=\"_blank\" rel=\"noreferrer\">\n            <BookOpen className=\"mr-2 size-4\" />\n            {t(\"options.documentation\")}\n          </a>\n        </DropdownMenuItem>\n        <DropdownMenuItem asChild>\n          <a href=\"https://x.com/karakeep_app\" target=\"_blank\" rel=\"noreferrer\">\n            <Twitter className=\"mr-2 size-4\" />\n            {t(\"options.follow_us_on_x\")}\n          </a>\n        </DropdownMenuItem>\n        <Separator className=\"my-2\" />\n        <DropdownMenuItem onClick={() => router.push(\"/logout\")}>\n          <LogOut className=\"mr-2 size-4\" />\n          <span>{t(\"actions.sign_out\")}</span>\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/highlights/AllHighlights.tsx",
    "content": "\"use client\";\n\nimport React, { useEffect, useState } from \"react\";\nimport Link from \"next/link\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport { Input } from \"@/components/ui/input\";\nimport useRelativeTime from \"@/lib/hooks/relative-time\";\nimport { Separator } from \"@radix-ui/react-dropdown-menu\";\nimport { useInfiniteQuery } from \"@tanstack/react-query\";\nimport { Dot, LinkIcon, Search, X } from \"lucide-react\";\nimport { useTranslation } from \"react-i18next\";\nimport { useInView } from \"react-intersection-observer\";\n\nimport { useDebounce } from \"@karakeep/shared-react/hooks/use-debounce\";\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\nimport {\n  ZGetAllHighlightsResponse,\n  ZHighlight,\n} from \"@karakeep/shared/types/highlights\";\n\nimport HighlightCard from \"./HighlightCard\";\n\nfunction Highlight({ highlight }: { highlight: ZHighlight }) {\n  const { fromNow, localCreatedAt } = useRelativeTime(highlight.createdAt);\n  const { t } = useTranslation();\n  return (\n    <div className=\"flex flex-col gap-2\">\n      <HighlightCard highlight={highlight} clickable={false} readOnly={false} />\n      <span className=\"flex items-center gap-0.5 text-xs italic text-gray-400\">\n        <span title={localCreatedAt}>{fromNow}</span>\n        <Dot />\n        <Link\n          href={`/dashboard/preview/${highlight.bookmarkId}`}\n          className=\"flex items-center gap-0.5\"\n        >\n          <LinkIcon className=\"size-3 italic\" />\n          {t(\"common.source\")}\n        </Link>\n      </span>\n    </div>\n  );\n}\n\nexport default function AllHighlights({\n  highlights: initialHighlights,\n}: {\n  highlights: ZGetAllHighlightsResponse;\n}) {\n  const api = useTRPC();\n  const { t } = useTranslation();\n  const [searchInput, setSearchInput] = useState(\"\");\n  const debouncedSearch = useDebounce(searchInput, 300);\n\n  // Use search endpoint if searchQuery is provided, otherwise use getAll\n  const useSearchQuery = debouncedSearch.trim().length > 0;\n\n  const getAllQuery = useInfiniteQuery(\n    api.highlights.getAll.infiniteQueryOptions(\n      {},\n      {\n        enabled: !useSearchQuery,\n        initialData: !useSearchQuery\n          ? () => ({\n              pages: [initialHighlights],\n              pageParams: [null],\n            })\n          : undefined,\n        initialCursor: null,\n        getNextPageParam: (lastPage) => lastPage.nextCursor,\n      },\n    ),\n  );\n\n  const searchQueryResult = useInfiniteQuery(\n    api.highlights.search.infiniteQueryOptions(\n      { text: debouncedSearch },\n      {\n        enabled: useSearchQuery,\n        initialCursor: null,\n        getNextPageParam: (lastPage) => lastPage.nextCursor,\n      },\n    ),\n  );\n\n  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =\n    useSearchQuery ? searchQueryResult : getAllQuery;\n\n  const { ref: loadMoreRef, inView: loadMoreButtonInView } = useInView();\n  useEffect(() => {\n    if (loadMoreButtonInView && hasNextPage && !isFetchingNextPage) {\n      fetchNextPage();\n    }\n  }, [loadMoreButtonInView]);\n\n  const allHighlights = data?.pages.flatMap((p) => p.highlights);\n\n  return (\n    <div className=\"flex flex-col gap-4\">\n      {/* Search Input */}\n      <div className=\"relative\">\n        <Input\n          type=\"text\"\n          placeholder=\"Search highlights...\"\n          value={searchInput}\n          onChange={(e) => setSearchInput(e.target.value)}\n          startIcon={<Search className=\"size-4 text-muted-foreground\" />}\n          endIcon={\n            searchInput && (\n              <button\n                onClick={() => setSearchInput(\"\")}\n                className=\"text-muted-foreground hover:text-foreground\"\n              >\n                <X className=\"size-4\" />\n              </button>\n            )\n          }\n          className=\"w-full\"\n        />\n      </div>\n\n      {/* Results */}\n      <div className=\"flex flex-col gap-2\">\n        {allHighlights &&\n          allHighlights.length > 0 &&\n          allHighlights.map((h) => (\n            <React.Fragment key={h.id}>\n              <Highlight highlight={h} />\n              <Separator className=\"m-2 h-0.5 bg-gray-100 last:hidden\" />\n            </React.Fragment>\n          ))}\n        {allHighlights && allHighlights.length == 0 && (\n          <p className=\"rounded-md bg-muted p-2 text-sm text-muted-foreground\">\n            {t(\"highlights.no_highlights\")}\n          </p>\n        )}\n        {hasNextPage && (\n          <div className=\"flex justify-center\">\n            <ActionButton\n              ref={loadMoreRef}\n              ignoreDemoMode={true}\n              loading={isFetchingNextPage}\n              onClick={() => fetchNextPage()}\n              variant=\"ghost\"\n            >\n              Load More\n            </ActionButton>\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/highlights/HighlightCard.tsx",
    "content": "import { ActionButton } from \"@/components/ui/action-button\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { cn } from \"@/lib/utils\";\nimport { Trash2 } from \"lucide-react\";\n\nimport { useDeleteHighlight } from \"@karakeep/shared-react/hooks/highlights\";\nimport { ZHighlight } from \"@karakeep/shared/types/highlights\";\n\nimport { HIGHLIGHT_COLOR_MAP } from \"../preview/highlights\";\n\nexport default function HighlightCard({\n  highlight,\n  clickable,\n  className,\n  readOnly,\n}: {\n  highlight: ZHighlight;\n  clickable: boolean;\n  className?: string;\n  readOnly: boolean;\n}) {\n  const { mutate: deleteHighlight, isPending: isDeleting } = useDeleteHighlight(\n    {\n      onSuccess: () => {\n        toast({\n          description: \"Highlight has been deleted!\",\n        });\n      },\n      onError: () => {\n        toast({\n          description: \"Something went wrong\",\n          variant: \"destructive\",\n        });\n      },\n    },\n  );\n\n  const onBookmarkClick = () => {\n    document\n      .querySelector(`[data-highlight-id=\"${highlight.id}\"]`)\n      ?.scrollIntoView({\n        behavior: \"smooth\",\n        block: \"center\",\n      });\n  };\n\n  const Wrapper = ({\n    className,\n    children,\n  }: {\n    className?: string;\n    children: React.ReactNode;\n  }) =>\n    clickable ? (\n      <button className={className} onClick={onBookmarkClick}>\n        {children}\n      </button>\n    ) : (\n      <div className={className}>{children}</div>\n    );\n\n  return (\n    <div className={cn(\"flex items-center justify-between\", className)}>\n      <Wrapper className=\"flex flex-col gap-2 text-left\">\n        <blockquote\n          cite={highlight.bookmarkId}\n          className={cn(\n            \"prose border-l-[6px] p-2 pl-6 italic dark:prose-invert prose-p:text-sm\",\n            HIGHLIGHT_COLOR_MAP[\"border-l\"][highlight.color],\n          )}\n        >\n          <p>{highlight.text}</p>\n        </blockquote>\n        {highlight.note && (\n          <span className=\"text-sm text-muted-foreground\">\n            {highlight.note}\n          </span>\n        )}\n      </Wrapper>\n      {!readOnly && (\n        <div className=\"flex gap-2\">\n          <ActionButton\n            loading={isDeleting}\n            variant=\"ghost\"\n            onClick={() => deleteHighlight({ highlightId: highlight.id })}\n          >\n            <Trash2 className=\"size-4 text-destructive\" />\n          </ActionButton>\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/lists/AllListsView.tsx",
    "content": "\"use client\";\n\nimport { useMemo, useState } from \"react\";\nimport Link from \"next/link\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Collapsible,\n  CollapsibleContent,\n  CollapsibleTriggerChevron,\n} from \"@/components/ui/collapsible\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { MoreHorizontal } from \"lucide-react\";\n\nimport type { ZBookmarkList } from \"@karakeep/shared/types/lists\";\nimport {\n  augmentBookmarkListsWithInitialData,\n  useBookmarkLists,\n} from \"@karakeep/shared-react/hooks/lists\";\n\nimport { CollapsibleBookmarkLists } from \"./CollapsibleBookmarkLists\";\nimport { ListOptions } from \"./ListOptions\";\n\nfunction ListItem({\n  name,\n  icon,\n  path,\n  style,\n  list,\n  open,\n  collapsible,\n}: {\n  name: string;\n  icon: string;\n  path: string;\n  style?: React.CSSProperties;\n  list?: ZBookmarkList;\n  open?: boolean;\n  collapsible: boolean;\n}) {\n  return (\n    <li\n      className=\"my-2 flex items-center justify-between rounded-md border border-border p-2 hover:bg-accent/50\"\n      style={style}\n    >\n      <span className=\"flex flex-1 items-center gap-1\">\n        {collapsible && (\n          <CollapsibleTriggerChevron className=\"size-5\" open={open ?? false} />\n        )}\n        <Link href={path} className=\"flex flex-1 gap-1\">\n          <p className=\"text-nowrap text-lg\">\n            {icon} {name}\n          </p>\n        </Link>\n      </span>\n      {list && (\n        <ListOptions list={list}>\n          <Button\n            className=\"flex h-full items-center justify-end\"\n            variant=\"ghost\"\n          >\n            <MoreHorizontal />\n          </Button>\n        </ListOptions>\n      )}\n    </li>\n  );\n}\n\nexport default function AllListsView({\n  initialData,\n}: {\n  initialData: ZBookmarkList[];\n}) {\n  const { t } = useTranslation();\n\n  // Fetch live lists data\n  const { data: listsData } = useBookmarkLists(undefined, {\n    initialData: { lists: initialData },\n  });\n  const lists = augmentBookmarkListsWithInitialData(listsData, initialData);\n\n  // Check if there are any shared lists\n  const hasSharedLists = useMemo(() => {\n    return lists.data.some((list) => list.userRole !== \"owner\");\n  }, [lists.data]);\n\n  const [sharedListsOpen, setSharedListsOpen] = useState(true);\n\n  return (\n    <ul>\n      <ListItem\n        collapsible={false}\n        name={t(\"lists.favourites\")}\n        icon=\"⭐️\"\n        path={`/dashboard/favourites`}\n      />\n      <ListItem\n        collapsible={false}\n        name={t(\"common.archive\")}\n        icon=\"🗄️\"\n        path={`/dashboard/archive`}\n      />\n\n      {/* Owned Lists */}\n      <CollapsibleBookmarkLists\n        listsData={lists}\n        filter={(node) => node.item.userRole === \"owner\"}\n        render={({ node, level, open }) => (\n          <ListItem\n            name={node.item.name}\n            icon={node.item.icon}\n            list={node.item}\n            path={`/dashboard/lists/${node.item.id}`}\n            collapsible={node.children.length > 0}\n            open={open}\n            style={{ marginLeft: `${level * 1}rem` }}\n          />\n        )}\n      />\n\n      {/* Shared Lists */}\n      {hasSharedLists && (\n        <Collapsible open={sharedListsOpen} onOpenChange={setSharedListsOpen}>\n          <ListItem\n            collapsible={true}\n            name={t(\"lists.shared_lists\")}\n            icon=\"👥\"\n            path=\"#\"\n            open={sharedListsOpen}\n          />\n          <CollapsibleContent>\n            <CollapsibleBookmarkLists\n              listsData={lists}\n              filter={(node) => node.item.userRole !== \"owner\"}\n              indentOffset={1}\n              render={({ node, level, open }) => (\n                <ListItem\n                  name={node.item.name}\n                  icon={node.item.icon}\n                  list={node.item}\n                  path={`/dashboard/lists/${node.item.id}`}\n                  collapsible={node.children.length > 0}\n                  open={open}\n                  style={{ marginLeft: `${level * 1}rem` }}\n                />\n              )}\n            />\n          </CollapsibleContent>\n        </Collapsible>\n      )}\n    </ul>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/lists/BookmarkListSelector.tsx",
    "content": "import { useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@/components/ui/command\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport LoadingSpinner from \"@/components/ui/spinner\";\nimport { cn } from \"@/lib/utils\";\nimport { Check, ChevronsUpDown } from \"lucide-react\";\n\nimport { useBookmarkLists } from \"@karakeep/shared-react/hooks/lists\";\nimport { ZBookmarkList } from \"@karakeep/shared/types/lists\";\n\nexport function BookmarkListSelector({\n  value,\n  onChange,\n  hideSubtreeOf,\n  hideBookmarkIds = [],\n  placeholder = \"Select a list\",\n  className,\n  listTypes = [\"manual\", \"smart\"],\n}: {\n  className?: string;\n  value?: string | null;\n  onChange: (value: string) => void;\n  placeholder?: string;\n  hideSubtreeOf?: string;\n  hideBookmarkIds?: string[];\n  listTypes?: ZBookmarkList[\"type\"][];\n}) {\n  const [open, setOpen] = useState(false);\n  const { data, isPending: isFetchingListsPending } = useBookmarkLists();\n  let { allPaths } = data ?? {};\n\n  if (isFetchingListsPending) {\n    return <LoadingSpinner />;\n  }\n\n  allPaths = allPaths?.filter((path) => {\n    const lastItem = path[path.length - 1];\n    if (hideBookmarkIds.includes(lastItem.id)) {\n      return false;\n    }\n    if (!listTypes.includes(lastItem.type)) {\n      return false;\n    }\n    // Hide lists where user is a viewer (can't add/remove bookmarks)\n    if (lastItem.userRole === \"viewer\") {\n      return false;\n    }\n    if (!hideSubtreeOf) {\n      return true;\n    }\n    return !path.map((p) => p.id).includes(hideSubtreeOf);\n  });\n\n  // Find the selected list's display name\n  const selectedListPath = allPaths?.find(\n    (path) => path[path.length - 1].id === value,\n  );\n  const selectedListName = selectedListPath\n    ? selectedListPath.map((p) => `${p.icon} ${p.name}`).join(\" / \")\n    : null;\n\n  return (\n    <Popover open={open} onOpenChange={setOpen}>\n      <PopoverTrigger asChild>\n        <Button\n          variant=\"outline\"\n          role=\"combobox\"\n          aria-expanded={open}\n          className={cn(\"w-full justify-between\", className)}\n        >\n          {selectedListName || placeholder}\n          <ChevronsUpDown className=\"ml-2 h-4 w-4 shrink-0 opacity-50\" />\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent\n        className=\"w-[--radix-popover-trigger-width] p-0\"\n        onWheel={(e) => e.stopPropagation()}\n      >\n        <Command>\n          <CommandInput placeholder=\"Search lists...\" />\n          <CommandList>\n            <CommandEmpty>\n              {allPaths && allPaths.length === 0\n                ? \"You don't currently have any lists.\"\n                : \"No lists found.\"}\n            </CommandEmpty>\n            <CommandGroup className=\"max-h-60 overflow-y-auto\">\n              {allPaths?.map((path) => {\n                const l = path[path.length - 1];\n                const name = path.map((p) => `${p.icon} ${p.name}`).join(\" / \");\n                return (\n                  <CommandItem\n                    key={l.id}\n                    value={l.id}\n                    keywords={[l.name, l.icon]}\n                    onSelect={(currentValue) => {\n                      onChange(currentValue);\n                      setOpen(false);\n                    }}\n                    className=\"cursor-pointer\"\n                  >\n                    <Check\n                      className={cn(\n                        \"mr-2 h-4 w-4\",\n                        value === l.id ? \"opacity-100\" : \"opacity-0\",\n                      )}\n                    />\n                    {name}\n                  </CommandItem>\n                );\n              })}\n            </CommandGroup>\n          </CommandList>\n        </Command>\n      </PopoverContent>\n    </Popover>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/lists/CollapsibleBookmarkLists.tsx",
    "content": "import { useEffect, useState } from \"react\";\nimport { Collapsible, CollapsibleContent } from \"@/components/ui/collapsible\";\nimport { FullPageSpinner } from \"@/components/ui/full-page-spinner\";\nimport { keepPreviousData, useQuery } from \"@tanstack/react-query\";\n\nimport { useBookmarkLists } from \"@karakeep/shared-react/hooks/lists\";\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\nimport { ZBookmarkList } from \"@karakeep/shared/types/lists\";\nimport { ZBookmarkListTreeNode } from \"@karakeep/shared/utils/listUtils\";\n\ntype RenderFunc = (params: {\n  node: ZBookmarkListTreeNode;\n  level: number;\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n  numBookmarks?: number;\n}) => React.ReactNode;\n\ntype IsOpenFunc = (list: ZBookmarkListTreeNode) => boolean;\n\nfunction ListItem({\n  node,\n  render,\n  level,\n  className,\n  isOpenFunc,\n  listStats,\n  indentOffset,\n}: {\n  node: ZBookmarkListTreeNode;\n  render: RenderFunc;\n  isOpenFunc: IsOpenFunc;\n  listStats?: Map<string, number>;\n  level: number;\n  indentOffset: number;\n  className?: string;\n}) {\n  // Not the most efficient way to do this, but it works for now\n  const isAnyChildOpen = (\n    node: ZBookmarkListTreeNode,\n    isOpenFunc: IsOpenFunc,\n  ): boolean => {\n    if (isOpenFunc(node)) {\n      return true;\n    }\n    return node.children.some((l) => isAnyChildOpen(l, isOpenFunc));\n  };\n  const [open, setOpen] = useState(false);\n  useEffect(() => {\n    setOpen((curr) => curr || isAnyChildOpen(node, isOpenFunc));\n  }, [node, isOpenFunc]);\n\n  return (\n    <Collapsible open={open} onOpenChange={setOpen} className={className}>\n      {render({\n        node,\n        level: level + indentOffset,\n        open,\n        onOpenChange: setOpen,\n        numBookmarks: listStats?.get(node.item.id),\n      })}\n      <CollapsibleContent>\n        {node.children\n          .sort((a, b) => a.item.name.localeCompare(b.item.name))\n          .map((l) => (\n            <ListItem\n              isOpenFunc={isOpenFunc}\n              key={l.item.id}\n              node={l}\n              render={render}\n              level={level + 1}\n              indentOffset={indentOffset}\n              listStats={listStats}\n              className={className}\n            />\n          ))}\n      </CollapsibleContent>\n    </Collapsible>\n  );\n}\n\nexport function CollapsibleBookmarkLists({\n  render,\n  initialData,\n  listsData,\n  className,\n  isOpenFunc,\n  filter,\n  indentOffset = 0,\n}: {\n  initialData?: ZBookmarkList[];\n  listsData?: {\n    data: ZBookmarkList[];\n    root: Record<string, ZBookmarkListTreeNode>;\n    allPaths: ZBookmarkList[][];\n    getPathById: (id: string) => ZBookmarkList[] | undefined;\n  };\n  render: RenderFunc;\n  isOpenFunc?: IsOpenFunc;\n  className?: string;\n  filter?: (node: ZBookmarkListTreeNode) => boolean;\n  indentOffset?: number;\n}) {\n  const api = useTRPC();\n  // If listsData is provided, use it directly. Otherwise, fetch it.\n  let { data: fetchedData } = useBookmarkLists(undefined, {\n    initialData: initialData ? { lists: initialData } : undefined,\n    enabled: !listsData, // Only fetch if listsData is not provided\n  });\n  const data = listsData || fetchedData;\n\n  const { data: listStats } = useQuery(\n    api.lists.stats.queryOptions(undefined, {\n      placeholderData: keepPreviousData,\n    }),\n  );\n\n  if (!data) {\n    return <FullPageSpinner />;\n  }\n\n  const rootNodes = Object.values(data.root);\n  const filteredRoots = filter ? rootNodes.filter(filter) : rootNodes;\n\n  return (\n    <div>\n      {filteredRoots\n        .sort((a, b) => a.item.name.localeCompare(b.item.name))\n        .map((node) => (\n          <ListItem\n            key={node.item.id}\n            node={node}\n            render={render}\n            level={0}\n            indentOffset={indentOffset}\n            className={className}\n            listStats={listStats?.stats}\n            isOpenFunc={isOpenFunc ?? (() => false)}\n          />\n        ))}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/lists/DeleteListConfirmationDialog.tsx",
    "content": "import React from \"react\";\nimport { usePathname, useRouter } from \"next/navigation\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport ActionConfirmingDialog from \"@/components/ui/action-confirming-dialog\";\nimport { Label } from \"@/components/ui/label\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { Switch } from \"@/components/ui/switch\";\nimport { useTranslation } from \"@/lib/i18n/client\";\n\nimport type { ZBookmarkList } from \"@karakeep/shared/types/lists\";\nimport { useDeleteBookmarkList } from \"@karakeep/shared-react/hooks/lists\";\n\nexport default function DeleteListConfirmationDialog({\n  list,\n  children,\n  open,\n  setOpen,\n}: {\n  list: ZBookmarkList;\n  children?: React.ReactNode;\n  open: boolean;\n  setOpen: (v: boolean) => void;\n}) {\n  const { t } = useTranslation();\n  const currentPath = usePathname();\n  const router = useRouter();\n  const [deleteChildren, setDeleteChildren] = React.useState(false);\n\n  const { mutate: deleteList, isPending } = useDeleteBookmarkList({\n    onSuccess: () => {\n      toast({\n        description: `List \"${list.icon} ${list.name}\" ${deleteChildren ? \"and all its children are \" : \"is \"} deleted!`,\n      });\n      setOpen(false);\n      if (currentPath.includes(list.id)) {\n        router.push(\"/dashboard/lists\");\n      }\n    },\n    onError: () => {\n      toast({\n        variant: \"destructive\",\n        description: `Something went wrong`,\n      });\n    },\n  });\n\n  return (\n    <ActionConfirmingDialog\n      open={open}\n      setOpen={setOpen}\n      title={t(\"lists.delete_list.title\")}\n      description={\n        <div className=\"space-y-3\">\n          <p className=\"text-balance\">\n            Are you sure you want to delete {list.icon} {list.name}?\n          </p>\n          <p className=\"text-balance text-sm text-muted-foreground\">\n            {t(\"lists.delete_list.description\")}\n          </p>\n\n          <div className=\"flex items-center justify-between rounded-lg border border-border/50 bg-muted/50 p-4\">\n            <div className=\"space-y-1\">\n              <Label\n                htmlFor=\"delete-children\"\n                className=\"cursor-pointer text-sm font-medium\"\n              >\n                {t(\"lists.delete_list.delete_children\")}\n              </Label>\n              <p className=\"text-xs text-muted-foreground\">\n                {t(\"lists.delete_list.delete_children_description\")}\n              </p>\n            </div>\n            <Switch\n              id=\"delete-children\"\n              checked={deleteChildren}\n              onCheckedChange={setDeleteChildren}\n            />\n          </div>\n        </div>\n      }\n      actionButton={() => (\n        <ActionButton\n          type=\"button\"\n          variant=\"destructive\"\n          loading={isPending}\n          onClick={() => deleteList({ listId: list.id, deleteChildren })}\n        >\n          {t(\"actions.delete\")}\n        </ActionButton>\n      )}\n    >\n      {children}\n    </ActionConfirmingDialog>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/lists/EditListModal.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useMemo, useState } from \"react\";\nimport Link from \"next/link\";\nimport { useRouter } from \"next/navigation\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@/components/ui/dialog\";\nimport {\n  Form,\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@/components/ui/form\";\nimport { Input } from \"@/components/ui/input\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport data from \"@emoji-mart/data\";\nimport Picker from \"@emoji-mart/react\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { X } from \"lucide-react\";\nimport { useForm } from \"react-hook-form\";\nimport { z } from \"zod\";\n\nimport {\n  useCreateBookmarkList,\n  useEditBookmarkList,\n} from \"@karakeep/shared-react/hooks/lists\";\nimport { parseSearchQuery } from \"@karakeep/shared/searchQueryParser\";\nimport {\n  ZBookmarkList,\n  zNewBookmarkListSchema,\n} from \"@karakeep/shared/types/lists\";\n\nimport QueryExplainerTooltip from \"../search/QueryExplainerTooltip\";\nimport { BookmarkListSelector } from \"./BookmarkListSelector\";\n\nexport function EditListModal({\n  open: userOpen,\n  setOpen: userSetOpen,\n  list,\n  prefill,\n  children,\n}: {\n  open?: boolean;\n  setOpen?: (v: boolean) => void;\n  list?: ZBookmarkList;\n  prefill?: Partial<Omit<ZBookmarkList, \"id\">>;\n  children?: React.ReactNode;\n}) {\n  const { t } = useTranslation();\n  const router = useRouter();\n  if (\n    (userOpen !== undefined && !userSetOpen) ||\n    (userOpen === undefined && userSetOpen)\n  ) {\n    throw new Error(\"You must provide both open and setOpen or neither\");\n  }\n  const [customOpen, customSetOpen] = useState(false);\n  const form = useForm<z.infer<typeof zNewBookmarkListSchema>>({\n    resolver: zodResolver(zNewBookmarkListSchema),\n    defaultValues: {\n      name: list?.name ?? prefill?.name ?? \"\",\n      description: list?.description ?? prefill?.description ?? \"\",\n      icon: list?.icon ?? prefill?.icon ?? \"🚀\",\n      parentId: list?.parentId ?? prefill?.parentId,\n      type: list?.type ?? prefill?.type ?? \"manual\",\n      query: list?.query ?? prefill?.query ?? undefined,\n    },\n  });\n  const [open, setOpen] = [\n    userOpen ?? customOpen,\n    userSetOpen ?? customSetOpen,\n  ];\n\n  useEffect(() => {\n    form.reset({\n      name: list?.name ?? prefill?.name ?? \"\",\n      description: list?.description ?? prefill?.description ?? \"\",\n      icon: list?.icon ?? prefill?.icon ?? \"🚀\",\n      parentId: list?.parentId ?? prefill?.parentId,\n      type: list?.type ?? prefill?.type ?? \"manual\",\n      query: list?.query ?? prefill?.query ?? undefined,\n    });\n  }, [open]);\n\n  const parsedSearchQuery = useMemo(() => {\n    const query = form.getValues().query;\n    if (!query) {\n      return undefined;\n    }\n    return parseSearchQuery(query);\n  }, [form.watch(\"query\")]);\n\n  const { mutate: createList, isPending: isCreating } = useCreateBookmarkList({\n    onSuccess: (resp) => {\n      toast({\n        description: t(\"toasts.lists.created\"),\n      });\n      setOpen(false);\n      router.push(`/dashboard/lists/${resp.id}`);\n      form.reset();\n    },\n    onError: (e) => {\n      if (e.data?.code == \"BAD_REQUEST\") {\n        if (e.data.zodError) {\n          toast({\n            variant: \"destructive\",\n            description: Object.values(e.data.zodError.fieldErrors)\n              .flat()\n              .join(\"\\n\"),\n          });\n        } else {\n          toast({\n            variant: \"destructive\",\n            description: e.message,\n          });\n        }\n      } else {\n        toast({\n          variant: \"destructive\",\n          title: t(\"common.something_went_wrong\"),\n        });\n      }\n    },\n  });\n\n  const { mutate: editList, isPending: isEditing } = useEditBookmarkList({\n    onSuccess: () => {\n      toast({\n        description: t(\"toasts.lists.updated\"),\n      });\n      setOpen(false);\n      form.reset();\n    },\n    onError: (e) => {\n      if (e.data?.code == \"BAD_REQUEST\") {\n        if (e.data.zodError) {\n          toast({\n            variant: \"destructive\",\n            description: Object.values(e.data.zodError.fieldErrors)\n              .flat()\n              .join(\"\\n\"),\n          });\n        } else {\n          toast({\n            variant: \"destructive\",\n            description: e.message,\n          });\n        }\n      } else {\n        toast({\n          variant: \"destructive\",\n          title: t(\"common.something_went_wrong\"),\n        });\n      }\n    },\n  });\n  const listType = form.watch(\"type\");\n\n  useEffect(() => {\n    if (listType !== \"smart\") {\n      form.resetField(\"query\");\n    }\n  }, [listType]);\n\n  const isEdit = !!list;\n  const isPending = isCreating || isEditing;\n\n  const onSubmit = form.handleSubmit(\n    (value: z.infer<typeof zNewBookmarkListSchema>) => {\n      value.parentId = value.parentId === \"\" ? null : value.parentId;\n      value.query = value.type === \"smart\" ? value.query : undefined;\n      if (isEdit) {\n        editList({ ...value, listId: list.id });\n      } else {\n        createList(value);\n      }\n    },\n  );\n\n  return (\n    <Dialog\n      open={open}\n      onOpenChange={(s) => {\n        form.reset();\n        setOpen(s);\n      }}\n    >\n      {children && <DialogTrigger asChild>{children}</DialogTrigger>}\n      <DialogContent>\n        <Form {...form}>\n          <form onSubmit={onSubmit}>\n            <DialogHeader>\n              <DialogTitle>\n                {isEdit ? t(\"lists.edit_list\") : t(\"lists.new_list\")}\n              </DialogTitle>\n            </DialogHeader>\n            <div className=\"flex w-full gap-2 py-4\">\n              <FormField\n                control={form.control}\n                name=\"icon\"\n                render={({ field }) => {\n                  return (\n                    <FormItem>\n                      <FormControl>\n                        <Popover>\n                          <PopoverTrigger className=\"h-full rounded border border-input px-2 text-2xl\">\n                            {field.value}\n                          </PopoverTrigger>\n                          <PopoverContent className=\"w-auto\">\n                            <Picker\n                              data={data}\n                              onEmojiSelect={(e: { native: string }) =>\n                                field.onChange(e.native)\n                              }\n                            />\n                          </PopoverContent>\n                        </Popover>\n                      </FormControl>\n                      <FormMessage />\n                    </FormItem>\n                  );\n                }}\n              />\n\n              <FormField\n                control={form.control}\n                name=\"name\"\n                render={({ field }) => {\n                  return (\n                    <FormItem className=\"grow\">\n                      <FormControl>\n                        <Input\n                          type=\"text\"\n                          className=\"w-full\"\n                          placeholder=\"List Name\"\n                          {...field}\n                        />\n                      </FormControl>\n                      <FormMessage />\n                    </FormItem>\n                  );\n                }}\n              />\n            </div>\n            <FormField\n              control={form.control}\n              name=\"description\"\n              render={({ field }) => {\n                return (\n                  <FormItem className=\"grow pb-4\">\n                    <FormLabel>{t(\"lists.description\")}</FormLabel>\n                    <FormControl>\n                      <Input\n                        type=\"text\"\n                        className=\"w-full\"\n                        placeholder=\"Description\"\n                        {...field}\n                      />\n                    </FormControl>\n                    <FormMessage />\n                  </FormItem>\n                );\n              }}\n            />\n            <FormField\n              control={form.control}\n              name=\"parentId\"\n              render={({ field }) => {\n                return (\n                  <FormItem className=\"grow pb-4\">\n                    <FormLabel>{t(\"lists.parent_list\")}</FormLabel>\n                    <div className=\"flex items-center gap-1\">\n                      <FormControl>\n                        <BookmarkListSelector\n                          // Hide the current list from the list of parents\n                          hideSubtreeOf={list ? list.id : undefined}\n                          value={field.value}\n                          onChange={field.onChange}\n                          placeholder={t(\"lists.no_parent\")}\n                        />\n                      </FormControl>\n                      <Button\n                        type=\"button\"\n                        variant=\"ghost\"\n                        onClick={() => {\n                          form.setValue(\"parentId\", \"\");\n                        }}\n                      >\n                        <X />\n                      </Button>\n                    </div>\n                    <FormMessage />\n                  </FormItem>\n                );\n              }}\n            />\n            <FormField\n              control={form.control}\n              name=\"type\"\n              render={({ field }) => {\n                return (\n                  <FormItem className=\"grow pb-4\">\n                    <FormLabel>{t(\"lists.list_type\")}</FormLabel>\n                    <FormControl>\n                      <Select\n                        disabled={isEdit}\n                        onValueChange={field.onChange}\n                        value={field.value}\n                      >\n                        <SelectTrigger className=\"w-full\">\n                          <SelectValue />\n                        </SelectTrigger>\n                        <SelectContent>\n                          <SelectItem value=\"manual\">\n                            {t(\"lists.manual_list\")}\n                          </SelectItem>\n                          <SelectItem value=\"smart\">\n                            {t(\"lists.smart_list\")}\n                          </SelectItem>\n                        </SelectContent>\n                      </Select>\n                    </FormControl>\n                    <FormMessage />\n                  </FormItem>\n                );\n              }}\n            />\n            {listType === \"smart\" && (\n              <FormField\n                control={form.control}\n                name=\"query\"\n                render={({ field }) => {\n                  return (\n                    <FormItem className=\"grow pb-4\">\n                      <FormLabel>{t(\"lists.search_query\")}</FormLabel>\n                      <div className=\"relative\">\n                        <FormControl>\n                          <Input\n                            value={field.value}\n                            onChange={field.onChange}\n                            placeholder={t(\"lists.search_query\")}\n                            endIcon={\n                              parsedSearchQuery ? (\n                                <QueryExplainerTooltip\n                                  className=\"stroke-foreground p-1\"\n                                  parsedSearchQuery={parsedSearchQuery}\n                                />\n                              ) : undefined\n                            }\n                          />\n                        </FormControl>\n                      </div>\n                      <FormDescription>\n                        <Link\n                          href=\"https://docs.karakeep.app/Guides/search-query-language\"\n                          className=\"italic\"\n                          target=\"_blank\"\n                        >\n                          {t(\"lists.search_query_help\")}\n                        </Link>\n                      </FormDescription>\n                      <FormMessage />\n                    </FormItem>\n                  );\n                }}\n              />\n            )}\n            <DialogFooter className=\"sm:justify-end\">\n              <DialogClose asChild>\n                <Button type=\"button\" variant=\"secondary\">\n                  {t(\"actions.close\")}\n                </Button>\n              </DialogClose>\n              <ActionButton\n                type=\"submit\"\n                onClick={onSubmit}\n                loading={isPending}\n              >\n                {list ? t(\"actions.save\") : t(\"actions.create\")}\n              </ActionButton>\n            </DialogFooter>\n          </form>\n        </Form>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/lists/LeaveListConfirmationDialog.tsx",
    "content": "import React from \"react\";\nimport { usePathname, useRouter } from \"next/navigation\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport ActionConfirmingDialog from \"@/components/ui/action-confirming-dialog\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { useMutation, useQueryClient } from \"@tanstack/react-query\";\n\nimport type { ZBookmarkList } from \"@karakeep/shared/types/lists\";\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\nexport default function LeaveListConfirmationDialog({\n  list,\n  children,\n  open,\n  setOpen,\n}: {\n  list: ZBookmarkList;\n  children?: React.ReactNode;\n  open: boolean;\n  setOpen: (v: boolean) => void;\n}) {\n  const api = useTRPC();\n  const { t } = useTranslation();\n  const currentPath = usePathname();\n  const router = useRouter();\n  const queryClient = useQueryClient();\n\n  const { mutate: leaveList, isPending } = useMutation(\n    api.lists.leaveList.mutationOptions({\n      onSuccess: () => {\n        toast({\n          description: t(\"lists.leave_list.success\", {\n            icon: list.icon,\n            name: list.name,\n          }),\n        });\n        setOpen(false);\n        // Invalidate the lists cache\n        queryClient.invalidateQueries(api.lists.list.pathFilter());\n        // If currently viewing this list, redirect to lists page\n        if (currentPath.includes(list.id)) {\n          router.push(\"/dashboard/lists\");\n        }\n      },\n      onError: (error) => {\n        toast({\n          variant: \"destructive\",\n          description: error.message || t(\"common.something_went_wrong\"),\n        });\n      },\n    }),\n  );\n\n  return (\n    <ActionConfirmingDialog\n      open={open}\n      setOpen={setOpen}\n      title={t(\"lists.leave_list.title\")}\n      description={\n        <div className=\"space-y-3\">\n          <p className=\"text-balance\">\n            {t(\"lists.leave_list.confirm_message\", {\n              icon: list.icon,\n              name: list.name,\n            })}\n          </p>\n          <p className=\"text-balance text-sm text-muted-foreground\">\n            {t(\"lists.leave_list.warning\")}\n          </p>\n        </div>\n      }\n      actionButton={() => (\n        <ActionButton\n          type=\"button\"\n          variant=\"destructive\"\n          loading={isPending}\n          onClick={() => leaveList({ listId: list.id })}\n        >\n          {t(\"lists.leave_list.action\")}\n        </ActionButton>\n      )}\n    >\n      {children}\n    </ActionConfirmingDialog>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/lists/ListHeader.tsx",
    "content": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { UserAvatar } from \"@/components/ui/user-avatar\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { MoreHorizontal, SearchIcon } from \"lucide-react\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\nimport { parseSearchQuery } from \"@karakeep/shared/searchQueryParser\";\nimport { ZBookmarkList } from \"@karakeep/shared/types/lists\";\n\nimport QueryExplainerTooltip from \"../search/QueryExplainerTooltip\";\nimport { ListOptions } from \"./ListOptions\";\n\nexport default function ListHeader({\n  initialData,\n}: {\n  initialData: ZBookmarkList;\n}) {\n  const api = useTRPC();\n  const { t } = useTranslation();\n  const router = useRouter();\n  const { data: list, error } = useQuery(\n    api.lists.get.queryOptions(\n      {\n        listId: initialData.id,\n      },\n      {\n        initialData,\n      },\n    ),\n  );\n\n  const { data: collaboratorsData } = useQuery(\n    api.lists.getCollaborators.queryOptions(\n      {\n        listId: initialData.id,\n      },\n      {\n        refetchOnWindowFocus: false,\n        enabled: list.hasCollaborators,\n      },\n    ),\n  );\n\n  const parsedQuery = useMemo(() => {\n    if (!list.query) {\n      return null;\n    }\n    return parseSearchQuery(list.query);\n  }, [list.query]);\n\n  if (error) {\n    // This is usually exercised during list deletions.\n    if (error.data?.code == \"NOT_FOUND\") {\n      router.push(\"/dashboard/lists\");\n    }\n  }\n\n  return (\n    <div className=\"flex items-center justify-between\">\n      <div className=\"flex items-center gap-2\">\n        <span className=\"text-2xl\">\n          {list.icon} {list.name}\n        </span>\n        {list.hasCollaborators && collaboratorsData && (\n          <div className=\"group flex\">\n            {collaboratorsData.owner && (\n              <Tooltip>\n                <TooltipTrigger>\n                  <div className=\"-mr-2 transition-all duration-300 ease-out group-hover:mr-1\">\n                    <UserAvatar\n                      name={collaboratorsData.owner.name}\n                      image={collaboratorsData.owner.image}\n                      className=\"size-5 shrink-0 rounded-full ring-2 ring-background\"\n                    />\n                  </div>\n                </TooltipTrigger>\n                <TooltipContent>\n                  <p>{collaboratorsData.owner.name}</p>\n                </TooltipContent>\n              </Tooltip>\n            )}\n            {collaboratorsData.collaborators.map((collab) => (\n              <Tooltip key={collab.userId}>\n                <TooltipTrigger>\n                  <div className=\"-mr-2 transition-all duration-300 ease-out group-hover:mr-1\">\n                    <UserAvatar\n                      name={collab.user.name}\n                      image={collab.user.image}\n                      className=\"size-5 shrink-0 rounded-full ring-2 ring-background\"\n                    />\n                  </div>\n                </TooltipTrigger>\n                <TooltipContent>\n                  <p>{collab.user.name}</p>\n                </TooltipContent>\n              </Tooltip>\n            ))}\n          </div>\n        )}\n        {list.description && (\n          <span className=\"text-lg text-gray-400\">{`(${list.description})`}</span>\n        )}\n      </div>\n      <div className=\"flex items-center\">\n        {parsedQuery && (\n          <QueryExplainerTooltip\n            header={\n              <div className=\"flex items-center justify-center gap-1\">\n                <SearchIcon className=\"size-3\" />\n                <span className=\"text-sm\">{t(\"lists.smart_list\")}</span>\n              </div>\n            }\n            parsedSearchQuery={parsedQuery}\n            className=\"size-6 stroke-foreground\"\n          />\n        )}\n        <ListOptions list={list}>\n          <Button variant=\"ghost\">\n            <MoreHorizontal />\n          </Button>\n        </ListOptions>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/lists/ListOptions.tsx",
    "content": "import { useState } from \"react\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { useShowArchived } from \"@/components/utils/useShowArchived\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport {\n  DoorOpen,\n  FolderInput,\n  Pencil,\n  Plus,\n  Share,\n  Square,\n  SquareCheck,\n  Trash2,\n  Users,\n} from \"lucide-react\";\n\nimport { ZBookmarkList } from \"@karakeep/shared/types/lists\";\n\nimport { EditListModal } from \"../lists/EditListModal\";\nimport DeleteListConfirmationDialog from \"./DeleteListConfirmationDialog\";\nimport LeaveListConfirmationDialog from \"./LeaveListConfirmationDialog\";\nimport { ManageCollaboratorsModal } from \"./ManageCollaboratorsModal\";\nimport { MergeListModal } from \"./MergeListModal\";\nimport { ShareListModal } from \"./ShareListModal\";\n\nexport function ListOptions({\n  list,\n  isOpen,\n  onOpenChange,\n  children,\n}: {\n  isOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  list: ZBookmarkList;\n  children?: React.ReactNode;\n}) {\n  const { t } = useTranslation();\n  const { showArchived, onClickShowArchived } = useShowArchived();\n\n  const [deleteListDialogOpen, setDeleteListDialogOpen] = useState(false);\n  const [leaveListDialogOpen, setLeaveListDialogOpen] = useState(false);\n  const [newNestedListModalOpen, setNewNestedListModalOpen] = useState(false);\n  const [mergeListModalOpen, setMergeListModalOpen] = useState(false);\n  const [editModalOpen, setEditModalOpen] = useState(false);\n  const [shareModalOpen, setShareModalOpen] = useState(false);\n  const [collaboratorsModalOpen, setCollaboratorsModalOpen] = useState(false);\n\n  // Only owners can manage the list (edit, delete, manage collaborators, etc.)\n  const isOwner = list.userRole === \"owner\";\n  // Collaborators (non-owners) can leave the list\n  const isCollaborator =\n    list.userRole === \"editor\" || list.userRole === \"viewer\";\n\n  // Define action items array\n  const actionItems = [\n    {\n      id: \"edit\",\n      title: t(\"actions.edit\"),\n      icon: <Pencil className=\"size-4\" />,\n      visible: isOwner,\n      disabled: false,\n      onClick: () => setEditModalOpen(true),\n    },\n    {\n      id: \"share\",\n      title: t(\"lists.share_list\"),\n      icon: <Share className=\"size-4\" />,\n      visible: isOwner,\n      disabled: false,\n      onClick: () => setShareModalOpen(true),\n    },\n    {\n      id: \"manage-collaborators\",\n      title: isOwner\n        ? t(\"lists.collaborators.manage\")\n        : t(\"lists.collaborators.view\"),\n      icon: <Users className=\"size-4\" />,\n      visible: list.type === \"manual\",\n      disabled: false,\n      onClick: () => setCollaboratorsModalOpen(true),\n    },\n    {\n      id: \"new-nested-list\",\n      title: t(\"lists.new_nested_list\"),\n      icon: <Plus className=\"size-4\" />,\n      visible: isOwner,\n      disabled: false,\n      onClick: () => setNewNestedListModalOpen(true),\n    },\n    {\n      id: \"merge-list\",\n      title: t(\"lists.merge_list\"),\n      icon: <FolderInput className=\"size-4\" />,\n      visible: isOwner,\n      disabled: false,\n      onClick: () => setMergeListModalOpen(true),\n    },\n    {\n      id: \"toggle-archived\",\n      title: t(\"actions.toggle_show_archived\"),\n      icon: showArchived ? (\n        <SquareCheck className=\"size-4\" />\n      ) : (\n        <Square className=\"size-4\" />\n      ),\n      visible: isOwner,\n      disabled: false,\n      onClick: onClickShowArchived,\n    },\n    {\n      id: \"leave-list\",\n      title: t(\"lists.leave_list.action\"),\n      icon: <DoorOpen className=\"size-4\" />,\n      visible: isCollaborator,\n      disabled: false,\n      className: \"flex gap-2 text-destructive\",\n      onClick: () => setLeaveListDialogOpen(true),\n    },\n    {\n      id: \"delete\",\n      title: t(\"actions.delete\"),\n      icon: <Trash2 className=\"size-4\" />,\n      visible: isOwner,\n      disabled: false,\n      className: \"flex gap-2 text-destructive\",\n      onClick: () => setDeleteListDialogOpen(true),\n    },\n  ];\n\n  // Filter visible items\n  const visibleItems = actionItems.filter((item) => item.visible);\n\n  // If no items are visible, don't render the dropdown\n  if (visibleItems.length === 0) {\n    return null;\n  }\n\n  return (\n    <DropdownMenu open={isOpen} onOpenChange={onOpenChange}>\n      <ShareListModal\n        open={shareModalOpen}\n        setOpen={setShareModalOpen}\n        list={list}\n      />\n      <ManageCollaboratorsModal\n        open={collaboratorsModalOpen}\n        setOpen={setCollaboratorsModalOpen}\n        list={list}\n        readOnly={!isOwner}\n      />\n      <EditListModal\n        open={newNestedListModalOpen}\n        setOpen={setNewNestedListModalOpen}\n        prefill={{\n          parentId: list.id,\n        }}\n      />\n      <EditListModal\n        open={editModalOpen}\n        setOpen={setEditModalOpen}\n        list={list}\n      />\n      <MergeListModal\n        open={mergeListModalOpen}\n        setOpen={setMergeListModalOpen}\n        list={list}\n      />\n      <DeleteListConfirmationDialog\n        list={list}\n        open={deleteListDialogOpen}\n        setOpen={setDeleteListDialogOpen}\n      />\n      <LeaveListConfirmationDialog\n        list={list}\n        open={leaveListDialogOpen}\n        setOpen={setLeaveListDialogOpen}\n      />\n      <DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>\n      <DropdownMenuContent>\n        {visibleItems.map((item) => (\n          <DropdownMenuItem\n            key={item.id}\n            className={item.className ?? \"flex gap-2\"}\n            disabled={item.disabled}\n            onClick={item.onClick}\n          >\n            {item.icon}\n            <span>{item.title}</span>\n          </DropdownMenuItem>\n        ))}\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/lists/ManageCollaboratorsModal.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@/components/ui/dialog\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { UserAvatar } from \"@/components/ui/user-avatar\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { Loader2, Trash2, UserPlus, Users } from \"lucide-react\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\nimport { ZBookmarkList } from \"@karakeep/shared/types/lists\";\n\nexport function ManageCollaboratorsModal({\n  open: userOpen,\n  setOpen: userSetOpen,\n  list,\n  children,\n  readOnly = false,\n}: {\n  open?: boolean;\n  setOpen?: (v: boolean) => void;\n  list: ZBookmarkList;\n  children?: React.ReactNode;\n  readOnly?: boolean;\n}) {\n  const api = useTRPC();\n  if (\n    (userOpen !== undefined && !userSetOpen) ||\n    (userOpen === undefined && userSetOpen)\n  ) {\n    throw new Error(\"You must provide both open and setOpen or neither\");\n  }\n  const [customOpen, customSetOpen] = useState(false);\n  const [open, setOpen] = [\n    userOpen ?? customOpen,\n    userSetOpen ?? customSetOpen,\n  ];\n\n  const [newCollaboratorEmail, setNewCollaboratorEmail] = useState(\"\");\n  const [newCollaboratorRole, setNewCollaboratorRole] = useState<\n    \"viewer\" | \"editor\"\n  >(\"viewer\");\n\n  const { t } = useTranslation();\n  const queryClient = useQueryClient();\n\n  const invalidateListCaches = () =>\n    Promise.all([\n      queryClient.invalidateQueries(\n        api.lists.getCollaborators.queryFilter({ listId: list.id }),\n      ),\n      queryClient.invalidateQueries(\n        api.lists.get.queryFilter({ listId: list.id }),\n      ),\n      queryClient.invalidateQueries(api.lists.list.pathFilter()),\n      queryClient.invalidateQueries(\n        api.bookmarks.getBookmarks.queryFilter({ listId: list.id }),\n      ),\n    ]);\n\n  // Fetch collaborators\n  const { data: collaboratorsData, isLoading } = useQuery(\n    api.lists.getCollaborators.queryOptions(\n      { listId: list.id },\n      { enabled: open },\n    ),\n  );\n\n  // Mutations\n  const addCollaborator = useMutation(\n    api.lists.addCollaborator.mutationOptions({\n      onSuccess: async () => {\n        toast({\n          description: t(\"lists.collaborators.invitation_sent\"),\n        });\n        setNewCollaboratorEmail(\"\");\n        await invalidateListCaches();\n      },\n      onError: (error) => {\n        toast({\n          variant: \"destructive\",\n          description: error.message || t(\"lists.collaborators.failed_to_add\"),\n        });\n      },\n    }),\n  );\n\n  const removeCollaborator = useMutation(\n    api.lists.removeCollaborator.mutationOptions({\n      onSuccess: async () => {\n        toast({\n          description: t(\"lists.collaborators.removed\"),\n        });\n        await invalidateListCaches();\n      },\n      onError: (error) => {\n        toast({\n          variant: \"destructive\",\n          description:\n            error.message || t(\"lists.collaborators.failed_to_remove\"),\n        });\n      },\n    }),\n  );\n\n  const updateCollaboratorRole = useMutation(\n    api.lists.updateCollaboratorRole.mutationOptions({\n      onSuccess: async () => {\n        toast({\n          description: t(\"lists.collaborators.role_updated\"),\n        });\n        await invalidateListCaches();\n      },\n      onError: (error) => {\n        toast({\n          variant: \"destructive\",\n          description:\n            error.message || t(\"lists.collaborators.failed_to_update_role\"),\n        });\n      },\n    }),\n  );\n\n  const revokeInvitation = useMutation(\n    api.lists.revokeInvitation.mutationOptions({\n      onSuccess: async () => {\n        toast({\n          description: t(\"lists.collaborators.invitation_revoked\"),\n        });\n        await invalidateListCaches();\n      },\n      onError: (error) => {\n        toast({\n          variant: \"destructive\",\n          description:\n            error.message || t(\"lists.collaborators.failed_to_revoke\"),\n        });\n      },\n    }),\n  );\n\n  const handleAddCollaborator = () => {\n    if (!newCollaboratorEmail.trim()) {\n      toast({\n        variant: \"destructive\",\n        description: t(\"lists.collaborators.please_enter_email\"),\n      });\n      return;\n    }\n\n    addCollaborator.mutate({\n      listId: list.id,\n      email: newCollaboratorEmail,\n      role: newCollaboratorRole,\n    });\n  };\n\n  return (\n    <Dialog\n      open={open}\n      onOpenChange={(s) => {\n        setOpen(s);\n      }}\n    >\n      {children && <DialogTrigger asChild>{children}</DialogTrigger>}\n      <DialogContent className=\"max-w-2xl\">\n        <DialogHeader>\n          <DialogTitle className=\"flex items-center gap-2\">\n            <Users className=\"h-5 w-5\" />\n            {readOnly\n              ? t(\"lists.collaborators.collaborators\")\n              : t(\"lists.collaborators.manage\")}\n            <Badge className=\"bg-green-600 text-white hover:bg-green-600/80\">\n              Beta\n            </Badge>\n          </DialogTitle>\n          <DialogDescription>\n            {readOnly\n              ? t(\"lists.collaborators.people_with_access\")\n              : t(\"lists.collaborators.add_or_remove\")}\n          </DialogDescription>\n        </DialogHeader>\n\n        <div className=\"space-y-6\">\n          {/* Add Collaborator Section */}\n          {!readOnly && (\n            <div className=\"space-y-3\">\n              <Label>{t(\"lists.collaborators.add\")}</Label>\n              <div className=\"flex gap-2\">\n                <div className=\"flex-1\">\n                  <Input\n                    type=\"email\"\n                    placeholder={t(\"lists.collaborators.enter_email\")}\n                    value={newCollaboratorEmail}\n                    onChange={(e) => setNewCollaboratorEmail(e.target.value)}\n                    onKeyDown={(e) => {\n                      if (e.key === \"Enter\") {\n                        handleAddCollaborator();\n                      }\n                    }}\n                  />\n                </div>\n                <Select\n                  value={newCollaboratorRole}\n                  onValueChange={(value) =>\n                    setNewCollaboratorRole(value as \"viewer\" | \"editor\")\n                  }\n                >\n                  <SelectTrigger className=\"w-32\">\n                    <SelectValue />\n                  </SelectTrigger>\n                  <SelectContent>\n                    <SelectItem value=\"viewer\">\n                      {t(\"lists.collaborators.viewer\")}\n                    </SelectItem>\n                    <SelectItem value=\"editor\">\n                      {t(\"lists.collaborators.editor\")}\n                    </SelectItem>\n                  </SelectContent>\n                </Select>\n                <Button\n                  onClick={handleAddCollaborator}\n                  disabled={addCollaborator.isPending}\n                >\n                  {addCollaborator.isPending ? (\n                    <Loader2 className=\"h-4 w-4 animate-spin\" />\n                  ) : (\n                    <UserPlus className=\"h-4 w-4\" />\n                  )}\n                </Button>\n              </div>\n              <p className=\"text-xs text-muted-foreground\">\n                <strong>{t(\"lists.collaborators.viewer\")}:</strong>{\" \"}\n                {t(\"lists.collaborators.viewer_description\")}\n                <br />\n                <strong>{t(\"lists.collaborators.editor\")}:</strong>{\" \"}\n                {t(\"lists.collaborators.editor_description\")}\n              </p>\n            </div>\n          )}\n\n          {/* Current Collaborators */}\n          <div className=\"space-y-3\">\n            <Label>\n              {readOnly\n                ? t(\"lists.collaborators.collaborators\")\n                : t(\"lists.collaborators.current\")}\n            </Label>\n            {isLoading ? (\n              <div className=\"flex justify-center py-8\">\n                <Loader2 className=\"h-6 w-6 animate-spin text-muted-foreground\" />\n              </div>\n            ) : collaboratorsData ? (\n              <div className=\"space-y-2\">\n                {/* Show owner first */}\n                {collaboratorsData.owner && (\n                  <div\n                    key={`owner-${collaboratorsData.owner.id}`}\n                    className=\"flex items-center justify-between rounded-lg border p-3\"\n                  >\n                    <div className=\"flex flex-1 items-center gap-3\">\n                      <UserAvatar\n                        name={collaboratorsData.owner.name}\n                        image={collaboratorsData.owner.image}\n                        className=\"size-10 ring-1 ring-border\"\n                      />\n                      <div className=\"flex-1\">\n                        <div className=\"font-medium\">\n                          {collaboratorsData.owner.name}\n                        </div>\n                        {collaboratorsData.owner.email && (\n                          <div className=\"text-sm text-muted-foreground\">\n                            {collaboratorsData.owner.email}\n                          </div>\n                        )}\n                      </div>\n                    </div>\n                    <div className=\"text-sm capitalize text-muted-foreground\">\n                      {t(\"lists.collaborators.owner\")}\n                    </div>\n                  </div>\n                )}\n                {/* Show collaborators */}\n                {collaboratorsData.collaborators.length > 0 ? (\n                  collaboratorsData.collaborators.map((collaborator) => (\n                    <div\n                      key={collaborator.id}\n                      className=\"flex items-center justify-between rounded-lg border p-3\"\n                    >\n                      <div className=\"flex flex-1 items-center gap-3\">\n                        <UserAvatar\n                          name={collaborator.user.name}\n                          image={collaborator.user.image}\n                          className=\"size-10 ring-1 ring-border\"\n                        />\n                        <div className=\"flex-1\">\n                          <div className=\"flex items-center gap-2\">\n                            <div className=\"font-medium\">\n                              {collaborator.user.name}\n                            </div>\n                            {collaborator.status === \"pending\" && (\n                              <Badge variant=\"outline\" className=\"text-xs\">\n                                {t(\"lists.collaborators.pending\")}\n                              </Badge>\n                            )}\n                            {collaborator.status === \"declined\" && (\n                              <Badge variant=\"destructive\" className=\"text-xs\">\n                                {t(\"lists.collaborators.declined\")}\n                              </Badge>\n                            )}\n                          </div>\n                          {collaborator.user.email && (\n                            <div className=\"text-sm text-muted-foreground\">\n                              {collaborator.user.email}\n                            </div>\n                          )}\n                        </div>\n                      </div>\n                      {readOnly ? (\n                        <div className=\"text-sm capitalize text-muted-foreground\">\n                          {collaborator.role}\n                        </div>\n                      ) : collaborator.status !== \"accepted\" ? (\n                        <div className=\"flex items-center gap-2\">\n                          <div className=\"text-sm capitalize text-muted-foreground\">\n                            {collaborator.role}\n                          </div>\n                          <Button\n                            variant=\"ghost\"\n                            size=\"sm\"\n                            onClick={() =>\n                              revokeInvitation.mutate({\n                                invitationId: collaborator.id,\n                              })\n                            }\n                            disabled={revokeInvitation.isPending}\n                          >\n                            {t(\"lists.collaborators.revoke\")}\n                          </Button>\n                        </div>\n                      ) : (\n                        <div className=\"flex items-center gap-2\">\n                          <Select\n                            value={collaborator.role}\n                            onValueChange={(value) =>\n                              updateCollaboratorRole.mutate({\n                                listId: list.id,\n                                userId: collaborator.userId,\n                                role: value as \"viewer\" | \"editor\",\n                              })\n                            }\n                          >\n                            <SelectTrigger className=\"w-28\">\n                              <SelectValue />\n                            </SelectTrigger>\n                            <SelectContent>\n                              <SelectItem value=\"viewer\">\n                                {t(\"lists.collaborators.viewer\")}\n                              </SelectItem>\n                              <SelectItem value=\"editor\">\n                                {t(\"lists.collaborators.editor\")}\n                              </SelectItem>\n                            </SelectContent>\n                          </Select>\n                          <Button\n                            variant=\"ghost\"\n                            size=\"icon\"\n                            onClick={() =>\n                              removeCollaborator.mutate({\n                                listId: list.id,\n                                userId: collaborator.userId,\n                              })\n                            }\n                            disabled={removeCollaborator.isPending}\n                          >\n                            <Trash2 className=\"h-4 w-4 text-destructive\" />\n                          </Button>\n                        </div>\n                      )}\n                    </div>\n                  ))\n                ) : !collaboratorsData.owner ? (\n                  <div className=\"rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground\">\n                    {readOnly\n                      ? t(\"lists.collaborators.no_collaborators_readonly\")\n                      : t(\"lists.collaborators.no_collaborators\")}\n                  </div>\n                ) : null}\n              </div>\n            ) : (\n              <div className=\"rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground\">\n                {readOnly\n                  ? t(\"lists.collaborators.no_collaborators_readonly\")\n                  : t(\"lists.collaborators.no_collaborators\")}\n              </div>\n            )}\n          </div>\n        </div>\n\n        <DialogFooter className=\"sm:justify-end\">\n          <DialogClose asChild>\n            <Button type=\"button\" variant=\"secondary\">\n              {t(\"actions.close\")}\n            </Button>\n          </DialogClose>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/lists/MergeListModal.tsx",
    "content": "import { useEffect, useState } from \"react\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@/components/ui/dialog\";\nimport {\n  Form,\n  FormControl,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@/components/ui/form\";\nimport { Input } from \"@/components/ui/input\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { Switch } from \"@/components/ui/switch\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { X } from \"lucide-react\";\nimport { useForm } from \"react-hook-form\";\nimport { z } from \"zod\";\n\nimport { useMergeLists } from \"@karakeep/shared-react/hooks/lists\";\nimport { ZBookmarkList, zMergeListSchema } from \"@karakeep/shared/types/lists\";\n\nimport { BookmarkListSelector } from \"./BookmarkListSelector\";\n\nexport function MergeListModal({\n  open: userOpen,\n  setOpen: userSetOpen,\n  list,\n  children,\n}: {\n  open?: boolean;\n  setOpen?: (v: boolean) => void;\n  list: ZBookmarkList;\n  children?: React.ReactNode;\n}) {\n  const { t } = useTranslation();\n  if (\n    (userOpen !== undefined && !userSetOpen) ||\n    (userOpen === undefined && userSetOpen)\n  ) {\n    throw new Error(\"You must provide both open and setOpen or neither\");\n  }\n  const [customOpen, customSetOpen] = useState(false);\n  const form = useForm<z.infer<typeof zMergeListSchema>>({\n    resolver: zodResolver(zMergeListSchema),\n    defaultValues: {\n      sourceId: list.id,\n      targetId: \"\",\n      deleteSourceAfterMerge: true,\n    },\n  });\n  const [open, setOpen] = [\n    userOpen ?? customOpen,\n    userSetOpen ?? customSetOpen,\n  ];\n\n  useEffect(() => {\n    form.reset({\n      sourceId: list.id,\n      targetId: \"\",\n      deleteSourceAfterMerge: true,\n    });\n  }, [open]);\n\n  const { mutate: mergeLists, isPending: isMerging } = useMergeLists({\n    onSuccess: () => {\n      toast({\n        description: t(\"toasts.lists.merged\"),\n      });\n      setOpen(false);\n      form.reset();\n    },\n    onError: (e) => {\n      if (e.data?.code == \"BAD_REQUEST\") {\n        if (e.data.zodError) {\n          toast({\n            variant: \"destructive\",\n            description: Object.values(e.data.zodError.fieldErrors)\n              .flat()\n              .join(\"\\n\"),\n          });\n        } else {\n          toast({\n            variant: \"destructive\",\n            description: e.message,\n          });\n        }\n      } else {\n        toast({\n          variant: \"destructive\",\n          title: t(\"common.something_went_wrong\"),\n        });\n      }\n    },\n  });\n\n  const onSubmit = form.handleSubmit(\n    async (value: z.infer<typeof zMergeListSchema>) => {\n      mergeLists(value);\n    },\n  );\n\n  return (\n    <Dialog\n      open={open}\n      onOpenChange={(s) => {\n        form.reset();\n        setOpen(s);\n      }}\n    >\n      {children && <DialogTrigger asChild>{children}</DialogTrigger>}\n      <DialogContent>\n        <Form {...form}>\n          <form onSubmit={onSubmit}>\n            <DialogHeader>\n              <DialogTitle>{t(\"lists.merge_list\")}</DialogTitle>\n            </DialogHeader>\n            <div className=\"flex w-full gap-2 py-4\">\n              <span className=\"inline-flex aspect-square h-10 items-center justify-center rounded border border-input bg-transparent px-2 text-2xl\">\n                {list.icon}\n              </span>\n              <Input\n                type=\"text\"\n                className=\"w-full\"\n                value={list.name}\n                disabled\n              />\n            </div>\n\n            <FormField\n              control={form.control}\n              name=\"deleteSourceAfterMerge\"\n              render={({ field }) => (\n                <FormItem className=\"flex flex-row items-center justify-between space-x-2 space-y-0 pb-4\">\n                  <label className=\"text-xs text-muted-foreground\">\n                    {t(\"lists.delete_after_merge\")}\n                  </label>\n                  <FormControl>\n                    <Switch\n                      checked={field.value}\n                      onCheckedChange={(checked) => {\n                        field.onChange(checked);\n                      }}\n                    />\n                  </FormControl>\n                </FormItem>\n              )}\n            />\n\n            <FormField\n              control={form.control}\n              name=\"targetId\"\n              render={({ field }) => (\n                <FormItem className=\"grow pb-4\">\n                  <FormLabel>{t(\"lists.destination_list\")}</FormLabel>\n                  <div className=\"flex items-center gap-1\">\n                    <FormControl>\n                      <BookmarkListSelector\n                        hideSubtreeOf={list.id}\n                        value={field.value}\n                        onChange={field.onChange}\n                        placeholder={t(\"lists.no_destination\")}\n                        listTypes={[\"manual\"]}\n                      />\n                    </FormControl>\n                    <Button\n                      type=\"button\"\n                      variant=\"ghost\"\n                      onClick={() => {\n                        form.resetField(\"targetId\");\n                      }}\n                    >\n                      <X />\n                    </Button>\n                  </div>\n                  <FormMessage />\n                </FormItem>\n              )}\n            />\n\n            <DialogFooter className=\"sm:justify-end\">\n              <DialogClose asChild>\n                <Button type=\"button\" variant=\"secondary\">\n                  {t(\"actions.cancel\")}\n                </Button>\n              </DialogClose>\n              <ActionButton\n                type=\"submit\"\n                onClick={onSubmit}\n                loading={isMerging}\n              >\n                {t(\"actions.merge\")}\n              </ActionButton>\n            </DialogFooter>\n          </form>\n        </Form>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/lists/PendingInvitationsCard.tsx",
    "content": "\"use client\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { Check, Loader2, Mail, X } from \"lucide-react\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\ninterface Invitation {\n  id: string;\n  role: string;\n  list: {\n    name: string;\n    icon?: string;\n    description?: string | null;\n    owner?: {\n      name?: string;\n    } | null;\n  };\n}\n\nfunction InvitationRow({ invitation }: { invitation: Invitation }) {\n  const api = useTRPC();\n  const { t } = useTranslation();\n  const queryClient = useQueryClient();\n\n  const acceptInvitation = useMutation(\n    api.lists.acceptInvitation.mutationOptions({\n      onSuccess: async () => {\n        toast({\n          description: t(\"lists.invitations.accepted\"),\n        });\n        await Promise.all([\n          queryClient.invalidateQueries(\n            api.lists.getPendingInvitations.pathFilter(),\n          ),\n          queryClient.invalidateQueries(api.lists.list.pathFilter()),\n        ]);\n      },\n      onError: (error) => {\n        toast({\n          variant: \"destructive\",\n          description: error.message || t(\"lists.invitations.failed_to_accept\"),\n        });\n      },\n    }),\n  );\n\n  const declineInvitation = useMutation(\n    api.lists.declineInvitation.mutationOptions({\n      onSuccess: async () => {\n        toast({\n          description: t(\"lists.invitations.declined\"),\n        });\n        await queryClient.invalidateQueries(\n          api.lists.getPendingInvitations.pathFilter(),\n        );\n      },\n      onError: (error) => {\n        toast({\n          variant: \"destructive\",\n          description:\n            error.message || t(\"lists.invitations.failed_to_decline\"),\n        });\n      },\n    }),\n  );\n\n  return (\n    <div className=\"flex items-center justify-between rounded-lg border p-4\">\n      <div className=\"flex-1\">\n        <div className=\"flex items-center gap-2\">\n          <span className=\"font-medium\">{invitation.list.name}</span>\n          <span className=\"text-xs text-muted-foreground\">\n            {invitation.list.icon}\n          </span>\n        </div>\n        {invitation.list.description && (\n          <div className=\"mt-1 text-sm text-muted-foreground\">\n            {invitation.list.description}\n          </div>\n        )}\n        <div className=\"mt-2 text-sm text-muted-foreground\">\n          {t(\"lists.invitations.invited_by\")}{\" \"}\n          <span className=\"font-medium\">\n            {invitation.list.owner?.name || \"Unknown\"}\n          </span>\n          {\" • \"}\n          <span className=\"capitalize\">{invitation.role}</span>\n        </div>\n      </div>\n      <div className=\"flex items-center gap-2\">\n        <Button\n          size=\"sm\"\n          variant=\"outline\"\n          onClick={() =>\n            declineInvitation.mutate({ invitationId: invitation.id })\n          }\n          disabled={declineInvitation.isPending || acceptInvitation.isPending}\n        >\n          {declineInvitation.isPending ? (\n            <Loader2 className=\"h-4 w-4 animate-spin\" />\n          ) : (\n            <>\n              <X className=\"mr-1 h-4 w-4\" />\n              {t(\"lists.invitations.decline\")}\n            </>\n          )}\n        </Button>\n        <Button\n          size=\"sm\"\n          onClick={() =>\n            acceptInvitation.mutate({ invitationId: invitation.id })\n          }\n          disabled={acceptInvitation.isPending || declineInvitation.isPending}\n        >\n          {acceptInvitation.isPending ? (\n            <Loader2 className=\"h-4 w-4 animate-spin\" />\n          ) : (\n            <>\n              <Check className=\"mr-1 h-4 w-4\" />\n              {t(\"lists.invitations.accept\")}\n            </>\n          )}\n        </Button>\n      </div>\n    </div>\n  );\n}\n\nexport function PendingInvitationsCard() {\n  const api = useTRPC();\n  const { t } = useTranslation();\n\n  const { data: invitations, isLoading } = useQuery(\n    api.lists.getPendingInvitations.queryOptions(),\n  );\n\n  if (isLoading) {\n    return null;\n  }\n\n  if (!invitations || invitations.length === 0) {\n    return null;\n  }\n\n  return (\n    <Card>\n      <CardHeader>\n        <CardTitle className=\"flex items-center gap-2 font-normal\">\n          <Mail className=\"h-5 w-5\" />\n          {t(\"lists.invitations.pending\")}\n\n          <span className=\"rounded bg-secondary p-1 text-sm text-secondary-foreground\">\n            {invitations.length}\n          </span>\n        </CardTitle>\n        <CardDescription>{t(\"lists.invitations.description\")}</CardDescription>\n      </CardHeader>\n      <CardContent className=\"space-y-3\">\n        {invitations.map((invitation) => (\n          <InvitationRow key={invitation.id} invitation={invitation} />\n        ))}\n      </CardContent>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/lists/PublicListLink.tsx",
    "content": "\"use client\";\n\nimport { CopyBtnV2 } from \"@/components/ui/copy-button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Switch } from \"@/components/ui/switch\";\nimport { useClientConfig } from \"@/lib/clientConfig\";\nimport { useTranslation } from \"react-i18next\";\n\nimport { useEditBookmarkList } from \"@karakeep/shared-react/hooks/lists\";\nimport { ZBookmarkList } from \"@karakeep/shared/types/lists\";\n\nexport default function PublicListLink({ list }: { list: ZBookmarkList }) {\n  const { t } = useTranslation();\n  const clientConfig = useClientConfig();\n\n  const { mutate: editList, isPending: isLoading } = useEditBookmarkList();\n\n  const publicListUrl = `${clientConfig.publicUrl}/public/lists/${list.id}`;\n  const isPublic = list.public;\n\n  return (\n    <>\n      {/* Public List Toggle */}\n      <div className=\"flex items-center justify-between\">\n        <div className=\"space-y-1\">\n          <Label htmlFor=\"public-toggle\" className=\"text-sm font-medium\">\n            {t(\"lists.public_list.title\")}\n          </Label>\n          <p className=\"text-xs text-muted-foreground\">\n            {t(\"lists.public_list.description\")}\n          </p>\n        </div>\n        <Switch\n          id=\"public-toggle\"\n          checked={isPublic}\n          disabled={isLoading || !!clientConfig.demoMode}\n          onCheckedChange={(checked) => {\n            editList({\n              listId: list.id,\n              public: checked,\n            });\n          }}\n        />\n      </div>\n\n      {/* Share URL - only show when public */}\n      {isPublic && (\n        <>\n          <div className=\"space-y-3\">\n            <Label className=\"text-sm font-medium\">\n              {t(\"lists.public_list.share_link\")}\n            </Label>\n            <div className=\"flex items-center space-x-2\">\n              <Input\n                value={publicListUrl}\n                readOnly\n                className=\"flex-1 text-sm\"\n              />\n              <CopyBtnV2 getStringToCopy={() => publicListUrl} />\n            </div>\n          </div>\n        </>\n      )}\n    </>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/lists/RssLink.tsx",
    "content": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { CopyBtnV2 } from \"@/components/ui/copy-button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Switch } from \"@/components/ui/switch\";\nimport { useClientConfig } from \"@/lib/clientConfig\";\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { Loader2, RotateCcw } from \"lucide-react\";\nimport { useTranslation } from \"react-i18next\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\nexport default function RssLink({ listId }: { listId: string }) {\n  const api = useTRPC();\n  const { t } = useTranslation();\n  const clientConfig = useClientConfig();\n  const queryClient = useQueryClient();\n\n  const { mutate: regenRssToken, isPending: isRegenPending } = useMutation(\n    api.lists.regenRssToken.mutationOptions({\n      onSuccess: () => {\n        queryClient.invalidateQueries(\n          api.lists.getRssToken.queryFilter({ listId }),\n        );\n      },\n    }),\n  );\n  const { mutate: clearRssToken, isPending: isClearPending } = useMutation(\n    api.lists.clearRssToken.mutationOptions({\n      onSuccess: () => {\n        queryClient.invalidateQueries(\n          api.lists.getRssToken.queryFilter({ listId }),\n        );\n      },\n    }),\n  );\n  const { data: rssToken, isLoading: isTokenLoading } = useQuery(\n    api.lists.getRssToken.queryOptions({ listId }),\n  );\n\n  const rssUrl = useMemo(() => {\n    if (!rssToken || !rssToken.token) {\n      return null;\n    }\n    return `${clientConfig.publicApiUrl}/v1/rss/lists/${listId}?token=${rssToken.token}`;\n  }, [rssToken]);\n\n  const rssEnabled = rssUrl !== null;\n\n  return (\n    <>\n      {/* RSS Feed Toggle */}\n      <div className=\"flex items-center justify-between\">\n        <div className=\"space-y-1\">\n          <Label htmlFor=\"rss-toggle\" className=\"text-sm font-medium\">\n            {t(\"lists.rss.title\")}\n          </Label>\n          <p className=\"text-xs text-muted-foreground\">\n            {t(\"lists.rss.description\")}\n          </p>\n        </div>\n        <Switch\n          id=\"rss-toggle\"\n          checked={rssEnabled}\n          onCheckedChange={(checked) =>\n            checked ? regenRssToken({ listId }) : clearRssToken({ listId })\n          }\n          disabled={\n            isTokenLoading ||\n            isClearPending ||\n            isRegenPending ||\n            !!clientConfig.demoMode\n          }\n        />\n      </div>\n      {/* RSS URL - only show when RSS is enabled */}\n      {rssEnabled && (\n        <div className=\"space-y-3\">\n          <Label className=\"text-sm font-medium\">\n            {t(\"lists.rss.feed_url\")}\n          </Label>\n          <div className=\"flex items-center space-x-2\">\n            <Input value={rssUrl} readOnly className=\"flex-1 text-sm\" />\n            <CopyBtnV2 getStringToCopy={() => rssUrl} />\n            <Button\n              variant=\"outline\"\n              size=\"sm\"\n              onClick={() => regenRssToken({ listId })}\n              disabled={isRegenPending}\n            >\n              {isRegenPending ? (\n                <Loader2 className=\"h-4 w-4 animate-spin\" />\n              ) : (\n                <RotateCcw className=\"h-4 w-4\" />\n              )}\n            </Button>\n          </div>\n        </div>\n      )}\n    </>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/lists/ShareListModal.tsx",
    "content": "import { useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@/components/ui/dialog\";\nimport { useTranslation } from \"@/lib/i18n/client\";\n\nimport { ZBookmarkList } from \"@karakeep/shared/types/lists\";\n\nimport PublicListLink from \"./PublicListLink\";\nimport RssLink from \"./RssLink\";\n\nexport function ShareListModal({\n  open: userOpen,\n  setOpen: userSetOpen,\n  list,\n  children,\n}: {\n  open?: boolean;\n  setOpen?: (v: boolean) => void;\n  list: ZBookmarkList;\n  children?: React.ReactNode;\n}) {\n  const { t } = useTranslation();\n  if (\n    (userOpen !== undefined && !userSetOpen) ||\n    (userOpen === undefined && userSetOpen)\n  ) {\n    throw new Error(\"You must provide both open and setOpen or neither\");\n  }\n  const [customOpen, customSetOpen] = useState(false);\n  const [open, setOpen] = [\n    userOpen ?? customOpen,\n    userSetOpen ?? customSetOpen,\n  ];\n\n  return (\n    <Dialog\n      open={open}\n      onOpenChange={(s) => {\n        setOpen(s);\n      }}\n    >\n      {children && <DialogTrigger asChild>{children}</DialogTrigger>}\n      <DialogContent className=\"max-w-xl\">\n        <DialogHeader>\n          <DialogTitle>{t(\"lists.share_list\")}</DialogTitle>\n        </DialogHeader>\n        <DialogDescription className=\"mt-4 space-y-6\">\n          <PublicListLink list={list} />\n          <RssLink listId={list.id} />\n        </DialogDescription>\n        <DialogFooter className=\"sm:justify-end\">\n          <DialogClose asChild>\n            <Button type=\"button\" variant=\"secondary\">\n              {t(\"actions.close\")}\n            </Button>\n          </DialogClose>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/preview/ActionBar.tsx",
    "content": "import { useState } from \"react\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport { Button } from \"@/components/ui/button\";\nimport { toast } from \"@/components/ui/sonner\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { Pencil, Trash2 } from \"lucide-react\";\n\nimport type { ZBookmark } from \"@karakeep/shared/types/bookmarks\";\nimport { useUpdateBookmark } from \"@karakeep/shared-react/hooks/bookmarks\";\n\nimport DeleteBookmarkConfirmationDialog from \"../bookmarks/DeleteBookmarkConfirmationDialog\";\nimport { EditBookmarkDialog } from \"../bookmarks/EditBookmarkDialog\";\nimport { ArchivedActionIcon, FavouritedActionIcon } from \"../bookmarks/icons\";\n\nexport default function ActionBar({ bookmark }: { bookmark: ZBookmark }) {\n  const { t } = useTranslation();\n  const [deleteBookmarkDialogOpen, setDeleteBookmarkDialogOpen] =\n    useState(false);\n\n  const [isEditBookmarkDialogOpen, setEditBookmarkDialogOpen] = useState(false);\n\n  const onError = () => {\n    toast({\n      variant: \"destructive\",\n      title: \"Something went wrong\",\n      description: \"There was a problem with your request.\",\n    });\n  };\n  const { mutate: favBookmark, isPending: pendingFav } = useUpdateBookmark({\n    onSuccess: () => {\n      toast({\n        description: \"The bookmark has been updated!\",\n      });\n    },\n    onError,\n  });\n  const { mutate: archiveBookmark, isPending: pendingArchive } =\n    useUpdateBookmark({\n      onSuccess: (resp) => {\n        toast({\n          description: `The bookmark has been ${resp.archived ? \"Archived\" : \"Un-archived\"}!`,\n        });\n      },\n      onError,\n    });\n\n  return (\n    <div className=\"flex items-center justify-center gap-3 text-muted-foreground\">\n      <Tooltip delayDuration={0}>\n        <EditBookmarkDialog\n          bookmark={bookmark}\n          open={isEditBookmarkDialogOpen}\n          setOpen={setEditBookmarkDialogOpen}\n        />\n\n        <TooltipTrigger asChild>\n          <Button\n            variant=\"ghost\"\n            size=\"none\"\n            className=\"size-8 rounded-md\"\n            onClick={() => {\n              setEditBookmarkDialogOpen(true);\n            }}\n          >\n            <Pencil size={18} strokeWidth={1.5} />\n          </Button>\n        </TooltipTrigger>\n        <TooltipContent side=\"bottom\">{t(\"actions.edit\")}</TooltipContent>\n      </Tooltip>\n      <Tooltip delayDuration={0}>\n        <TooltipTrigger asChild>\n          <ActionButton\n            variant=\"ghost\"\n            size=\"none\"\n            className=\"size-8 rounded-md\"\n            loading={pendingFav}\n            onClick={() => {\n              favBookmark({\n                bookmarkId: bookmark.id,\n                favourited: !bookmark.favourited,\n              });\n            }}\n          >\n            <FavouritedActionIcon\n              favourited={bookmark.favourited}\n              size={18}\n              strokeWidth={1.5}\n            />\n          </ActionButton>\n        </TooltipTrigger>\n        <TooltipContent side=\"bottom\">\n          {bookmark.favourited\n            ? t(\"actions.unfavorite\")\n            : t(\"actions.favorite\")}\n        </TooltipContent>\n      </Tooltip>\n      <Tooltip delayDuration={0}>\n        <TooltipTrigger asChild>\n          <ActionButton\n            variant=\"ghost\"\n            size=\"none\"\n            loading={pendingArchive}\n            className=\"size-8 rounded-md\"\n            onClick={() => {\n              archiveBookmark({\n                bookmarkId: bookmark.id,\n                archived: !bookmark.archived,\n              });\n            }}\n          >\n            <ArchivedActionIcon\n              archived={bookmark.archived}\n              size={18}\n              strokeWidth={1.5}\n            />\n          </ActionButton>\n        </TooltipTrigger>\n        <TooltipContent side=\"bottom\">\n          {bookmark.archived ? t(\"actions.unarchive\") : t(\"actions.archive\")}\n        </TooltipContent>\n      </Tooltip>\n      <Tooltip delayDuration={0}>\n        <DeleteBookmarkConfirmationDialog\n          bookmark={bookmark}\n          open={deleteBookmarkDialogOpen}\n          setOpen={setDeleteBookmarkDialogOpen}\n        />\n        <TooltipTrigger asChild>\n          <Button\n            className=\"size-8 rounded-md\"\n            variant=\"ghost\"\n            size=\"none\"\n            onClick={() => setDeleteBookmarkDialogOpen(true)}\n          >\n            <Trash2 size={18} strokeWidth={1.5} />\n          </Button>\n        </TooltipTrigger>\n        <TooltipContent side=\"bottom\">{t(\"actions.delete\")}</TooltipContent>\n      </Tooltip>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/preview/AssetContentSection.tsx",
    "content": "import { useMemo, useState } from \"react\";\nimport Image from \"next/image\";\nimport Link from \"next/link\";\nimport {\n  Select,\n  SelectContent,\n  SelectGroup,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\";\nimport { useTranslation } from \"@/lib/i18n/client\";\n\nimport { BookmarkTypes, ZBookmark } from \"@karakeep/shared/types/bookmarks\";\nimport { getAssetUrl } from \"@karakeep/shared/utils/assetUtils\";\n\n// 20 MB\nconst BIG_FILE_SIZE = 20 * 1024 * 1024;\n\nfunction PDFContentSection({ bookmark }: { bookmark: ZBookmark }) {\n  if (bookmark.content.type != BookmarkTypes.ASSET) {\n    throw new Error(\"Invalid content type\");\n  }\n  const { t } = useTranslation();\n\n  const initialSection = useMemo(() => {\n    if (bookmark.content.type != BookmarkTypes.ASSET) {\n      throw new Error(\"Invalid content type\");\n    }\n\n    const screenshot = bookmark.assets.find(\n      (item) => item.assetType === \"assetScreenshot\",\n    );\n    const bigSize =\n      bookmark.content.size && bookmark.content.size > BIG_FILE_SIZE;\n    if (bigSize && screenshot) {\n      return \"screenshot\";\n    }\n    return \"pdf\";\n  }, [bookmark]);\n  const [section, setSection] = useState(initialSection);\n\n  const screenshot = bookmark.assets.find(\n    (r) => r.assetType === \"assetScreenshot\",\n  )?.id;\n\n  const content =\n    section === \"screenshot\" && screenshot ? (\n      <div className=\"relative h-full min-w-full\">\n        <Image\n          alt=\"screenshot\"\n          src={getAssetUrl(screenshot)}\n          fill={true}\n          className=\"object-contain\"\n        />\n      </div>\n    ) : (\n      <iframe\n        title={bookmark.content.assetId}\n        className=\"h-full w-full\"\n        src={getAssetUrl(bookmark.content.assetId)}\n      />\n    );\n\n  return (\n    <div className=\"flex h-full flex-col items-center gap-2\">\n      <div className=\"flex w-full items-center justify-center gap-4\">\n        <Select onValueChange={setSection} value={section}>\n          <SelectTrigger className=\"w-fit\">\n            <SelectValue />\n          </SelectTrigger>\n          <SelectContent>\n            <SelectGroup>\n              <SelectItem value=\"screenshot\" disabled={!screenshot}>\n                {t(\"common.screenshot\")}\n              </SelectItem>\n              <SelectItem value=\"pdf\">PDF</SelectItem>\n            </SelectGroup>\n          </SelectContent>\n        </Select>\n      </div>\n      {content}\n    </div>\n  );\n}\n\nfunction ImageContentSection({ bookmark }: { bookmark: ZBookmark }) {\n  if (bookmark.content.type != BookmarkTypes.ASSET) {\n    throw new Error(\"Invalid content type\");\n  }\n  return (\n    <div className=\"relative h-full min-w-full\">\n      <Link href={getAssetUrl(bookmark.content.assetId)} target=\"_blank\">\n        <Image\n          alt=\"asset\"\n          fill={true}\n          className=\"object-contain\"\n          src={getAssetUrl(bookmark.content.assetId)}\n        />\n      </Link>\n    </div>\n  );\n}\n\nexport function AssetContentSection({ bookmark }: { bookmark: ZBookmark }) {\n  if (bookmark.content.type != BookmarkTypes.ASSET) {\n    throw new Error(\"Invalid content type\");\n  }\n  switch (bookmark.content.assetType) {\n    case \"image\":\n      return <ImageContentSection bookmark={bookmark} />;\n    case \"pdf\":\n      return <PDFContentSection bookmark={bookmark} />;\n    default:\n      return <div>Unsupported asset type</div>;\n  }\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/preview/AttachmentBox.tsx",
    "content": "import Link from \"next/link\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport ActionConfirmingDialog from \"@/components/ui/action-confirming-dialog\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Collapsible,\n  CollapsibleContent,\n  CollapsibleTrigger,\n} from \"@/components/ui/collapsible\";\nimport FilePickerButton from \"@/components/ui/file-picker-button\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { ASSET_TYPE_TO_ICON } from \"@/lib/attachments\";\nimport useUpload from \"@/lib/hooks/upload-file\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport {\n  ChevronsDownUp,\n  Download,\n  ImagePlus,\n  Paperclip,\n  Pencil,\n  Trash2,\n} from \"lucide-react\";\n\nimport {\n  useAttachBookmarkAsset,\n  useDetachBookmarkAsset,\n  useReplaceBookmarkAsset,\n} from \"@karakeep/shared-react/hooks/assets\";\nimport { BookmarkTypes, ZBookmark } from \"@karakeep/shared/types/bookmarks\";\nimport { getAssetUrl } from \"@karakeep/shared/utils/assetUtils\";\nimport {\n  humanFriendlyNameForAssertType,\n  isAllowedToAttachAsset,\n  isAllowedToDetachAsset,\n} from \"@karakeep/trpc/lib/attachments\";\n\nexport default function AttachmentBox({\n  bookmark,\n  readOnly = false,\n}: {\n  bookmark: ZBookmark;\n  readOnly?: boolean;\n}) {\n  const { t } = useTranslation();\n  const { mutate: attachAsset, isPending: isAttaching } =\n    useAttachBookmarkAsset({\n      onSuccess: () => {\n        toast({\n          description: \"Attachment has been attached!\",\n        });\n      },\n      onError: (e) => {\n        toast({\n          description: e.message,\n          variant: \"destructive\",\n        });\n      },\n    });\n\n  const { mutate: replaceAsset, isPending: isReplacing } =\n    useReplaceBookmarkAsset({\n      onSuccess: () => {\n        toast({\n          description: \"Attachment has been replaced!\",\n        });\n      },\n      onError: (e) => {\n        toast({\n          description: e.message,\n          variant: \"destructive\",\n        });\n      },\n    });\n\n  const { mutate: detachAsset, isPending: isDetaching } =\n    useDetachBookmarkAsset({\n      onSuccess: () => {\n        toast({\n          description: \"Attachment has been detached!\",\n        });\n      },\n      onError: (e) => {\n        toast({\n          description: e.message,\n          variant: \"destructive\",\n        });\n      },\n    });\n\n  const { mutate: uploadAsset } = useUpload({\n    onError: (e) => {\n      toast({\n        description: e.error,\n        variant: \"destructive\",\n      });\n    },\n  });\n\n  bookmark.assets.sort((a, b) => a.assetType.localeCompare(b.assetType));\n\n  const hasAssets = bookmark.assets.length > 0;\n\n  return (\n    <Collapsible defaultOpen={true}>\n      <div className=\"flex w-full items-center justify-between gap-2 text-xs font-medium uppercase tracking-wide text-muted-foreground\">\n        {t(\"common.attachments\")}\n        <div className=\"flex items-center gap-1\">\n          {!readOnly && (\n            <>\n              {!bookmark.assets.some(\n                (asset) => asset.assetType == \"bannerImage\",\n              ) &&\n                bookmark.content.type != BookmarkTypes.ASSET && (\n                  <FilePickerButton\n                    title=\"Attach a Banner\"\n                    loading={isAttaching}\n                    accept=\".jgp,.JPG,.jpeg,.png,.webp\"\n                    multiple={false}\n                    variant=\"none\"\n                    size=\"none\"\n                    className=\"rounded-md p-1 hover:text-foreground\"\n                    onFileSelect={(file) =>\n                      uploadAsset(file, {\n                        onSuccess: (resp) => {\n                          attachAsset({\n                            bookmarkId: bookmark.id,\n                            asset: {\n                              id: resp.assetId,\n                              assetType: \"bannerImage\",\n                            },\n                          });\n                        },\n                      })\n                    }\n                  >\n                    <ImagePlus className=\"size-3.5\" strokeWidth={1.5} />\n                  </FilePickerButton>\n                )}\n              <FilePickerButton\n                title=\"Upload File\"\n                loading={isAttaching}\n                multiple={false}\n                variant=\"none\"\n                size=\"none\"\n                className=\"rounded-md p-1 hover:text-foreground\"\n                onFileSelect={(file) =>\n                  uploadAsset(file, {\n                    onSuccess: (resp) => {\n                      attachAsset({\n                        bookmarkId: bookmark.id,\n                        asset: {\n                          id: resp.assetId,\n                          assetType: \"userUploaded\",\n                        },\n                      });\n                    },\n                  })\n                }\n              >\n                <Paperclip className=\"size-3.5\" strokeWidth={1.5} />\n              </FilePickerButton>\n            </>\n          )}\n          {hasAssets && (\n            <CollapsibleTrigger>\n              <ChevronsDownUp className=\"size-4\" />\n            </CollapsibleTrigger>\n          )}\n        </div>\n      </div>\n      <CollapsibleContent className=\"flex flex-col gap-1 py-3 text-sm\">\n        {bookmark.assets.map((asset) => (\n          <div key={asset.id} className=\"flex items-center justify-between\">\n            <Link\n              target=\"_blank\"\n              href={getAssetUrl(asset.id)}\n              className=\"flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground\"\n              prefetch={false}\n            >\n              {ASSET_TYPE_TO_ICON[asset.assetType]}\n              <p>\n                {asset.assetType === \"userUploaded\" && asset.fileName\n                  ? asset.fileName\n                  : humanFriendlyNameForAssertType(asset.assetType)}\n              </p>\n            </Link>\n            <div className=\"flex gap-1 text-muted-foreground\">\n              <Link\n                title=\"Download\"\n                target=\"_blank\"\n                href={getAssetUrl(asset.id)}\n                className=\"flex items-center gap-1 rounded-md p-1 hover:text-foreground\"\n                download={\n                  asset.assetType === \"userUploaded\" && asset.fileName\n                    ? asset.fileName\n                    : humanFriendlyNameForAssertType(asset.assetType)\n                }\n                prefetch={false}\n              >\n                <Download className=\"size-3.5\" strokeWidth={1.5} />\n              </Link>\n              {!readOnly &&\n                isAllowedToAttachAsset(asset.assetType) &&\n                asset.assetType !== \"userUploaded\" && (\n                  <FilePickerButton\n                    title=\"Replace\"\n                    loading={isReplacing}\n                    accept=\".jgp,.JPG,.jpeg,.png,.webp\"\n                    multiple={false}\n                    variant=\"none\"\n                    size=\"none\"\n                    className=\"flex items-center gap-2 rounded-md p-1 hover:text-foreground\"\n                    onFileSelect={(file) =>\n                      uploadAsset(file, {\n                        onSuccess: (resp) => {\n                          replaceAsset({\n                            bookmarkId: bookmark.id,\n                            oldAssetId: asset.id,\n                            newAssetId: resp.assetId,\n                          });\n                        },\n                      })\n                    }\n                  >\n                    <Pencil className=\"size-3.5\" strokeWidth={1.5} />\n                  </FilePickerButton>\n                )}\n              {!readOnly && isAllowedToDetachAsset(asset.assetType) && (\n                <ActionConfirmingDialog\n                  title=\"Delete Attachment?\"\n                  description={`Are you sure you want to delete the attachment of the bookmark?`}\n                  actionButton={(setDialogOpen) => (\n                    <ActionButton\n                      loading={isDetaching}\n                      variant=\"destructive\"\n                      onClick={() =>\n                        detachAsset(\n                          { bookmarkId: bookmark.id, assetId: asset.id },\n                          { onSettled: () => setDialogOpen(false) },\n                        )\n                      }\n                    >\n                      <Trash2 className=\"mr-2 size-4\" />\n                      Delete\n                    </ActionButton>\n                  )}\n                >\n                  <Button\n                    variant=\"none\"\n                    size=\"none\"\n                    title=\"Delete\"\n                    className=\"rounded-md p-1 hover:text-foreground\"\n                  >\n                    <Trash2 className=\"size-3.5\" strokeWidth={1.5} />\n                  </Button>\n                </ActionConfirmingDialog>\n              )}\n            </div>\n          </div>\n        ))}\n        {!hasAssets && readOnly && (\n          <p className=\"py-1 text-xs text-muted-foreground\">No attachments</p>\n        )}\n        {!hasAssets && !readOnly && (\n          <p className=\"py-1 text-xs text-muted-foreground\">\n            No attachments yet\n          </p>\n        )}\n      </CollapsibleContent>\n    </Collapsible>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/preview/BookmarkPreview.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport Link from \"next/link\";\nimport { BookmarkTagsEditor } from \"@/components/dashboard/bookmarks/BookmarkTagsEditor\";\nimport { FullPageSpinner } from \"@/components/ui/full-page-spinner\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipPortal,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { useSession } from \"@/lib/auth/client\";\nimport useRelativeTime from \"@/lib/hooks/relative-time\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport {\n  Building,\n  CalendarDays,\n  ExternalLink,\n  Globe,\n  PanelRightClose,\n  PanelRightOpen,\n  User,\n} from \"lucide-react\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\nimport { BookmarkTypes, ZBookmark } from \"@karakeep/shared/types/bookmarks\";\nimport {\n  getBookmarkRefreshInterval,\n  getBookmarkTitle,\n  getSourceUrl,\n  isBookmarkStillCrawling,\n} from \"@karakeep/shared/utils/bookmarkUtils\";\n\nimport SummarizeBookmarkArea from \"../bookmarks/SummarizeBookmarkArea\";\nimport ActionBar from \"./ActionBar\";\nimport { AssetContentSection } from \"./AssetContentSection\";\nimport AttachmentBox from \"./AttachmentBox\";\nimport HighlightsBox from \"./HighlightsBox\";\nimport LinkContentSection from \"./LinkContentSection\";\nimport { NoteEditor } from \"./NoteEditor\";\nimport { TextContentSection } from \"./TextContentSection\";\n\nfunction ContentLoading() {\n  const { t } = useTranslation();\n  return (\n    <div className=\"flex h-full w-full flex-col items-center justify-center gap-4\">\n      <Globe className=\"h-12 w-12 animate-bounce text-muted-foreground\" />\n      <p className=\"text-sm text-muted-foreground\">\n        {t(\"preview.crawling_in_progress\")}\n      </p>\n    </div>\n  );\n}\n\nfunction CreationTime({ createdAt }: { createdAt: Date }) {\n  const { fromNow, localCreatedAt } = useRelativeTime(createdAt);\n  return (\n    <Tooltip delayDuration={0}>\n      <TooltipTrigger asChild>\n        <span className=\"flex w-fit items-center gap-2 text-sm text-muted-foreground\">\n          <CalendarDays size={16} /> {fromNow}\n        </span>\n      </TooltipTrigger>\n      <TooltipPortal>\n        <TooltipContent>{localCreatedAt}</TooltipContent>\n      </TooltipPortal>\n    </Tooltip>\n  );\n}\n\nfunction BookmarkMetadata({ bookmark }: { bookmark: ZBookmark }) {\n  let { author, publisher, datePublished } =\n    bookmark.content.type !== BookmarkTypes.LINK\n      ? {\n          author: null,\n          publisher: null,\n          datePublished: null,\n        }\n      : bookmark.content;\n\n  return (\n    <div className=\"flex flex-col gap-2\">\n      <CreationTime createdAt={bookmark.createdAt} />\n      {author && (\n        <div className=\"flex w-fit items-center gap-2 text-sm text-muted-foreground\">\n          <User size={16} />\n          <span>By {author}</span>\n        </div>\n      )}\n      {publisher && (\n        <div className=\"flex w-fit items-center gap-2 text-sm text-muted-foreground\">\n          <Building size={16} />\n          <span>{publisher}</span>\n        </div>\n      )}\n      {datePublished && <PublishedDate datePublished={datePublished} />}\n    </div>\n  );\n}\n\nfunction PublishedDate({ datePublished }: { datePublished: Date }) {\n  const { fromNow, localCreatedAt } = useRelativeTime(datePublished);\n  return (\n    <Tooltip delayDuration={0}>\n      <TooltipTrigger asChild>\n        <div className=\"flex w-fit items-center gap-2 text-sm text-muted-foreground\">\n          <CalendarDays size={16} />\n          <span>Published {fromNow}</span>\n        </div>\n      </TooltipTrigger>\n      <TooltipPortal>\n        <TooltipContent>{localCreatedAt}</TooltipContent>\n      </TooltipPortal>\n    </Tooltip>\n  );\n}\n\nexport default function BookmarkPreview({\n  bookmarkId,\n  initialData,\n}: {\n  bookmarkId: string;\n  initialData?: ZBookmark;\n  onClose?: () => void;\n}) {\n  const api = useTRPC();\n  const { t } = useTranslation();\n  const [activeTab, setActiveTab] = useState<string>(\"content\");\n  const [sidebarCollapsed, setSidebarCollapsed] = useState(false);\n  const { data: session } = useSession();\n\n  const { data: bookmark } = useQuery(\n    api.bookmarks.getBookmark.queryOptions(\n      {\n        bookmarkId,\n      },\n      {\n        initialData,\n        refetchInterval: (query) => {\n          const data = query.state.data;\n          if (!data) {\n            return false;\n          }\n          return getBookmarkRefreshInterval(data);\n        },\n      },\n    ),\n  );\n\n  if (!bookmark) {\n    return <FullPageSpinner />;\n  }\n\n  // Check if the current user owns this bookmark\n  const isOwner = session?.user?.id === bookmark.userId;\n\n  let content;\n  switch (bookmark.content.type) {\n    case BookmarkTypes.LINK: {\n      content = <LinkContentSection bookmark={bookmark} />;\n      break;\n    }\n    case BookmarkTypes.TEXT: {\n      content = <TextContentSection bookmark={bookmark} />;\n      break;\n    }\n    case BookmarkTypes.ASSET: {\n      content = <AssetContentSection bookmark={bookmark} />;\n      break;\n    }\n  }\n\n  const sourceUrl = getSourceUrl(bookmark);\n  const title = getBookmarkTitle(bookmark);\n\n  // Common content for both layouts\n  const contentSection = isBookmarkStillCrawling(bookmark) ? (\n    <ContentLoading />\n  ) : (\n    content\n  );\n\n  const detailsSection = (\n    <div className=\"flex flex-col gap-5\">\n      <div className=\"flex flex-col gap-1\">\n        <p className=\"line-clamp-2 text-ellipsis break-words text-lg font-medium\">\n          {!title ? \"Untitled\" : title}\n        </p>\n        {sourceUrl && (\n          <Link\n            href={sourceUrl}\n            target=\"_blank\"\n            className=\"flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground\"\n          >\n            <ExternalLink className=\"size-3\" />\n            <span>{t(\"preview.view_original\")}</span>\n          </Link>\n        )}\n      </div>\n      <Separator />\n      <BookmarkMetadata bookmark={bookmark} />\n      <SummarizeBookmarkArea bookmark={bookmark} readOnly={!isOwner} />\n      <Separator />\n      <div className=\"flex flex-col gap-1.5\">\n        <p className=\"text-xs font-medium uppercase tracking-wide text-muted-foreground\">\n          {t(\"common.tags\")}\n        </p>\n        <BookmarkTagsEditor bookmark={bookmark} disabled={!isOwner} />\n      </div>\n      <Separator />\n      <div className=\"flex flex-col gap-1.5\">\n        <p className=\"text-xs font-medium uppercase tracking-wide text-muted-foreground\">\n          {t(\"common.note\")}\n        </p>\n        <NoteEditor bookmark={bookmark} disabled={!isOwner} />\n      </div>\n      <Separator />\n      <AttachmentBox bookmark={bookmark} readOnly={!isOwner} />\n      <HighlightsBox bookmarkId={bookmark.id} readOnly={!isOwner} />\n      <Separator />\n      {isOwner && <ActionBar bookmark={bookmark} />}\n    </div>\n  );\n\n  return (\n    <>\n      {/* Render original layout for wide screens */}\n      <div className=\"hidden h-full flex-col overflow-hidden bg-background lg:flex\">\n        <div className=\"flex min-h-0 flex-1\">\n          <div className=\"relative h-full flex-1 overflow-auto px-4 py-4\">\n            <button\n              onClick={() => setSidebarCollapsed(!sidebarCollapsed)}\n              className=\"absolute right-4 top-4 z-10 rounded-md p-1.5 text-muted-foreground hover:bg-muted hover:text-foreground\"\n            >\n              {sidebarCollapsed ? (\n                <PanelRightOpen size={20} />\n              ) : (\n                <PanelRightClose size={20} />\n              )}\n            </button>\n            {contentSection}\n          </div>\n          {!sidebarCollapsed && (\n            <div className=\"flex w-1/3 flex-col gap-3 overflow-auto border-l bg-muted/40 p-5\">\n              {detailsSection}\n            </div>\n          )}\n        </div>\n      </div>\n      {/* Render tabbed layout for narrow/vertical screens */}\n      <div className=\"flex h-full w-full flex-col overflow-hidden lg:hidden\">\n        <Tabs\n          value={activeTab}\n          onValueChange={setActiveTab}\n          className=\"flex min-h-0 flex-1 flex-col overflow-hidden\"\n        >\n          <TabsList className=\"z-10 mx-4 mt-2 grid w-auto grid-cols-2\">\n            <TabsTrigger value=\"content\">\n              {t(\"preview.tabs.content\")}\n            </TabsTrigger>\n            <TabsTrigger value=\"details\">\n              {t(\"preview.tabs.details\")}\n            </TabsTrigger>\n          </TabsList>\n          <TabsContent\n            value=\"content\"\n            className=\"h-full flex-1 overflow-hidden overflow-y-auto bg-background px-4 py-3 data-[state=inactive]:hidden\"\n          >\n            {contentSection}\n          </TabsContent>\n          <TabsContent\n            value=\"details\"\n            className=\"h-full overflow-y-auto bg-background px-4 py-3 data-[state=inactive]:hidden\"\n          >\n            {detailsSection}\n          </TabsContent>\n        </Tabs>\n      </div>\n    </>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/preview/HighlightsBox.tsx",
    "content": "import { Fragment } from \"react\";\nimport {\n  Collapsible,\n  CollapsibleContent,\n  CollapsibleTrigger,\n} from \"@/components/ui/collapsible\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { Separator } from \"@radix-ui/react-dropdown-menu\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { ChevronsDownUp } from \"lucide-react\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\nimport HighlightCard from \"../highlights/HighlightCard\";\n\nexport default function HighlightsBox({\n  bookmarkId,\n  readOnly,\n}: {\n  bookmarkId: string;\n  readOnly: boolean;\n}) {\n  const api = useTRPC();\n  const { t } = useTranslation();\n\n  const { data: highlights, isPending: isLoading } = useQuery(\n    api.highlights.getForBookmark.queryOptions({ bookmarkId }),\n  );\n\n  if (isLoading || !highlights || highlights?.highlights.length === 0) {\n    return null;\n  }\n\n  return (\n    <Collapsible defaultOpen={true}>\n      <CollapsibleTrigger className=\"flex w-full items-center justify-between gap-2 text-xs font-medium uppercase tracking-wide text-muted-foreground\">\n        {t(\"common.highlights\")}\n        <ChevronsDownUp className=\"size-4\" />\n      </CollapsibleTrigger>\n      <CollapsibleContent className=\"group flex flex-col py-3 text-sm\">\n        {highlights.highlights.map((highlight) => (\n          <Fragment key={highlight.id}>\n            <HighlightCard\n              highlight={highlight}\n              clickable\n              readOnly={readOnly}\n            />\n            <Separator className=\"m-2 h-0.5 bg-gray-200 last:hidden\" />\n          </Fragment>\n        ))}\n      </CollapsibleContent>\n    </Collapsible>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/preview/LinkContentSection.tsx",
    "content": "import Image from \"next/image\";\nimport Link from \"next/link\";\nimport { Alert, AlertDescription, AlertTitle } from \"@/components/ui/alert\";\nimport { buttonVariants } from \"@/components/ui/button\";\nimport {\n  Select,\n  SelectContent,\n  SelectGroup,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { useSession } from \"@/lib/auth/client\";\nimport { Trans, useTranslation } from \"@/lib/i18n/client\";\nimport { useReaderSettings } from \"@/lib/readerSettings\";\nimport {\n  AlertTriangle,\n  Archive,\n  BookOpen,\n  Camera,\n  ExpandIcon,\n  FileText,\n  Info,\n  Video,\n} from \"lucide-react\";\nimport { useQueryState } from \"nuqs\";\nimport { ErrorBoundary } from \"react-error-boundary\";\n\nimport {\n  BookmarkTypes,\n  ZBookmark,\n  ZBookmarkedLink,\n} from \"@karakeep/shared/types/bookmarks\";\nimport { READER_FONT_FAMILIES } from \"@karakeep/shared/types/readers\";\n\nimport { contentRendererRegistry } from \"./content-renderers\";\nimport ReaderSettingsPopover from \"./ReaderSettingsPopover\";\nimport ReaderView from \"./ReaderView\";\n\nfunction CustomRendererErrorFallback({ error }: { error: Error }) {\n  return (\n    <div className=\"flex h-full w-full items-center justify-center p-4\">\n      <Alert variant=\"destructive\" className=\"max-w-md\">\n        <AlertTriangle className=\"h-4 w-4\" />\n        <AlertTitle>Renderer Error</AlertTitle>\n        <AlertDescription>\n          Failed to load custom content renderer.{\" \"}\n          <details className=\"mt-2\">\n            <summary className=\"cursor-pointer text-xs\">\n              Technical details\n            </summary>\n            <code className=\"mt-1 block text-xs\">{error.message}</code>\n          </details>\n        </AlertDescription>\n      </Alert>\n    </div>\n  );\n}\n\nfunction FullPageArchiveSection({ link }: { link: ZBookmarkedLink }) {\n  const archiveAssetId =\n    link.fullPageArchiveAssetId ?? link.precrawledArchiveAssetId;\n  return (\n    <iframe\n      sandbox=\"\"\n      title={link.url}\n      src={`/api/assets/${archiveAssetId}`}\n      className=\"relative h-full min-w-full\"\n    />\n  );\n}\n\nfunction ScreenshotSection({ link }: { link: ZBookmarkedLink }) {\n  return (\n    <div className=\"relative h-full min-w-full\">\n      <Image\n        alt=\"screenshot\"\n        src={`/api/assets/${link.screenshotAssetId}`}\n        width={0}\n        height={0}\n        sizes=\"100vw\"\n        style={{ width: \"100%\", height: \"auto\" }}\n      />\n    </div>\n  );\n}\n\nfunction VideoSection({ link }: { link: ZBookmarkedLink }) {\n  return (\n    <div className=\"relative h-full w-full overflow-hidden\">\n      <div className=\"absolute inset-0 h-full w-full\">\n        {/* eslint-disable-next-line jsx-a11y/media-has-caption -- captions not (yet) available */}\n        <video className=\"m-auto max-h-full max-w-full\" controls>\n          <source src={`/api/assets/${link.videoAssetId}`} />\n          Not supported by your browser\n        </video>\n      </div>\n    </div>\n  );\n}\n\nfunction PDFSection({ link }: { link: ZBookmarkedLink }) {\n  return (\n    <iframe\n      title=\"PDF Viewer\"\n      src={`/api/assets/${link.pdfAssetId}`}\n      className=\"relative h-full min-w-full\"\n    />\n  );\n}\n\nexport default function LinkContentSection({\n  bookmark,\n}: {\n  bookmark: ZBookmark;\n}) {\n  const { t } = useTranslation();\n  const { settings } = useReaderSettings();\n  const availableRenderers = contentRendererRegistry.getRenderers(bookmark);\n  const defaultSection =\n    availableRenderers.length > 0 ? availableRenderers[0].id : \"cached\";\n  const [section, setSection] = useQueryState(\"section\", {\n    defaultValue: defaultSection,\n  });\n  const { data: session } = useSession();\n  const isOwner = session?.user?.id === bookmark.userId;\n\n  if (bookmark.content.type != BookmarkTypes.LINK) {\n    throw new Error(\"Invalid content type\");\n  }\n\n  let content;\n\n  // Check if current section is a custom renderer\n  const customRenderer = availableRenderers.find((r) => r.id === section);\n  if (customRenderer) {\n    const RendererComponent = customRenderer.component;\n    content = (\n      <ErrorBoundary FallbackComponent={CustomRendererErrorFallback}>\n        <RendererComponent bookmark={bookmark} />\n      </ErrorBoundary>\n    );\n  } else if (section === \"cached\") {\n    content = (\n      <div className=\"h-full w-full overflow-y-auto overflow-x-hidden px-3 sm:px-6\">\n        <ReaderView\n          className=\"mx-auto max-w-3xl\"\n          style={{\n            fontFamily: READER_FONT_FAMILIES[settings.fontFamily],\n            fontSize: `${settings.fontSize}px`,\n            lineHeight: settings.lineHeight,\n          }}\n          bookmarkId={bookmark.id}\n          readOnly={!isOwner}\n        />\n      </div>\n    );\n  } else if (section === \"archive\") {\n    content = <FullPageArchiveSection link={bookmark.content} />;\n  } else if (section === \"video\") {\n    content = <VideoSection link={bookmark.content} />;\n  } else if (section === \"pdf\") {\n    content = <PDFSection link={bookmark.content} />;\n  } else {\n    content = <ScreenshotSection link={bookmark.content} />;\n  }\n\n  return (\n    <div className=\"flex h-full w-full min-w-0 flex-col items-center overflow-hidden\">\n      <div className=\"flex w-full items-center justify-center gap-2 border-b px-3 py-1.5\">\n        <Select onValueChange={setSection} value={section}>\n          <SelectTrigger className=\"w-fit\">\n            <span className=\"mr-2\">\n              <SelectValue />\n            </span>\n          </SelectTrigger>\n          <SelectContent>\n            <SelectGroup>\n              {/* Custom renderers first */}\n              {availableRenderers.map((renderer) => {\n                const IconComponent = renderer.icon;\n                return (\n                  <SelectItem key={renderer.id} value={renderer.id}>\n                    <div className=\"flex items-center\">\n                      <IconComponent className=\"mr-2 h-4 w-4\" />\n                      {renderer.name}\n                    </div>\n                  </SelectItem>\n                );\n              })}\n\n              {/* Default renderers */}\n              <SelectItem value=\"cached\">\n                <div className=\"flex items-center\">\n                  <BookOpen className=\"mr-2 h-4 w-4\" />\n                  {t(\"preview.reader_view\")}\n                </div>\n              </SelectItem>\n              <SelectItem\n                value=\"screenshot\"\n                disabled={!bookmark.content.screenshotAssetId}\n              >\n                <div className=\"flex items-center\">\n                  <Camera className=\"mr-2 h-4 w-4\" />\n                  {t(\"common.screenshot\")}\n                </div>\n              </SelectItem>\n              <SelectItem value=\"pdf\" disabled={!bookmark.content.pdfAssetId}>\n                <div className=\"flex items-center\">\n                  <FileText className=\"mr-2 h-4 w-4\" />\n                  {t(\"common.pdf\")}\n                </div>\n              </SelectItem>\n              <SelectItem\n                value=\"archive\"\n                disabled={\n                  !bookmark.content.fullPageArchiveAssetId &&\n                  !bookmark.content.precrawledArchiveAssetId\n                }\n              >\n                <div className=\"flex items-center\">\n                  <Archive className=\"mr-2 h-4 w-4\" />\n                  {t(\"common.archive\")}\n                </div>\n              </SelectItem>\n              <SelectItem\n                value=\"video\"\n                disabled={!bookmark.content.videoAssetId}\n              >\n                <div className=\"flex items-center\">\n                  <Video className=\"mr-2 h-4 w-4\" />\n                  {t(\"common.video\")}\n                </div>\n              </SelectItem>\n            </SelectGroup>\n          </SelectContent>\n        </Select>\n        {section === \"cached\" && (\n          <>\n            <ReaderSettingsPopover />\n            <Tooltip>\n              <TooltipTrigger>\n                <Link\n                  href={`/reader/${bookmark.id}`}\n                  className={buttonVariants({ variant: \"outline\" })}\n                >\n                  <ExpandIcon className=\"h-4 w-4\" />\n                </Link>\n              </TooltipTrigger>\n              <TooltipContent side=\"bottom\">FullScreen</TooltipContent>\n            </Tooltip>\n          </>\n        )}\n        {section === \"archive\" && (\n          <Tooltip>\n            <TooltipTrigger asChild>\n              <div className=\"flex h-10 items-center gap-1 rounded-md border border-blue-500/50 bg-blue-50 px-3 text-blue-700 dark:bg-blue-950 dark:text-blue-300\">\n                <Info className=\"h-4 w-4\" />\n              </div>\n            </TooltipTrigger>\n            <TooltipContent side=\"bottom\" className=\"max-w-sm\">\n              <p className=\"text-sm\">\n                <Trans\n                  i18nKey=\"preview.archive_info\"\n                  components={{\n                    1: (\n                      <Link\n                        prefetch={false}\n                        href={`/api/assets/${bookmark.content.fullPageArchiveAssetId ?? bookmark.content.precrawledArchiveAssetId}`}\n                        download\n                        className=\"font-medium underline\"\n                      >\n                        link\n                      </Link>\n                    ),\n                  }}\n                />\n              </p>\n            </TooltipContent>\n          </Tooltip>\n        )}\n      </div>\n      <div className=\"min-h-0 w-full min-w-0 flex-1\">{content}</div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/preview/NoteEditor.tsx",
    "content": "import { toast } from \"@/components/ui/sonner\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { useClientConfig } from \"@/lib/clientConfig\";\n\nimport type { ZBookmark } from \"@karakeep/shared/types/bookmarks\";\nimport { useUpdateBookmark } from \"@karakeep/shared-react/hooks/bookmarks\";\n\nexport function NoteEditor({\n  bookmark,\n  disabled,\n}: {\n  bookmark: ZBookmark;\n  disabled?: boolean;\n}) {\n  const demoMode = !!useClientConfig().demoMode;\n\n  const updateBookmarkMutator = useUpdateBookmark({\n    onSuccess: () => {\n      toast({\n        description: \"The bookmark has been updated!\",\n      });\n    },\n    onError: () => {\n      toast({\n        description: \"Something went wrong while saving the note\",\n        variant: \"destructive\",\n      });\n    },\n  });\n\n  return (\n    <Textarea\n      className=\"min-h-[5rem] w-full resize-y overflow-auto rounded-md bg-background p-2.5 text-sm text-foreground placeholder:text-muted-foreground\"\n      defaultValue={bookmark.note ?? \"\"}\n      disabled={demoMode || disabled}\n      placeholder=\"Write some notes ...\"\n      onBlur={(e) => {\n        if (e.currentTarget.value == bookmark.note) {\n          return;\n        }\n        updateBookmarkMutator.mutate({\n          bookmarkId: bookmark.id,\n          note: e.currentTarget.value,\n        });\n      }}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/preview/ReaderSettingsPopover.tsx",
    "content": "\"use client\";\n\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { Slider } from \"@/components/ui/slider\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { useReaderSettings } from \"@/lib/readerSettings\";\nimport {\n  Globe,\n  Laptop,\n  Minus,\n  Plus,\n  RotateCcw,\n  Settings,\n  Type,\n  X,\n} from \"lucide-react\";\n\nimport {\n  formatFontSize,\n  formatLineHeight,\n  READER_DEFAULTS,\n  READER_SETTING_CONSTRAINTS,\n} from \"@karakeep/shared/types/readers\";\n\ninterface ReaderSettingsPopoverProps {\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  variant?: \"outline\" | \"ghost\";\n}\n\nexport default function ReaderSettingsPopover({\n  open,\n  onOpenChange,\n  variant = \"outline\",\n}: ReaderSettingsPopoverProps) {\n  const { t } = useTranslation();\n  const {\n    settings,\n    serverSettings,\n    localOverrides,\n    sessionOverrides,\n    hasSessionChanges,\n    hasLocalOverrides,\n    isSaving,\n    updateSession,\n    clearSession,\n    saveToDevice,\n    clearLocalOverride,\n    saveToServer,\n  } = useReaderSettings();\n\n  // Helper to get the effective server value (server setting or default)\n  const getServerValue = <K extends keyof typeof serverSettings>(key: K) => {\n    return serverSettings[key] ?? READER_DEFAULTS[key];\n  };\n\n  // Helper to check if a setting has a local override\n  const hasLocalOverride = (key: keyof typeof localOverrides) => {\n    return localOverrides[key] !== undefined;\n  };\n\n  // Build tooltip message for the settings button\n  const getSettingsTooltip = () => {\n    if (hasSessionChanges && hasLocalOverrides) {\n      return t(\"settings.info.reader_settings.tooltip_preview_and_local\");\n    }\n    if (hasSessionChanges) {\n      return t(\"settings.info.reader_settings.tooltip_preview\");\n    }\n    if (hasLocalOverrides) {\n      return t(\"settings.info.reader_settings.tooltip_local\");\n    }\n    return t(\"settings.info.reader_settings.tooltip_default\");\n  };\n\n  return (\n    <Popover open={open} onOpenChange={onOpenChange}>\n      <Tooltip>\n        <TooltipTrigger asChild>\n          <PopoverTrigger asChild>\n            <Button variant={variant} size=\"icon\" className=\"relative\">\n              <Settings className=\"h-4 w-4\" />\n              {(hasSessionChanges || hasLocalOverrides) && (\n                <span className=\"absolute -right-0.5 -top-0.5 h-2 w-2 rounded-full bg-primary\" />\n              )}\n            </Button>\n          </PopoverTrigger>\n        </TooltipTrigger>\n        <TooltipContent side=\"bottom\">\n          <p>{getSettingsTooltip()}</p>\n        </TooltipContent>\n      </Tooltip>\n      <PopoverContent\n        side=\"bottom\"\n        align=\"center\"\n        collisionPadding={32}\n        className=\"flex w-80 flex-col overflow-hidden p-0\"\n        style={{\n          maxHeight: \"var(--radix-popover-content-available-height)\",\n        }}\n      >\n        <div className=\"min-h-0 flex-1 space-y-4 overflow-y-auto p-4\">\n          <div className=\"flex items-center justify-between pb-2\">\n            <div className=\"flex items-center gap-2\">\n              <Type className=\"h-4 w-4\" />\n              <h3 className=\"font-semibold\">\n                {t(\"settings.info.reader_settings.title\")}\n              </h3>\n            </div>\n            {hasSessionChanges && (\n              <span className=\"rounded-full bg-primary/10 px-2 py-0.5 text-xs text-primary\">\n                {t(\"settings.info.reader_settings.preview\")}\n              </span>\n            )}\n          </div>\n\n          <div className=\"space-y-4\">\n            <div className=\"space-y-2\">\n              <div className=\"flex items-center justify-between\">\n                <label className=\"text-sm font-medium\">\n                  {t(\"settings.info.reader_settings.font_family\")}\n                </label>\n                <div className=\"flex items-center gap-1\">\n                  {sessionOverrides.fontFamily !== undefined && (\n                    <span className=\"text-xs text-muted-foreground\">\n                      {t(\"settings.info.reader_settings.preview_inline\")}\n                    </span>\n                  )}\n                  {hasLocalOverride(\"fontFamily\") &&\n                    sessionOverrides.fontFamily === undefined && (\n                      <Tooltip>\n                        <TooltipTrigger asChild>\n                          <Button\n                            variant=\"ghost\"\n                            size=\"icon\"\n                            className=\"h-5 w-5 text-muted-foreground hover:text-foreground\"\n                            onClick={() => clearLocalOverride(\"fontFamily\")}\n                          >\n                            <X className=\"h-3 w-3\" />\n                          </Button>\n                        </TooltipTrigger>\n                        <TooltipContent>\n                          <p>\n                            {t(\n                              \"settings.info.reader_settings.clear_override_hint\",\n                              {\n                                value: t(\n                                  `settings.info.reader_settings.${getServerValue(\"fontFamily\")}` as const,\n                                ),\n                              },\n                            )}\n                          </p>\n                        </TooltipContent>\n                      </Tooltip>\n                    )}\n                </div>\n              </div>\n              <Select\n                value={settings.fontFamily}\n                onValueChange={(value) =>\n                  updateSession({\n                    fontFamily: value as \"serif\" | \"sans\" | \"mono\",\n                  })\n                }\n              >\n                <SelectTrigger\n                  className={\n                    hasLocalOverride(\"fontFamily\") &&\n                    sessionOverrides.fontFamily === undefined\n                      ? \"border-primary/50\"\n                      : \"\"\n                  }\n                >\n                  <SelectValue />\n                </SelectTrigger>\n                <SelectContent>\n                  <SelectItem value=\"serif\">\n                    {t(\"settings.info.reader_settings.serif\")}\n                  </SelectItem>\n                  <SelectItem value=\"sans\">\n                    {t(\"settings.info.reader_settings.sans\")}\n                  </SelectItem>\n                  <SelectItem value=\"mono\">\n                    {t(\"settings.info.reader_settings.mono\")}\n                  </SelectItem>\n                </SelectContent>\n              </Select>\n            </div>\n\n            <div className=\"space-y-2\">\n              <div className=\"flex items-center justify-between\">\n                <label className=\"text-sm font-medium\">\n                  {t(\"settings.info.reader_settings.font_size\")}\n                </label>\n                <div className=\"flex items-center gap-1\">\n                  <span className=\"text-sm text-muted-foreground\">\n                    {formatFontSize(settings.fontSize)}\n                    {sessionOverrides.fontSize !== undefined && (\n                      <span className=\"ml-1 text-xs\">\n                        {t(\"settings.info.reader_settings.preview_inline\")}\n                      </span>\n                    )}\n                  </span>\n                  {hasLocalOverride(\"fontSize\") &&\n                    sessionOverrides.fontSize === undefined && (\n                      <Tooltip>\n                        <TooltipTrigger asChild>\n                          <Button\n                            variant=\"ghost\"\n                            size=\"icon\"\n                            className=\"h-5 w-5 text-muted-foreground hover:text-foreground\"\n                            onClick={() => clearLocalOverride(\"fontSize\")}\n                          >\n                            <X className=\"h-3 w-3\" />\n                          </Button>\n                        </TooltipTrigger>\n                        <TooltipContent>\n                          <p>\n                            {t(\n                              \"settings.info.reader_settings.clear_override_hint\",\n                              {\n                                value: formatFontSize(\n                                  getServerValue(\"fontSize\"),\n                                ),\n                              },\n                            )}\n                          </p>\n                        </TooltipContent>\n                      </Tooltip>\n                    )}\n                </div>\n              </div>\n              <div className=\"flex items-center gap-2\">\n                <Button\n                  variant=\"outline\"\n                  size=\"icon\"\n                  className=\"h-7 w-7 bg-transparent\"\n                  onClick={() =>\n                    updateSession({\n                      fontSize: Math.max(\n                        READER_SETTING_CONSTRAINTS.fontSize.min,\n                        settings.fontSize -\n                          READER_SETTING_CONSTRAINTS.fontSize.step,\n                      ),\n                    })\n                  }\n                >\n                  <Minus className=\"h-3 w-3\" />\n                </Button>\n                <Slider\n                  value={[settings.fontSize]}\n                  onValueChange={([value]) =>\n                    updateSession({ fontSize: value })\n                  }\n                  max={READER_SETTING_CONSTRAINTS.fontSize.max}\n                  min={READER_SETTING_CONSTRAINTS.fontSize.min}\n                  step={READER_SETTING_CONSTRAINTS.fontSize.step}\n                  className={`flex-1 ${\n                    hasLocalOverride(\"fontSize\") &&\n                    sessionOverrides.fontSize === undefined\n                      ? \"[&_[role=slider]]:border-primary/50\"\n                      : \"\"\n                  }`}\n                />\n                <Button\n                  variant=\"outline\"\n                  size=\"icon\"\n                  className=\"h-7 w-7 bg-transparent\"\n                  onClick={() =>\n                    updateSession({\n                      fontSize: Math.min(\n                        READER_SETTING_CONSTRAINTS.fontSize.max,\n                        settings.fontSize +\n                          READER_SETTING_CONSTRAINTS.fontSize.step,\n                      ),\n                    })\n                  }\n                >\n                  <Plus className=\"h-3 w-3\" />\n                </Button>\n              </div>\n            </div>\n\n            <div className=\"space-y-2\">\n              <div className=\"flex items-center justify-between\">\n                <label className=\"text-sm font-medium\">\n                  {t(\"settings.info.reader_settings.line_height\")}\n                </label>\n                <div className=\"flex items-center gap-1\">\n                  <span className=\"text-sm text-muted-foreground\">\n                    {formatLineHeight(settings.lineHeight)}\n                    {sessionOverrides.lineHeight !== undefined && (\n                      <span className=\"ml-1 text-xs\">\n                        {t(\"settings.info.reader_settings.preview_inline\")}\n                      </span>\n                    )}\n                  </span>\n                  {hasLocalOverride(\"lineHeight\") &&\n                    sessionOverrides.lineHeight === undefined && (\n                      <Tooltip>\n                        <TooltipTrigger asChild>\n                          <Button\n                            variant=\"ghost\"\n                            size=\"icon\"\n                            className=\"h-5 w-5 text-muted-foreground hover:text-foreground\"\n                            onClick={() => clearLocalOverride(\"lineHeight\")}\n                          >\n                            <X className=\"h-3 w-3\" />\n                          </Button>\n                        </TooltipTrigger>\n                        <TooltipContent>\n                          <p>\n                            {t(\n                              \"settings.info.reader_settings.clear_override_hint\",\n                              {\n                                value: formatLineHeight(\n                                  getServerValue(\"lineHeight\"),\n                                ),\n                              },\n                            )}\n                          </p>\n                        </TooltipContent>\n                      </Tooltip>\n                    )}\n                </div>\n              </div>\n              <div className=\"flex items-center gap-2\">\n                <Button\n                  variant=\"outline\"\n                  size=\"icon\"\n                  className=\"h-7 w-7 bg-transparent\"\n                  onClick={() =>\n                    updateSession({\n                      lineHeight: Math.max(\n                        READER_SETTING_CONSTRAINTS.lineHeight.min,\n                        Math.round(\n                          (settings.lineHeight -\n                            READER_SETTING_CONSTRAINTS.lineHeight.step) *\n                            10,\n                        ) / 10,\n                      ),\n                    })\n                  }\n                >\n                  <Minus className=\"h-3 w-3\" />\n                </Button>\n                <Slider\n                  value={[settings.lineHeight]}\n                  onValueChange={([value]) =>\n                    updateSession({ lineHeight: value })\n                  }\n                  max={READER_SETTING_CONSTRAINTS.lineHeight.max}\n                  min={READER_SETTING_CONSTRAINTS.lineHeight.min}\n                  step={READER_SETTING_CONSTRAINTS.lineHeight.step}\n                  className={`flex-1 ${\n                    hasLocalOverride(\"lineHeight\") &&\n                    sessionOverrides.lineHeight === undefined\n                      ? \"[&_[role=slider]]:border-primary/50\"\n                      : \"\"\n                  }`}\n                />\n                <Button\n                  variant=\"outline\"\n                  size=\"icon\"\n                  className=\"h-7 w-7 bg-transparent\"\n                  onClick={() =>\n                    updateSession({\n                      lineHeight: Math.min(\n                        READER_SETTING_CONSTRAINTS.lineHeight.max,\n                        Math.round(\n                          (settings.lineHeight +\n                            READER_SETTING_CONSTRAINTS.lineHeight.step) *\n                            10,\n                        ) / 10,\n                      ),\n                    })\n                  }\n                >\n                  <Plus className=\"h-3 w-3\" />\n                </Button>\n              </div>\n            </div>\n\n            {hasSessionChanges && (\n              <>\n                <Separator />\n\n                <div className=\"space-y-2\">\n                  <Button\n                    variant=\"outline\"\n                    size=\"sm\"\n                    className=\"w-full\"\n                    onClick={() => clearSession()}\n                  >\n                    <RotateCcw className=\"mr-2 h-4 w-4\" />\n                    {t(\"settings.info.reader_settings.reset_preview\")}\n                  </Button>\n\n                  <div className=\"flex gap-2\">\n                    <Button\n                      variant=\"outline\"\n                      size=\"sm\"\n                      className=\"flex-1\"\n                      disabled={isSaving}\n                      onClick={() => saveToDevice()}\n                    >\n                      <Laptop className=\"mr-2 h-4 w-4\" />\n                      {t(\"settings.info.reader_settings.save_to_device\")}\n                    </Button>\n                    <Button\n                      variant=\"default\"\n                      size=\"sm\"\n                      className=\"flex-1\"\n                      disabled={isSaving}\n                      onClick={() => saveToServer()}\n                    >\n                      <Globe className=\"mr-2 h-4 w-4\" />\n                      {t(\"settings.info.reader_settings.save_to_all_devices\")}\n                    </Button>\n                  </div>\n\n                  <p className=\"text-center text-xs text-muted-foreground\">\n                    {t(\"settings.info.reader_settings.save_hint\")}\n                  </p>\n                </div>\n              </>\n            )}\n\n            {!hasSessionChanges && (\n              <p className=\"text-center text-xs text-muted-foreground\">\n                {t(\"settings.info.reader_settings.adjust_hint\")}\n              </p>\n            )}\n          </div>\n        </div>\n      </PopoverContent>\n    </Popover>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/preview/ReaderView.tsx",
    "content": "import { FullPageSpinner } from \"@/components/ui/full-page-spinner\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { FileX } from \"lucide-react\";\n\nimport BookmarkHTMLHighlighter from \"@karakeep/shared-react/components/BookmarkHtmlHighlighter\";\nimport ScrollProgressTracker from \"@karakeep/shared-react/components/ScrollProgressTracker\";\nimport {\n  useCreateHighlight,\n  useDeleteHighlight,\n  useUpdateHighlight,\n} from \"@karakeep/shared-react/hooks/highlights\";\nimport { useReadingProgress } from \"@karakeep/shared-react/hooks/reading-progress\";\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\nimport { BookmarkTypes } from \"@karakeep/shared/types/bookmarks\";\n\nimport ReadingProgressBanner from \"./ReadingProgressBanner\";\n\nexport default function ReaderView({\n  bookmarkId,\n  className,\n  style,\n  readOnly,\n  progressBarStyle,\n}: {\n  bookmarkId: string;\n  className?: string;\n  style?: React.CSSProperties;\n  readOnly: boolean;\n  progressBarStyle?: React.CSSProperties;\n}) {\n  const { t } = useTranslation();\n  const api = useTRPC();\n  const { data: highlights } = useQuery(\n    api.highlights.getForBookmark.queryOptions({\n      bookmarkId,\n    }),\n  );\n  const { data: cachedContent, isPending: isCachedContentLoading } = useQuery(\n    api.bookmarks.getBookmark.queryOptions(\n      {\n        bookmarkId,\n        includeContent: true,\n      },\n      {\n        select: (data) =>\n          data.content.type == BookmarkTypes.LINK\n            ? data.content.htmlContent\n            : null,\n      },\n    ),\n  );\n\n  const {\n    showBanner,\n    bannerPercent,\n    onContinue,\n    onDismiss,\n    restorePosition,\n    readingProgressOffset,\n    readingProgressAnchor,\n    onSavePosition,\n    onScrollPositionChange,\n  } = useReadingProgress({\n    bookmarkId,\n  });\n\n  const { mutate: createHighlight } = useCreateHighlight({\n    onSuccess: () => {\n      toast({\n        description: \"Highlight has been created!\",\n      });\n    },\n    onError: () => {\n      toast({\n        variant: \"destructive\",\n        description: \"Something went wrong\",\n      });\n    },\n  });\n\n  const { mutate: updateHighlight } = useUpdateHighlight({\n    onSuccess: () => {\n      toast({\n        description: \"Highlight has been updated!\",\n      });\n    },\n    onError: () => {\n      toast({\n        variant: \"destructive\",\n        description: \"Something went wrong\",\n      });\n    },\n  });\n\n  const { mutate: deleteHighlight } = useDeleteHighlight({\n    onSuccess: () => {\n      toast({\n        description: \"Highlight has been deleted!\",\n      });\n    },\n    onError: () => {\n      toast({\n        variant: \"destructive\",\n        description: \"Something went wrong\",\n      });\n    },\n  });\n\n  let content;\n  if (isCachedContentLoading) {\n    content = <FullPageSpinner />;\n  } else if (!cachedContent) {\n    content = (\n      <div className=\"flex h-full w-full items-center justify-center p-4\">\n        <div className=\"max-w-sm space-y-4 text-center\">\n          <div className=\"flex justify-center\">\n            <div className=\"flex h-16 w-16 items-center justify-center rounded-full bg-muted\">\n              <FileX className=\"h-8 w-8 text-muted-foreground\" />\n            </div>\n          </div>\n          <div className=\"space-y-2\">\n            <h3 className=\"text-lg font-medium text-foreground\">\n              {t(\"preview.fetch_error_title\")}\n            </h3>\n            <p className=\"text-sm leading-relaxed text-muted-foreground\">\n              {t(\"preview.fetch_error_description\")}\n            </p>\n          </div>\n        </div>\n      </div>\n    );\n  } else {\n    content = (\n      <ScrollProgressTracker\n        onSavePosition={onSavePosition}\n        onScrollPositionChange={onScrollPositionChange}\n        restorePosition={restorePosition}\n        readingProgressOffset={readingProgressOffset}\n        readingProgressAnchor={readingProgressAnchor}\n        showProgressBar\n        progressBarStyle={progressBarStyle}\n      >\n        {showBanner && (\n          <ReadingProgressBanner\n            percent={bannerPercent}\n            onContinue={onContinue}\n            onDismiss={onDismiss}\n          />\n        )}\n        <BookmarkHTMLHighlighter\n          className={className}\n          style={style}\n          htmlContent={cachedContent || \"\"}\n          highlights={highlights?.highlights ?? []}\n          readOnly={readOnly}\n          onDeleteHighlight={(h) =>\n            deleteHighlight({\n              highlightId: h.id,\n            })\n          }\n          onUpdateHighlight={(h) =>\n            updateHighlight({\n              highlightId: h.id,\n              color: h.color,\n              note: h.note,\n            })\n          }\n          onHighlight={(h) =>\n            createHighlight({\n              startOffset: h.startOffset,\n              endOffset: h.endOffset,\n              color: h.color,\n              bookmarkId,\n              text: h.text,\n              note: h.note ?? null,\n            })\n          }\n        />\n      </ScrollProgressTracker>\n    );\n  }\n  return content;\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/preview/ReadingProgressBanner.tsx",
    "content": "\"use client\";\n\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { BookOpen, X } from \"lucide-react\";\n\nexport default function ReadingProgressBanner({\n  percent,\n  onContinue,\n  onDismiss,\n}: {\n  percent?: number | null;\n  onContinue: () => void;\n  onDismiss: () => void;\n}) {\n  const { t } = useTranslation();\n\n  const message =\n    percent && percent > 0\n      ? t(\"preview.continue_reading_percent\", { percent })\n      : t(\"preview.continue_reading\");\n\n  return (\n    <div className=\"sticky top-0 z-10 flex justify-center px-4 py-3\">\n      <div className=\"flex items-center gap-3 rounded-full border border-border/60 bg-background/80 px-4 py-2 text-sm shadow-sm backdrop-blur-md\">\n        <BookOpen className=\"h-4 w-4 shrink-0 text-muted-foreground\" />\n        <span className=\"truncate text-muted-foreground\">{message}</span>\n        <button\n          onClick={onContinue}\n          className=\"shrink-0 rounded-full bg-foreground px-3 py-1 text-xs font-medium text-background transition-opacity hover:opacity-80\"\n        >\n          {t(\"preview.continue_button\")}\n        </button>\n        <button\n          onClick={onDismiss}\n          className=\"shrink-0 rounded-full p-1 text-muted-foreground transition-colors hover:text-foreground\"\n          aria-label=\"Dismiss\"\n        >\n          <X className=\"h-3.5 w-3.5\" />\n        </button>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/preview/TextContentSection.tsx",
    "content": "import Image from \"next/image\";\nimport { BookmarkMarkdownComponent } from \"@/components/dashboard/bookmarks/BookmarkMarkdownComponent\";\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\n\nimport type { ZBookmarkTypeText } from \"@karakeep/shared/types/bookmarks\";\nimport { BookmarkTypes, ZBookmark } from \"@karakeep/shared/types/bookmarks\";\nimport { getAssetUrl } from \"@karakeep/shared/utils/assetUtils\";\n\nexport function TextContentSection({ bookmark }: { bookmark: ZBookmark }) {\n  if (bookmark.content.type != BookmarkTypes.TEXT) {\n    throw new Error(\"Invalid content type\");\n  }\n  const banner = bookmark.assets.find(\n    (asset) => asset.assetType == \"bannerImage\",\n  );\n\n  return (\n    <ScrollArea className=\"h-full\">\n      {banner && (\n        <div className=\"relative h-52 min-w-full\">\n          <Image\n            alt=\"banner\"\n            src={getAssetUrl(banner.id)}\n            width={0}\n            height={0}\n            layout=\"fill\"\n            objectFit=\"cover\"\n          />\n        </div>\n      )}\n      <div className=\"mx-auto max-w-3xl px-4 py-4\">\n        <BookmarkMarkdownComponent>\n          {bookmark as ZBookmarkTypeText}\n        </BookmarkMarkdownComponent>\n      </div>\n    </ScrollArea>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/preview/content-renderers/AmazonRenderer.tsx",
    "content": "import { ShoppingCart } from \"lucide-react\";\n\nimport { BookmarkTypes, ZBookmark } from \"@karakeep/shared/types/bookmarks\";\n\nimport { ContentRenderer } from \"./types\";\n\nfunction extractAmazonProductInfo(\n  url: string,\n): { asin: string; domain: string } | null {\n  const patterns = [\n    // Standard product URLs\n    /amazon\\.([a-z.]+)\\/.*\\/dp\\/([A-Z0-9]{10})/,\n    /amazon\\.([a-z.]+)\\/dp\\/([A-Z0-9]{10})/,\n    // Shortened URLs\n    /amazon\\.([a-z.]+)\\/gp\\/product\\/([A-Z0-9]{10})/,\n    // Mobile URLs\n    /amazon\\.([a-z.]+)\\/.*\\/product\\/([A-Z0-9]{10})/,\n    // International variations\n    /amazon\\.([a-z.]+)\\/.*\\/([A-Z0-9]{10})/,\n  ];\n\n  for (const pattern of patterns) {\n    const match = url.match(pattern);\n    if (match) {\n      return {\n        domain: match[1],\n        asin: match[2],\n      };\n    }\n  }\n  return null;\n}\n\nfunction canRenderAmazon(bookmark: ZBookmark): boolean {\n  if (bookmark.content.type !== BookmarkTypes.LINK) {\n    return false;\n  }\n\n  const url = bookmark.content.url;\n  return extractAmazonProductInfo(url) !== null;\n}\n\nfunction AmazonRendererComponent({ bookmark }: { bookmark: ZBookmark }) {\n  if (bookmark.content.type !== BookmarkTypes.LINK) {\n    return null;\n  }\n\n  const productInfo = extractAmazonProductInfo(bookmark.content.url);\n  if (!productInfo) {\n    return null;\n  }\n\n  const { title, description, imageUrl } = bookmark.content;\n\n  return (\n    <div className=\"relative h-full w-full overflow-auto\">\n      <div className=\"mx-auto flex max-w-2xl flex-col items-center p-6\">\n        {/* Product Image */}\n        {imageUrl && (\n          <div className=\"mb-6 w-full max-w-md\">\n            {/* oxlint-disable-next-line no-img-element */}\n            <img\n              src={imageUrl}\n              alt={title || \"Amazon Product\"}\n              className=\"h-auto max-h-96 w-full rounded-lg object-contain shadow-lg\"\n            />\n          </div>\n        )}\n\n        {/* Product Info Card */}\n        <div className=\"w-full rounded-lg border bg-card p-6 shadow-sm\">\n          {/* Title */}\n          {title && (\n            <h2 className=\"mb-4 line-clamp-3 text-xl font-semibold\">{title}</h2>\n          )}\n\n          {/* Description */}\n          {description && (\n            <p className=\"mb-6 line-clamp-4 text-muted-foreground\">\n              {description}\n            </p>\n          )}\n\n          {/* Product Details */}\n          <div className=\"mb-6 space-y-3\">\n            <div className=\"flex items-center gap-2 text-sm\">\n              <span className=\"font-medium\">ASIN:</span>\n              <span className=\"font-mono text-muted-foreground\">\n                {productInfo.asin}\n              </span>\n            </div>\n            <div className=\"flex items-center gap-2 text-sm\">\n              <span className=\"font-medium\">Domain:</span>\n              <span className=\"text-muted-foreground\">\n                amazon.{productInfo.domain}\n              </span>\n            </div>\n          </div>\n\n          {/* Action Buttons */}\n          <div className=\"flex gap-3\">\n            {/* oxlint-disable-next-line no-html-link-for-pages */}\n            <a\n              href={bookmark.content.url}\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"flex flex-1 items-center justify-center gap-2 rounded-md bg-[#FF9900] px-4 py-2 text-center font-medium text-white transition-colors hover:bg-[#FF9900]/90\"\n            >\n              <ShoppingCart size={16} />\n              View on Amazon\n            </a>\n            {/* oxlint-disable-next-line no-html-link-for-pages */}\n            <a\n              href={`https://www.amazon.${productInfo.domain}/dp/${productInfo.asin}`}\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n              className=\"rounded-md border border-border px-4 py-2 text-sm transition-colors hover:bg-accent\"\n            >\n              Direct Link\n            </a>\n          </div>\n\n          {/* Amazon Disclaimer */}\n          <p className=\"mt-4 text-center text-xs text-muted-foreground\">\n            Product information from Amazon. Prices and availability may vary.\n          </p>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport const amazonRenderer: ContentRenderer = {\n  id: \"amazon\",\n  name: \"Amazon Product\",\n  icon: ShoppingCart,\n  canRender: canRenderAmazon,\n  component: AmazonRendererComponent,\n  priority: 10,\n};\n"
  },
  {
    "path": "apps/web/components/dashboard/preview/content-renderers/README.md",
    "content": "# Content-Aware Renderers\n\nThis directory contains the content-aware rendering system for LinkContentPreview. It allows for special rendering of different types of links based on their URL patterns.\n\n## Architecture\n\nThe system consists of:\n\n1. **Types** (`types.ts`): Defines the `ContentRenderer` interface\n2. **Registry** (`registry.ts`): Manages registration and retrieval of renderers\n3. **Individual Renderers**: Each renderer handles a specific type of content\n\n## Creating a New Renderer\n\nTo add support for a new website or content type:\n\n1. Create a new file (e.g., `MyWebsiteRenderer.tsx`)\n2. Implement the `ContentRenderer` interface:\n\n```typescript\nimport { ContentRenderer } from \"./types\";\nimport { BookmarkTypes, ZBookmark } from \"@karakeep/shared/types/bookmarks\";\nimport { MyIcon } from \"lucide-react\";\n\nfunction canRenderMyWebsite(bookmark: ZBookmark): boolean {\n  if (bookmark.content.type !== BookmarkTypes.LINK) {\n    return false;\n  }\n\n  // Add your URL pattern matching logic here\n  return bookmark.content.url.includes(\"mywebsite.com\");\n}\n\nfunction MyWebsiteRendererComponent({ bookmark }: { bookmark: ZBookmark }) {\n  // Your custom rendering logic here\n  return <div>Custom content for MyWebsite</div>;\n}\n\nexport const myWebsiteRenderer: ContentRenderer = {\n  id: \"mywebsite\",\n  name: \"My Website\",\n  icon: MyIcon,\n  canRender: canRenderMyWebsite,\n  component: MyWebsiteRendererComponent,\n  priority: 10, // Higher priority = appears first in dropdown\n};\n```\n\n3. Register your renderer in `index.ts`:\n\n```typescript\nimport { myWebsiteRenderer } from \"./MyWebsiteRenderer\";\n\ncontentRendererRegistry.register(myWebsiteRenderer);\n```\n"
  },
  {
    "path": "apps/web/components/dashboard/preview/content-renderers/TikTokRenderer.tsx",
    "content": "import { Video } from \"lucide-react\";\n\nimport { BookmarkTypes, ZBookmark } from \"@karakeep/shared/types/bookmarks\";\n\nimport { ContentRenderer } from \"./types\";\n\nfunction extractTikTokVideoId(url: string): string | null {\n  const patterns = [\n    /tiktok\\.com\\/@[^/]+\\/video\\/(\\d+)/,\n    /tiktok\\.com\\/t\\/([A-Za-z0-9]+)/,\n    /vm\\.tiktok\\.com\\/([A-Za-z0-9]+)/,\n    /tiktok\\.com\\/v\\/(\\d+)/,\n  ];\n\n  for (const pattern of patterns) {\n    const match = url.match(pattern);\n    if (match) {\n      return match[1];\n    }\n  }\n  return null;\n}\n\nfunction canRenderTikTok(bookmark: ZBookmark): boolean {\n  if (bookmark.content.type !== BookmarkTypes.LINK) {\n    return false;\n  }\n\n  const url = bookmark.content.url;\n  return extractTikTokVideoId(url) !== null;\n}\n\nfunction TikTokRendererComponent({ bookmark }: { bookmark: ZBookmark }) {\n  if (bookmark.content.type !== BookmarkTypes.LINK) {\n    return null;\n  }\n\n  const videoId = extractTikTokVideoId(bookmark.content.url);\n  if (!videoId) {\n    return null;\n  }\n\n  // TikTok embed URL format\n  const embedUrl = `https://www.tiktok.com/embed/v2/${videoId}`;\n\n  return (\n    <div className=\"relative h-full w-full overflow-hidden\">\n      <div className=\"absolute inset-0 h-full w-full\">\n        <iframe\n          src={embedUrl}\n          title=\"TikTok video\"\n          className=\"h-full w-full border-0\"\n          allow=\"encrypted-media\"\n          sandbox=\"allow-scripts allow-same-origin allow-popups\"\n        />\n      </div>\n    </div>\n  );\n}\n\nexport const tikTokRenderer: ContentRenderer = {\n  id: \"tiktok\",\n  name: \"TikTok\",\n  icon: Video,\n  canRender: canRenderTikTok,\n  component: TikTokRendererComponent,\n  priority: 10,\n};\n"
  },
  {
    "path": "apps/web/components/dashboard/preview/content-renderers/XRenderer.tsx",
    "content": "import { MessageSquare } from \"lucide-react\";\nimport { Tweet } from \"react-tweet\";\n\nimport { BookmarkTypes, ZBookmark } from \"@karakeep/shared/types/bookmarks\";\n\nimport { ContentRenderer } from \"./types\";\n\nfunction extractTweetId(url: string): string | null {\n  const patterns = [\n    /(?:twitter\\.com|x\\.com)\\/\\w+\\/status\\/(\\d+)/,\n    /(?:twitter\\.com|x\\.com)\\/i\\/web\\/status\\/(\\d+)/,\n  ];\n\n  for (const pattern of patterns) {\n    const match = url.match(pattern);\n    if (match) {\n      return match[1];\n    }\n  }\n  return null;\n}\n\nfunction canRenderX(bookmark: ZBookmark): boolean {\n  if (bookmark.content.type !== BookmarkTypes.LINK) {\n    return false;\n  }\n\n  const url = bookmark.content.url;\n  return extractTweetId(url) !== null;\n}\n\nfunction XRendererComponent({ bookmark }: { bookmark: ZBookmark }) {\n  if (bookmark.content.type !== BookmarkTypes.LINK) {\n    return null;\n  }\n\n  const tweetId = extractTweetId(bookmark.content.url);\n  if (!tweetId) {\n    return null;\n  }\n\n  return (\n    <div className=\"relative h-full w-full overflow-auto\">\n      <div className=\"flex justify-center p-4\">\n        <Tweet id={tweetId} />\n      </div>\n    </div>\n  );\n}\n\nexport const xRenderer: ContentRenderer = {\n  id: \"x\",\n  name: \"X (Twitter)\",\n  icon: MessageSquare,\n  canRender: canRenderX,\n  component: XRendererComponent,\n  priority: 10,\n};\n"
  },
  {
    "path": "apps/web/components/dashboard/preview/content-renderers/YouTubeRenderer.tsx",
    "content": "import { Play } from \"lucide-react\";\n\nimport { BookmarkTypes, ZBookmark } from \"@karakeep/shared/types/bookmarks\";\n\nimport { ContentRenderer } from \"./types\";\n\nfunction extractYouTubeVideoId(url: string): string | null {\n  const patterns = [\n    /(?:youtube\\.com\\/watch\\?v=|youtu\\.be\\/|youtube\\.com\\/embed\\/)([^&\\n?#]+)/,\n    /youtube\\.com\\/v\\/([^&\\n?#]+)/,\n    /youtube\\.com\\/shorts\\/([^&\\n?#]+)/,\n  ];\n\n  for (const pattern of patterns) {\n    const match = url.match(pattern);\n    if (match) {\n      return match[1];\n    }\n  }\n  return null;\n}\n\nfunction canRenderYouTube(bookmark: ZBookmark): boolean {\n  if (bookmark.content.type !== BookmarkTypes.LINK) {\n    return false;\n  }\n\n  const url = bookmark.content.url;\n  return extractYouTubeVideoId(url) !== null;\n}\n\nfunction YouTubeRendererComponent({ bookmark }: { bookmark: ZBookmark }) {\n  if (bookmark.content.type !== BookmarkTypes.LINK) {\n    return null;\n  }\n\n  const videoId = extractYouTubeVideoId(bookmark.content.url);\n  if (!videoId) {\n    return null;\n  }\n\n  const embedUrl = `https://www.youtube.com/embed/${videoId}`;\n\n  return (\n    <div className=\"relative h-full w-full overflow-hidden\">\n      <div className=\"absolute inset-0 h-full w-full\">\n        <iframe\n          src={embedUrl}\n          title=\"YouTube video player\"\n          allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share\"\n          allowFullScreen\n          className=\"h-full w-full border-0\"\n        />\n      </div>\n    </div>\n  );\n}\n\nexport const youTubeRenderer: ContentRenderer = {\n  id: \"youtube\",\n  name: \"YouTube\",\n  icon: Play,\n  canRender: canRenderYouTube,\n  component: YouTubeRendererComponent,\n  priority: 10,\n};\n"
  },
  {
    "path": "apps/web/components/dashboard/preview/content-renderers/index.ts",
    "content": "import { amazonRenderer } from \"./AmazonRenderer\";\nimport { contentRendererRegistry } from \"./registry\";\nimport { tikTokRenderer } from \"./TikTokRenderer\";\nimport { xRenderer } from \"./XRenderer\";\nimport { youTubeRenderer } from \"./YouTubeRenderer\";\n\ncontentRendererRegistry.register(youTubeRenderer);\ncontentRendererRegistry.register(xRenderer);\ncontentRendererRegistry.register(amazonRenderer);\ncontentRendererRegistry.register(tikTokRenderer);\n\nexport { contentRendererRegistry };\nexport * from \"./types\";\n"
  },
  {
    "path": "apps/web/components/dashboard/preview/content-renderers/registry.ts",
    "content": "import { ZBookmark } from \"@karakeep/shared/types/bookmarks\";\n\nimport { ContentRenderer, ContentRendererRegistry } from \"./types\";\n\nclass ContentRendererRegistryImpl implements ContentRendererRegistry {\n  private renderers: Map<string, ContentRenderer> = new Map<\n    string,\n    ContentRenderer\n  >();\n\n  register(renderer: ContentRenderer): void {\n    this.renderers.set(renderer.id, renderer);\n  }\n\n  getRenderers(bookmark: ZBookmark): ContentRenderer[] {\n    return [...this.renderers.values()]\n      .filter((renderer) => renderer.canRender(bookmark))\n      .sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));\n  }\n\n  getAllRenderers(): ContentRenderer[] {\n    return [...this.renderers.values()];\n  }\n}\n\nexport const contentRendererRegistry = new ContentRendererRegistryImpl();\n"
  },
  {
    "path": "apps/web/components/dashboard/preview/content-renderers/types.ts",
    "content": "import { ZBookmark } from \"@karakeep/shared/types/bookmarks\";\n\nexport interface ContentRenderer {\n  id: string;\n  name: string;\n  icon: React.ComponentType<{ className?: string }>;\n  canRender: (bookmark: ZBookmark) => boolean;\n  component: React.ComponentType<{ bookmark: ZBookmark }>;\n  priority?: number;\n}\n\nexport interface ContentRendererRegistry {\n  register: (renderer: ContentRenderer) => void;\n  getRenderers: (bookmark: ZBookmark) => ContentRenderer[];\n  getAllRenderers: () => ContentRenderer[];\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/preview/highlights.ts",
    "content": "export { HIGHLIGHT_COLOR_MAP } from \"@karakeep/shared-react/components/highlights\";\n"
  },
  {
    "path": "apps/web/components/dashboard/rules/RuleEngineActionBuilder.tsx",
    "content": "import { Button } from \"@/components/ui/button\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\";\nimport {\n  Archive,\n  Download,\n  List,\n  PlusCircle,\n  Star,\n  Tag,\n  Trash2,\n} from \"lucide-react\";\nimport { useTranslation } from \"react-i18next\";\n\nimport type { RuleEngineAction } from \"@karakeep/shared/types/rules\";\n\nimport { BookmarkListSelector } from \"../lists/BookmarkListSelector\";\nimport { TagAutocomplete } from \"../tags/TagAutocomplete\";\n\ninterface ActionBuilderProps {\n  value: RuleEngineAction[];\n  onChange: (actions: RuleEngineAction[]) => void;\n}\n\nexport function ActionBuilder({ value, onChange }: ActionBuilderProps) {\n  const { t } = useTranslation();\n  const handleAddAction = () => {\n    onChange([...value, { type: \"addTag\", tagId: \"\" }]);\n  };\n\n  const handleRemoveAction = (index: number) => {\n    const newActions = [...value];\n    newActions.splice(index, 1);\n    onChange(newActions);\n  };\n\n  const handleActionTypeChange = (\n    index: number,\n    type: RuleEngineAction[\"type\"],\n  ) => {\n    const newActions = [...value];\n\n    switch (type) {\n      case \"addTag\":\n        newActions[index] = { type: \"addTag\", tagId: \"\" };\n        break;\n      case \"removeTag\":\n        newActions[index] = { type: \"removeTag\", tagId: \"\" };\n        break;\n      case \"addToList\":\n        newActions[index] = { type: \"addToList\", listId: \"\" };\n        break;\n      case \"removeFromList\":\n        newActions[index] = { type: \"removeFromList\", listId: \"\" };\n        break;\n      case \"downloadFullPageArchive\":\n        newActions[index] = { type: \"downloadFullPageArchive\" };\n        break;\n      case \"favouriteBookmark\":\n        newActions[index] = { type: \"favouriteBookmark\" };\n        break;\n      case \"archiveBookmark\":\n        newActions[index] = { type: \"archiveBookmark\" };\n        break;\n      default: {\n        const _exhaustiveCheck: never = type;\n        return null;\n      }\n    }\n\n    onChange(newActions);\n  };\n\n  const handleActionFieldChange = (\n    index: number,\n    selectVal: RuleEngineAction,\n  ) => {\n    const newActions = [...value];\n    newActions[index] = selectVal;\n    onChange(newActions);\n  };\n\n  const renderActionIcon = (type: string) => {\n    switch (type) {\n      case \"addTag\":\n      case \"removeTag\":\n        return <Tag className=\"h-4 w-4\" />;\n      case \"addToList\":\n      case \"removeFromList\":\n        return <List className=\"h-4 w-4\" />;\n      case \"downloadFullPageArchive\":\n        return <Download className=\"h-4 w-4\" />;\n      case \"favouriteBookmark\":\n        return <Star className=\"h-4 w-4\" />;\n      case \"archiveBookmark\":\n        return <Archive className=\"h-4 w-4\" />;\n      default:\n        return null;\n    }\n  };\n\n  return (\n    <div className=\"space-y-3\">\n      {value.length === 0 ? (\n        <div className=\"rounded-md border border-dashed p-4 text-center\">\n          <p className=\"text-sm text-muted-foreground\">No actions added yet</p>\n        </div>\n      ) : (\n        value.map((action, index) => (\n          <Card key={index}>\n            <CardContent className=\"p-3\">\n              <div className=\"flex items-center justify-between\">\n                <div className=\"flex flex-1 items-center\">\n                  {renderActionIcon(action.type)}\n                  <Select\n                    value={action.type}\n                    onValueChange={(value) =>\n                      handleActionTypeChange(\n                        index,\n                        value as RuleEngineAction[\"type\"],\n                      )\n                    }\n                  >\n                    <SelectTrigger className=\"ml-2 h-8 w-auto border-none bg-transparent px-2\">\n                      <SelectValue />\n                    </SelectTrigger>\n                    <SelectContent>\n                      <SelectItem value=\"addTag\">\n                        {t(\"settings.rules.actions_types.add_tag\")}\n                      </SelectItem>\n                      <SelectItem value=\"removeTag\">\n                        {t(\"settings.rules.actions_types.remove_tag\")}\n                      </SelectItem>\n                      <SelectItem value=\"addToList\">\n                        {t(\"settings.rules.actions_types.add_to_list\")}\n                      </SelectItem>\n                      <SelectItem value=\"removeFromList\">\n                        {t(\"settings.rules.actions_types.remove_from_list\")}\n                      </SelectItem>\n                      <SelectItem value=\"downloadFullPageArchive\">\n                        {t(\n                          \"settings.rules.actions_types.download_full_page_archive\",\n                        )}\n                      </SelectItem>\n                      <SelectItem value=\"favouriteBookmark\">\n                        {t(\"settings.rules.actions_types.favourite_bookmark\")}\n                      </SelectItem>\n                      <SelectItem value=\"archiveBookmark\">\n                        {t(\"settings.rules.actions_types.archive_bookmark\")}\n                      </SelectItem>\n                    </SelectContent>\n                  </Select>\n\n                  {(action.type === \"addTag\" ||\n                    action.type === \"removeTag\") && (\n                    <TagAutocomplete\n                      className=\"ml-2 h-8 flex-1\"\n                      tagId={action.tagId}\n                      onChange={(t) =>\n                        handleActionFieldChange(index, {\n                          type: action.type,\n                          tagId: t,\n                        })\n                      }\n                    />\n                  )}\n\n                  {(action.type === \"addToList\" ||\n                    action.type === \"removeFromList\") && (\n                    <BookmarkListSelector\n                      className=\"ml-2 h-8 flex-1\"\n                      value={action.listId}\n                      listTypes={[\"manual\"]}\n                      onChange={(e) =>\n                        handleActionFieldChange(index, {\n                          type: action.type,\n                          listId: e,\n                        })\n                      }\n                    />\n                  )}\n                </div>\n\n                <Button\n                  type=\"button\"\n                  variant=\"ghost\"\n                  size=\"sm\"\n                  onClick={() => handleRemoveAction(index)}\n                  className=\"h-7 w-7 p-0\"\n                >\n                  <Trash2 className=\"h-4 w-4 text-red-500\" />\n                </Button>\n              </div>\n            </CardContent>\n          </Card>\n        ))\n      )}\n\n      <Button\n        type=\"button\"\n        variant=\"outline\"\n        size=\"sm\"\n        onClick={handleAddAction}\n        className=\"w-full\"\n      >\n        <PlusCircle className=\"mr-2 h-4 w-4\" />\n        Add Action\n      </Button>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/rules/RuleEngineConditionBuilder.tsx",
    "content": "import { useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport {\n  Collapsible,\n  CollapsibleContent,\n  CollapsibleTrigger,\n} from \"@/components/ui/collapsible\";\nimport { Input } from \"@/components/ui/input\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\";\nimport {\n  Archive,\n  ChevronDown,\n  ChevronRight,\n  FileType,\n  Heading,\n  Link,\n  PlusCircle,\n  Rss,\n  Star,\n  Tag,\n  Trash2,\n} from \"lucide-react\";\nimport { useTranslation } from \"react-i18next\";\n\nimport type {\n  RuleEngineCondition,\n  RuleEngineEvent,\n} from \"@karakeep/shared/types/rules\";\nimport { zBookmarkSourceSchema } from \"@karakeep/shared/types/bookmarks\";\n\nimport { FeedSelector } from \"../feeds/FeedSelector\";\nimport { TagAutocomplete } from \"../tags/TagAutocomplete\";\n\ninterface ConditionBuilderProps {\n  value: RuleEngineCondition;\n  onChange: (condition: RuleEngineCondition) => void;\n  eventType: RuleEngineEvent[\"type\"];\n  level?: number;\n  onRemove?: () => void;\n}\n\nexport function ConditionBuilder({\n  value,\n  onChange,\n  eventType,\n  level = 0,\n  onRemove,\n}: ConditionBuilderProps) {\n  const { t } = useTranslation();\n  const [isOpen, setIsOpen] = useState(true);\n\n  const handleTypeChange = (type: RuleEngineCondition[\"type\"]) => {\n    switch (type) {\n      case \"urlContains\":\n        onChange({ type: \"urlContains\", str: \"\" });\n        break;\n      case \"urlDoesNotContain\":\n        onChange({ type: \"urlDoesNotContain\", str: \"\" });\n        break;\n      case \"titleContains\":\n        onChange({ type: \"titleContains\", str: \"\" });\n        break;\n      case \"titleDoesNotContain\":\n        onChange({ type: \"titleDoesNotContain\", str: \"\" });\n        break;\n      case \"importedFromFeed\":\n        onChange({ type: \"importedFromFeed\", feedId: \"\" });\n        break;\n      case \"bookmarkTypeIs\":\n        onChange({ type: \"bookmarkTypeIs\", bookmarkType: \"link\" });\n        break;\n      case \"bookmarkSourceIs\":\n        onChange({ type: \"bookmarkSourceIs\", source: \"rss\" });\n        break;\n      case \"hasTag\":\n        onChange({ type: \"hasTag\", tagId: \"\" });\n        break;\n      case \"isFavourited\":\n        onChange({ type: \"isFavourited\" });\n        break;\n      case \"isArchived\":\n        onChange({ type: \"isArchived\" });\n        break;\n      case \"and\":\n        onChange({ type: \"and\", conditions: [] });\n        break;\n      case \"or\":\n        onChange({ type: \"or\", conditions: [] });\n        break;\n      case \"alwaysTrue\":\n        onChange({ type: \"alwaysTrue\" });\n        break;\n      default: {\n        const _exhaustiveCheck: never = type;\n        return null;\n      }\n    }\n  };\n\n  const renderConditionIcon = (type: RuleEngineCondition[\"type\"]) => {\n    switch (type) {\n      case \"urlContains\":\n      case \"urlDoesNotContain\":\n        return <Link className=\"h-4 w-4\" />;\n      case \"titleContains\":\n      case \"titleDoesNotContain\":\n        return <Heading className=\"h-4 w-4\" />;\n      case \"importedFromFeed\":\n        return <Rss className=\"h-4 w-4\" />;\n      case \"bookmarkTypeIs\":\n        return <FileType className=\"h-4 w-4\" />;\n      case \"bookmarkSourceIs\":\n        return <Rss className=\"h-4 w-4\" />;\n      case \"hasTag\":\n        return <Tag className=\"h-4 w-4\" />;\n      case \"isFavourited\":\n        return <Star className=\"h-4 w-4\" />;\n      case \"isArchived\":\n        return <Archive className=\"h-4 w-4\" />;\n      default:\n        return null;\n    }\n  };\n\n  const renderConditionFields = () => {\n    switch (value.type) {\n      case \"urlContains\":\n        return (\n          <div className=\"mt-2\">\n            <Input\n              value={value.str}\n              onChange={(e) => onChange({ ...value, str: e.target.value })}\n              placeholder=\"URL contains...\"\n              className=\"w-full\"\n            />\n          </div>\n        );\n\n      case \"urlDoesNotContain\":\n        return (\n          <div className=\"mt-2\">\n            <Input\n              value={value.str}\n              onChange={(e) => onChange({ ...value, str: e.target.value })}\n              placeholder=\"URL does not contain...\"\n              className=\"w-full\"\n            />\n          </div>\n        );\n\n      case \"titleContains\":\n        return (\n          <div className=\"mt-2\">\n            <Input\n              value={value.str}\n              onChange={(e) => onChange({ ...value, str: e.target.value })}\n              placeholder=\"Title contains...\"\n              className=\"w-full\"\n            />\n          </div>\n        );\n\n      case \"titleDoesNotContain\":\n        return (\n          <div className=\"mt-2\">\n            <Input\n              value={value.str}\n              onChange={(e) => onChange({ ...value, str: e.target.value })}\n              placeholder=\"Title does not contain...\"\n              className=\"w-full\"\n            />\n          </div>\n        );\n\n      case \"importedFromFeed\":\n        return (\n          <div className=\"mt-2\">\n            <FeedSelector\n              value={value.feedId}\n              onChange={(e) => onChange({ ...value, feedId: e })}\n              className=\"w-full\"\n            />\n          </div>\n        );\n\n      case \"bookmarkTypeIs\":\n        return (\n          <div className=\"mt-2\">\n            <Select\n              value={value.bookmarkType}\n              onValueChange={(bookmarkType) =>\n                onChange({\n                  ...value,\n                  bookmarkType: bookmarkType as \"link\" | \"text\" | \"asset\",\n                })\n              }\n            >\n              <SelectTrigger>\n                <SelectValue placeholder=\"Select bookmark type\" />\n              </SelectTrigger>\n              <SelectContent>\n                <SelectItem value=\"link\">\n                  {t(\"common.bookmark_types.link\")}\n                </SelectItem>\n                <SelectItem value=\"text\">\n                  {t(\"common.bookmark_types.text\")}\n                </SelectItem>\n                <SelectItem value=\"asset\">\n                  {t(\"common.bookmark_types.media\")}\n                </SelectItem>\n              </SelectContent>\n            </Select>\n          </div>\n        );\n\n      case \"bookmarkSourceIs\":\n        return (\n          <div className=\"mt-2\">\n            <Select\n              value={value.source}\n              onValueChange={(source) =>\n                onChange({\n                  ...value,\n                  source:\n                    source as (typeof zBookmarkSourceSchema.options)[number],\n                })\n              }\n            >\n              <SelectTrigger>\n                <SelectValue placeholder=\"Select bookmark source\" />\n              </SelectTrigger>\n              <SelectContent>\n                {zBookmarkSourceSchema.options.map((source) => (\n                  <SelectItem key={source} value={source}>\n                    {source}\n                  </SelectItem>\n                ))}\n              </SelectContent>\n            </Select>\n          </div>\n        );\n\n      case \"hasTag\":\n        return (\n          <div className=\"mt-2\">\n            <TagAutocomplete\n              tagId={value.tagId}\n              onChange={(t) => onChange({ type: value.type, tagId: t })}\n            />\n          </div>\n        );\n\n      case \"and\":\n      case \"or\":\n        return (\n          <div className=\"mt-2 space-y-2\">\n            {value.conditions.map((condition, index) => (\n              <ConditionBuilder\n                key={index}\n                value={condition}\n                onChange={(newCondition) => {\n                  const newConditions = [...value.conditions];\n                  newConditions[index] = newCondition;\n                  onChange({ ...value, conditions: newConditions });\n                }}\n                eventType={eventType}\n                level={level + 1}\n                onRemove={() => {\n                  const newConditions = [...value.conditions];\n                  newConditions.splice(index, 1);\n                  onChange({ ...value, conditions: newConditions });\n                }}\n              />\n            ))}\n\n            <Button\n              type=\"button\"\n              variant=\"outline\"\n              size=\"sm\"\n              className=\"mt-2\"\n              onClick={() => {\n                onChange({\n                  ...value,\n                  conditions: [\n                    ...value.conditions,\n                    { type: \"urlContains\", str: \"\" },\n                  ],\n                });\n              }}\n            >\n              <PlusCircle className=\"mr-2 h-4 w-4\" />\n              Add Condition\n            </Button>\n          </div>\n        );\n\n      default:\n        return null;\n    }\n  };\n\n  // Title conditions are hidden for \"bookmarkAdded\" event because\n  // titles are not available at bookmark creation time (they're fetched during crawling)\n  const showTitleConditions = eventType !== \"bookmarkAdded\";\n\n  const ConditionSelector = () => (\n    <Select value={value.type} onValueChange={handleTypeChange}>\n      <SelectTrigger className=\"ml-2 h-8 border-none bg-transparent px-2\">\n        <SelectValue />\n      </SelectTrigger>\n      <SelectContent>\n        <SelectItem value=\"alwaysTrue\">\n          {t(\"settings.rules.conditions_types.always\")}\n        </SelectItem>\n        <SelectItem value=\"and\">\n          {t(\"settings.rules.conditions_types.and\")}\n        </SelectItem>\n        <SelectItem value=\"or\">\n          {t(\"settings.rules.conditions_types.or\")}\n        </SelectItem>\n        <SelectItem value=\"urlContains\">\n          {t(\"settings.rules.conditions_types.url_contains\")}\n        </SelectItem>\n        <SelectItem value=\"urlDoesNotContain\">\n          {t(\"settings.rules.conditions_types.url_does_not_contain\")}\n        </SelectItem>\n        {showTitleConditions && (\n          <SelectItem value=\"titleContains\">\n            {t(\"settings.rules.conditions_types.title_contains\")}\n          </SelectItem>\n        )}\n        {showTitleConditions && (\n          <SelectItem value=\"titleDoesNotContain\">\n            {t(\"settings.rules.conditions_types.title_does_not_contain\")}\n          </SelectItem>\n        )}\n        <SelectItem value=\"importedFromFeed\">\n          {t(\"settings.rules.conditions_types.imported_from_feed\")}\n        </SelectItem>\n        <SelectItem value=\"bookmarkTypeIs\">\n          {t(\"settings.rules.conditions_types.bookmark_type_is\")}\n        </SelectItem>\n        <SelectItem value=\"bookmarkSourceIs\">\n          {t(\"settings.rules.conditions_types.bookmark_source_is\")}\n        </SelectItem>\n        <SelectItem value=\"hasTag\">\n          {t(\"settings.rules.conditions_types.has_tag\")}\n        </SelectItem>\n        <SelectItem value=\"isFavourited\">\n          {t(\"settings.rules.conditions_types.is_favourited\")}\n        </SelectItem>\n        <SelectItem value=\"isArchived\">\n          {t(\"settings.rules.conditions_types.is_archived\")}\n        </SelectItem>\n      </SelectContent>\n    </Select>\n  );\n\n  return (\n    <Card\n      className={`border-l-4 ${value.type === \"and\" ? \"border-l-emerald-500\" : value.type === \"or\" ? \"border-l-amber-500\" : \"border-l-slate-300\"}`}\n    >\n      <CardContent className=\"p-3\">\n        {value.type === \"and\" || value.type === \"or\" ? (\n          <Collapsible open={isOpen} onOpenChange={setIsOpen}>\n            <div className=\"flex items-center justify-between\">\n              <div className=\"flex items-center\">\n                <CollapsibleTrigger asChild>\n                  <Button variant=\"ghost\" size=\"sm\" className=\"h-7 w-7 p-1\">\n                    {isOpen ? (\n                      <ChevronDown className=\"h-4 w-4\" />\n                    ) : (\n                      <ChevronRight className=\"h-4 w-4\" />\n                    )}\n                  </Button>\n                </CollapsibleTrigger>\n                <ConditionSelector />\n                <span className=\"ml-1 text-sm text-muted-foreground\">\n                  {value.conditions.length} condition\n                  {value.conditions.length !== 1 ? \"s\" : \"\"}\n                </span>\n              </div>\n\n              {onRemove && (\n                <Button\n                  variant=\"ghost\"\n                  size=\"sm\"\n                  onClick={onRemove}\n                  className=\"h-7 w-7 p-0\"\n                >\n                  <Trash2 className=\"h-4 w-4 text-red-500\" />\n                </Button>\n              )}\n            </div>\n\n            <CollapsibleContent>{renderConditionFields()}</CollapsibleContent>\n          </Collapsible>\n        ) : (\n          <div>\n            <div className=\"flex items-center justify-between\">\n              <div className=\"flex items-center\">\n                {renderConditionIcon(value.type)}\n                <ConditionSelector />\n              </div>\n\n              {onRemove && (\n                <Button\n                  variant=\"ghost\"\n                  size=\"sm\"\n                  onClick={onRemove}\n                  className=\"h-7 w-7 p-0\"\n                >\n                  <Trash2 className=\"h-4 w-4 text-red-500\" />\n                </Button>\n              )}\n            </div>\n\n            {renderConditionFields()}\n          </div>\n        )}\n      </CardContent>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/rules/RuleEngineEventSelector.tsx",
    "content": "import { Card, CardContent } from \"@/components/ui/card\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\";\nimport { useTranslation } from \"react-i18next\";\n\nimport type { RuleEngineEvent } from \"@karakeep/shared/types/rules\";\n\nimport { BookmarkListSelector } from \"../lists/BookmarkListSelector\";\nimport { TagAutocomplete } from \"../tags/TagAutocomplete\";\n\ninterface EventSelectorProps {\n  value: RuleEngineEvent;\n  onChange: (event: RuleEngineEvent) => void;\n}\n\nexport function EventSelector({ value, onChange }: EventSelectorProps) {\n  const { t } = useTranslation();\n  const handleTypeChange = (type: RuleEngineEvent[\"type\"]) => {\n    switch (type) {\n      case \"bookmarkAdded\":\n        onChange({ type: \"bookmarkAdded\" });\n        break;\n      case \"tagAdded\":\n        onChange({ type: \"tagAdded\", tagId: \"\" });\n        break;\n      case \"tagRemoved\":\n        onChange({ type: \"tagRemoved\", tagId: \"\" });\n        break;\n      case \"addedToList\":\n        onChange({ type: \"addedToList\", listId: \"\" });\n        break;\n      case \"removedFromList\":\n        onChange({ type: \"removedFromList\", listId: \"\" });\n        break;\n      case \"favourited\":\n        onChange({ type: \"favourited\" });\n        break;\n      case \"archived\":\n        onChange({ type: \"archived\" });\n        break;\n      default: {\n        const _exhaustiveCheck: never = type;\n        return null;\n      }\n    }\n  };\n\n  return (\n    <Card>\n      <CardContent className=\"p-4\">\n        <div className=\"flex gap-4\">\n          <Select value={value.type} onValueChange={handleTypeChange}>\n            <SelectTrigger id=\"event-type\">\n              <SelectValue placeholder=\"Select event type\" />\n            </SelectTrigger>\n            <SelectContent>\n              <SelectItem value=\"bookmarkAdded\">\n                {t(\"settings.rules.events_types.bookmark_added\")}\n              </SelectItem>\n              <SelectItem value=\"tagAdded\">\n                {t(\"settings.rules.events_types.tag_added\")}\n              </SelectItem>\n              <SelectItem value=\"tagRemoved\">\n                {t(\"settings.rules.events_types.tag_removed\")}\n              </SelectItem>\n              <SelectItem value=\"addedToList\">\n                {t(\"settings.rules.events_types.added_to_list\")}\n              </SelectItem>\n              <SelectItem value=\"removedFromList\">\n                {t(\"settings.rules.events_types.removed_from_list\")}\n              </SelectItem>\n              <SelectItem value=\"favourited\">\n                {t(\"settings.rules.events_types.favourited\")}\n              </SelectItem>\n              <SelectItem value=\"archived\">\n                {t(\"settings.rules.events_types.archived\")}\n              </SelectItem>\n            </SelectContent>\n          </Select>\n\n          {/* Additional fields based on event type */}\n          {(value.type === \"tagAdded\" || value.type === \"tagRemoved\") && (\n            <TagAutocomplete\n              className=\"w-full\"\n              tagId={value.tagId}\n              onChange={(t) => onChange({ type: value.type, tagId: t ?? \"\" })}\n            />\n          )}\n\n          {(value.type === \"addedToList\" ||\n            value.type === \"removedFromList\") && (\n            <BookmarkListSelector\n              listTypes={[\"manual\"]}\n              value={value.listId}\n              onChange={(l) => onChange({ type: value.type, listId: l })}\n            />\n          )}\n        </div>\n      </CardContent>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/rules/RuleEngineRuleEditor.tsx",
    "content": "import type React from \"react\";\nimport { useEffect, useState } from \"react\";\nimport { ActionBuilder } from \"@/components/dashboard/rules/RuleEngineActionBuilder\";\nimport { ConditionBuilder } from \"@/components/dashboard/rules/RuleEngineConditionBuilder\";\nimport { EventSelector } from \"@/components/dashboard/rules/RuleEngineEventSelector\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardContent, CardHeader, CardTitle } from \"@/components/ui/card\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { Textarea } from \"@/components/ui/textarea\";\nimport { Save, X } from \"lucide-react\";\nimport { useTranslation } from \"react-i18next\";\n\nimport type {\n  RuleEngineAction,\n  RuleEngineCondition,\n  RuleEngineEvent,\n  RuleEngineRule,\n} from \"@karakeep/shared/types/rules\";\nimport {\n  useCreateRule,\n  useUpdateRule,\n} from \"@karakeep/shared-react/hooks/rules\";\n\ninterface RuleEditorProps {\n  rule: Omit<RuleEngineRule, \"id\"> & { id: string | null };\n  onCancel: () => void;\n}\n\nexport function RuleEditor({ rule, onCancel }: RuleEditorProps) {\n  const { t } = useTranslation();\n  const { mutate: createRule, isPending: isCreating } = useCreateRule({\n    onSuccess: () => {\n      toast({\n        description: t(\"settings.rules.rule_has_been_created\"),\n      });\n      onCancel();\n    },\n    onError: (e) => {\n      if (e.data?.code == \"BAD_REQUEST\") {\n        if (e.data.zodError) {\n          toast({\n            variant: \"destructive\",\n            description: Object.values(e.data.zodError.fieldErrors)\n              .flat()\n              .join(\"\\n\"),\n          });\n        } else {\n          toast({\n            variant: \"destructive\",\n            description: e.message,\n          });\n        }\n      } else {\n        toast({\n          variant: \"destructive\",\n          title: t(\"common.something_went_wrong\"),\n        });\n      }\n    },\n  });\n  const { mutate: updateRule, isPending: isUpdating } = useUpdateRule({\n    onSuccess: () => {\n      toast({\n        description: t(\"settings.rules.rule_has_been_updated\"),\n      });\n      onCancel();\n    },\n    onError: (e) => {\n      if (e.data?.code == \"BAD_REQUEST\") {\n        if (e.data.zodError) {\n          toast({\n            variant: \"destructive\",\n            description: Object.values(e.data.zodError.fieldErrors)\n              .flat()\n              .join(\"\\n\"),\n          });\n        } else {\n          toast({\n            variant: \"destructive\",\n            description: e.message,\n          });\n        }\n      } else {\n        toast({\n          variant: \"destructive\",\n          title: t(\"common.something_went_wrong\"),\n        });\n      }\n    },\n  });\n\n  const [editedRule, setEditedRule] = useState<typeof rule>({ ...rule });\n\n  useEffect(() => {\n    setEditedRule({ ...rule });\n  }, [rule]);\n\n  const handleEventChange = (event: RuleEngineEvent) => {\n    setEditedRule({ ...editedRule, event });\n  };\n\n  const handleConditionChange = (condition: RuleEngineCondition) => {\n    setEditedRule({ ...editedRule, condition });\n  };\n\n  const handleActionsChange = (actions: RuleEngineAction[]) => {\n    setEditedRule({ ...editedRule, actions });\n  };\n\n  const handleSubmit = (e: React.FormEvent) => {\n    e.preventDefault();\n    const rule = editedRule;\n    if (rule.id) {\n      updateRule({\n        ...rule,\n        id: rule.id,\n      });\n    } else {\n      createRule(rule);\n    }\n  };\n\n  return (\n    <form onSubmit={handleSubmit}>\n      <Card>\n        <CardHeader>\n          <CardTitle>\n            {rule.id\n              ? t(\"settings.rules.edit_rule\")\n              : t(\"settings.rules.ceate_rule\")}\n          </CardTitle>\n        </CardHeader>\n        <CardContent className=\"space-y-6\">\n          <div className=\"space-y-4\">\n            <div>\n              <Label htmlFor=\"name\">{t(\"settings.rules.rule_name\")}</Label>\n              <Input\n                id=\"name\"\n                value={editedRule.name}\n                onChange={(e) =>\n                  setEditedRule({ ...editedRule, name: e.target.value })\n                }\n                placeholder={t(\"settings.rules.enter_rule_name\")}\n                required\n              />\n            </div>\n\n            <div>\n              <Label htmlFor=\"description\">Description</Label>\n              <Textarea\n                id=\"description\"\n                value={editedRule.description ?? \"\"}\n                onChange={(e) =>\n                  setEditedRule({ ...editedRule, description: e.target.value })\n                }\n                placeholder={t(\"settings.rules.describe_what_this_rule_does\")}\n                rows={2}\n              />\n            </div>\n          </div>\n\n          <div className=\"space-y-2\">\n            <Label>{t(\"settings.rules.whenever\")}</Label>\n            <EventSelector\n              value={editedRule.event}\n              onChange={handleEventChange}\n            />\n          </div>\n\n          <div className=\"space-y-2\">\n            <Label>{t(\"settings.rules.if\")}</Label>\n            <ConditionBuilder\n              value={editedRule.condition}\n              onChange={handleConditionChange}\n              eventType={editedRule.event.type}\n            />\n          </div>\n\n          <div className=\"space-y-2\">\n            <Label>{t(\"common.actions\")}</Label>\n            <ActionBuilder\n              value={editedRule.actions}\n              onChange={handleActionsChange}\n            />\n          </div>\n\n          <div className=\"flex justify-end space-x-2 pt-4\">\n            <Button type=\"button\" variant=\"outline\" onClick={onCancel}>\n              <X className=\"mr-2 h-4 w-4\" />\n              {t(\"actions.cancel\")}\n            </Button>\n            <ActionButton loading={isCreating || isUpdating} type=\"submit\">\n              <Save className=\"mr-2 h-4 w-4\" />\n              {t(\"settings.rules.save_rule\")}\n            </ActionButton>\n          </div>\n        </CardContent>\n      </Card>\n    </form>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/rules/RuleEngineRuleList.tsx",
    "content": "import { ActionButton } from \"@/components/ui/action-button\";\nimport ActionConfirmingDialog from \"@/components/ui/action-confirming-dialog\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { Switch } from \"@/components/ui/switch\";\nimport { useClientConfig } from \"@/lib/clientConfig\";\nimport { Edit, Trash2 } from \"lucide-react\";\nimport { useTranslation } from \"react-i18next\";\n\nimport type { RuleEngineRule } from \"@karakeep/shared/types/rules\";\nimport {\n  useDeleteRule,\n  useUpdateRule,\n} from \"@karakeep/shared-react/hooks/rules\";\n\nexport default function RuleList({\n  rules,\n  onEditRule,\n  onDeleteRule,\n}: {\n  rules: RuleEngineRule[];\n  onEditRule: (rule: RuleEngineRule) => void;\n  onDeleteRule?: (ruleId: string) => void;\n}) {\n  const { t } = useTranslation();\n  const { demoMode } = useClientConfig();\n  const { mutate: updateRule, isPending: isUpdating } = useUpdateRule({\n    onSuccess: () => {\n      toast({\n        description: t(\"settings.rules.rule_has_been_updated\"),\n      });\n    },\n    onError: (e) => {\n      toast({\n        description: e.message,\n        variant: \"destructive\",\n      });\n    },\n  });\n\n  const { mutate: deleteRule, isPending: isDeleting } = useDeleteRule({\n    onSuccess: (_ret, req) => {\n      toast({\n        description: t(\"settings.rules.rule_has_been_deleted\"),\n      });\n      onDeleteRule?.(req.id);\n    },\n    onError: (e) => {\n      toast({\n        description: e.message,\n        variant: \"destructive\",\n      });\n    },\n  });\n\n  if (rules.length === 0) {\n    return (\n      <div className=\"rounded-lg border border-dashed p-8 text-center\">\n        <h3 className=\"text-lg font-medium\">\n          {t(\"settings.rules.no_rules_created_yet\")}\n        </h3>\n        <p className=\"mt-2 text-sm text-muted-foreground\">\n          {t(\"settings.rules.create_your_first_rule\")}\n        </p>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"space-y-4\">\n      {rules.map((rule) => (\n        <Card key={rule.id} className={rule.enabled ? \"\" : \"opacity-70\"}>\n          <CardContent className=\"p-4\">\n            <div className=\"flex items-center justify-between\">\n              <div className=\"mr-4 flex-1\">\n                <h3 className=\"font-medium\">{rule.name}</h3>\n                <p className=\"line-clamp-2 text-sm text-muted-foreground\">\n                  {rule.description}\n                </p>\n                <div className=\"mt-2 flex items-center text-xs text-muted-foreground\">\n                  <span className=\"flex items-center\">\n                    <span className=\"mr-1\">Trigger:</span>\n                    <span className=\"font-medium\">\n                      {formatEventType(rule.event.type)}\n                    </span>\n                  </span>\n                  <span className=\"mx-2\">•</span>\n                  <span>\n                    {rule.actions.length} action\n                    {rule.actions.length !== 1 ? \"s\" : \"\"}\n                  </span>\n                </div>\n              </div>\n              <div className=\"flex items-center space-x-2\">\n                <Switch\n                  disabled={!!demoMode || isUpdating}\n                  checked={rule.enabled}\n                  onCheckedChange={(checked) =>\n                    updateRule({\n                      ...rule,\n                      enabled: checked,\n                    })\n                  }\n                />\n                <Button\n                  variant=\"ghost\"\n                  size=\"icon\"\n                  onClick={() => onEditRule(rule)}\n                >\n                  <Edit className=\"h-4 w-4\" />\n                </Button>\n                <ActionConfirmingDialog\n                  title={t(\"settings.rules.delete_rule\")}\n                  description={t(\"settings.rules.delete_rule_confirmation\")}\n                  actionButton={(setDialogOpen) => (\n                    <ActionButton\n                      loading={isDeleting}\n                      variant=\"destructive\"\n                      onClick={() => {\n                        deleteRule({ id: rule.id });\n                        setDialogOpen(true);\n                      }}\n                    >\n                      <Trash2 className=\"mr-2 h-4 w-4\" />\n                      {t(\"actions.delete\")}\n                    </ActionButton>\n                  )}\n                >\n                  <Button variant=\"ghostDestructive\" size=\"icon\">\n                    <Trash2 className=\"h-4 w-4\" />\n                  </Button>\n                </ActionConfirmingDialog>\n              </div>\n            </div>\n          </CardContent>\n        </Card>\n      ))}\n    </div>\n  );\n}\n\nfunction formatEventType(type: string): string {\n  switch (type) {\n    case \"bookmarkAdded\":\n      return \"Bookmark Added\";\n    case \"tagAdded\":\n      return \"Tag Added\";\n    case \"tagRemoved\":\n      return \"Tag Removed\";\n    case \"addedToList\":\n      return \"Added to List\";\n    case \"removedFromList\":\n      return \"Removed from List\";\n    case \"favourited\":\n      return \"Favourited\";\n    case \"archived\":\n      return \"Archived\";\n    default:\n      return type;\n  }\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/search/QueryExplainerTooltip.tsx",
    "content": "import InfoTooltip from \"@/components/ui/info-tooltip\";\nimport { Table, TableBody, TableCell, TableRow } from \"@/components/ui/table\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { match } from \"@/lib/utils\";\n\nimport { TextAndMatcher } from \"@karakeep/shared/searchQueryParser\";\nimport { Matcher } from \"@karakeep/shared/types/search\";\n\nexport default function QueryExplainerTooltip({\n  parsedSearchQuery,\n  header,\n  className,\n}: {\n  header?: React.ReactNode;\n  parsedSearchQuery: TextAndMatcher & { result: string };\n  className?: string;\n}) {\n  const { t } = useTranslation();\n  if (parsedSearchQuery.result == \"invalid\") {\n    return null;\n  }\n\n  const MatcherComp = ({ matcher }: { matcher: Matcher }) => {\n    switch (matcher.type) {\n      case \"tagName\":\n        return (\n          <TableRow>\n            <TableCell>\n              {matcher.inverse\n                ? t(\"search.does_not_have_tag\")\n                : t(\"search.has_tag\")}\n            </TableCell>\n            <TableCell>{matcher.tagName}</TableCell>\n          </TableRow>\n        );\n      case \"listName\":\n        return (\n          <TableRow>\n            <TableCell>\n              {matcher.inverse\n                ? t(\"search.is_not_in_list\")\n                : t(\"search.is_in_list\")}\n            </TableCell>\n            <TableCell>{matcher.listName}</TableCell>\n          </TableRow>\n        );\n      case \"dateAfter\":\n        return (\n          <TableRow>\n            <TableCell>\n              {matcher.inverse\n                ? t(\"search.not_created_on_or_after\")\n                : t(\"search.created_on_or_after\")}\n            </TableCell>\n            <TableCell>{matcher.dateAfter.toDateString()}</TableCell>\n          </TableRow>\n        );\n      case \"dateBefore\":\n        return (\n          <TableRow>\n            <TableCell>\n              {matcher.inverse\n                ? t(\"search.not_created_on_or_before\")\n                : t(\"search.created_on_or_before\")}\n            </TableCell>\n            <TableCell>{matcher.dateBefore.toDateString()}</TableCell>\n          </TableRow>\n        );\n      case \"age\":\n        return (\n          <TableRow>\n            <TableCell>\n              {matcher.relativeDate.direction === \"newer\"\n                ? t(\"search.created_within\")\n                : t(\"search.created_earlier_than\")}\n            </TableCell>\n            <TableCell>\n              {matcher.relativeDate.amount.toString() +\n                (matcher.relativeDate.direction === \"newer\"\n                  ? {\n                      day: t(\"search.day_s\"),\n                      week: t(\"search.week_s\"),\n                      month: t(\"search.month_s\"),\n                      year: t(\"search.year_s\"),\n                    }[matcher.relativeDate.unit]\n                  : {\n                      day: t(\"search.day_s_ago\"),\n                      week: t(\"search.week_s_ago\"),\n                      month: t(\"search.month_s_ago\"),\n                      year: t(\"search.year_s_ago\"),\n                    }[matcher.relativeDate.unit])}\n            </TableCell>\n          </TableRow>\n        );\n      case \"favourited\":\n        return (\n          <TableRow>\n            <TableCell colSpan={2} className=\"text-center\">\n              {matcher.favourited\n                ? t(\"search.is_favorited\")\n                : t(\"search.is_not_favorited\")}\n            </TableCell>\n          </TableRow>\n        );\n      case \"archived\":\n        return (\n          <TableRow>\n            <TableCell colSpan={2} className=\"text-center\">\n              {matcher.archived\n                ? t(\"search.is_archived\")\n                : t(\"search.is_not_archived\")}\n            </TableCell>\n          </TableRow>\n        );\n      case \"tagged\":\n        return (\n          <TableRow>\n            <TableCell colSpan={2} className=\"text-center\">\n              {matcher.tagged\n                ? t(\"search.has_any_tag\")\n                : t(\"search.has_no_tags\")}\n            </TableCell>\n          </TableRow>\n        );\n      case \"inlist\":\n        return (\n          <TableRow>\n            <TableCell colSpan={2} className=\"text-center\">\n              {matcher.inList\n                ? t(\"search.is_in_any_list\")\n                : t(\"search.is_not_in_any_list\")}\n            </TableCell>\n          </TableRow>\n        );\n      case \"and\":\n      case \"or\":\n        return (\n          <TableRow>\n            <TableCell>\n              {matcher.type === \"and\" ? t(\"search.and\") : t(\"search.or\")}\n            </TableCell>\n            <TableCell>\n              <Table>\n                <TableBody>\n                  {matcher.matchers.map((m, i) => (\n                    <MatcherComp key={i} matcher={m} />\n                  ))}\n                </TableBody>\n              </Table>\n            </TableCell>\n          </TableRow>\n        );\n      case \"url\":\n        return (\n          <TableRow>\n            <TableCell>\n              {matcher.inverse\n                ? t(\"search.url_does_not_contain\")\n                : t(\"search.url_contains\")}\n            </TableCell>\n            <TableCell>{matcher.url}</TableCell>\n          </TableRow>\n        );\n      case \"title\":\n        return (\n          <TableRow>\n            <TableCell>\n              {matcher.inverse\n                ? t(\"search.title_does_not_contain\")\n                : t(\"search.title_contains\")}\n            </TableCell>\n            <TableCell>{matcher.title}</TableCell>\n          </TableRow>\n        );\n      case \"rssFeedName\":\n        return (\n          <TableRow>\n            <TableCell>\n              {matcher.inverse\n                ? t(\"search.is_not_from_feed\")\n                : t(\"search.is_from_feed\")}\n            </TableCell>\n            <TableCell>{matcher.feedName}</TableCell>\n          </TableRow>\n        );\n      case \"type\":\n        return (\n          <TableRow>\n            <TableCell>\n              {matcher.inverse ? t(\"search.type_is_not\") : t(\"search.type_is\")}\n            </TableCell>\n            <TableCell>\n              {match(matcher.typeName, {\n                link: t(\"common.bookmark_types.link\"),\n                text: t(\"common.bookmark_types.text\"),\n                asset: t(\"common.bookmark_types.media\"),\n              })}\n            </TableCell>\n          </TableRow>\n        );\n      case \"brokenLinks\":\n        return (\n          <TableRow>\n            <TableCell colSpan={2} className=\"text-center\">\n              {matcher.brokenLinks\n                ? t(\"search.is_broken_link\")\n                : t(\"search.is_not_broken_link\")}\n            </TableCell>\n          </TableRow>\n        );\n      case \"source\":\n        return (\n          <TableRow>\n            <TableCell>\n              {matcher.inverse\n                ? t(\"search.is_not_from_source\")\n                : t(\"search.is_from_source\")}\n            </TableCell>\n            <TableCell>{matcher.source}</TableCell>\n          </TableRow>\n        );\n      default: {\n        const _exhaustiveCheck: never = matcher;\n        return null;\n      }\n    }\n  };\n\n  return (\n    <InfoTooltip className={className}>\n      {header}\n      <Table>\n        <TableBody>\n          {parsedSearchQuery.text && (\n            <TableRow>\n              <TableCell>{t(\"search.full_text_search\")}</TableCell>\n              <TableCell>{parsedSearchQuery.text}</TableCell>\n            </TableRow>\n          )}\n          {parsedSearchQuery.matcher && (\n            <MatcherComp matcher={parsedSearchQuery.matcher} />\n          )}\n        </TableBody>\n      </Table>\n    </InfoTooltip>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/search/SearchInput.tsx",
    "content": "\"use client\";\n\nimport React, {\n  useCallback,\n  useEffect,\n  useImperativeHandle,\n  useRef,\n  useState,\n} from \"react\";\nimport Link from \"next/link\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Command,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@/components/ui/command\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { useDoBookmarkSearch } from \"@/lib/hooks/bookmark-search\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { cn } from \"@/lib/utils\";\n\nimport { useSearchHistory } from \"@karakeep/shared-react/hooks/search-history\";\n\nimport { EditListModal } from \"../lists/EditListModal\";\nimport QueryExplainerTooltip from \"./QueryExplainerTooltip\";\nimport { useSearchAutocomplete } from \"./useSearchAutocomplete\";\n\nfunction useFocusSearchOnKeyPress(\n  inputRef: React.RefObject<HTMLInputElement | null>,\n  value: string,\n  setValue: (value: string) => void,\n  setPopoverOpen: React.Dispatch<React.SetStateAction<boolean>>,\n) {\n  useEffect(() => {\n    function handleKeyPress(e: KeyboardEvent) {\n      if (!inputRef.current) {\n        return;\n      }\n      if ((e.metaKey || e.ctrlKey) && e.code === \"KeyK\") {\n        e.preventDefault();\n        inputRef.current.focus();\n        // Move the cursor to the end of the input field, so you can continue typing\n        const length = inputRef.current.value.length;\n        inputRef.current.setSelectionRange(length, length);\n        setPopoverOpen(true);\n      }\n      if (e.code === \"Escape\" && e.target == inputRef.current && value !== \"\") {\n        e.preventDefault();\n        inputRef.current.blur();\n        setValue(\"\");\n      }\n    }\n\n    document.addEventListener(\"keydown\", handleKeyPress);\n    return () => {\n      document.removeEventListener(\"keydown\", handleKeyPress);\n    };\n  }, [inputRef, value, setValue, setPopoverOpen]);\n}\n\nconst SearchInput = React.forwardRef<\n  HTMLInputElement,\n  React.HTMLAttributes<HTMLInputElement> & { loading?: boolean }\n>(({ className, ...props }, ref) => {\n  const { t } = useTranslation();\n  const {\n    debounceSearch,\n    searchQuery,\n    doSearch,\n    parsedSearchQuery,\n    isInSearchPage,\n  } = useDoBookmarkSearch();\n  const { addTerm, history } = useSearchHistory({\n    getItem: (k: string) => localStorage.getItem(k),\n    setItem: (k: string, v: string) => localStorage.setItem(k, v),\n    removeItem: (k: string) => localStorage.removeItem(k),\n  });\n\n  const [value, setValue] = useState(searchQuery);\n  const [isPopoverOpen, setIsPopoverOpen] = useState(false);\n  const [newNestedListModalOpen, setNewNestedListModalOpen] = useState(false);\n\n  const inputRef = useRef<HTMLInputElement>(null);\n  const isHistorySelected = useRef(false);\n  const isComposing = useRef(false);\n\n  const handleValueChange = useCallback(\n    (newValue: string) => {\n      setValue(newValue);\n      // Only trigger debounced search if not in IME composition mode\n      if (!isComposing.current) {\n        debounceSearch(newValue);\n      }\n      isHistorySelected.current = false; // Reset flag when user types\n    },\n    [debounceSearch],\n  );\n\n  const handleCompositionStart = useCallback(() => {\n    isComposing.current = true;\n  }, []);\n\n  const handleCompositionEnd = useCallback(\n    (e: React.CompositionEvent<HTMLInputElement>) => {\n      isComposing.current = false;\n      // Trigger search with the final composed value\n      const target = e.target as HTMLInputElement;\n      debounceSearch(target.value);\n    },\n    [debounceSearch],\n  );\n\n  const {\n    suggestionGroups,\n    hasSuggestions,\n    isPopoverVisible,\n    handleSuggestionSelect,\n    handleCommandKeyDown,\n  } = useSearchAutocomplete({\n    value,\n    onValueChange: handleValueChange,\n    inputRef,\n    isPopoverOpen,\n    setIsPopoverOpen,\n    t,\n    history,\n  });\n\n  const handleHistorySelect = useCallback(\n    (term: string) => {\n      isHistorySelected.current = true;\n      setValue(term);\n      doSearch(term);\n      addTerm(term);\n      setIsPopoverOpen(false);\n      inputRef.current?.blur();\n    },\n    [doSearch, addTerm],\n  );\n\n  useFocusSearchOnKeyPress(inputRef, value, setValue, setIsPopoverOpen);\n  useImperativeHandle(ref, () => inputRef.current!);\n\n  useEffect(() => {\n    if (!isInSearchPage) {\n      setValue(\"\");\n    }\n  }, [isInSearchPage]);\n\n  const handleFocus = useCallback(() => {\n    setIsPopoverOpen(true);\n  }, []);\n\n  const handleBlur = useCallback(() => {\n    // Only add to history if it wasn't a history selection\n    if (value && !isHistorySelected.current) {\n      addTerm(value);\n    }\n\n    // Reset the flag\n    isHistorySelected.current = false;\n    setIsPopoverOpen(false);\n  }, [value, addTerm]);\n\n  return (\n    <div className={cn(\"relative flex-1\", className)}>\n      <EditListModal\n        open={newNestedListModalOpen}\n        setOpen={setNewNestedListModalOpen}\n        prefill={{\n          type: \"smart\",\n          query: value,\n        }}\n      />\n      <Link\n        href=\"https://docs.karakeep.app/Guides/search-query-language\"\n        target=\"_blank\"\n        className=\"-translate-1/2 absolute right-1.5 top-2 z-50 stroke-foreground px-0.5\"\n      >\n        <QueryExplainerTooltip parsedSearchQuery={parsedSearchQuery} />\n      </Link>\n      {parsedSearchQuery.result === \"full\" &&\n        parsedSearchQuery.text.length == 0 && (\n          <Button\n            onClick={() => setNewNestedListModalOpen(true)}\n            size=\"none\"\n            variant=\"secondary\"\n            className=\"absolute right-10 top-2 z-50 px-2 py-1 text-xs\"\n          >\n            {t(\"actions.save\")}\n          </Button>\n        )}\n      <Command\n        shouldFilter={false}\n        className=\"relative rounded-md bg-transparent\"\n        onKeyDown={handleCommandKeyDown}\n      >\n        <Popover open={isPopoverVisible}>\n          <PopoverTrigger asChild>\n            <div className=\"relative\">\n              <CommandInput\n                ref={inputRef}\n                placeholder={t(\"common.search\")}\n                value={value}\n                onValueChange={handleValueChange}\n                onCompositionStart={handleCompositionStart}\n                onCompositionEnd={handleCompositionEnd}\n                onFocus={handleFocus}\n                onBlur={handleBlur}\n                className={cn(\"h-10\", className)}\n                {...props}\n              />\n            </div>\n          </PopoverTrigger>\n          <PopoverContent\n            className=\"w-[--radix-popover-trigger-width] p-0\"\n            onOpenAutoFocus={(e) => e.preventDefault()}\n            onCloseAutoFocus={(e) => e.preventDefault()}\n          >\n            <CommandList className=\"max-h-96 overflow-y-auto\">\n              {hasSuggestions && <CommandItem value=\"-\" className=\"hidden\" />}\n              {suggestionGroups.map((group) => (\n                <CommandGroup key={group.id} heading={group.label}>\n                  {group.items.map((item) => {\n                    if (item.type === \"history\") {\n                      return (\n                        <CommandItem\n                          key={item.id}\n                          value={item.label}\n                          onSelect={() => handleHistorySelect(item.term)}\n                          onMouseDown={() => {\n                            isHistorySelected.current = true;\n                          }}\n                          className=\"cursor-pointer\"\n                        >\n                          <item.Icon className=\"mr-2 h-4 w-4\" />\n                          <span>{item.label}</span>\n                        </CommandItem>\n                      );\n                    }\n\n                    return (\n                      <CommandItem\n                        key={item.id}\n                        value={item.label}\n                        onSelect={() => handleSuggestionSelect(item)}\n                        className=\"cursor-pointer\"\n                      >\n                        <item.Icon className=\"mr-2 h-4 w-4\" />\n                        <div className=\"flex flex-col\">\n                          <span>{item.label}</span>\n                          {item.description && (\n                            <span className=\"text-xs text-muted-foreground\">\n                              {item.description}\n                            </span>\n                          )}\n                        </div>\n                      </CommandItem>\n                    );\n                  })}\n                </CommandGroup>\n              ))}\n            </CommandList>\n          </PopoverContent>\n        </Popover>\n      </Command>\n    </div>\n  );\n});\nSearchInput.displayName = \"SearchInput\";\n\nexport { SearchInput };\n"
  },
  {
    "path": "apps/web/components/dashboard/search/useSearchAutocomplete.ts",
    "content": "import type translation from \"@/lib/i18n/locales/en/translation.json\";\nimport type { TFunction } from \"i18next\";\nimport type { LucideIcon } from \"lucide-react\";\nimport { useCallback, useMemo } from \"react\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport {\n  Globe,\n  History,\n  ListTree,\n  RssIcon,\n  Sparkles,\n  Tag as TagIcon,\n} from \"lucide-react\";\n\nimport { useBookmarkLists } from \"@karakeep/shared-react/hooks/lists\";\nimport { useTagAutocomplete } from \"@karakeep/shared-react/hooks/tags\";\nimport { useDebounce } from \"@karakeep/shared-react/hooks/use-debounce\";\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\nimport { zBookmarkSourceSchema } from \"@karakeep/shared/types/bookmarks\";\n\nconst MAX_DISPLAY_SUGGESTIONS = 5;\n\ntype SearchTranslationKey = `search.${keyof typeof translation.search}`;\n\ninterface QualifierDefinition {\n  value: string;\n  descriptionKey?: SearchTranslationKey;\n  negatedDescriptionKey?: SearchTranslationKey;\n  appendSpace?: boolean;\n}\n\nconst QUALIFIER_DEFINITIONS = [\n  {\n    value: \"is:fav\",\n    descriptionKey: \"search.is_favorited\",\n    negatedDescriptionKey: \"search.is_not_favorited\",\n    appendSpace: true,\n  },\n  {\n    value: \"is:archived\",\n    descriptionKey: \"search.is_archived\",\n    negatedDescriptionKey: \"search.is_not_archived\",\n    appendSpace: true,\n  },\n  {\n    value: \"is:tagged\",\n    descriptionKey: \"search.has_any_tag\",\n    negatedDescriptionKey: \"search.has_no_tags\",\n    appendSpace: true,\n  },\n  {\n    value: \"is:inlist\",\n    descriptionKey: \"search.is_in_any_list\",\n    negatedDescriptionKey: \"search.is_not_in_any_list\",\n    appendSpace: true,\n  },\n  {\n    value: \"is:link\",\n    appendSpace: true,\n  },\n  {\n    value: \"is:text\",\n    appendSpace: true,\n  },\n  {\n    value: \"is:media\",\n    appendSpace: true,\n  },\n  {\n    value: \"is:broken\",\n    descriptionKey: \"search.is_broken_link\",\n    negatedDescriptionKey: \"search.is_not_broken_link\",\n    appendSpace: true,\n  },\n  {\n    value: \"url:\",\n    descriptionKey: \"search.url_contains\",\n  },\n  {\n    value: \"title:\",\n    descriptionKey: \"search.title_contains\",\n  },\n  {\n    value: \"list:\",\n    descriptionKey: \"search.is_in_list\",\n  },\n  {\n    value: \"after:\",\n    descriptionKey: \"search.created_on_or_after\",\n  },\n  {\n    value: \"before:\",\n    descriptionKey: \"search.created_on_or_before\",\n  },\n  {\n    value: \"feed:\",\n    descriptionKey: \"search.is_from_feed\",\n  },\n  {\n    value: \"age:\",\n    descriptionKey: \"search.created_within\",\n  },\n  {\n    value: \"source:\",\n    descriptionKey: \"search.is_from_source\",\n  },\n] satisfies readonly QualifierDefinition[];\n\nexport interface AutocompleteSuggestionItem {\n  type: \"token\" | \"tag\" | \"list\" | \"feed\" | \"source\";\n  id: string;\n  label: string;\n  insertText: string;\n  appendSpace?: boolean;\n  description?: string;\n  Icon: LucideIcon;\n}\n\nexport interface HistorySuggestionItem {\n  type: \"history\";\n  id: string;\n  term: string;\n  label: string;\n  Icon: LucideIcon;\n}\n\nexport type SuggestionItem = AutocompleteSuggestionItem | HistorySuggestionItem;\n\nexport interface SuggestionGroup {\n  id: string;\n  label: string;\n  items: SuggestionItem[];\n}\n\nconst stripSurroundingQuotes = (value: string) => {\n  let nextValue = value;\n  if (nextValue.startsWith('\"')) {\n    nextValue = nextValue.slice(1);\n  }\n  if (nextValue.endsWith('\"')) {\n    nextValue = nextValue.slice(0, -1);\n  }\n  return nextValue;\n};\n\nconst shouldQuoteValue = (value: string) => /[\\s:]/.test(value);\n\nconst formatSearchValue = (value: string) =>\n  shouldQuoteValue(value) ? `\"${value}\"` : value;\n\ninterface ParsedSearchState {\n  activeToken: string;\n  isTokenNegative: boolean;\n  tokenWithoutMinus: string;\n  normalizedTokenWithoutMinus: string;\n  getActiveToken: (cursorPosition: number) => { token: string; start: number };\n}\n\ninterface UseSearchAutocompleteParams {\n  value: string;\n  onValueChange: (value: string) => void;\n  inputRef: React.RefObject<HTMLInputElement | null>;\n  isPopoverOpen: boolean;\n  setIsPopoverOpen: React.Dispatch<React.SetStateAction<boolean>>;\n  t: TFunction;\n  history: string[];\n}\n\nconst useParsedSearchState = (value: string): ParsedSearchState => {\n  const getActiveToken = useCallback(\n    (cursorPosition: number) => {\n      let start = 0;\n      let inQuotes = false;\n\n      for (let index = 0; index < cursorPosition; index += 1) {\n        const char = value[index];\n        if (char === '\"') {\n          inQuotes = !inQuotes;\n          continue;\n        }\n\n        if (!inQuotes) {\n          if (char === \" \" || char === \"\\t\" || char === \"\\n\") {\n            start = index + 1;\n            continue;\n          }\n\n          if (char === \"(\") {\n            start = index + 1;\n          }\n        }\n      }\n\n      return {\n        token: value.slice(start, cursorPosition),\n        start,\n      };\n    },\n    [value],\n  );\n\n  const activeTokenInfo = useMemo(\n    () => getActiveToken(value.length),\n    [getActiveToken, value],\n  );\n  const activeToken = activeTokenInfo.token;\n  const isTokenNegative = activeToken.startsWith(\"-\");\n  const tokenWithoutMinus = isTokenNegative\n    ? activeToken.slice(1)\n    : activeToken;\n  const normalizedTokenWithoutMinus = tokenWithoutMinus.toLowerCase();\n\n  return {\n    activeToken,\n    isTokenNegative,\n    tokenWithoutMinus,\n    normalizedTokenWithoutMinus,\n    getActiveToken,\n  };\n};\n\nconst useQualifierSuggestions = (\n  parsed: ParsedSearchState,\n  t: TFunction,\n): AutocompleteSuggestionItem[] => {\n  const qualifierSuggestions = useMemo<AutocompleteSuggestionItem[]>(() => {\n    // Don't suggest qualifiers if the user hasn't started typing\n    if (parsed.normalizedTokenWithoutMinus.length === 0) {\n      return [];\n    }\n\n    return QUALIFIER_DEFINITIONS.filter((definition) => {\n      return definition.value\n        .toLowerCase()\n        .startsWith(parsed.normalizedTokenWithoutMinus);\n    })\n      .slice(0, MAX_DISPLAY_SUGGESTIONS)\n      .map((definition) => {\n        const insertText = `${parsed.isTokenNegative ? \"-\" : \"\"}${definition.value}`;\n        const descriptionKey = parsed.isTokenNegative\n          ? (definition.negatedDescriptionKey ?? definition.descriptionKey)\n          : definition.descriptionKey;\n        const description = descriptionKey ? t(descriptionKey) : undefined;\n\n        return {\n          type: \"token\" as const,\n          id: `qualifier-${definition.value}`,\n          label: insertText,\n          insertText,\n          appendSpace: definition.appendSpace,\n          description,\n          Icon: Sparkles,\n        } satisfies AutocompleteSuggestionItem;\n      });\n  }, [parsed.normalizedTokenWithoutMinus, parsed.isTokenNegative, t]);\n\n  return qualifierSuggestions;\n};\n\nconst useTagSuggestions = (\n  parsed: ParsedSearchState,\n): AutocompleteSuggestionItem[] => {\n  const shouldSuggestTags = parsed.tokenWithoutMinus.startsWith(\"#\");\n  const tagSearchTermRaw = shouldSuggestTags\n    ? parsed.tokenWithoutMinus.slice(1)\n    : \"\";\n  const tagSearchTerm = stripSurroundingQuotes(tagSearchTermRaw);\n  const debouncedTagSearchTerm = useDebounce(tagSearchTerm, 200);\n\n  const { data: tagResults } = useTagAutocomplete({\n    nameContains: debouncedTagSearchTerm,\n    select: (data) => data.tags,\n    enabled: parsed.activeToken.length > 0,\n  });\n\n  const tagSuggestions = useMemo<AutocompleteSuggestionItem[]>(() => {\n    if (!shouldSuggestTags) {\n      return [];\n    }\n\n    return (tagResults ?? []).slice(0, MAX_DISPLAY_SUGGESTIONS).map((tag) => {\n      const formattedName = formatSearchValue(tag.name);\n      const insertText = `${parsed.isTokenNegative ? \"-\" : \"\"}#${formattedName}`;\n\n      return {\n        type: \"tag\" as const,\n        id: `tag-${tag.id}`,\n        label: insertText,\n        insertText,\n        appendSpace: true,\n        description: undefined,\n        Icon: TagIcon,\n      } satisfies AutocompleteSuggestionItem;\n    });\n  }, [shouldSuggestTags, tagResults, parsed.isTokenNegative]);\n\n  return tagSuggestions;\n};\n\nconst useFeedSuggestions = (\n  parsed: ParsedSearchState,\n): AutocompleteSuggestionItem[] => {\n  const api = useTRPC();\n  const shouldSuggestFeeds =\n    parsed.normalizedTokenWithoutMinus.startsWith(\"feed:\");\n  const feedSearchTermRaw = shouldSuggestFeeds\n    ? parsed.tokenWithoutMinus.slice(\"feed:\".length)\n    : \"\";\n  const feedSearchTerm = stripSurroundingQuotes(feedSearchTermRaw);\n  const normalizedFeedSearchTerm = feedSearchTerm.toLowerCase();\n  const { data: feedResults } = useQuery(\n    api.feeds.list.queryOptions(undefined, {\n      enabled: parsed.activeToken.length > 0,\n    }),\n  );\n\n  const feedSuggestions = useMemo<AutocompleteSuggestionItem[]>(() => {\n    if (!shouldSuggestFeeds) {\n      return [];\n    }\n\n    const feeds = feedResults?.feeds ?? [];\n\n    return feeds\n      .filter((feed) => {\n        if (normalizedFeedSearchTerm.length === 0) {\n          return true;\n        }\n        return feed.name.toLowerCase().includes(normalizedFeedSearchTerm);\n      })\n      .slice(0, MAX_DISPLAY_SUGGESTIONS)\n      .map((feed) => {\n        const formattedName = formatSearchValue(feed.name);\n        const insertText = `${parsed.isTokenNegative ? \"-\" : \"\"}feed:${formattedName}`;\n        return {\n          type: \"feed\" as const,\n          id: `feed-${feed.id}`,\n          label: insertText,\n          insertText,\n          appendSpace: true,\n          description: undefined,\n          Icon: RssIcon,\n        } satisfies AutocompleteSuggestionItem;\n      });\n  }, [\n    shouldSuggestFeeds,\n    feedResults,\n    normalizedFeedSearchTerm,\n    parsed.isTokenNegative,\n  ]);\n\n  return feedSuggestions;\n};\n\nconst useListSuggestions = (\n  parsed: ParsedSearchState,\n): AutocompleteSuggestionItem[] => {\n  const shouldSuggestLists =\n    parsed.normalizedTokenWithoutMinus.startsWith(\"list:\");\n  const listSearchTermRaw = shouldSuggestLists\n    ? parsed.tokenWithoutMinus.slice(\"list:\".length)\n    : \"\";\n  const listSearchTerm = stripSurroundingQuotes(listSearchTermRaw);\n  const normalizedListSearchTerm = listSearchTerm.toLowerCase();\n  const { data: listResults } = useBookmarkLists(undefined, {\n    enabled: parsed.activeToken.length > 0,\n  });\n\n  const listSuggestions = useMemo<AutocompleteSuggestionItem[]>(() => {\n    if (!shouldSuggestLists) {\n      return [];\n    }\n\n    const lists = listResults?.data ?? [];\n    const seenListNames = new Set<string>();\n\n    return lists\n      .filter((list) => {\n        if (normalizedListSearchTerm.length === 0) {\n          return true;\n        }\n        return list.name.toLowerCase().includes(normalizedListSearchTerm);\n      })\n      .filter((list) => {\n        const normalizedListName = list.name.trim().toLowerCase();\n        if (seenListNames.has(normalizedListName)) {\n          return false;\n        }\n\n        seenListNames.add(normalizedListName);\n        return true;\n      })\n      .slice(0, MAX_DISPLAY_SUGGESTIONS)\n      .map((list) => {\n        const formattedName = formatSearchValue(list.name);\n        const insertText = `${parsed.isTokenNegative ? \"-\" : \"\"}list:${formattedName}`;\n        return {\n          type: \"list\" as const,\n          id: `list-${list.id}`,\n          label: insertText,\n          insertText,\n          appendSpace: true,\n          description: undefined,\n          Icon: ListTree,\n        } satisfies AutocompleteSuggestionItem;\n      });\n  }, [\n    shouldSuggestLists,\n    listResults,\n    normalizedListSearchTerm,\n    parsed.isTokenNegative,\n  ]);\n\n  return listSuggestions;\n};\n\nconst SOURCE_VALUES = zBookmarkSourceSchema.options;\n\nconst useSourceSuggestions = (\n  parsed: ParsedSearchState,\n): AutocompleteSuggestionItem[] => {\n  const shouldSuggestSources =\n    parsed.normalizedTokenWithoutMinus.startsWith(\"source:\");\n  const sourceSearchTerm = shouldSuggestSources\n    ? parsed.normalizedTokenWithoutMinus.slice(\"source:\".length)\n    : \"\";\n\n  const sourceSuggestions = useMemo<AutocompleteSuggestionItem[]>(() => {\n    if (!shouldSuggestSources) {\n      return [];\n    }\n\n    return SOURCE_VALUES.filter((source) => {\n      if (sourceSearchTerm.length === 0) {\n        return true;\n      }\n      return source.startsWith(sourceSearchTerm);\n    })\n      .slice(0, MAX_DISPLAY_SUGGESTIONS)\n      .map((source) => {\n        const insertText = `${parsed.isTokenNegative ? \"-\" : \"\"}source:${source}`;\n        return {\n          type: \"source\" as const,\n          id: `source-${source}`,\n          label: insertText,\n          insertText,\n          appendSpace: true,\n          description: undefined,\n          Icon: Globe,\n        } satisfies AutocompleteSuggestionItem;\n      });\n  }, [shouldSuggestSources, sourceSearchTerm, parsed.isTokenNegative]);\n\n  return sourceSuggestions;\n};\n\nconst useHistorySuggestions = (\n  value: string,\n  history: string[],\n): HistorySuggestionItem[] => {\n  const historyItems = useMemo<HistorySuggestionItem[]>(() => {\n    const trimmedValue = value.trim();\n    const seenTerms = new Set<string>();\n    const results =\n      trimmedValue.length === 0\n        ? history\n        : history.filter((item) =>\n            item.toLowerCase().includes(trimmedValue.toLowerCase()),\n          );\n\n    return results\n      .filter((term) => {\n        const normalizedTerm = term.trim().toLowerCase();\n        if (seenTerms.has(normalizedTerm)) {\n          return false;\n        }\n\n        seenTerms.add(normalizedTerm);\n        return true;\n      })\n      .slice(0, MAX_DISPLAY_SUGGESTIONS)\n      .map(\n        (term) =>\n          ({\n            type: \"history\" as const,\n            id: `history-${term}`,\n            term,\n            label: term,\n            Icon: History,\n          }) satisfies HistorySuggestionItem,\n      );\n  }, [history, value]);\n\n  return historyItems;\n};\n\nexport const useSearchAutocomplete = ({\n  value,\n  onValueChange,\n  inputRef,\n  isPopoverOpen,\n  setIsPopoverOpen,\n  t,\n  history,\n}: UseSearchAutocompleteParams) => {\n  const parsedState = useParsedSearchState(value);\n  const qualifierSuggestions = useQualifierSuggestions(parsedState, t);\n  const tagSuggestions = useTagSuggestions(parsedState);\n  const listSuggestions = useListSuggestions(parsedState);\n  const feedSuggestions = useFeedSuggestions(parsedState);\n  const sourceSuggestions = useSourceSuggestions(parsedState);\n  const historyItems = useHistorySuggestions(value, history);\n  const { getActiveToken } = parsedState;\n\n  const suggestionGroups = useMemo<SuggestionGroup[]>(() => {\n    const groups: SuggestionGroup[] = [];\n\n    if (tagSuggestions.length > 0) {\n      groups.push({\n        id: \"tags\",\n        label: t(\"search.tags\"),\n        items: tagSuggestions,\n      });\n    }\n\n    if (listSuggestions.length > 0) {\n      groups.push({\n        id: \"lists\",\n        label: t(\"search.lists\"),\n        items: listSuggestions,\n      });\n    }\n\n    if (feedSuggestions.length > 0) {\n      groups.push({\n        id: \"feeds\",\n        label: t(\"search.feeds\"),\n        items: feedSuggestions,\n      });\n    }\n\n    if (sourceSuggestions.length > 0) {\n      groups.push({\n        id: \"sources\",\n        label: t(\"search.is_from_source\"),\n        items: sourceSuggestions,\n      });\n    }\n\n    // Only suggest qualifiers if no other suggestions are available\n    if (groups.length === 0 && qualifierSuggestions.length > 0) {\n      groups.push({\n        id: \"qualifiers\",\n        label: t(\"search.filters\"),\n        items: qualifierSuggestions,\n      });\n    }\n\n    if (historyItems.length > 0) {\n      groups.push({\n        id: \"history\",\n        label: t(\"search.history\"),\n        items: historyItems,\n      });\n    }\n\n    return groups;\n  }, [\n    qualifierSuggestions,\n    tagSuggestions,\n    listSuggestions,\n    feedSuggestions,\n    sourceSuggestions,\n    historyItems,\n    t,\n  ]);\n\n  const hasSuggestions = suggestionGroups.length > 0;\n  const isPopoverVisible = isPopoverOpen && hasSuggestions;\n\n  const handleSuggestionSelect = useCallback(\n    (item: AutocompleteSuggestionItem) => {\n      const input = inputRef.current;\n      const selectionStart = input?.selectionStart ?? value.length;\n      const selectionEnd = input?.selectionEnd ?? selectionStart;\n      const { start } = getActiveToken(selectionStart);\n      const beforeToken = value.slice(0, start);\n      const afterToken = value.slice(selectionEnd);\n\n      const needsSpace =\n        item.appendSpace &&\n        (afterToken.length === 0 || !/^\\s/.test(afterToken));\n      const baseValue = `${beforeToken}${item.insertText}${afterToken}`;\n      const finalValue = needsSpace\n        ? `${beforeToken}${item.insertText} ${afterToken}`\n        : baseValue;\n\n      onValueChange(finalValue);\n\n      requestAnimationFrame(() => {\n        const target = inputRef.current;\n        if (!target) {\n          return;\n        }\n        const cursorPosition =\n          beforeToken.length + item.insertText.length + (needsSpace ? 1 : 0);\n        target.focus();\n        target.setSelectionRange(cursorPosition, cursorPosition);\n      });\n\n      setIsPopoverOpen(true);\n    },\n    [getActiveToken, onValueChange, value, inputRef, setIsPopoverOpen],\n  );\n\n  const handleCommandKeyDown = useCallback(\n    (e: React.KeyboardEvent) => {\n      if (e.key === \"Enter\") {\n        const selectedItem = document.querySelector(\n          '[cmdk-item][data-selected=\"true\"]',\n        );\n        const isPlaceholderSelected =\n          selectedItem?.getAttribute(\"data-value\") === \"-\";\n        if (!selectedItem || isPlaceholderSelected) {\n          e.preventDefault();\n          setIsPopoverOpen(false);\n          inputRef.current?.blur();\n        }\n      } else if (e.key === \"Escape\") {\n        e.preventDefault();\n        setIsPopoverOpen(false);\n        inputRef.current?.blur();\n      }\n    },\n    [setIsPopoverOpen, inputRef],\n  );\n\n  return {\n    suggestionGroups,\n    hasSuggestions,\n    isPopoverVisible,\n    handleSuggestionSelect,\n    handleCommandKeyDown,\n  };\n};\n"
  },
  {
    "path": "apps/web/components/dashboard/sidebar/AllLists.tsx",
    "content": "\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport Link from \"next/link\";\nimport { usePathname } from \"next/navigation\";\nimport SidebarItem from \"@/components/shared/sidebar/SidebarItem\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Collapsible,\n  CollapsibleContent,\n  CollapsibleTriggerTriangle,\n} from \"@/components/ui/collapsible\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { BOOKMARK_DRAG_MIME } from \"@/lib/bookmark-drag\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { cn } from \"@/lib/utils\";\nimport { MoreHorizontal, Plus } from \"lucide-react\";\n\nimport type { ZBookmarkList } from \"@karakeep/shared/types/lists\";\nimport {\n  augmentBookmarkListsWithInitialData,\n  useAddBookmarkToList,\n  useBookmarkLists,\n} from \"@karakeep/shared-react/hooks/lists\";\nimport { ZBookmarkListTreeNode } from \"@karakeep/shared/utils/listUtils\";\n\nimport { CollapsibleBookmarkLists } from \"../lists/CollapsibleBookmarkLists\";\nimport { EditListModal } from \"../lists/EditListModal\";\nimport { ListOptions } from \"../lists/ListOptions\";\nimport { InvitationNotificationBadge } from \"./InvitationNotificationBadge\";\n\nfunction useDropTarget(listId: string, listName: string) {\n  const { mutateAsync: addToList } = useAddBookmarkToList();\n  const [dropHighlight, setDropHighlight] = useState(false);\n  const dragCounterRef = useRef(0);\n  const { t } = useTranslation();\n\n  const onDragOver = useCallback((e: React.DragEvent) => {\n    if (e.dataTransfer.types.includes(BOOKMARK_DRAG_MIME)) {\n      e.preventDefault();\n      e.dataTransfer.dropEffect = \"copy\";\n    }\n  }, []);\n\n  const onDragEnter = useCallback((e: React.DragEvent) => {\n    if (e.dataTransfer.types.includes(BOOKMARK_DRAG_MIME)) {\n      e.preventDefault();\n      dragCounterRef.current++;\n      setDropHighlight(true);\n    }\n  }, []);\n\n  const onDragLeave = useCallback(() => {\n    dragCounterRef.current--;\n    if (dragCounterRef.current <= 0) {\n      dragCounterRef.current = 0;\n      setDropHighlight(false);\n    }\n  }, []);\n\n  const onDrop = useCallback(\n    async (e: React.DragEvent) => {\n      dragCounterRef.current = 0;\n      setDropHighlight(false);\n      const bookmarkId = e.dataTransfer.getData(BOOKMARK_DRAG_MIME);\n      if (!bookmarkId) return;\n      e.preventDefault();\n      try {\n        await addToList({ bookmarkId, listId });\n        toast({\n          description: t(\"lists.add_to_list_success\", {\n            list: listName,\n            defaultValue: `Added to \"${listName}\"`,\n          }),\n        });\n      } catch {\n        toast({\n          description: t(\"common.something_went_wrong\", {\n            defaultValue: \"Something went wrong\",\n          }),\n          variant: \"destructive\",\n        });\n      }\n    },\n    [addToList, listId, listName, t],\n  );\n\n  return { dropHighlight, onDragOver, onDragEnter, onDragLeave, onDrop };\n}\n\nfunction DroppableListSidebarItem({\n  node,\n  level,\n  open,\n  numBookmarks,\n  selectedListId,\n  setSelectedListId,\n}: {\n  node: ZBookmarkListTreeNode;\n  level: number;\n  open: boolean;\n  numBookmarks?: number;\n  selectedListId: string | null;\n  setSelectedListId: (id: string | null) => void;\n}) {\n  const canDrop =\n    node.item.type === \"manual\" &&\n    (node.item.userRole === \"owner\" || node.item.userRole === \"editor\");\n  const { dropHighlight, onDragOver, onDragEnter, onDragLeave, onDrop } =\n    useDropTarget(node.item.id, node.item.name);\n\n  return (\n    <SidebarItem\n      collapseButton={\n        node.children.length > 0 && (\n          <CollapsibleTriggerTriangle\n            className=\"absolute left-0.5 top-1/2 size-2 -translate-y-1/2\"\n            open={open}\n          />\n        )\n      }\n      logo={\n        <span className=\"flex\">\n          <span className=\"text-lg\"> {node.item.icon}</span>\n        </span>\n      }\n      name={node.item.name}\n      path={`/dashboard/lists/${node.item.id}`}\n      className=\"group px-0.5\"\n      right={\n        <ListOptions\n          onOpenChange={(isOpen) => {\n            if (isOpen) {\n              setSelectedListId(node.item.id);\n            } else {\n              setSelectedListId(null);\n            }\n          }}\n          list={node.item}\n        >\n          <Button size=\"none\" variant=\"ghost\" className=\"relative\">\n            <MoreHorizontal\n              className={cn(\n                \"absolute inset-0 m-auto size-4 opacity-0 transition-opacity duration-100 group-hover:opacity-100\",\n                selectedListId == node.item.id ? \"opacity-100\" : \"opacity-0\",\n              )}\n            />\n            <span\n              className={cn(\n                \"px-2.5 text-xs font-light text-muted-foreground opacity-100 transition-opacity duration-100 group-hover:opacity-0\",\n                selectedListId == node.item.id || numBookmarks === undefined\n                  ? \"opacity-0\"\n                  : \"opacity-100\",\n              )}\n            >\n              {numBookmarks}\n            </span>\n          </Button>\n        </ListOptions>\n      }\n      linkClassName=\"py-0.5\"\n      style={{ marginLeft: `${level * 1}rem` }}\n      dropHighlight={canDrop && dropHighlight}\n      onDragOver={canDrop ? onDragOver : undefined}\n      onDragEnter={canDrop ? onDragEnter : undefined}\n      onDragLeave={canDrop ? onDragLeave : undefined}\n      onDrop={canDrop ? onDrop : undefined}\n    />\n  );\n}\n\nexport default function AllLists({\n  initialData,\n}: {\n  initialData: { lists: ZBookmarkList[] };\n}) {\n  const { t } = useTranslation();\n  const pathName = usePathname();\n  const isNodeOpen = useCallback(\n    (node: ZBookmarkListTreeNode) => pathName.includes(node.item.id),\n    [pathName],\n  );\n\n  const [selectedListId, setSelectedListId] = useState<string | null>(null);\n\n  // Fetch live lists data\n  const { data: listsData } = useBookmarkLists(undefined, {\n    initialData: { lists: initialData.lists },\n  });\n  const lists = augmentBookmarkListsWithInitialData(\n    listsData,\n    initialData.lists,\n  );\n\n  // Check if any shared list is currently being viewed\n  const isViewingSharedList = useMemo(() => {\n    return lists.data.some(\n      (list) => list.userRole !== \"owner\" && pathName.includes(list.id),\n    );\n  }, [lists.data, pathName]);\n\n  // Check if there are any shared lists\n  const hasSharedLists = useMemo(() => {\n    return lists.data.some((list) => list.userRole !== \"owner\");\n  }, [lists.data]);\n\n  const [sharedListsOpen, setSharedListsOpen] = useState(isViewingSharedList);\n\n  // Auto-open shared lists if viewing one\n  useEffect(() => {\n    if (isViewingSharedList && !sharedListsOpen) {\n      setSharedListsOpen(true);\n    }\n  }, [isViewingSharedList, sharedListsOpen]);\n\n  return (\n    <ul className=\"sidebar-scrollbar max-h-full gap-y-2 overflow-auto text-sm\">\n      <li className=\"flex justify-between pb-3\">\n        <p className=\"text-xs uppercase tracking-wider text-muted-foreground\">\n          Lists\n        </p>\n        <EditListModal>\n          <Link href=\"#\">\n            <Plus\n              className=\"mr-2 size-4 text-muted-foreground\"\n              strokeWidth={1.5}\n            />\n          </Link>\n        </EditListModal>\n      </li>\n      <SidebarItem\n        logo={<span className=\"text-lg\">📋</span>}\n        name={t(\"lists.all_lists\")}\n        path={`/dashboard/lists`}\n        linkClassName=\"py-0.5\"\n        className=\"px-0.5\"\n        right={<InvitationNotificationBadge />}\n      />\n      <SidebarItem\n        logo={<span className=\"text-lg\">⭐️</span>}\n        name={t(\"lists.favourites\")}\n        path={`/dashboard/favourites`}\n        linkClassName=\"py-0.5\"\n        className=\"px-0.5\"\n      />\n\n      {/* Owned Lists */}\n      <CollapsibleBookmarkLists\n        listsData={lists}\n        filter={(node) => node.item.userRole === \"owner\"}\n        isOpenFunc={isNodeOpen}\n        render={({ node, level, open, numBookmarks }) => (\n          <DroppableListSidebarItem\n            node={node}\n            level={level}\n            open={open}\n            numBookmarks={numBookmarks}\n            selectedListId={selectedListId}\n            setSelectedListId={setSelectedListId}\n          />\n        )}\n      />\n\n      {/* Shared Lists */}\n      {hasSharedLists && (\n        <Collapsible open={sharedListsOpen} onOpenChange={setSharedListsOpen}>\n          <SidebarItem\n            collapseButton={\n              <CollapsibleTriggerTriangle\n                className=\"absolute left-0.5 top-1/2 size-2 -translate-y-1/2\"\n                open={sharedListsOpen}\n              />\n            }\n            logo={<span className=\"text-lg\">👥</span>}\n            name={t(\"lists.shared_lists\")}\n            path=\"#\"\n            linkClassName=\"py-0.5\"\n            className=\"px-0.5\"\n          />\n          <CollapsibleContent>\n            <CollapsibleBookmarkLists\n              listsData={lists}\n              filter={(node) => node.item.userRole !== \"owner\"}\n              isOpenFunc={isNodeOpen}\n              indentOffset={1}\n              render={({ node, level, open, numBookmarks }) => (\n                <DroppableListSidebarItem\n                  node={node}\n                  level={level}\n                  open={open}\n                  numBookmarks={numBookmarks}\n                  selectedListId={selectedListId}\n                  setSelectedListId={setSelectedListId}\n                />\n              )}\n            />\n          </CollapsibleContent>\n        </Collapsible>\n      )}\n    </ul>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/sidebar/InvitationNotificationBadge.tsx",
    "content": "\"use client\";\n\nimport { useQuery } from \"@tanstack/react-query\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\nexport function InvitationNotificationBadge() {\n  const api = useTRPC();\n  const { data: pendingInvitations } = useQuery(\n    api.lists.getPendingInvitations.queryOptions(undefined, {\n      refetchInterval: 1000 * 60 * 5,\n    }),\n  );\n  const pendingInvitationsCount = pendingInvitations?.length ?? 0;\n\n  if (pendingInvitationsCount === 0) {\n    return null;\n  }\n\n  return (\n    <div className=\"flex items-center px-1\">\n      <span className=\"rounded-full bg-blue-500 px-2 py-0.5 text-center text-xs text-white\">\n        {pendingInvitationsCount}\n      </span>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/tags/AllTagsView.tsx",
    "content": "\"use client\";\n\nimport React, { useEffect } from \"react\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport ActionConfirmingDialog from \"@/components/ui/action-confirming-dialog\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport InfoTooltip from \"@/components/ui/info-tooltip\";\nimport { Input } from \"@/components/ui/input\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport { toast } from \"@/components/ui/sonner\";\nimport Spinner from \"@/components/ui/spinner\";\nimport { Toggle } from \"@/components/ui/toggle\";\nimport useBulkTagActionsStore from \"@/lib/bulkTagActions\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { ArrowDownAZ, ChevronDown, Combine, Search, Tag } from \"lucide-react\";\nimport { parseAsStringEnum, useQueryState } from \"nuqs\";\n\nimport type { ZGetTagResponse, ZTagBasic } from \"@karakeep/shared/types/tags\";\nimport {\n  useDeleteUnusedTags,\n  usePaginatedSearchTags,\n} from \"@karakeep/shared-react/hooks/tags\";\nimport { useDebounce } from \"@karakeep/shared-react/hooks/use-debounce\";\n\nimport BulkTagAction from \"./BulkTagAction\";\nimport { CreateTagModal } from \"./CreateTagModal\";\nimport DeleteTagConfirmationDialog from \"./DeleteTagConfirmationDialog\";\nimport { MultiTagSelector } from \"./MultiTagSelector\";\nimport { TagPill } from \"./TagPill\";\n\nfunction DeleteAllUnusedTags({ numUnusedTags }: { numUnusedTags: number }) {\n  const { t } = useTranslation();\n  const { mutate, isPending } = useDeleteUnusedTags({\n    onSuccess: () => {\n      toast({\n        description: `Deleted all ${numUnusedTags} unused tags`,\n      });\n    },\n    onError: () => {\n      toast({\n        description: \"Something went wrong\",\n        variant: \"destructive\",\n      });\n    },\n  });\n  return (\n    <ActionConfirmingDialog\n      title={t(\"tags.delete_all_unused_tags\")}\n      description={`Are you sure you want to delete the ${numUnusedTags} unused tags?`}\n      actionButton={() => (\n        <ActionButton\n          variant=\"destructive\"\n          loading={isPending}\n          onClick={() => mutate()}\n        >\n          DELETE THEM ALL\n        </ActionButton>\n      )}\n    >\n      <Button variant=\"destructive\" disabled={numUnusedTags == 0}>\n        {t(\"tags.delete_all_unused_tags\")}\n      </Button>\n    </ActionConfirmingDialog>\n  );\n}\n\nexport default function AllTagsView() {\n  const { t } = useTranslation();\n\n  const [searchQueryRaw, setSearchQuery] = useQueryState(\"q\", {\n    defaultValue: \"\",\n  });\n  const searchQuery = useDebounce(searchQueryRaw, 100);\n  const [sortBy, setSortBy] = useQueryState<\"name\" | \"usage\" | \"relevance\">(\n    \"sort\",\n    parseAsStringEnum([\"name\", \"usage\", \"relevance\"])\n      .withOptions({\n        clearOnDefault: true,\n      })\n      .withDefault(\"usage\"),\n  );\n  const hasActiveSearch = searchQuery.length > 0;\n  const [draggingEnabled, setDraggingEnabled] = React.useState(false);\n\n  const [selectedTag, setSelectedTag] = React.useState<ZTagBasic | null>(null);\n  const isDialogOpen = !!selectedTag;\n\n  const { setVisibleTagIds, isBulkEditEnabled } = useBulkTagActionsStore();\n\n  const handleOpenDialog = React.useCallback((tag: ZTagBasic) => {\n    setSelectedTag(tag);\n  }, []);\n\n  function toggleDraggingEnabled(): void {\n    setDraggingEnabled(!draggingEnabled);\n  }\n\n  const {\n    data: allHumanTagsRaw,\n    isFetching: isHumanTagsFetching,\n    isLoading: isHumanTagsLoading,\n    hasNextPage: hasNextPageHumanTags,\n    fetchNextPage: fetchNextPageHumanTags,\n    isFetchingNextPage: isFetchingNextPageHumanTags,\n  } = usePaginatedSearchTags({\n    nameContains: searchQuery,\n    sortBy,\n    attachedBy: \"human\",\n    limit: 50,\n  });\n\n  const {\n    data: allAiTagsRaw,\n    isFetching: isAiTagsFetching,\n    isLoading: isAiTagsLoading,\n    hasNextPage: hasNextPageAiTags,\n    fetchNextPage: fetchNextPageAiTags,\n    isFetchingNextPage: isFetchingNextPageAiTags,\n  } = usePaginatedSearchTags({\n    nameContains: searchQuery,\n    sortBy,\n    attachedBy: \"ai\",\n    limit: 50,\n  });\n\n  const {\n    data: allEmptyTagsRaw,\n    isFetching: isEmptyTagsFetching,\n    isLoading: isEmptyTagsLoading,\n    hasNextPage: hasNextPageEmptyTags,\n    fetchNextPage: fetchNextPageEmptyTags,\n    isFetchingNextPage: isFetchingNextPageEmptyTags,\n  } = usePaginatedSearchTags({\n    nameContains: searchQuery,\n    sortBy,\n    attachedBy: \"none\",\n    limit: 50,\n  });\n\n  const isFetching =\n    isHumanTagsFetching || isAiTagsFetching || isEmptyTagsFetching;\n\n  const { allHumanTags, allAiTags, allEmptyTags } = React.useMemo(() => {\n    return {\n      allHumanTags: allHumanTagsRaw?.tags ?? [],\n      allAiTags: allAiTagsRaw?.tags ?? [],\n      allEmptyTags: allEmptyTagsRaw?.tags ?? [],\n    };\n  }, [allHumanTagsRaw, allAiTagsRaw, allEmptyTagsRaw]);\n\n  useEffect(() => {\n    const allTags = [...allHumanTags, ...allAiTags, ...allEmptyTags];\n    setVisibleTagIds(allTags.map((tag) => tag.id) ?? []);\n    return () => {\n      setVisibleTagIds([]);\n    };\n  }, [allHumanTags, allAiTags, allEmptyTags, setVisibleTagIds]);\n\n  const sortLabels: Record<typeof sortBy, string> = {\n    name: t(\"tags.sort_by_name\"),\n    usage: t(\"tags.sort_by_usage\"),\n    relevance: t(\"tags.sort_by_relevance\"),\n  };\n\n  const tagsToPill = React.useMemo(\n    () =>\n      (\n        tags: ZGetTagResponse[],\n        bulkEditEnabled: boolean,\n        {\n          emptyMessage,\n          searchEmptyMessage,\n        }: { emptyMessage: string; searchEmptyMessage: string },\n        isLoading: boolean,\n      ) => {\n        if (isLoading && tags.length === 0) {\n          return (\n            <div className=\"flex flex-wrap gap-3\">\n              {Array.from({ length: 15 }).map((_, index) => (\n                <Skeleton key={`tag-skeleton-${index}`} className=\"h-9 w-24\" />\n              ))}\n            </div>\n          );\n        }\n\n        if (tags.length === 0) {\n          return (\n            <div className=\"py-8 text-center\">\n              <Tag className=\"mx-auto mb-4 h-12 w-12 text-gray-300\" />\n              <p className=\"mb-4 text-gray-500\">\n                {hasActiveSearch ? searchEmptyMessage : emptyMessage}\n              </p>\n            </div>\n          );\n        }\n\n        return (\n          <div className=\"flex flex-wrap gap-3\">\n            {tags.map((t) =>\n              bulkEditEnabled ? (\n                <MultiTagSelector\n                  key={t.id}\n                  id={t.id}\n                  name={t.name}\n                  count={t.numBookmarks}\n                />\n              ) : (\n                <TagPill\n                  key={t.id}\n                  id={t.id}\n                  name={t.name}\n                  count={t.numBookmarks}\n                  isDraggable={draggingEnabled}\n                  onOpenDialog={handleOpenDialog}\n                />\n              ),\n            )}\n            {isLoading &&\n              Array.from({ length: 3 }).map((_, index) => (\n                <Skeleton\n                  key={`tag-skeleton-loading-${index}`}\n                  className=\"h-9 w-24\"\n                />\n              ))}\n          </div>\n        );\n      },\n    [draggingEnabled, handleOpenDialog, hasActiveSearch],\n  );\n  return (\n    <div className=\"flex flex-col gap-4\">\n      {selectedTag && (\n        <DeleteTagConfirmationDialog\n          tag={selectedTag}\n          open={isDialogOpen}\n          setOpen={(o) => {\n            if (!o) {\n              setSelectedTag(null);\n            }\n          }}\n        />\n      )}\n      <div className=\"flex flex-col gap-4\">\n        <div className=\"flex flex-wrap items-center justify-between gap-x-2 gap-y-3\">\n          <span className=\"text-2xl\">{t(\"tags.all_tags\")}</span>\n          <div className=\"flex flex-wrap items-center justify-end gap-2\">\n            <CreateTagModal />\n            <BulkTagAction />\n            <Toggle\n              variant=\"outline\"\n              className=\"bg-background\"\n              aria-label={t(\"tags.drag_and_drop_merging\")}\n              pressed={draggingEnabled}\n              onPressedChange={toggleDraggingEnabled}\n              disabled={isBulkEditEnabled}\n            >\n              <Combine className=\"mr-2 size-4\" />\n              {t(\"tags.drag_and_drop_merging\")}\n              <InfoTooltip size={15} className=\"my-auto ml-2\" variant=\"explain\">\n                <p>{t(\"tags.drag_and_drop_merging_info\")}</p>\n              </InfoTooltip>\n            </Toggle>\n          </div>\n        </div>\n        <div className=\"flex flex-col gap-3\">\n          <div className=\"flex w-full items-center gap-2\">\n            <div className=\"flex-1\">\n              <Input\n                type=\"search\"\n                value={searchQueryRaw}\n                onChange={(event) => setSearchQuery(event.target.value)}\n                placeholder={t(\"common.search\")}\n                aria-label={t(\"common.search\")}\n                startIcon={<Search className=\"h-4 w-4 text-muted-foreground\" />}\n                endIcon={isFetching && <Spinner className=\"h-4 w-4\" />}\n                autoComplete=\"off\"\n                className=\"h-10\"\n              />\n            </div>\n            <DropdownMenu>\n              <DropdownMenuTrigger asChild>\n                <Button\n                  variant=\"outline\"\n                  className=\"flex-shrink-0 bg-background\"\n                >\n                  <ArrowDownAZ className=\"mr-2 size-4\" />\n                  <span className=\"mr-1 text-sm\">\n                    {t(\"actions.sort.title\")}\n                  </span>\n                  <span className=\"hidden text-sm font-medium sm:inline\">\n                    {sortLabels[sortBy]}\n                  </span>\n                  <ChevronDown className=\"ml-2 size-4\" />\n                </Button>\n              </DropdownMenuTrigger>\n              <DropdownMenuContent align=\"end\" className=\"w-48\">\n                <DropdownMenuRadioGroup\n                  value={sortBy}\n                  onValueChange={(value) => setSortBy(value as typeof sortBy)}\n                >\n                  <DropdownMenuRadioItem value=\"usage\">\n                    {sortLabels[\"usage\"]}\n                  </DropdownMenuRadioItem>\n                  <DropdownMenuRadioItem value=\"name\">\n                    {sortLabels[\"name\"]}\n                  </DropdownMenuRadioItem>\n                  <DropdownMenuRadioItem value=\"relevance\">\n                    {sortLabels[\"relevance\"]}\n                  </DropdownMenuRadioItem>\n                </DropdownMenuRadioGroup>\n              </DropdownMenuContent>\n            </DropdownMenu>\n          </div>\n        </div>\n      </div>\n      <Card>\n        <CardHeader>\n          <CardTitle className=\"flex items-center gap-2\">\n            <span>{t(\"tags.your_tags\")}</span>\n            <Badge variant=\"secondary\">\n              {allHumanTags.length}\n              {hasNextPageHumanTags ? \"+\" : \"\"}\n            </Badge>\n          </CardTitle>\n          <CardDescription>{t(\"tags.your_tags_info\")}</CardDescription>\n        </CardHeader>\n        <CardContent className=\"flex flex-col gap-4\">\n          {tagsToPill(\n            allHumanTags,\n            isBulkEditEnabled,\n            {\n              emptyMessage: t(\"tags.no_custom_tags\"),\n              searchEmptyMessage: t(\"tags.no_tags_match_your_search\"),\n            },\n            isHumanTagsLoading,\n          )}\n          {hasNextPageHumanTags && (\n            <ActionButton\n              variant=\"secondary\"\n              onClick={() => fetchNextPageHumanTags()}\n              loading={isFetchingNextPageHumanTags}\n              ignoreDemoMode\n            >\n              {t(\"actions.load_more\")}\n            </ActionButton>\n          )}\n        </CardContent>\n      </Card>\n      <Card>\n        <CardHeader>\n          <CardTitle className=\"flex items-center gap-2\">\n            <span>{t(\"tags.ai_tags\")}</span>\n            <Badge variant=\"secondary\">\n              {allAiTags.length}\n              {hasNextPageAiTags ? \"+\" : \"\"}\n            </Badge>\n          </CardTitle>\n          <CardDescription>{t(\"tags.ai_tags_info\")}</CardDescription>\n        </CardHeader>\n        <CardContent className=\"flex flex-col gap-4\">\n          {tagsToPill(\n            allAiTags,\n            isBulkEditEnabled,\n            {\n              emptyMessage: t(\"tags.no_ai_tags\"),\n              searchEmptyMessage: t(\"tags.no_tags_match_your_search\"),\n            },\n            isAiTagsLoading,\n          )}\n          {hasNextPageAiTags && (\n            <ActionButton\n              variant=\"secondary\"\n              onClick={() => fetchNextPageAiTags()}\n              loading={isFetchingNextPageAiTags}\n              ignoreDemoMode\n            >\n              {t(\"actions.load_more\")}\n            </ActionButton>\n          )}\n        </CardContent>\n      </Card>\n      <Card>\n        <CardHeader>\n          <CardTitle className=\"flex items-center gap-2\">\n            <span>{t(\"tags.unused_tags\")}</span>\n            <Badge variant=\"secondary\">\n              {allEmptyTags.length}\n              {hasNextPageEmptyTags ? \"+\" : \"\"}\n            </Badge>\n          </CardTitle>\n          <CardDescription>{t(\"tags.unused_tags_info\")}</CardDescription>\n        </CardHeader>\n        <CardContent className=\"flex flex-col gap-4\">\n          {tagsToPill(\n            allEmptyTags,\n            isBulkEditEnabled,\n            {\n              emptyMessage: t(\"tags.no_unused_tags\"),\n              searchEmptyMessage: t(\"tags.no_unused_tags_match_your_search\"),\n            },\n            isEmptyTagsLoading,\n          )}\n          {hasNextPageEmptyTags && (\n            <ActionButton\n              variant=\"secondary\"\n              onClick={() => fetchNextPageEmptyTags()}\n              loading={isFetchingNextPageEmptyTags}\n              ignoreDemoMode\n            >\n              {t(\"actions.load_more\")}\n            </ActionButton>\n          )}\n          {allEmptyTags.length > 0 && (\n            <DeleteAllUnusedTags numUnusedTags={allEmptyTags.length} />\n          )}\n        </CardContent>\n      </Card>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/tags/BulkTagAction.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport ActionConfirmingDialog from \"@/components/ui/action-confirming-dialog\";\nimport { ButtonWithTooltip } from \"@/components/ui/button\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { Toggle } from \"@/components/ui/toggle\";\nimport useBulkTagActionsStore from \"@/lib/bulkTagActions\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { CheckCheck, Pencil, Trash2, X } from \"lucide-react\";\n\nimport { useDeleteTag } from \"@karakeep/shared-react/hooks/tags\";\nimport { limitConcurrency } from \"@karakeep/shared/concurrency\";\n\nconst MAX_CONCURRENT_BULK_ACTIONS = 50;\n\nexport default function BulkTagAction() {\n  const { t } = useTranslation();\n\n  const {\n    selectedTagIds,\n    isBulkEditEnabled,\n    selectAll: selectAllTags,\n    unSelectAll: unSelectAllTags,\n    isEverythingSelected,\n    setIsBulkEditEnabled,\n  } = useBulkTagActionsStore();\n\n  const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);\n\n  useEffect(() => {\n    return () => {\n      setIsBulkEditEnabled(false);\n    };\n  }, []);\n\n  const onError = () => {\n    toast({\n      variant: \"destructive\",\n      title: t(\"common.something_went_wrong\"),\n      description: \"There was a problem with your request.\",\n    });\n  };\n\n  const deleteTagMutator = useDeleteTag({\n    onSuccess: () => {\n      setIsBulkEditEnabled(false);\n    },\n    onError,\n  });\n\n  const deleteSelectedTags = async () => {\n    await Promise.all(\n      limitConcurrency(\n        selectedTagIds.map(\n          (item) => () => deleteTagMutator.mutateAsync({ tagId: item }),\n        ),\n        MAX_CONCURRENT_BULK_ACTIONS,\n      ),\n    );\n    toast({\n      description: `${selectedTagIds.length} tags have been deleted!`,\n    });\n    setIsDeleteDialogOpen(false);\n  };\n\n  const actionList = [\n    {\n      name: isEverythingSelected()\n        ? t(\"actions.unselect_all\")\n        : t(\"actions.select_all\"),\n      icon: (\n        <p className=\"flex items-center gap-2\">\n          ( <CheckCheck size={18} /> {selectedTagIds.length} )\n        </p>\n      ),\n      action: () =>\n        isEverythingSelected() ? unSelectAllTags() : selectAllTags(),\n      alwaysEnable: true,\n    },\n    {\n      name: t(\"actions.delete\"),\n      icon: <Trash2 size={18} color=\"red\" />,\n      action: () => setIsDeleteDialogOpen(true),\n    },\n    {\n      name: t(\"actions.close_bulk_edit\"),\n      icon: <X size={18} />,\n      action: () => setIsBulkEditEnabled(false),\n      alwaysEnable: true,\n    },\n  ];\n\n  return (\n    <div>\n      <ActionConfirmingDialog\n        open={isDeleteDialogOpen}\n        setOpen={setIsDeleteDialogOpen}\n        title={\"Delete Tags\"}\n        description={<p>Are you sure you want to delete these tags?</p>}\n        actionButton={() => (\n          <ActionButton\n            type=\"button\"\n            variant=\"destructive\"\n            loading={deleteTagMutator.isPending}\n            onClick={() => deleteSelectedTags()}\n          >\n            {t(\"actions.delete\")}\n          </ActionButton>\n        )}\n      />\n\n      {!isBulkEditEnabled ? (\n        <Toggle\n          variant=\"outline\"\n          className=\"bg-background\"\n          aria-label=\"Toggle bulk edit\"\n          pressed={isBulkEditEnabled}\n          onPressedChange={setIsBulkEditEnabled}\n        >\n          <Pencil className=\"mr-2 size-4\" />\n          {t(\"actions.bulk_edit\")}\n        </Toggle>\n      ) : (\n        <div className=\"flex items-center rounded-md bg-background\">\n          {actionList.map(({ name, icon, action, alwaysEnable }) => (\n            <ButtonWithTooltip\n              tooltip={name}\n              disabled={!selectedTagIds.length && !alwaysEnable}\n              delayDuration={100}\n              variant=\"none\"\n              key={name}\n              onClick={action}\n            >\n              {icon}\n            </ButtonWithTooltip>\n          ))}\n        </div>\n      )}\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/tags/CreateTagModal.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@/components/ui/dialog\";\nimport {\n  Form,\n  FormControl,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@/components/ui/form\";\nimport { Input } from \"@/components/ui/input\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { Plus } from \"lucide-react\";\nimport { useForm } from \"react-hook-form\";\nimport { z } from \"zod\";\n\nimport { useCreateTag } from \"@karakeep/shared-react/hooks/tags\";\n\nconst formSchema = z.object({\n  name: z.string().trim().min(1, \"Tag name is required\"),\n});\n\nexport function CreateTagModal() {\n  const { t } = useTranslation();\n  const [open, setOpen] = useState(false);\n\n  const form = useForm<z.infer<typeof formSchema>>({\n    resolver: zodResolver(formSchema),\n    defaultValues: {\n      name: \"\",\n    },\n  });\n\n  const { mutate: createTag, isPending } = useCreateTag({\n    onSuccess: () => {\n      toast({\n        description: t(\"toasts.tags.created\"),\n      });\n      setOpen(false);\n      form.reset();\n    },\n    onError: (e) => {\n      if (e.data?.code === \"BAD_REQUEST\") {\n        if (e.data.zodError) {\n          toast({\n            variant: \"destructive\",\n            description: Object.values(e.data.zodError.fieldErrors)\n              .flat()\n              .join(\"\\n\"),\n          });\n        } else {\n          toast({\n            variant: \"destructive\",\n            description: e.message,\n          });\n        }\n      } else {\n        toast({\n          variant: \"destructive\",\n          title: t(\"common.something_went_wrong\"),\n          description: t(\"toasts.tags.failed_to_create\"),\n        });\n      }\n    },\n  });\n\n  return (\n    <Dialog\n      open={open}\n      onOpenChange={(isOpen) => {\n        setOpen(isOpen);\n        if (!isOpen) {\n          form.reset();\n        }\n      }}\n    >\n      <DialogTrigger asChild>\n        <Button variant=\"outline\" className=\"bg-background\">\n          <Plus className=\"mr-2 size-4\" />\n          {t(\"tags.create_tag\")}\n        </Button>\n      </DialogTrigger>\n      <DialogContent>\n        <Form {...form}>\n          <form\n            onSubmit={form.handleSubmit((values) => {\n              createTag(values);\n            })}\n          >\n            <DialogHeader>\n              <DialogTitle>{t(\"tags.create_tag\")}</DialogTitle>\n              <DialogDescription>\n                {t(\"tags.create_tag_description\")}\n              </DialogDescription>\n            </DialogHeader>\n            <div className=\"py-4\">\n              <FormField\n                control={form.control}\n                name=\"name\"\n                render={({ field }) => (\n                  <FormItem>\n                    <FormLabel>{t(\"tags.tag_name\")}</FormLabel>\n                    <FormControl>\n                      <Input\n                        placeholder={t(\"tags.enter_tag_name\")}\n                        autoFocus\n                        {...field}\n                      />\n                    </FormControl>\n                    <FormMessage />\n                  </FormItem>\n                )}\n              />\n            </div>\n            <DialogFooter>\n              <DialogClose asChild>\n                <Button type=\"button\" variant=\"outline\">\n                  {t(\"actions.cancel\")}\n                </Button>\n              </DialogClose>\n              <ActionButton type=\"submit\" loading={isPending}>\n                {t(\"actions.create\")}\n              </ActionButton>\n            </DialogFooter>\n          </form>\n        </Form>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/tags/DeleteTagConfirmationDialog.tsx",
    "content": "import { usePathname, useRouter } from \"next/navigation\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport ActionConfirmingDialog from \"@/components/ui/action-confirming-dialog\";\nimport { toast } from \"@/components/ui/sonner\";\n\nimport { useDeleteTag } from \"@karakeep/shared-react/hooks/tags\";\n\nexport default function DeleteTagConfirmationDialog({\n  tag,\n  open,\n  setOpen,\n}: {\n  tag: { id: string; name: string };\n  open?: boolean;\n  setOpen?: (v: boolean) => void;\n}) {\n  const currentPath = usePathname();\n  const router = useRouter();\n  const { mutate: deleteTag, isPending } = useDeleteTag({\n    onSuccess: () => {\n      toast({\n        description: `Tag \"${tag.name}\" has been deleted!`,\n      });\n      if (currentPath.includes(tag.id)) {\n        router.push(\"/dashboard/tags\");\n      }\n    },\n    onError: () => {\n      toast({\n        variant: \"destructive\",\n        description: `Something went wrong`,\n      });\n    },\n  });\n  return (\n    <ActionConfirmingDialog\n      open={open}\n      setOpen={setOpen}\n      title={`Delete ${tag.name}?`}\n      description={`Are you sure you want to delete the tag \"${tag.name}\"?`}\n      actionButton={(setDialogOpen) => (\n        <ActionButton\n          type=\"button\"\n          variant=\"destructive\"\n          loading={isPending}\n          onClick={() =>\n            deleteTag(\n              { tagId: tag.id },\n              { onSuccess: () => setDialogOpen(false) },\n            )\n          }\n        >\n          Delete\n        </ActionButton>\n      )}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/tags/EditableTagName.tsx",
    "content": "\"use client\";\n\nimport { usePathname, useRouter } from \"next/navigation\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { cn } from \"@/lib/utils\";\n\nimport { useUpdateTag } from \"@karakeep/shared-react/hooks/tags\";\n\nimport { EditableText } from \"../EditableText\";\n\nexport default function EditableTagName({\n  tag,\n  className,\n}: {\n  tag: { id: string; name: string };\n  className?: string;\n}) {\n  const router = useRouter();\n  const currentPath = usePathname();\n  const { mutate: updateTag, isPending } = useUpdateTag({\n    onSuccess: () => {\n      toast({\n        description: \"Tag updated!\",\n      });\n      if (currentPath.includes(tag.id)) {\n        router.refresh();\n      }\n    },\n  });\n  return (\n    <EditableText\n      viewClassName={className}\n      editClassName={cn(\"p-2\", className)}\n      originalText={tag.name}\n      onSave={(newName) => {\n        if (!newName || newName == \"\") {\n          toast({\n            description: \"You must set a name for the tag!\",\n            variant: \"destructive\",\n          });\n          return;\n        }\n        updateTag(\n          {\n            tagId: tag.id,\n            name: newName,\n          },\n          {\n            onError: (e) => {\n              toast({\n                description: e.message,\n                variant: \"destructive\",\n              });\n            },\n          },\n        );\n      }}\n      isSaving={isPending}\n    />\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/tags/MergeTagModal.tsx",
    "content": "import { usePathname, useRouter } from \"next/navigation\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@/components/ui/dialog\";\nimport {\n  Form,\n  FormControl,\n  FormField,\n  FormItem,\n  FormMessage,\n} from \"@/components/ui/form\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { useForm } from \"react-hook-form\";\nimport { z } from \"zod\";\n\nimport { useMergeTag } from \"@karakeep/shared-react/hooks/tags\";\n\nimport { TagAutocomplete } from \"./TagAutocomplete\";\n\nexport function MergeTagModal({\n  open,\n  setOpen,\n  tag,\n  children,\n}: {\n  open: boolean;\n  setOpen: (v: boolean) => void;\n  tag: { id: string; name: string };\n  children?: React.ReactNode;\n}) {\n  const currentPath = usePathname();\n  const router = useRouter();\n  const formSchema = z.object({\n    intoTagId: z.string(),\n  });\n  const form = useForm<z.infer<typeof formSchema>>({\n    resolver: zodResolver(formSchema),\n    defaultValues: {\n      intoTagId: undefined,\n    },\n  });\n\n  const { mutate: mergeTag, isPending } = useMergeTag({\n    onSuccess: (resp) => {\n      toast({\n        description: \"Tag has been updated!\",\n      });\n      setOpen(false);\n      if (currentPath.includes(tag.id)) {\n        router.push(`/dashboard/tags/${resp.mergedIntoTagId}`);\n      }\n    },\n    onError: (e) => {\n      if (e.data?.code == \"BAD_REQUEST\") {\n        if (e.data.zodError) {\n          toast({\n            variant: \"destructive\",\n            description: Object.values(e.data.zodError.fieldErrors)\n              .flat()\n              .join(\"\\n\"),\n          });\n        } else {\n          toast({\n            variant: \"destructive\",\n            description: e.message,\n          });\n        }\n      } else {\n        toast({\n          variant: \"destructive\",\n          title: \"Something went wrong\",\n        });\n      }\n    },\n  });\n\n  return (\n    <Dialog\n      open={open}\n      onOpenChange={(s) => {\n        form.reset();\n        setOpen(s);\n      }}\n    >\n      {children && <DialogTrigger asChild>{children}</DialogTrigger>}\n      <DialogContent>\n        <Form {...form}>\n          <form\n            onSubmit={form.handleSubmit((value) => {\n              mergeTag({\n                fromTagIds: [tag.id],\n                intoTagId: value.intoTagId,\n              });\n            })}\n          >\n            <DialogHeader>\n              <DialogTitle>Merge Tag</DialogTitle>\n            </DialogHeader>\n\n            <DialogDescription className=\"pt-4\">\n              You&apos;re about to move all the bookmarks in the tag &quot;\n              {tag.name}&quot; into the tag you select.\n            </DialogDescription>\n\n            <FormField\n              control={form.control}\n              name=\"intoTagId\"\n              render={({ field }) => {\n                return (\n                  <FormItem className=\"grow py-4\">\n                    <FormControl>\n                      <TagAutocomplete\n                        tagId={field.value}\n                        onChange={field.onChange}\n                        className=\"w-full\"\n                      />\n                    </FormControl>\n                    <FormMessage />\n                  </FormItem>\n                );\n              }}\n            />\n            <DialogFooter className=\"sm:justify-end\">\n              <DialogClose asChild>\n                <Button type=\"button\" variant=\"secondary\">\n                  Close\n                </Button>\n              </DialogClose>\n              <ActionButton type=\"submit\" loading={isPending}>\n                Save\n              </ActionButton>\n            </DialogFooter>\n          </form>\n        </Form>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/tags/MultiTagSelector.tsx",
    "content": "import React from \"react\";\nimport { Separator } from \"@/components/ui/separator\";\nimport useBulkTagActionsStore from \"@/lib/bulkTagActions\";\nimport { cn } from \"@/lib/utils\";\nimport { Check } from \"lucide-react\";\nimport { useTheme } from \"next-themes\";\n\nexport const MultiTagSelector = React.memo(function MultiTagSelector({\n  id,\n  name,\n  count,\n}: {\n  id: string;\n  name: string;\n  count: number;\n}) {\n  const toggleTag = useBulkTagActionsStore((state) => state.toggleTag);\n  const { theme } = useTheme();\n  const isSelected = useBulkTagActionsStore((state) => state.isTagSelected(id));\n\n  const getIconColor = () => {\n    if (theme === \"dark\") {\n      return isSelected ? \"black\" : \"white\";\n    }\n    return isSelected ? \"white\" : \"black\";\n  };\n\n  const getIconBackgroundColor = () => {\n    if (theme === \"dark\") {\n      return isSelected ? \"bg-white\" : \"bg-white bg-opacity-10\";\n    }\n    return isSelected ? \"bg-black\" : \"bg-white\";\n  };\n\n  const pill = (\n    <div className=\"group relative flex\">\n      <button\n        className={cn(\n          \"flex gap-2 rounded-md border border-border px-2 py-1\",\n          isSelected\n            ? \"bg-black bg-opacity-10\"\n            : \"bg-background text-foreground hover:bg-foreground hover:text-background\",\n        )}\n        data-id={id}\n        onClick={() => toggleTag(id)}\n      >\n        {name} <Separator orientation=\"vertical\" /> {count}\n        <div\n          className={cn(\n            \"absolute -right-1 -top-1 flex h-4 w-4 items-center justify-center rounded-full border border-gray-600\",\n            getIconBackgroundColor(),\n          )}\n        >\n          <Check size={12} color={getIconColor()} />\n        </div>\n      </button>\n    </div>\n  );\n\n  return pill;\n});\n"
  },
  {
    "path": "apps/web/components/dashboard/tags/TagAutocomplete.tsx",
    "content": "import React, { useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@/components/ui/command\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport LoadingSpinner from \"@/components/ui/spinner\";\nimport { cn } from \"@/lib/utils\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { Check, ChevronsUpDown, X } from \"lucide-react\";\n\nimport { useTagAutocomplete } from \"@karakeep/shared-react/hooks/tags\";\nimport { useDebounce } from \"@karakeep/shared-react/hooks/use-debounce\";\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\ninterface TagAutocompleteProps {\n  tagId: string;\n  onChange?: (value: string) => void;\n  className?: string;\n}\n\nexport function TagAutocomplete({\n  tagId,\n  onChange,\n  className,\n}: TagAutocompleteProps) {\n  const api = useTRPC();\n  const [open, setOpen] = useState(false);\n  const [searchQuery, setSearchQuery] = useState(\"\");\n  const searchQueryDebounced = useDebounce(searchQuery, 500);\n\n  const { data: tags, isLoading } = useTagAutocomplete({\n    nameContains: searchQueryDebounced,\n    select: (data) => data.tags,\n  });\n\n  const { data: selectedTag, isLoading: isSelectedTagLoading } = useQuery(\n    api.tags.get.queryOptions(\n      {\n        tagId,\n      },\n      {\n        select: ({ id, name }) => ({\n          id,\n          name,\n        }),\n        enabled: !!tagId,\n      },\n    ),\n  );\n\n  const handleSelect = (currentValue: string) => {\n    setOpen(false);\n    onChange?.(currentValue);\n  };\n\n  const clearSelection = () => {\n    onChange?.(\"\");\n  };\n\n  if (!tags || isLoading || isSelectedTagLoading) {\n    return <LoadingSpinner />;\n  }\n\n  return (\n    <Popover open={open} onOpenChange={setOpen}>\n      <PopoverTrigger asChild>\n        <Button\n          variant=\"outline\"\n          role=\"combobox\"\n          aria-expanded={open}\n          className={cn(\"justify-between\", className)}\n        >\n          {selectedTag ? (\n            <div className=\"flex w-full items-center justify-between\">\n              <span>{selectedTag.name}</span>\n              <X\n                className=\"h-4 w-4 shrink-0 cursor-pointer opacity-50 hover:opacity-100\"\n                onClick={(e) => {\n                  e.stopPropagation();\n                  clearSelection();\n                }}\n              />\n            </div>\n          ) : (\n            \"Select a tag...\"\n          )}\n          <ChevronsUpDown className=\"ml-2 h-4 w-4 shrink-0 opacity-50\" />\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent className=\"w-[--radix-popover-trigger-width] p-0\">\n        <Command shouldFilter={false}>\n          <CommandInput\n            placeholder=\"Search tags...\"\n            value={searchQuery}\n            onValueChange={setSearchQuery}\n            className={cn(\"h-9\", className)}\n          />\n          <CommandList>\n            <CommandEmpty>No tags found.</CommandEmpty>\n            <CommandGroup className=\"max-h-60 overflow-y-auto\">\n              {tags.map((tag) => (\n                <CommandItem\n                  key={tag.id}\n                  value={tag.id}\n                  onSelect={handleSelect}\n                  className=\"cursor-pointer\"\n                >\n                  <Check\n                    className={cn(\n                      \"mr-2 h-4 w-4\",\n                      selectedTag?.id === tag.id ? \"opacity-100\" : \"opacity-0\",\n                    )}\n                  />\n                  {tag.name}\n                </CommandItem>\n              ))}\n            </CommandGroup>\n          </CommandList>\n        </Command>\n      </PopoverContent>\n    </Popover>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/tags/TagOptions.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"@/components/ui/dropdown-menu\";\nimport { useShowArchived } from \"@/components/utils/useShowArchived\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { Combine, Square, SquareCheck, Trash2 } from \"lucide-react\";\n\nimport DeleteTagConfirmationDialog from \"./DeleteTagConfirmationDialog\";\nimport { MergeTagModal } from \"./MergeTagModal\";\n\nexport function TagOptions({\n  tag,\n  children,\n}: {\n  tag: { id: string; name: string };\n  children?: React.ReactNode;\n}) {\n  const { t } = useTranslation();\n  const { showArchived, onClickShowArchived } = useShowArchived();\n\n  const [deleteTagDialogOpen, setDeleteTagDialogOpen] = useState(false);\n  const [mergeTagDialogOpen, setMergeTagDialogOpen] = useState(false);\n\n  return (\n    <DropdownMenu>\n      <DeleteTagConfirmationDialog\n        tag={tag}\n        open={deleteTagDialogOpen}\n        setOpen={setDeleteTagDialogOpen}\n      />\n      <MergeTagModal\n        open={mergeTagDialogOpen}\n        setOpen={setMergeTagDialogOpen}\n        tag={tag}\n      />\n      <DropdownMenuTrigger asChild>{children}</DropdownMenuTrigger>\n      <DropdownMenuContent>\n        <DropdownMenuItem\n          className=\"flex gap-2\"\n          onClick={() => setMergeTagDialogOpen(true)}\n        >\n          <Combine className=\"size-4\" />\n          <span>{t(\"actions.merge\")}</span>\n        </DropdownMenuItem>\n        <DropdownMenuItem className=\"flex gap-2\" onClick={onClickShowArchived}>\n          {showArchived ? (\n            <SquareCheck className=\"size-4\" />\n          ) : (\n            <Square className=\"size-4\" />\n          )}\n          <span>{t(\"actions.toggle_show_archived\")}</span>\n        </DropdownMenuItem>\n        <DropdownMenuItem\n          className=\"flex gap-2\"\n          onClick={() => setDeleteTagDialogOpen(true)}\n        >\n          <Trash2 className=\"size-4\" />\n          <span>{t(\"actions.delete\")}</span>\n        </DropdownMenuItem>\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/dashboard/tags/TagPill.tsx",
    "content": "import React, { useRef, useState } from \"react\";\nimport Link from \"next/link\";\nimport { Button } from \"@/components/ui/button\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { useDragAndDrop } from \"@/lib/drag-and-drop\";\nimport { X } from \"lucide-react\";\nimport Draggable from \"react-draggable\";\n\nimport { useMergeTag } from \"@karakeep/shared-react/hooks/tags\";\n\nexport const TagPill = React.memo(function TagPill({\n  id,\n  name,\n  count,\n  isDraggable,\n  onOpenDialog,\n}: {\n  id: string;\n  name: string;\n  count: number;\n  isDraggable: boolean;\n  onOpenDialog: (tag: { id: string; name: string }) => void;\n}) {\n  const [isHovered, setIsHovered] = useState(false);\n  const draggableRef = useRef<HTMLDivElement>(null);\n\n  const handleMouseOver = () => setIsHovered(true);\n  const handleMouseOut = () => setIsHovered(false);\n\n  const { mutate: mergeTag } = useMergeTag({\n    onSuccess: () => {\n      toast({\n        description: \"Tags have been merged!\",\n      });\n    },\n    onError: (e) => {\n      if (e.data?.code == \"BAD_REQUEST\") {\n        if (e.data.zodError) {\n          toast({\n            variant: \"destructive\",\n            description: Object.values(e.data.zodError.fieldErrors)\n              .flat()\n              .join(\"\\n\"),\n          });\n        } else {\n          toast({\n            variant: \"destructive\",\n            description: e.message,\n          });\n        }\n      } else {\n        toast({\n          variant: \"destructive\",\n          title: \"Something went wrong\",\n        });\n      }\n    },\n  });\n\n  const dragAndDropFunction = useDragAndDrop(\n    \"data-id\",\n    (dragTargetId: string) => {\n      mergeTag({\n        fromTagIds: [id],\n        intoTagId: dragTargetId,\n      });\n    },\n  );\n\n  const pill = (\n    <div\n      className=\"group relative flex\"\n      onMouseOver={handleMouseOver}\n      onFocus={handleMouseOver}\n      onMouseOut={handleMouseOut}\n      onBlur={handleMouseOut}\n      ref={draggableRef}\n    >\n      <Link\n        className={\n          \"flex gap-2 rounded-md border border-border bg-background px-2 py-1 text-foreground hover:bg-foreground hover:text-background\"\n        }\n        href={`/dashboard/tags/${id}`}\n        data-id={id}\n        draggable={false}\n        prefetch={false}\n      >\n        {name} <Separator orientation=\"vertical\" /> {count}\n      </Link>\n\n      {isHovered && !isDraggable && (\n        <Button\n          size=\"none\"\n          variant=\"secondary\"\n          className=\"-translate-1/2 absolute -right-1 -top-1 hidden rounded-full group-hover:block\"\n          onClick={() => onOpenDialog({ id, name })}\n        >\n          <X className=\"size-3\" />\n        </Button>\n      )}\n    </div>\n  );\n  if (!isDraggable) {\n    return pill;\n  }\n  return (\n    <Draggable\n      key={id}\n      axis=\"both\"\n      onStart={dragAndDropFunction.handleDragStart}\n      onStop={dragAndDropFunction.handleDragEnd}\n      disabled={!isDraggable}\n      defaultClassNameDragging={\"position-relative z-10 pointer-events-none\"}\n      position={{ x: 0, y: 0 }}\n      nodeRef={draggableRef}\n    >\n      {pill}\n    </Draggable>\n  );\n});\n"
  },
  {
    "path": "apps/web/components/invite/InviteAcceptForm.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport { Alert, AlertDescription } from \"@/components/ui/alert\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\";\nimport {\n  Form,\n  FormControl,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@/components/ui/form\";\nimport { Input } from \"@/components/ui/input\";\nimport { signIn } from \"@/lib/auth/client\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { useMutation, useQuery } from \"@tanstack/react-query\";\nimport { TRPCClientError } from \"@trpc/client\";\nimport { AlertCircle, Clock, Loader2, Mail, UserPlus } from \"lucide-react\";\nimport { useForm } from \"react-hook-form\";\nimport { z } from \"zod\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\nconst inviteAcceptSchema = z\n  .object({\n    name: z.string().min(1, \"Name is required\"),\n    password: z.string().min(8, \"Password must be at least 8 characters\"),\n    confirmPassword: z\n      .string()\n      .min(8, \"Password must be at least 8 characters\"),\n  })\n  .refine((data) => data.password === data.confirmPassword, {\n    message: \"Passwords don't match\",\n    path: [\"confirmPassword\"],\n  });\n\ninterface InviteAcceptFormProps {\n  token: string;\n}\n\nexport default function InviteAcceptForm({ token }: InviteAcceptFormProps) {\n  const api = useTRPC();\n  const router = useRouter();\n\n  const form = useForm<z.infer<typeof inviteAcceptSchema>>({\n    resolver: zodResolver(inviteAcceptSchema),\n  });\n\n  const [errorMessage, setErrorMessage] = useState<string | null>(null);\n\n  const {\n    isPending: loading,\n    data: inviteData,\n    error,\n  } = useQuery(api.invites.get.queryOptions({ token }));\n\n  useEffect(() => {\n    if (error) {\n      setErrorMessage(error.message);\n    }\n  }, [error]);\n\n  const acceptInviteMutation = useMutation(\n    api.invites.accept.mutationOptions(),\n  );\n\n  const handleBackToSignIn = () => {\n    router.push(\"/signin\");\n  };\n\n  if (loading) {\n    return (\n      <Card className=\"w-full\">\n        <CardHeader className=\"text-center\">\n          <CardTitle className=\"text-2xl font-bold\">\n            Loading Invitation\n          </CardTitle>\n          <CardDescription>\n            Please wait while we verify your invitation...\n          </CardDescription>\n        </CardHeader>\n        <CardContent className=\"space-y-4\">\n          <div className=\"flex items-center justify-center\">\n            <Loader2 className=\"h-8 w-8 animate-spin text-primary\" />\n          </div>\n        </CardContent>\n      </Card>\n    );\n  }\n\n  if (!inviteData) {\n    return (\n      <Card className=\"w-full\">\n        <CardHeader className=\"text-center\">\n          <CardTitle className=\"text-2xl font-bold\">\n            Invalid Invitation\n          </CardTitle>\n          <CardDescription>\n            This invitation link is not valid or has been removed.\n          </CardDescription>\n        </CardHeader>\n        <CardContent className=\"space-y-4\">\n          <div className=\"flex items-center justify-center\">\n            <AlertCircle className=\"h-12 w-12 text-red-500\" />\n          </div>\n\n          {errorMessage && (\n            <Alert variant=\"destructive\">\n              <AlertCircle className=\"h-4 w-4\" />\n              <AlertDescription>{errorMessage}</AlertDescription>\n            </Alert>\n          )}\n\n          <Button onClick={handleBackToSignIn} className=\"w-full\">\n            Back to Sign In\n          </Button>\n        </CardContent>\n      </Card>\n    );\n  }\n\n  if (inviteData.expired) {\n    return (\n      <Card className=\"w-full\">\n        <CardHeader className=\"text-center\">\n          <CardTitle className=\"text-2xl font-bold\">\n            Invitation Expired\n          </CardTitle>\n          <CardDescription>\n            This invitation link has expired and is no longer valid.\n          </CardDescription>\n        </CardHeader>\n        <CardContent className=\"space-y-4\">\n          <div className=\"flex items-center justify-center\">\n            <Clock className=\"h-12 w-12 text-orange-500\" />\n          </div>\n\n          <div className=\"space-y-2 text-center\">\n            <p className=\"text-sm text-muted-foreground\">\n              Please contact an administrator to request a new invitation.\n            </p>\n          </div>\n\n          <Button\n            onClick={handleBackToSignIn}\n            variant=\"outline\"\n            className=\"w-full\"\n          >\n            Back to Sign In\n          </Button>\n        </CardContent>\n      </Card>\n    );\n  }\n\n  return (\n    <Card className=\"w-full\">\n      <CardHeader className=\"text-center\">\n        <CardTitle className=\"text-2xl font-bold\">\n          Accept Your Invitation\n        </CardTitle>\n        <CardDescription>\n          Complete your account setup to join Karakeep\n        </CardDescription>\n      </CardHeader>\n      <CardContent className=\"space-y-6\">\n        <div className=\"flex items-center justify-center\">\n          <UserPlus className=\"h-12 w-12 text-primary\" />\n        </div>\n\n        <div className=\"space-y-2 text-center\">\n          <div className=\"flex items-center justify-center space-x-2\">\n            <Mail className=\"h-4 w-4 text-muted-foreground\" />\n            <p className=\"text-sm text-muted-foreground\">Invited email:</p>\n          </div>\n          <p className=\"font-medium text-foreground\">{inviteData.email}</p>\n        </div>\n\n        <Form {...form}>\n          <form\n            onSubmit={form.handleSubmit(async (value) => {\n              try {\n                await acceptInviteMutation.mutateAsync({\n                  token,\n                  name: value.name,\n                  password: value.password,\n                });\n\n                // Sign in the user after successful account creation\n                const resp = await signIn(\"credentials\", {\n                  redirect: false,\n                  email: inviteData.email,\n                  password: value.password,\n                });\n\n                if (!resp || !resp.ok || resp.error) {\n                  setErrorMessage(\n                    resp?.error ??\n                      \"Account created but sign in failed. Please try signing in manually.\",\n                  );\n                  return;\n                }\n\n                router.replace(\"/\");\n              } catch (e) {\n                if (e instanceof TRPCClientError) {\n                  setErrorMessage(e.message);\n                } else {\n                  setErrorMessage(\"An unexpected error occurred\");\n                }\n              }\n            })}\n            className=\"space-y-4\"\n          >\n            {errorMessage && (\n              <Alert variant=\"destructive\">\n                <AlertCircle className=\"h-4 w-4\" />\n                <AlertDescription>{errorMessage}</AlertDescription>\n              </Alert>\n            )}\n\n            <FormField\n              control={form.control}\n              name=\"name\"\n              render={({ field }) => (\n                <FormItem>\n                  <FormLabel>Full Name</FormLabel>\n                  <FormControl>\n                    <Input\n                      type=\"text\"\n                      placeholder=\"Enter your full name\"\n                      {...field}\n                    />\n                  </FormControl>\n                  <FormMessage />\n                </FormItem>\n              )}\n            />\n\n            <FormField\n              control={form.control}\n              name=\"password\"\n              render={({ field }) => (\n                <FormItem>\n                  <FormLabel>Password</FormLabel>\n                  <FormControl>\n                    <Input\n                      type=\"password\"\n                      placeholder=\"Create a password\"\n                      {...field}\n                    />\n                  </FormControl>\n                  <FormMessage />\n                </FormItem>\n              )}\n            />\n\n            <FormField\n              control={form.control}\n              name=\"confirmPassword\"\n              render={({ field }) => (\n                <FormItem>\n                  <FormLabel>Confirm Password</FormLabel>\n                  <FormControl>\n                    <Input\n                      type=\"password\"\n                      placeholder=\"Confirm your password\"\n                      {...field}\n                    />\n                  </FormControl>\n                  <FormMessage />\n                </FormItem>\n              )}\n            />\n\n            <ActionButton\n              type=\"submit\"\n              loading={\n                form.formState.isSubmitting || acceptInviteMutation.isPending\n              }\n              className=\"w-full\"\n            >\n              {form.formState.isSubmitting || acceptInviteMutation.isPending ? (\n                <>\n                  <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n                  Creating Account...\n                </>\n              ) : (\n                \"Create Account & Sign In\"\n              )}\n            </ActionButton>\n\n            <Button\n              type=\"button\"\n              variant=\"ghost\"\n              onClick={handleBackToSignIn}\n              className=\"w-full\"\n            >\n              Back to Sign In\n            </Button>\n          </form>\n        </Form>\n      </CardContent>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/public/lists/PublicBookmarkGrid.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useMemo } from \"react\";\nimport Link from \"next/link\";\nimport BookmarkFormattedCreatedAt from \"@/components/dashboard/bookmarks/BookmarkFormattedCreatedAt\";\nimport { BookmarkMarkdownComponent } from \"@/components/dashboard/bookmarks/BookmarkMarkdownComponent\";\nimport FooterLinkURL from \"@/components/dashboard/bookmarks/FooterLinkURL\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport { badgeVariants } from \"@/components/ui/badge\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport { Dialog, DialogContent, DialogTrigger } from \"@/components/ui/dialog\";\nimport { cn } from \"@/lib/utils\";\nimport tailwindConfig from \"@/tailwind.config\";\nimport { useInfiniteQuery } from \"@tanstack/react-query\";\nimport { Expand, FileIcon, ImageIcon } from \"lucide-react\";\nimport { useInView } from \"react-intersection-observer\";\nimport Masonry from \"react-masonry-css\";\nimport resolveConfig from \"tailwindcss/resolveConfig\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\nimport {\n  BookmarkTypes,\n  ZPublicBookmark,\n} from \"@karakeep/shared/types/bookmarks\";\nimport { ZCursor } from \"@karakeep/shared/types/pagination\";\n\nfunction TagPill({ tag }: { tag: string }) {\n  return (\n    <div\n      className={cn(\n        badgeVariants({ variant: \"secondary\" }),\n        \"text-nowrap font-light text-gray-700 hover:bg-foreground hover:text-secondary dark:text-gray-400\",\n      )}\n      key={tag}\n    >\n      {tag}\n    </div>\n  );\n}\n\nfunction BookmarkCard({ bookmark }: { bookmark: ZPublicBookmark }) {\n  const renderContent = () => {\n    switch (bookmark.content.type) {\n      case BookmarkTypes.LINK:\n        return (\n          <div className=\"space-y-2\">\n            {bookmark.bannerImageUrl && (\n              <div className=\"aspect-video w-full overflow-hidden rounded bg-gray-100\">\n                <Link href={bookmark.content.url} target=\"_blank\">\n                  {/* oxlint-disable-next-line no-img-element */}\n                  <img\n                    src={bookmark.bannerImageUrl}\n                    alt={bookmark.title ?? \"Link preview\"}\n                    className=\"h-full w-full object-cover\"\n                  />\n                </Link>\n              </div>\n            )}\n            <div className=\"space-y-2\">\n              <Link\n                href={bookmark.content.url}\n                target=\"_blank\"\n                className=\"line-clamp-2 text-ellipsis text-lg font-medium leading-tight\"\n              >\n                {bookmark.title}\n              </Link>\n            </div>\n          </div>\n        );\n\n      case BookmarkTypes.TEXT:\n        return (\n          <div className=\"space-y-2\">\n            {bookmark.title && (\n              <h3 className=\"line-clamp-2 text-ellipsis text-lg font-medium leading-tight\">\n                {bookmark.title}\n              </h3>\n            )}\n            <div className=\"group relative max-h-64 overflow-hidden\">\n              <BookmarkMarkdownComponent readOnly={true}>\n                {{\n                  id: bookmark.id,\n                  content: {\n                    text: bookmark.content.text,\n                  },\n                }}\n              </BookmarkMarkdownComponent>\n              <Dialog>\n                <DialogTrigger className=\"absolute bottom-2 right-2 z-50 h-4 w-4 opacity-0 group-hover:opacity-100\">\n                  <Expand className=\"h-4 w-4\" />\n                </DialogTrigger>\n                <DialogContent className=\"max-h-96 max-w-3xl overflow-auto\">\n                  <BookmarkMarkdownComponent readOnly={true}>\n                    {{\n                      id: bookmark.id,\n                      content: {\n                        text: bookmark.content.text,\n                      },\n                    }}\n                  </BookmarkMarkdownComponent>\n                </DialogContent>\n              </Dialog>\n            </div>\n          </div>\n        );\n\n      case BookmarkTypes.ASSET:\n        return (\n          <div className=\"space-y-2\">\n            {bookmark.bannerImageUrl ? (\n              <div className=\"aspect-video w-full overflow-hidden rounded bg-gray-100\">\n                <Link href={bookmark.content.assetUrl} target=\"_blank\">\n                  {/* oxlint-disable-next-line no-img-element */}\n                  <img\n                    src={bookmark.bannerImageUrl}\n                    alt={bookmark.title ?? \"Asset preview\"}\n                    className=\"h-full w-full object-cover\"\n                  />\n                </Link>\n              </div>\n            ) : (\n              <div className=\"flex aspect-video w-full items-center justify-center overflow-hidden rounded bg-gray-100\">\n                {bookmark.content.assetType === \"image\" ? (\n                  <ImageIcon className=\"h-8 w-8 text-gray-400\" />\n                ) : (\n                  <FileIcon className=\"h-8 w-8 text-gray-400\" />\n                )}\n              </div>\n            )}\n            <div className=\"space-y-1\">\n              <Link\n                href={bookmark.content.assetUrl}\n                target=\"_blank\"\n                className=\"line-clamp-2 text-ellipsis text-lg font-medium leading-tight\"\n              >\n                {bookmark.title}\n              </Link>\n            </div>\n          </div>\n        );\n    }\n  };\n\n  return (\n    <Card className=\"group mb-3 border-0 shadow-sm transition-all duration-200 hover:shadow-lg\">\n      <CardContent className=\"p-3\">\n        {renderContent()}\n\n        {/* Tags */}\n        {bookmark.tags.length > 0 && (\n          <div className=\"mt-2 flex flex-wrap gap-1\">\n            {bookmark.tags.map((tag, index) => (\n              <TagPill key={index} tag={tag} />\n            ))}\n          </div>\n        )}\n\n        {/* Footer */}\n        <div className=\"mt-3 flex items-center justify-between pt-2\">\n          <div className=\"flex items-center gap-2 text-xs text-gray-500\">\n            {bookmark.content.type === BookmarkTypes.LINK && (\n              <>\n                <FooterLinkURL url={bookmark.content.url} />\n                <span>•</span>\n              </>\n            )}\n            <BookmarkFormattedCreatedAt createdAt={bookmark.createdAt} />\n          </div>\n        </div>\n      </CardContent>\n    </Card>\n  );\n}\n\nfunction getBreakpointConfig() {\n  const fullConfig = resolveConfig(tailwindConfig);\n\n  const breakpointColumnsObj: { [key: number]: number; default: number } = {\n    default: 3,\n  };\n  breakpointColumnsObj[parseInt(fullConfig.theme.screens.lg)] = 2;\n  breakpointColumnsObj[parseInt(fullConfig.theme.screens.md)] = 1;\n  breakpointColumnsObj[parseInt(fullConfig.theme.screens.sm)] = 1;\n  return breakpointColumnsObj;\n}\n\nexport default function PublicBookmarkGrid({\n  bookmarks: initialBookmarks,\n  nextCursor,\n  list,\n}: {\n  list: {\n    id: string;\n    name: string;\n    description: string | null | undefined;\n    icon: string;\n    numItems: number;\n    ownerName: string;\n  };\n  bookmarks: ZPublicBookmark[];\n  nextCursor: ZCursor | null;\n}) {\n  const api = useTRPC();\n  const { ref: loadMoreRef, inView: loadMoreButtonInView } = useInView();\n  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =\n    useInfiniteQuery(\n      api.publicBookmarks.getPublicBookmarksInList.infiniteQueryOptions(\n        { listId: list.id },\n        {\n          initialData: () => ({\n            pages: [{ bookmarks: initialBookmarks, nextCursor, list }],\n            pageParams: [null],\n          }),\n          initialCursor: null,\n          getNextPageParam: (lastPage) => lastPage.nextCursor,\n          refetchOnMount: true,\n        },\n      ),\n    );\n\n  useEffect(() => {\n    if (loadMoreButtonInView && hasNextPage && !isFetchingNextPage) {\n      fetchNextPage();\n    }\n  }, [loadMoreButtonInView]);\n\n  const breakpointConfig = useMemo(() => getBreakpointConfig(), []);\n\n  const bookmarks = useMemo(() => {\n    return data.pages.flatMap((b) => b.bookmarks);\n  }, [data]);\n  return (\n    <>\n      <Masonry\n        className=\"-ml-4 flex w-auto\"\n        columnClassName=\"pl-4\"\n        breakpointCols={breakpointConfig}\n      >\n        {bookmarks.map((bookmark) => (\n          <BookmarkCard key={bookmark.id} bookmark={bookmark} />\n        ))}\n      </Masonry>\n      {hasNextPage && (\n        <div className=\"flex justify-center\">\n          <ActionButton\n            ref={loadMoreRef}\n            ignoreDemoMode={true}\n            loading={isFetchingNextPage}\n            onClick={() => fetchNextPage()}\n            variant=\"ghost\"\n          >\n            Load More\n          </ActionButton>\n        </div>\n      )}\n    </>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/public/lists/PublicListHeader.tsx",
    "content": "import Link from \"next/link\";\nimport KarakeepLogo from \"@/components/KarakeepIcon\";\nimport { buttonVariants } from \"@/components/ui/button\";\nimport { BookmarkIcon, RssIcon } from \"lucide-react\";\n\nexport default function PublicListHeader({\n  list,\n}: {\n  list: {\n    id: string;\n    name: string;\n    description: string | null | undefined;\n    icon: string;\n    ownerName: string;\n    numItems: number;\n  };\n}) {\n  const rssLink = `/api/v1/rss/lists/${list.id}`;\n  return (\n    <div className=\"rounded-lg border bg-gradient-to-br from-purple-50/50 via-purple-100/30 to-purple-200/40 p-6 transition-all duration-300 dark:from-purple-950/20 dark:via-purple-900/15 dark:to-purple-800/20\">\n      <div className=\"space-y-4\">\n        <KarakeepLogo height={38} />\n        <div className=\"flex flex-col items-start justify-between gap-6 md:flex-row md:items-center\">\n          {/* Header */}\n          <div className=\"flex min-w-0 flex-1 items-start gap-3\">\n            <span className=\"text-3xl transition-transform duration-200 hover:scale-110\">\n              {list.icon}\n            </span>\n            <div className=\"min-w-0 flex-1\">\n              <h1 className=\"text-3xl font-bold leading-tight text-foreground\">\n                {list.name}\n              </h1>\n              {list.description && list.description.length > 0 && (\n                <p className=\"mt-2 text-lg leading-relaxed text-muted-foreground\">\n                  {list.description}\n                </p>\n              )}\n            </div>\n          </div>\n          {/* Created by */}\n          <div className=\"flex gap-3 md:justify-end\">\n            <div className=\"flex aspect-square size-10 flex-col items-center justify-center rounded-full bg-primary font-medium text-primary-foreground transition-all duration-200 hover:scale-105 hover:shadow-md\">\n              {list.ownerName[0]?.toUpperCase()}\n            </div>\n            <div>\n              <p className=\"text-sm text-muted-foreground\">Created by</p>\n              <p className=\"font-medium text-foreground\">{list.ownerName}</p>\n            </div>\n          </div>\n        </div>\n        {/* Options */}\n        <div className=\"flex items-center justify-start gap-1 md:justify-end\">\n          <div className=\"flex items-center gap-1 text-xs font-light uppercase text-gray-500 transition-colors duration-200 hover:text-gray-700 dark:hover:text-gray-300\">\n            <BookmarkIcon\n              size={12}\n              className=\"transition-transform duration-200 hover:scale-110\"\n            />\n            <span>{list.numItems} bookmarks</span>\n          </div>\n          <Link\n            href={rssLink}\n            target=\"_blank\"\n            className={buttonVariants({ variant: \"none\", size: \"icon\" })}\n          >\n            <RssIcon className=\"size-3 text-gray-500\" />\n          </Link>\n        </div>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/settings/AISettings.tsx",
    "content": "\"use client\";\n\nimport React from \"react\";\nimport { TagsEditor } from \"@/components/dashboard/bookmarks/TagsEditor\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport { Badge } from \"@/components/ui/badge\";\nimport {\n  Field,\n  FieldContent,\n  FieldDescription,\n  FieldError,\n  FieldGroup,\n  FieldLabel,\n  FieldTitle,\n} from \"@/components/ui/field\";\nimport {\n  Form,\n  FormControl,\n  FormField,\n  FormItem,\n  FormMessage,\n} from \"@/components/ui/form\";\nimport { FullPageSpinner } from \"@/components/ui/full-page-spinner\";\nimport { Input } from \"@/components/ui/input\";\nimport { RadioGroup, RadioGroupItem } from \"@/components/ui/radio-group\";\nimport {\n  Select,\n  SelectContent,\n  SelectGroup,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { Switch } from \"@/components/ui/switch\";\nimport { useClientConfig } from \"@/lib/clientConfig\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { useUserSettings } from \"@/lib/userSettings\";\nimport { cn } from \"@/lib/utils\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { Info, Plus, Save, Trash2 } from \"lucide-react\";\nimport { Controller, useForm } from \"react-hook-form\";\nimport { z } from \"zod\";\n\nimport type { ZBookmarkTags } from \"@karakeep/shared/types/tags\";\nimport { useDebounce } from \"@karakeep/shared-react/hooks/use-debounce\";\nimport { useUpdateUserSettings } from \"@karakeep/shared-react/hooks/users\";\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\nimport {\n  buildImagePrompt,\n  buildSummaryPromptUntruncated,\n  buildTextPromptUntruncated,\n} from \"@karakeep/shared/prompts\";\nimport {\n  zNewPromptSchema,\n  ZPrompt,\n  zUpdatePromptSchema,\n} from \"@karakeep/shared/types/prompts\";\nimport { zUpdateUserSettingsSchema } from \"@karakeep/shared/types/users\";\n\nimport { SettingsPage, SettingsSection } from \"./SettingsPage\";\n\nexport function AIPreferences() {\n  const { t } = useTranslation();\n  const clientConfig = useClientConfig();\n  const settings = useUserSettings();\n\n  const { mutate: updateSettings, isPending } = useUpdateUserSettings({\n    onSuccess: () => {\n      toast({\n        description: \"Settings updated successfully!\",\n      });\n    },\n    onError: () => {\n      toast({\n        description: \"Failed to update settings\",\n        variant: \"destructive\",\n      });\n    },\n  });\n\n  const form = useForm<z.infer<typeof zUpdateUserSettingsSchema>>({\n    resolver: zodResolver(zUpdateUserSettingsSchema),\n    values: settings\n      ? {\n          inferredTagLang: settings.inferredTagLang ?? \"\",\n          autoTaggingEnabled: settings.autoTaggingEnabled,\n          autoSummarizationEnabled: settings.autoSummarizationEnabled,\n        }\n      : undefined,\n  });\n\n  const showAutoTagging = clientConfig.inference.enableAutoTagging;\n  const showAutoSummarization = clientConfig.inference.enableAutoSummarization;\n\n  const onSubmit = (data: z.infer<typeof zUpdateUserSettingsSchema>) => {\n    updateSettings(data);\n  };\n\n  return (\n    <SettingsSection title=\"AI preferences\">\n      <form onSubmit={form.handleSubmit(onSubmit)}>\n        <FieldGroup className=\"gap-3\">\n          <Controller\n            name=\"inferredTagLang\"\n            control={form.control}\n            render={({ field, fieldState }) => (\n              <Field\n                className=\"rounded-lg border p-3\"\n                data-invalid={fieldState.invalid}\n              >\n                <FieldContent>\n                  <FieldLabel htmlFor=\"inferredTagLang\">\n                    {t(\"settings.ai.inference_language\")}\n                  </FieldLabel>\n                  <FieldDescription>\n                    {t(\"settings.ai.inference_language_description\")}\n                  </FieldDescription>\n                </FieldContent>\n                <Input\n                  {...field}\n                  id=\"inferredTagLang\"\n                  value={field.value ?? \"\"}\n                  onChange={(e) =>\n                    field.onChange(\n                      e.target.value.length > 0 ? e.target.value : null,\n                    )\n                  }\n                  aria-invalid={fieldState.invalid}\n                  placeholder={`Default (${clientConfig.inference.inferredTagLang})`}\n                  type=\"text\"\n                />\n                {fieldState.invalid && (\n                  <FieldError errors={[fieldState.error]} />\n                )}\n              </Field>\n            )}\n          />\n\n          {showAutoTagging && (\n            <Controller\n              name=\"autoTaggingEnabled\"\n              control={form.control}\n              render={({ field, fieldState }) => (\n                <Field\n                  orientation=\"horizontal\"\n                  className=\"rounded-lg border p-3\"\n                  data-invalid={fieldState.invalid}\n                >\n                  <FieldContent>\n                    <FieldLabel htmlFor=\"autoTaggingEnabled\">\n                      {t(\"settings.ai.auto_tagging\")}\n                    </FieldLabel>\n                    <FieldDescription>\n                      {t(\"settings.ai.auto_tagging_description\")}\n                    </FieldDescription>\n                  </FieldContent>\n                  <Switch\n                    id=\"autoTaggingEnabled\"\n                    name={field.name}\n                    checked={field.value ?? true}\n                    onCheckedChange={field.onChange}\n                    aria-invalid={fieldState.invalid}\n                  />\n                  {fieldState.invalid && (\n                    <FieldError errors={[fieldState.error]} />\n                  )}\n                </Field>\n              )}\n            />\n          )}\n\n          {showAutoSummarization && (\n            <Controller\n              name=\"autoSummarizationEnabled\"\n              control={form.control}\n              render={({ field, fieldState }) => (\n                <Field\n                  orientation=\"horizontal\"\n                  className=\"rounded-lg border p-3\"\n                  data-invalid={fieldState.invalid}\n                >\n                  <FieldContent>\n                    <FieldLabel htmlFor=\"autoSummarizationEnabled\">\n                      {t(\"settings.ai.auto_summarization\")}\n                    </FieldLabel>\n                    <FieldDescription>\n                      {t(\"settings.ai.auto_summarization_description\")}\n                    </FieldDescription>\n                  </FieldContent>\n                  <Switch\n                    id=\"autoSummarizationEnabled\"\n                    name={field.name}\n                    checked={field.value ?? true}\n                    onCheckedChange={field.onChange}\n                    aria-invalid={fieldState.invalid}\n                  />\n                  {fieldState.invalid && (\n                    <FieldError errors={[fieldState.error]} />\n                  )}\n                </Field>\n              )}\n            />\n          )}\n\n          <div className=\"flex justify-end pt-4\">\n            <ActionButton type=\"submit\" loading={isPending} variant=\"default\">\n              <Save className=\"mr-2 size-4\" />\n              {t(\"actions.save\")}\n            </ActionButton>\n          </div>\n        </FieldGroup>\n      </form>\n    </SettingsSection>\n  );\n}\n\nexport function TagStyleSelector() {\n  const { t } = useTranslation();\n  const settings = useUserSettings();\n\n  const { mutate: updateSettings, isPending: isUpdating } =\n    useUpdateUserSettings({\n      onSuccess: () => {\n        toast({\n          description: \"Tag style updated successfully!\",\n        });\n      },\n      onError: () => {\n        toast({\n          description: \"Failed to update tag style\",\n          variant: \"destructive\",\n        });\n      },\n    });\n\n  const tagStyleOptions = [\n    {\n      value: \"lowercase-hyphens\",\n      label: t(\"settings.ai.lowercase_hyphens\"),\n      examples: [\"machine-learning\", \"web-development\"],\n    },\n    {\n      value: \"lowercase-spaces\",\n      label: t(\"settings.ai.lowercase_spaces\"),\n      examples: [\"machine learning\", \"web development\"],\n    },\n    {\n      value: \"lowercase-underscores\",\n      label: t(\"settings.ai.lowercase_underscores\"),\n      examples: [\"machine_learning\", \"web_development\"],\n    },\n    {\n      value: \"titlecase-spaces\",\n      label: t(\"settings.ai.titlecase_spaces\"),\n      examples: [\"Machine Learning\", \"Web Development\"],\n    },\n    {\n      value: \"titlecase-hyphens\",\n      label: t(\"settings.ai.titlecase_hyphens\"),\n      examples: [\"Machine-Learning\", \"Web-Development\"],\n    },\n    {\n      value: \"camelCase\",\n      label: t(\"settings.ai.camelCase\"),\n      examples: [\"machineLearning\", \"webDevelopment\"],\n    },\n    {\n      value: \"as-generated\",\n      label: t(\"settings.ai.no_preference\"),\n      examples: [\"Machine Learning\", \"web development\", \"AI_generated\"],\n    },\n  ] as const;\n\n  const selectedStyle = settings?.tagStyle ?? \"as-generated\";\n\n  return (\n    <SettingsSection\n      title={t(\"settings.ai.tag_style\")}\n      description={t(\"settings.ai.tag_style_description\")}\n    >\n      <RadioGroup\n        value={selectedStyle}\n        onValueChange={(value) => {\n          updateSettings({ tagStyle: value as typeof selectedStyle });\n        }}\n        disabled={isUpdating}\n        className=\"grid gap-3 sm:grid-cols-2\"\n      >\n        {tagStyleOptions.map((option) => (\n          <FieldLabel\n            key={option.value}\n            htmlFor={option.value}\n            className={cn(selectedStyle === option.value && \"ring-1\")}\n          >\n            <Field orientation=\"horizontal\">\n              <FieldContent>\n                <FieldTitle>{option.label}</FieldTitle>\n                <div className=\"flex flex-wrap gap-1\">\n                  {option.examples.map((example) => (\n                    <Badge\n                      key={example}\n                      variant=\"secondary\"\n                      className=\"text-xs font-light\"\n                    >\n                      {example}\n                    </Badge>\n                  ))}\n                </div>\n              </FieldContent>\n              <RadioGroupItem value={option.value} id={option.value} />\n            </Field>\n          </FieldLabel>\n        ))}\n      </RadioGroup>\n    </SettingsSection>\n  );\n}\n\nexport function CuratedTagsSelector() {\n  const api = useTRPC();\n  const { t } = useTranslation();\n  const settings = useUserSettings();\n\n  const { mutate: updateSettings, isPending: isUpdatingCuratedTags } =\n    useUpdateUserSettings({\n      onSuccess: () => {\n        toast({\n          description: t(\"settings.ai.curated_tags_updated\"),\n        });\n      },\n      onError: () => {\n        toast({\n          description: t(\"settings.ai.curated_tags_update_failed\"),\n          variant: \"destructive\",\n        });\n      },\n    });\n\n  const areTagIdsEqual = React.useCallback((a: string[], b: string[]) => {\n    return a.length === b.length && a.every((id, index) => id === b[index]);\n  }, []);\n\n  const curatedTagIds = React.useMemo(\n    () => settings?.curatedTagIds ?? [],\n    [settings?.curatedTagIds],\n  );\n  const [localCuratedTagIds, setLocalCuratedTagIds] =\n    React.useState<string[]>(curatedTagIds);\n  const debouncedCuratedTagIds = useDebounce(localCuratedTagIds, 300);\n  const lastServerCuratedTagIdsRef = React.useRef(curatedTagIds);\n  const lastSubmittedCuratedTagIdsRef = React.useRef<string[] | null>(null);\n\n  React.useEffect(() => {\n    const hadUnsyncedLocalChanges = !areTagIdsEqual(\n      localCuratedTagIds,\n      lastServerCuratedTagIdsRef.current,\n    );\n\n    if (\n      !hadUnsyncedLocalChanges &&\n      !areTagIdsEqual(localCuratedTagIds, curatedTagIds)\n    ) {\n      setLocalCuratedTagIds(curatedTagIds);\n    }\n\n    lastServerCuratedTagIdsRef.current = curatedTagIds;\n  }, [areTagIdsEqual, curatedTagIds, localCuratedTagIds]);\n\n  React.useEffect(() => {\n    if (isUpdatingCuratedTags) {\n      return;\n    }\n\n    if (areTagIdsEqual(debouncedCuratedTagIds, curatedTagIds)) {\n      lastSubmittedCuratedTagIdsRef.current = null;\n      return;\n    }\n\n    if (\n      lastSubmittedCuratedTagIdsRef.current &&\n      areTagIdsEqual(\n        lastSubmittedCuratedTagIdsRef.current,\n        debouncedCuratedTagIds,\n      )\n    ) {\n      return;\n    }\n\n    lastSubmittedCuratedTagIdsRef.current = debouncedCuratedTagIds;\n    updateSettings({\n      curatedTagIds:\n        debouncedCuratedTagIds.length > 0 ? debouncedCuratedTagIds : null,\n    });\n  }, [\n    areTagIdsEqual,\n    curatedTagIds,\n    debouncedCuratedTagIds,\n    isUpdatingCuratedTags,\n    updateSettings,\n  ]);\n\n  // Fetch selected tags to display their names\n  const { data: selectedTagsData } = useQuery(\n    api.tags.list.queryOptions(\n      { ids: localCuratedTagIds },\n      { enabled: localCuratedTagIds.length > 0 },\n    ),\n  );\n\n  const selectedTags: ZBookmarkTags[] = React.useMemo(() => {\n    const tagsMap = new Map(\n      (selectedTagsData?.tags ?? []).map((tag) => [tag.id, tag]),\n    );\n    // Preserve the order from curatedTagIds instead of server sort order\n    return localCuratedTagIds\n      .map((id) => tagsMap.get(id))\n      .filter((tag): tag is NonNullable<typeof tag> => tag != null)\n      .map((tag) => ({\n        id: tag.id,\n        name: tag.name,\n        attachedBy: \"human\" as const,\n      }));\n  }, [selectedTagsData?.tags, localCuratedTagIds]);\n\n  return (\n    <SettingsSection\n      title={t(\"settings.ai.curated_tags\")}\n      description={t(\"settings.ai.curated_tags_description\")}\n    >\n      <TagsEditor\n        tags={selectedTags}\n        placeholder=\"Select curated tags...\"\n        onAttach={(tag) => {\n          const tagId = tag.tagId;\n          if (tagId) {\n            setLocalCuratedTagIds((prev) => {\n              if (prev.includes(tagId)) {\n                return prev;\n              }\n              return [...prev, tagId];\n            });\n          }\n        }}\n        onDetach={(tag) => {\n          setLocalCuratedTagIds((prev) => {\n            return prev.filter((id) => id !== tag.tagId);\n          });\n        }}\n        allowCreation={false}\n      />\n    </SettingsSection>\n  );\n}\n\nexport function PromptEditor() {\n  const api = useTRPC();\n  const { t } = useTranslation();\n  const queryClient = useQueryClient();\n\n  const form = useForm<z.infer<typeof zNewPromptSchema>>({\n    resolver: zodResolver(zNewPromptSchema),\n    defaultValues: {\n      text: \"\",\n      appliesTo: \"all_tagging\",\n    },\n  });\n\n  const { mutateAsync: createPrompt, isPending: isCreating } = useMutation(\n    api.prompts.create.mutationOptions({\n      onSuccess: () => {\n        toast({\n          description: \"Prompt has been created!\",\n        });\n        queryClient.invalidateQueries(api.prompts.list.pathFilter());\n      },\n    }),\n  );\n\n  return (\n    <Form {...form}>\n      <form\n        className=\"flex gap-2\"\n        onSubmit={form.handleSubmit(async (value) => {\n          await createPrompt(value);\n          form.resetField(\"text\");\n        })}\n      >\n        <FormField\n          control={form.control}\n          name=\"text\"\n          render={({ field }) => {\n            return (\n              <FormItem className=\"flex-1\">\n                <FormControl>\n                  <Input\n                    placeholder=\"Add a custom prompt\"\n                    type=\"text\"\n                    {...field}\n                  />\n                </FormControl>\n                <FormMessage />\n              </FormItem>\n            );\n          }}\n        />\n\n        <FormField\n          control={form.control}\n          name=\"appliesTo\"\n          render={({ field }) => {\n            return (\n              <FormItem className=\"flex-0\">\n                <FormControl>\n                  <Select\n                    onValueChange={field.onChange}\n                    defaultValue={field.value}\n                  >\n                    <SelectTrigger>\n                      <SelectValue placeholder=\"Applies To\" />\n                    </SelectTrigger>\n                    <SelectContent>\n                      <SelectGroup>\n                        <SelectItem value=\"all_tagging\">\n                          {t(\"settings.ai.all_tagging\")}\n                        </SelectItem>\n                        <SelectItem value=\"text\">\n                          {t(\"settings.ai.text_tagging\")}\n                        </SelectItem>\n                        <SelectItem value=\"images\">\n                          {t(\"settings.ai.image_tagging\")}\n                        </SelectItem>\n                        <SelectItem value=\"summary\">\n                          {t(\"settings.ai.summarization\")}\n                        </SelectItem>\n                      </SelectGroup>\n                    </SelectContent>\n                  </Select>\n                </FormControl>\n                <FormMessage />\n              </FormItem>\n            );\n          }}\n        />\n        <ActionButton\n          type=\"submit\"\n          loading={isCreating}\n          variant=\"default\"\n          className=\"items-center\"\n        >\n          <Plus className=\"mr-2 size-4\" />\n          {t(\"actions.add\")}\n        </ActionButton>\n      </form>\n    </Form>\n  );\n}\n\nexport function PromptRow({ prompt }: { prompt: ZPrompt }) {\n  const api = useTRPC();\n  const { t } = useTranslation();\n  const queryClient = useQueryClient();\n  const { mutateAsync: updatePrompt, isPending: isUpdating } = useMutation(\n    api.prompts.update.mutationOptions({\n      onSuccess: () => {\n        toast({\n          description: \"Prompt has been updated!\",\n        });\n        queryClient.invalidateQueries(api.prompts.list.pathFilter());\n      },\n    }),\n  );\n  const { mutate: deletePrompt, isPending: isDeleting } = useMutation(\n    api.prompts.delete.mutationOptions({\n      onSuccess: () => {\n        toast({\n          description: \"Prompt has been deleted!\",\n        });\n        queryClient.invalidateQueries(api.prompts.list.pathFilter());\n      },\n    }),\n  );\n\n  const form = useForm<z.infer<typeof zUpdatePromptSchema>>({\n    resolver: zodResolver(zUpdatePromptSchema),\n    defaultValues: {\n      promptId: prompt.id,\n      text: prompt.text,\n      appliesTo: prompt.appliesTo,\n    },\n  });\n\n  return (\n    <Form {...form}>\n      <form\n        className=\"flex gap-2\"\n        onSubmit={form.handleSubmit(async (value) => {\n          await updatePrompt(value);\n        })}\n      >\n        <FormField\n          control={form.control}\n          name=\"promptId\"\n          render={({ field }) => {\n            return (\n              <FormItem className=\"hidden\">\n                <FormControl>\n                  <Input type=\"hidden\" {...field} />\n                </FormControl>\n                <FormMessage />\n              </FormItem>\n            );\n          }}\n        />\n        <FormField\n          control={form.control}\n          name=\"text\"\n          render={({ field }) => {\n            return (\n              <FormItem className=\"flex-1\">\n                <FormControl>\n                  <Input\n                    placeholder=\"Add a custom prompt\"\n                    type=\"text\"\n                    {...field}\n                  />\n                </FormControl>\n                <FormMessage />\n              </FormItem>\n            );\n          }}\n        />\n\n        <FormField\n          control={form.control}\n          name=\"appliesTo\"\n          render={({ field }) => {\n            return (\n              <FormItem className=\"flex-0\">\n                <FormControl>\n                  <Select\n                    onValueChange={field.onChange}\n                    defaultValue={field.value}\n                  >\n                    <SelectTrigger>\n                      <SelectValue placeholder=\"Applies To\" />\n                    </SelectTrigger>\n                    <SelectContent>\n                      <SelectGroup>\n                        <SelectItem value=\"all_tagging\">\n                          {t(\"settings.ai.all_tagging\")}\n                        </SelectItem>\n                        <SelectItem value=\"text\">\n                          {t(\"settings.ai.text_tagging\")}\n                        </SelectItem>\n                        <SelectItem value=\"images\">\n                          {t(\"settings.ai.image_tagging\")}\n                        </SelectItem>\n                        <SelectItem value=\"summary\">\n                          {t(\"settings.ai.summarization\")}\n                        </SelectItem>\n                      </SelectGroup>\n                    </SelectContent>\n                  </Select>\n                </FormControl>\n                <FormMessage />\n              </FormItem>\n            );\n          }}\n        />\n        <ActionButton\n          loading={isUpdating}\n          variant=\"secondary\"\n          type=\"submit\"\n          className=\"items-center\"\n        >\n          <Save className=\"mr-2 size-4\" />\n          {t(\"actions.save\")}\n        </ActionButton>\n        <ActionButton\n          loading={isDeleting}\n          variant=\"destructive\"\n          onClick={() => deletePrompt({ promptId: prompt.id })}\n          className=\"items-center\"\n          type=\"button\"\n        >\n          <Trash2 className=\"mr-2 size-4\" />\n          {t(\"actions.delete\")}\n        </ActionButton>\n      </form>\n    </Form>\n  );\n}\n\nexport function TaggingRules() {\n  const api = useTRPC();\n  const { t } = useTranslation();\n  const { data: prompts, isLoading } = useQuery(\n    api.prompts.list.queryOptions(),\n  );\n\n  return (\n    <SettingsSection\n      title={t(\"settings.ai.tagging_rules\")}\n      description={t(\"settings.ai.tagging_rule_description\")}\n    >\n      {prompts && prompts.length == 0 && (\n        <div className=\"flex items-start gap-2 rounded-md bg-muted p-4 text-sm text-muted-foreground\">\n          <Info className=\"size-4 flex-shrink-0\" />\n          <p>You don&apos;t have any custom prompts yet.</p>\n        </div>\n      )}\n      <div className=\"flex flex-col gap-2\">\n        {isLoading && <FullPageSpinner />}\n        {prompts &&\n          prompts.map((prompt) => (\n            <PromptRow key={prompt.id} prompt={prompt} />\n          ))}\n        <PromptEditor />\n      </div>\n    </SettingsSection>\n  );\n}\n\n/**\n * Expand $tags / $aiTags / $userTags placeholders in custom prompt texts so\n * that the preview panel shows exactly what will be sent to the AI.\n *\n * The logic mirrors apps/workers/workers/inference/tagging.ts\n * `replaceTagsPlaceholders` to keep the preview accurate.\n */\nfunction expandTagPlaceholders(\n  texts: string[],\n  tags: { name: string; numBookmarksByAttachedType: Record<string, number> }[],\n): string[] {\n  const tagsStr = `[${tags.map((t) => t.name).join(\", \")}]`;\n  const aiTagsStr = `[${tags\n    .filter((t) => t.numBookmarksByAttachedType[\"human\"] ?? true)\n    .map((t) => t.name)\n    .join(\", \")}]`;\n  const userTagsStr = `[${tags\n    .filter((t) => t.numBookmarksByAttachedType[\"human\"] ?? false)\n    .map((t) => t.name)\n    .join(\", \")}]`;\n\n  return texts.map((text) =>\n    text\n      .replaceAll(\"$tags\", tagsStr)\n      .replaceAll(\"$aiTags\", aiTagsStr)\n      .replaceAll(\"$userTags\", userTagsStr),\n  );\n}\n\nfunction hasTagPlaceholder(texts: string[]): boolean {\n  return texts.some(\n    (t) =>\n      t.includes(\"$tags\") || t.includes(\"$aiTags\") || t.includes(\"$userTags\"),\n  );\n}\n\nexport function PromptDemo() {\n  const api = useTRPC();\n  const { t } = useTranslation();\n  const { data: prompts } = useQuery(api.prompts.list.queryOptions());\n  const settings = useUserSettings();\n  const clientConfig = useClientConfig();\n\n  const tagStyle = settings?.tagStyle ?? \"as-generated\";\n  const curatedTagIds = settings?.curatedTagIds ?? [];\n  const { data: tagsData } = useQuery(\n    api.tags.list.queryOptions(\n      { ids: curatedTagIds },\n      { enabled: curatedTagIds.length > 0 },\n    ),\n  );\n  const inferredTagLang =\n    settings?.inferredTagLang ?? clientConfig.inference.inferredTagLang;\n\n  // Resolve curated tag names for preview\n  const curatedTagNames =\n    curatedTagIds.length > 0 && tagsData?.tags\n      ? curatedTagIds\n          .map((id) => tagsData.tags.find((tag) => tag.id === id)?.name)\n          .filter((name): name is string => Boolean(name))\n      : undefined;\n\n  // Detect whether any prompt uses a tag-list placeholder ($tags / $aiTags / $userTags)\n  const allPromptTexts = (prompts ?? []).map((p) => p.text);\n  const needsTagExpansion = hasTagPlaceholder(allPromptTexts);\n\n  // Fetch all tags only when a placeholder is actually used (avoids an\n  // unnecessary round-trip for users who don't use this feature).\n  const { data: allTagsData } = useQuery(\n    api.tags.list.queryOptions({}, { enabled: needsTagExpansion }),\n  );\n\n  // Build a function that expands placeholders in a list of prompt texts.\n  const withTagsExpanded = React.useCallback(\n    (texts: string[]): string[] => {\n      if (!needsTagExpansion || !allTagsData?.tags) return texts;\n      return expandTagPlaceholders(texts, allTagsData.tags);\n    },\n    [needsTagExpansion, allTagsData],\n  );\n\n  return (\n    <SettingsSection\n      title={t(\"settings.ai.prompt_preview\")}\n      description=\"Preview the actual prompts sent to AI based on your settings\"\n    >\n      <div className=\"space-y-4\">\n        <div>\n          <p className=\"mb-2 text-sm font-medium\">\n            {t(\"settings.ai.text_prompt\")}\n          </p>\n          <code className=\"block whitespace-pre-wrap rounded-md bg-muted p-3 text-sm text-muted-foreground\">\n            {buildTextPromptUntruncated(\n              inferredTagLang,\n              withTagsExpanded(\n                (prompts ?? [])\n                  .filter(\n                    (p) =>\n                      p.appliesTo == \"text\" || p.appliesTo == \"all_tagging\",\n                  )\n                  .map((p) => p.text),\n              ),\n              \"\\n<CONTENT_HERE>\\n\",\n              tagStyle,\n              curatedTagNames,\n            ).trim()}\n          </code>\n        </div>\n        <div>\n          <p className=\"mb-2 text-sm font-medium\">\n            {t(\"settings.ai.images_prompt\")}\n          </p>\n          <code className=\"block whitespace-pre-wrap rounded-md bg-muted p-3 text-sm text-muted-foreground\">\n            {buildImagePrompt(\n              inferredTagLang,\n              withTagsExpanded(\n                (prompts ?? [])\n                  .filter(\n                    (p) =>\n                      p.appliesTo == \"images\" || p.appliesTo == \"all_tagging\",\n                  )\n                  .map((p) => p.text),\n              ),\n              tagStyle,\n              curatedTagNames,\n            ).trim()}\n          </code>\n        </div>\n        <div>\n          <p className=\"mb-2 text-sm font-medium\">\n            {t(\"settings.ai.summarization_prompt\")}\n          </p>\n          <code className=\"block whitespace-pre-wrap rounded-md bg-muted p-3 text-sm text-muted-foreground\">\n            {buildSummaryPromptUntruncated(\n              inferredTagLang,\n              withTagsExpanded(\n                (prompts ?? [])\n                  .filter((p) => p.appliesTo == \"summary\")\n                  .map((p) => p.text),\n              ),\n              \"\\n<CONTENT_HERE>\\n\",\n            ).trim()}\n          </code>\n        </div>\n      </div>\n    </SettingsSection>\n  );\n}\n\nexport default function AISettings() {\n  const { t } = useTranslation();\n  return (\n    <SettingsPage title={t(\"settings.ai.ai_settings\")}>\n      <AIPreferences />\n      <TagStyleSelector />\n      <CuratedTagsSelector />\n      <TaggingRules />\n      <PromptDemo />\n    </SettingsPage>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/settings/AddApiKey.tsx",
    "content": "\"use client\";\n\nimport type { SubmitErrorHandler } from \"react-hook-form\";\nimport { useState } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@/components/ui/dialog\";\nimport {\n  Form,\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@/components/ui/form\";\nimport { Input } from \"@/components/ui/input\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { useMutation } from \"@tanstack/react-query\";\nimport { PlusCircle } from \"lucide-react\";\nimport { useForm } from \"react-hook-form\";\nimport { z } from \"zod\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\nimport ApiKeySuccess from \"./ApiKeySuccess\";\n\nfunction AddApiKeyForm({ onSuccess }: { onSuccess: (key: string) => void }) {\n  const api = useTRPC();\n  const { t } = useTranslation();\n  const formSchema = z.object({\n    name: z.string(),\n  });\n  const router = useRouter();\n  const mutator = useMutation(\n    api.apiKeys.create.mutationOptions({\n      onSuccess: (resp) => {\n        onSuccess(resp.key);\n        router.refresh();\n      },\n      onError: () => {\n        toast({\n          description: t(\"common.something_went_wrong\"),\n          variant: \"destructive\",\n        });\n      },\n    }),\n  );\n\n  const form = useForm<z.infer<typeof formSchema>>({\n    resolver: zodResolver(formSchema),\n  });\n\n  async function onSubmit(value: z.infer<typeof formSchema>) {\n    mutator.mutate({ name: value.name });\n  }\n\n  const onError: SubmitErrorHandler<z.infer<typeof formSchema>> = (errors) => {\n    toast({\n      description: Object.values(errors)\n        .map((v) => v.message)\n        .join(\"\\n\"),\n      variant: \"destructive\",\n    });\n  };\n\n  return (\n    <Form {...form}>\n      <form\n        onSubmit={form.handleSubmit(onSubmit, onError)}\n        className=\"flex w-full space-x-3 space-y-8 pt-4\"\n      >\n        <FormField\n          control={form.control}\n          name=\"name\"\n          render={({ field }) => {\n            return (\n              <FormItem className=\"flex-1\">\n                <FormLabel>{t(\"common.name\")}</FormLabel>\n                <FormControl>\n                  <Input\n                    type=\"text\"\n                    placeholder={t(\"common.name\")}\n                    {...field}\n                  />\n                </FormControl>\n                <FormDescription>\n                  {t(\"settings.api_keys.new_api_key_desc\")}\n                </FormDescription>\n                <FormMessage />\n              </FormItem>\n            );\n          }}\n        />\n        <ActionButton type=\"submit\" loading={mutator.isPending}>\n          {t(\"actions.create\")}\n        </ActionButton>\n      </form>\n    </Form>\n  );\n}\n\nexport default function AddApiKey() {\n  const { t } = useTranslation();\n  const [key, setKey] = useState<string | undefined>(undefined);\n  const [dialogOpen, setDialogOpen] = useState<boolean>(false);\n  return (\n    <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>\n      <DialogTrigger asChild>\n        <Button>\n          <PlusCircle className=\"mr-2 h-4 w-4\" />\n          {t(\"settings.api_keys.new_api_key\")}\n        </Button>\n      </DialogTrigger>\n      <DialogContent>\n        <DialogHeader>\n          <DialogTitle>\n            {key\n              ? t(\"settings.api_keys.key_success\")\n              : t(\"settings.api_keys.new_api_key\")}\n          </DialogTitle>\n        </DialogHeader>\n        {key ? (\n          <ApiKeySuccess\n            apiKey={key}\n            message={t(\"settings.api_keys.key_success\")}\n          />\n        ) : (\n          <AddApiKeyForm onSuccess={setKey} />\n        )}\n        <DialogFooter className=\"sm:justify-end\">\n          <DialogClose asChild>\n            <Button\n              type=\"button\"\n              variant=\"outline\"\n              onClick={() => setKey(undefined)}\n            >\n              {t(\"actions.close\")}\n            </Button>\n          </DialogClose>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/settings/ApiKeySettings.tsx",
    "content": "import {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@/components/ui/table\";\nimport { useTranslation } from \"@/lib/i18n/server\";\nimport { api } from \"@/server/api/client\";\nimport { formatDistanceToNow } from \"date-fns\";\n\nimport DeleteApiKey from \"./DeleteApiKey\";\nimport RegenerateApiKey from \"./RegenerateApiKey\";\nimport { SettingsSection } from \"./SettingsPage\";\n\nexport default async function ApiKeys() {\n  // oxlint-disable-next-line rules-of-hooks\n  const { t } = await useTranslation();\n  const keys = await api.apiKeys.list();\n  return (\n    <SettingsSection>\n      <Table>\n        <TableHeader>\n          <TableRow>\n            <TableHead>{t(\"common.name\")}</TableHead>\n            <TableHead>{t(\"common.key\")}</TableHead>\n            <TableHead>{t(\"common.created_at\")}</TableHead>\n            <TableHead>{t(\"common.last_used\")}</TableHead>\n            <TableHead>{t(\"common.action\")}</TableHead>\n          </TableRow>\n        </TableHeader>\n        <TableBody>\n          {keys.keys.map((key) => {\n            return (\n              <TableRow key={key.id}>\n                <TableCell>{key.name}</TableCell>\n                <TableCell>**_{key.keyId}_**</TableCell>\n                <TableCell>\n                  {formatDistanceToNow(key.createdAt, { addSuffix: true })}\n                </TableCell>\n                <TableCell>\n                  {key.lastUsedAt\n                    ? formatDistanceToNow(key.lastUsedAt, { addSuffix: true })\n                    : \"—\"}\n                </TableCell>\n                <TableCell>\n                  <div className=\"flex items-center gap-2\">\n                    <RegenerateApiKey name={key.name} id={key.id} />\n                    <DeleteApiKey name={key.name} id={key.id} />\n                  </div>\n                </TableCell>\n              </TableRow>\n            );\n          })}\n          <TableRow></TableRow>\n        </TableBody>\n      </Table>\n    </SettingsSection>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/settings/ApiKeySuccess.tsx",
    "content": "import CopyBtn from \"@/components/ui/copy-button\";\nimport { Input } from \"@/components/ui/input\";\n\nexport default function ApiKeySuccess({\n  apiKey,\n  message,\n}: {\n  apiKey: string;\n  message: string;\n}) {\n  return (\n    <div>\n      <div className=\"py-4 text-sm text-muted-foreground\">{message}</div>\n      <div className=\"flex space-x-2 pt-2\">\n        <Input value={apiKey} readOnly />\n        <CopyBtn\n          getStringToCopy={() => {\n            return apiKey;\n          }}\n        />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/settings/BackupSettings.tsx",
    "content": "\"use client\";\n\nimport React from \"react\";\nimport Link from \"next/link\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport {\n  Form,\n  FormControl,\n  FormDescription,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@/components/ui/form\";\nimport { FullPageSpinner } from \"@/components/ui/full-page-spinner\";\nimport { Input } from \"@/components/ui/input\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { Switch } from \"@/components/ui/switch\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { useUserSettings } from \"@/lib/userSettings\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport {\n  CheckCircle,\n  Download,\n  Play,\n  Save,\n  Trash2,\n  XCircle,\n} from \"lucide-react\";\nimport { useForm } from \"react-hook-form\";\nimport { z } from \"zod\";\n\nimport { useUpdateUserSettings } from \"@karakeep/shared-react/hooks/users\";\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\nimport { zBackupSchema } from \"@karakeep/shared/types/backups\";\nimport { zUpdateBackupSettingsSchema } from \"@karakeep/shared/types/users\";\nimport { getAssetUrl } from \"@karakeep/shared/utils/assetUtils\";\n\nimport ActionConfirmingDialog from \"../ui/action-confirming-dialog\";\nimport { Button } from \"../ui/button\";\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"../ui/table\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../ui/tooltip\";\nimport { SettingsSection } from \"./SettingsPage\";\n\nfunction BackupConfigurationForm() {\n  const { t } = useTranslation();\n\n  const settings = useUserSettings();\n  const { mutate: updateSettings, isPending: isUpdating } =\n    useUpdateUserSettings({\n      onSuccess: () => {\n        toast({\n          description: t(\"settings.info.user_settings.user_settings_updated\"),\n        });\n      },\n      onError: () => {\n        toast({\n          description: t(\"common.something_went_wrong\"),\n          variant: \"destructive\",\n        });\n      },\n    });\n\n  const form = useForm<z.infer<typeof zUpdateBackupSettingsSchema>>({\n    resolver: zodResolver(zUpdateBackupSettingsSchema),\n    values: settings\n      ? {\n          backupsEnabled: settings.backupsEnabled,\n          backupsFrequency: settings.backupsFrequency,\n          backupsRetentionDays: settings.backupsRetentionDays,\n        }\n      : undefined,\n  });\n\n  return (\n    <SettingsSection title={t(\"settings.backups.configuration.title\")}>\n      <Form {...form}>\n        <form\n          className=\"space-y-4\"\n          onSubmit={form.handleSubmit((value) => {\n            updateSettings(value);\n          })}\n        >\n          <FormField\n            control={form.control}\n            name=\"backupsEnabled\"\n            render={({ field }) => (\n              <FormItem className=\"flex flex-row items-center justify-between rounded-lg border p-3\">\n                <div className=\"space-y-0.5\">\n                  <FormLabel>\n                    {t(\n                      \"settings.backups.configuration.enable_automatic_backups\",\n                    )}\n                  </FormLabel>\n                  <FormDescription>\n                    {t(\n                      \"settings.backups.configuration.enable_automatic_backups_description\",\n                    )}\n                  </FormDescription>\n                </div>\n                <FormControl>\n                  <Switch\n                    checked={field.value}\n                    onCheckedChange={field.onChange}\n                  />\n                </FormControl>\n              </FormItem>\n            )}\n          />\n\n          <FormField\n            control={form.control}\n            name=\"backupsFrequency\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>\n                  {t(\"settings.backups.configuration.backup_frequency\")}\n                </FormLabel>\n                <FormControl>\n                  <Select\n                    onValueChange={field.onChange}\n                    defaultValue={field.value}\n                    {...field}\n                  >\n                    <SelectTrigger>\n                      <SelectValue\n                        placeholder={t(\n                          \"settings.backups.configuration.select_frequency\",\n                        )}\n                      />\n                    </SelectTrigger>\n                    <SelectContent>\n                      <SelectItem value=\"daily\">\n                        {t(\"settings.backups.configuration.frequency.daily\")}\n                      </SelectItem>\n                      <SelectItem value=\"weekly\">\n                        {t(\"settings.backups.configuration.frequency.weekly\")}\n                      </SelectItem>\n                    </SelectContent>\n                  </Select>\n                </FormControl>\n                <FormDescription>\n                  {t(\n                    \"settings.backups.configuration.backup_frequency_description\",\n                  )}\n                </FormDescription>\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n\n          <FormField\n            control={form.control}\n            name=\"backupsRetentionDays\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>\n                  {t(\"settings.backups.configuration.retention_period\")}\n                </FormLabel>\n                <FormControl>\n                  <Input\n                    type=\"number\"\n                    min={1}\n                    max={365}\n                    {...field}\n                    onChange={(e) => field.onChange(parseInt(e.target.value))}\n                  />\n                </FormControl>\n                <FormDescription>\n                  {t(\n                    \"settings.backups.configuration.retention_period_description\",\n                  )}\n                </FormDescription>\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n\n          <ActionButton\n            type=\"submit\"\n            loading={isUpdating}\n            className=\"items-center\"\n          >\n            <Save className=\"mr-2 size-4\" />\n            {t(\"settings.backups.configuration.save_settings\")}\n          </ActionButton>\n        </form>\n      </Form>\n    </SettingsSection>\n  );\n}\n\nfunction BackupRow({ backup }: { backup: z.infer<typeof zBackupSchema> }) {\n  const api = useTRPC();\n  const { t } = useTranslation();\n  const queryClient = useQueryClient();\n\n  const { mutate: deleteBackup, isPending: isDeleting } = useMutation(\n    api.backups.delete.mutationOptions({\n      onSuccess: () => {\n        toast({\n          description: t(\"settings.backups.toasts.backup_deleted\"),\n        });\n        queryClient.invalidateQueries(api.backups.list.pathFilter());\n      },\n      onError: (error) => {\n        toast({\n          description: `Error: ${error.message}`,\n          variant: \"destructive\",\n        });\n      },\n    }),\n  );\n\n  const formatSize = (bytes: number) => {\n    if (bytes < 1024) return `${bytes} B`;\n    if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;\n    return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;\n  };\n\n  return (\n    <TableRow>\n      <TableCell>{backup.createdAt.toLocaleString()}</TableCell>\n      <TableCell>\n        {backup.status === \"pending\"\n          ? \"-\"\n          : backup.bookmarkCount.toLocaleString()}\n      </TableCell>\n      <TableCell>\n        {backup.status === \"pending\" ? \"-\" : formatSize(backup.size)}\n      </TableCell>\n      <TableCell>\n        {backup.status === \"success\" ? (\n          <span\n            title={t(\"settings.backups.list.status.success\")}\n            className=\"flex items-center gap-1\"\n          >\n            <CheckCircle className=\"size-4 text-green-600\" />\n            {t(\"settings.backups.list.status.success\")}\n          </span>\n        ) : backup.status === \"failure\" ? (\n          <Tooltip>\n            <TooltipTrigger asChild>\n              <span\n                title={\n                  backup.errorMessage ||\n                  t(\"settings.backups.list.status.failed\")\n                }\n                className=\"flex items-center gap-1\"\n              >\n                <XCircle className=\"size-4 text-red-600\" />\n                {t(\"settings.backups.list.status.failed\")}\n              </span>\n            </TooltipTrigger>\n            <TooltipContent>{backup.errorMessage}</TooltipContent>\n          </Tooltip>\n        ) : (\n          <span className=\"flex items-center gap-1\">\n            <div className=\"size-4 animate-spin rounded-full border-2 border-gray-300 border-t-gray-600\" />\n            {t(\"settings.backups.list.status.pending\")}\n          </span>\n        )}\n      </TableCell>\n      <TableCell className=\"flex items-center gap-2\">\n        {backup.assetId && (\n          <Tooltip>\n            <TooltipTrigger asChild>\n              <Button\n                asChild\n                variant=\"ghost\"\n                className=\"items-center\"\n                disabled={backup.status !== \"success\"}\n              >\n                <Link\n                  href={getAssetUrl(backup.assetId)}\n                  download\n                  prefetch={false}\n                  className={\n                    backup.status !== \"success\"\n                      ? \"pointer-events-none opacity-50\"\n                      : \"\"\n                  }\n                >\n                  <Download className=\"size-4\" />\n                </Link>\n              </Button>\n            </TooltipTrigger>\n            <TooltipContent>\n              {t(\"settings.backups.list.actions.download_backup\")}\n            </TooltipContent>\n          </Tooltip>\n        )}\n        <ActionConfirmingDialog\n          title={t(\"settings.backups.dialogs.delete_backup_title\")}\n          description={t(\"settings.backups.dialogs.delete_backup_description\")}\n          actionButton={() => (\n            <ActionButton\n              loading={isDeleting}\n              variant=\"destructive\"\n              onClick={() => deleteBackup({ backupId: backup.id })}\n              className=\"items-center\"\n              type=\"button\"\n            >\n              <Trash2 className=\"mr-2 size-4\" />\n              {t(\"settings.backups.list.actions.delete_backup\")}\n            </ActionButton>\n          )}\n        >\n          <Button variant=\"ghostDestructive\" disabled={isDeleting}>\n            <Trash2 className=\"size-4\" />\n          </Button>\n        </ActionConfirmingDialog>\n      </TableCell>\n    </TableRow>\n  );\n}\n\nfunction BackupsList() {\n  const api = useTRPC();\n  const { t } = useTranslation();\n  const queryClient = useQueryClient();\n  const { data: backups, isLoading } = useQuery(\n    api.backups.list.queryOptions(undefined, {\n      refetchInterval: (query) => {\n        const data = query.state.data;\n        // Poll every 3 seconds if there's a pending backup, otherwise don't poll\n        return data?.backups.some((backup) => backup.status === \"pending\")\n          ? 3000\n          : false;\n      },\n    }),\n  );\n\n  const { mutate: triggerBackup, isPending: isTriggering } = useMutation(\n    api.backups.triggerBackup.mutationOptions({\n      onSuccess: () => {\n        toast({\n          description: t(\"settings.backups.toasts.backup_queued\"),\n        });\n        queryClient.invalidateQueries(api.backups.list.pathFilter());\n      },\n      onError: (error) => {\n        toast({\n          description: `Error: ${error.message}`,\n          variant: \"destructive\",\n        });\n      },\n    }),\n  );\n\n  return (\n    <SettingsSection\n      title={t(\"settings.backups.list.title\")}\n      action={\n        <ActionButton\n          onClick={() => triggerBackup()}\n          loading={isTriggering}\n          variant=\"default\"\n          className=\"items-center\"\n        >\n          <Play className=\"mr-2 size-4\" />\n          {t(\"settings.backups.list.create_backup_now\")}\n        </ActionButton>\n      }\n    >\n      {isLoading && <FullPageSpinner />}\n\n      {backups && backups.backups.length === 0 && (\n        <p className=\"rounded-md bg-muted p-3 text-center text-sm text-muted-foreground\">\n          {t(\"settings.backups.list.no_backups\")}\n        </p>\n      )}\n\n      {backups && backups.backups.length > 0 && (\n        <Table>\n          <TableHeader>\n            <TableRow>\n              <TableHead>\n                {t(\"settings.backups.list.table.created_at\")}\n              </TableHead>\n              <TableHead>\n                {t(\"settings.backups.list.table.bookmarks\")}\n              </TableHead>\n              <TableHead>{t(\"settings.backups.list.table.size\")}</TableHead>\n              <TableHead>{t(\"settings.backups.list.table.status\")}</TableHead>\n              <TableHead>{t(\"settings.backups.list.table.actions\")}</TableHead>\n            </TableRow>\n          </TableHeader>\n          <TableBody>\n            {backups.backups.map((backup) => (\n              <BackupRow key={backup.id} backup={backup} />\n            ))}\n          </TableBody>\n        </Table>\n      )}\n    </SettingsSection>\n  );\n}\n\nexport default function BackupSettings() {\n  return (\n    <>\n      <BackupConfigurationForm />\n      <BackupsList />\n    </>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/settings/ChangePassword.tsx",
    "content": "\"use client\";\n\nimport type { z } from \"zod\";\nimport { useState } from \"react\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport {\n  Form,\n  FormControl,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@/components/ui/form\";\nimport { Input } from \"@/components/ui/input\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { useMutation } from \"@tanstack/react-query\";\nimport { Eye, EyeOff } from \"lucide-react\";\nimport { useForm } from \"react-hook-form\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\nimport { zChangePasswordSchema } from \"@karakeep/shared/types/users\";\n\nimport { Button } from \"../ui/button\";\nimport { SettingsSection } from \"./SettingsPage\";\n\nexport function ChangePassword() {\n  const api = useTRPC();\n  const { t } = useTranslation();\n  const [showCurrentPassword, setShowCurrentPassword] = useState(false);\n  const [showNewPassword, setShowNewPassword] = useState(false);\n  const [showConfirmPassword, setShowConfirmPassword] = useState(false);\n  const form = useForm<z.infer<typeof zChangePasswordSchema>>({\n    resolver: zodResolver(zChangePasswordSchema),\n    defaultValues: {\n      currentPassword: \"\",\n      newPassword: \"\",\n      newPasswordConfirm: \"\",\n    },\n  });\n\n  const mutator = useMutation(\n    api.users.changePassword.mutationOptions({\n      onSuccess: () => {\n        toast({ description: \"Password changed successfully\" });\n        form.reset();\n      },\n      onError: (e) => {\n        if (e.data?.code == \"UNAUTHORIZED\") {\n          toast({\n            description: \"Your current password is incorrect\",\n            variant: \"destructive\",\n          });\n        } else {\n          toast({\n            description: \"Something went wrong\",\n            variant: \"destructive\",\n          });\n        }\n      },\n    }),\n  );\n\n  async function onSubmit(value: z.infer<typeof zChangePasswordSchema>) {\n    mutator.mutate({\n      currentPassword: value.currentPassword,\n      newPassword: value.newPassword,\n    });\n  }\n\n  return (\n    <SettingsSection title=\"Security\">\n      <Form {...form}>\n        <form onSubmit={form.handleSubmit(onSubmit)} className=\"space-y-4\">\n          <FormField\n            control={form.control}\n            name=\"currentPassword\"\n            render={({ field }) => (\n              <FormItem className=\"space-y-2\">\n                <FormLabel\n                  htmlFor=\"current-password\"\n                  className=\"text-sm font-medium\"\n                >\n                  {t(\"settings.info.current_password\")}\n                </FormLabel>\n                <div className=\"relative\">\n                  <FormControl>\n                    <Input\n                      id=\"current-password\"\n                      type={showCurrentPassword ? \"text\" : \"password\"}\n                      placeholder={t(\"settings.info.current_password\")}\n                      className=\"h-11 pr-10\"\n                      {...field}\n                    />\n                  </FormControl>\n                  <Button\n                    type=\"button\"\n                    variant=\"ghost\"\n                    size=\"sm\"\n                    className=\"absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent\"\n                    onClick={() => setShowCurrentPassword(!showCurrentPassword)}\n                  >\n                    {showCurrentPassword ? (\n                      <EyeOff className=\"h-4 w-4\" />\n                    ) : (\n                      <Eye className=\"h-4 w-4\" />\n                    )}\n                  </Button>\n                </div>\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n\n          <div className=\"grid gap-4 md:grid-cols-2\">\n            <FormField\n              control={form.control}\n              name=\"newPassword\"\n              render={({ field }) => (\n                <FormItem className=\"space-y-2\">\n                  <FormLabel\n                    htmlFor=\"new-password\"\n                    className=\"text-sm font-medium\"\n                  >\n                    {t(\"settings.info.new_password\")}\n                  </FormLabel>\n                  <div className=\"relative\">\n                    <FormControl>\n                      <Input\n                        id=\"new-password\"\n                        type={showNewPassword ? \"text\" : \"password\"}\n                        placeholder={t(\"settings.info.new_password\")}\n                        className=\"h-11 pr-10\"\n                        {...field}\n                      />\n                    </FormControl>\n                    <Button\n                      type=\"button\"\n                      variant=\"ghost\"\n                      size=\"sm\"\n                      className=\"absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent\"\n                      onClick={() => setShowNewPassword(!showNewPassword)}\n                    >\n                      {showNewPassword ? (\n                        <EyeOff className=\"h-4 w-4\" />\n                      ) : (\n                        <Eye className=\"h-4 w-4\" />\n                      )}\n                    </Button>\n                  </div>\n                  <FormMessage />\n                </FormItem>\n              )}\n            />\n\n            <FormField\n              control={form.control}\n              name=\"newPasswordConfirm\"\n              render={({ field }) => (\n                <FormItem className=\"space-y-2\">\n                  <FormLabel\n                    htmlFor=\"confirm-password\"\n                    className=\"text-sm font-medium\"\n                  >\n                    {t(\"settings.info.confirm_new_password\")}\n                  </FormLabel>\n                  <div className=\"relative\">\n                    <FormControl>\n                      <Input\n                        id=\"confirm-password\"\n                        type={showConfirmPassword ? \"text\" : \"password\"}\n                        placeholder={t(\"settings.info.confirm_new_password\")}\n                        className=\"h-11 pr-10\"\n                        {...field}\n                      />\n                    </FormControl>\n                    <Button\n                      type=\"button\"\n                      variant=\"ghost\"\n                      size=\"sm\"\n                      className=\"absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent\"\n                      onClick={() =>\n                        setShowConfirmPassword(!showConfirmPassword)\n                      }\n                    >\n                      {showConfirmPassword ? (\n                        <EyeOff className=\"h-4 w-4\" />\n                      ) : (\n                        <Eye className=\"h-4 w-4\" />\n                      )}\n                    </Button>\n                  </div>\n                  <FormMessage />\n                </FormItem>\n              )}\n            />\n          </div>\n\n          <div className=\"flex justify-end\">\n            <ActionButton type=\"submit\" loading={mutator.isPending}>\n              {t(\"actions.save\")}\n            </ActionButton>\n          </div>\n        </form>\n      </Form>\n    </SettingsSection>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/settings/DeleteAccount.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport ActionConfirmingDialog from \"@/components/ui/action-confirming-dialog\";\nimport {\n  Form,\n  FormControl,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@/components/ui/form\";\nimport { Input } from \"@/components/ui/input\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { AlertTriangle, Eye, EyeOff, Trash2 } from \"lucide-react\";\nimport { useForm } from \"react-hook-form\";\nimport { z } from \"zod\";\n\nimport {\n  useDeleteAccount,\n  useWhoAmI,\n} from \"@karakeep/shared-react/hooks/users\";\n\nimport { Button } from \"../ui/button\";\nimport { SettingsSection } from \"./SettingsPage\";\n\nconst createDeleteAccountSchema = (isLocalUser: boolean) =>\n  z.object({\n    password: isLocalUser\n      ? z.string().min(1, \"Password is required\")\n      : z.string().optional(),\n  });\n\nexport function DeleteAccount() {\n  const router = useRouter();\n  const [showPassword, setShowPassword] = useState(false);\n  const [isDialogOpen, setIsDialogOpen] = useState(false);\n  const { data: user } = useWhoAmI();\n\n  const isLocalUser = user?.localUser ?? false;\n  const deleteAccountSchema = createDeleteAccountSchema(isLocalUser);\n\n  const form = useForm<z.infer<typeof deleteAccountSchema>>({\n    resolver: zodResolver(deleteAccountSchema),\n    defaultValues: {\n      password: \"\",\n    },\n  });\n\n  const deleteAccountMutation = useDeleteAccount({\n    onSuccess: () => {\n      toast({\n        description: \"Your account has been successfully deleted.\",\n      });\n      // Redirect to home page after successful deletion\n      router.push(\"/\");\n      setIsDialogOpen(false);\n    },\n    onError: (error) => {\n      if (error.data?.code === \"UNAUTHORIZED\") {\n        toast({\n          description: \"Invalid password. Please try again.\",\n          variant: \"destructive\",\n        });\n      } else {\n        toast({\n          description: \"Failed to delete account. Please try again.\",\n          variant: \"destructive\",\n        });\n      }\n    },\n  });\n\n  const onSubmit = (values: z.infer<typeof deleteAccountSchema>) => {\n    deleteAccountMutation.mutate({ password: values.password });\n  };\n\n  return (\n    <SettingsSection title=\"Danger Zone\" variant=\"danger\">\n      <div className=\"space-y-2\">\n        <h3 className=\"text-lg font-medium\">Delete Account</h3>\n        <p className=\"text-sm text-muted-foreground\">\n          Permanently delete your account and all associated data. This action\n          cannot be undone.\n        </p>\n      </div>\n\n      <ActionConfirmingDialog\n        open={isDialogOpen}\n        setOpen={setIsDialogOpen}\n        title=\"Delete Account\"\n        description={\n          <div className=\"space-y-4\">\n            <div className=\"flex items-start gap-3 rounded-lg border border-destructive/20 bg-destructive/5 p-4\">\n              <AlertTriangle className=\"mt-0.5 h-5 w-5 flex-shrink-0 text-destructive\" />\n              <div className=\"space-y-2\">\n                <p className=\"font-medium text-destructive\">\n                  This action is irreversible\n                </p>\n                <p className=\"text-sm text-muted-foreground\">\n                  All your bookmarks, lists, tags, highlights, and other data\n                  will be permanently deleted. This cannot be undone.\n                </p>\n              </div>\n            </div>\n\n            <Form {...form}>\n              <form\n                onSubmit={form.handleSubmit(onSubmit)}\n                className=\"space-y-4\"\n              >\n                {isLocalUser && (\n                  <FormField\n                    control={form.control}\n                    name=\"password\"\n                    render={({ field }) => (\n                      <FormItem>\n                        <FormLabel>\n                          Enter your password to confirm deletion\n                        </FormLabel>\n                        <div className=\"relative\">\n                          <FormControl>\n                            <Input\n                              type={showPassword ? \"text\" : \"password\"}\n                              placeholder=\"Enter your password\"\n                              className=\"pr-10\"\n                              {...field}\n                            />\n                          </FormControl>\n                          <Button\n                            type=\"button\"\n                            variant=\"ghost\"\n                            size=\"sm\"\n                            className=\"absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent\"\n                            onClick={() => setShowPassword(!showPassword)}\n                          >\n                            {showPassword ? (\n                              <EyeOff className=\"h-4 w-4\" />\n                            ) : (\n                              <Eye className=\"h-4 w-4\" />\n                            )}\n                          </Button>\n                        </div>\n                        <FormMessage />\n                      </FormItem>\n                    )}\n                  />\n                )}\n              </form>\n            </Form>\n          </div>\n        }\n        actionButton={() => (\n          <ActionButton\n            variant=\"destructive\"\n            loading={deleteAccountMutation.isPending}\n            onClick={form.handleSubmit(onSubmit)}\n          >\n            <Trash2 className=\"mr-2 h-4 w-4\" />\n            Delete Account\n          </ActionButton>\n        )}\n      >\n        <Button variant=\"destructive\" className=\"w-full\">\n          <Trash2 className=\"mr-2 h-4 w-4\" />\n          Delete Account\n        </Button>\n      </ActionConfirmingDialog>\n    </SettingsSection>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/settings/DeleteApiKey.tsx",
    "content": "\"use client\";\n\nimport { useRouter } from \"next/navigation\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport ActionConfirmingDialog from \"@/components/ui/action-confirming-dialog\";\nimport { Button } from \"@/components/ui/button\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { useMutation } from \"@tanstack/react-query\";\nimport { Trash } from \"lucide-react\";\nimport { toast } from \"sonner\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\nexport default function DeleteApiKey({\n  name,\n  id,\n}: {\n  name: string;\n  id: string;\n}) {\n  const api = useTRPC();\n  const { t } = useTranslation();\n  const router = useRouter();\n  const mutator = useMutation(\n    api.apiKeys.revoke.mutationOptions({\n      onSuccess: () => {\n        toast.success(\"Key was successfully deleted\");\n        router.refresh();\n      },\n    }),\n  );\n\n  return (\n    <ActionConfirmingDialog\n      title={\"Delete API Key\"}\n      description={\n        <p>\n          Are you sure you want to delete the API key &quot;{name}&quot;? Any\n          service using this API key will lose access.\n        </p>\n      }\n      actionButton={(setDialogOpen) => (\n        <ActionButton\n          type=\"button\"\n          variant=\"destructive\"\n          loading={mutator.isPending}\n          onClick={() =>\n            mutator.mutate({ id }, { onSuccess: () => setDialogOpen(false) })\n          }\n        >\n          {t(\"actions.delete\")}\n        </ActionButton>\n      )}\n    >\n      <Button variant=\"ghost\" title={t(\"actions.delete\")}>\n        <Trash size={18} />\n      </Button>\n    </ActionConfirmingDialog>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/settings/FeedSettings.tsx",
    "content": "\"use client\";\n\nimport React from \"react\";\nimport Link from \"next/link\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport {\n  Form,\n  FormControl,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@/components/ui/form\";\nimport { FullPageSpinner } from \"@/components/ui/full-page-spinner\";\nimport { Input } from \"@/components/ui/input\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { Switch } from \"@/components/ui/switch\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { cn } from \"@/lib/utils\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport {\n  ArrowDownToLine,\n  CheckCircle,\n  CircleDashed,\n  CirclePlus,\n  Edit,\n  Plus,\n  Save,\n  Trash2,\n  XCircle,\n} from \"lucide-react\";\nimport { useForm } from \"react-hook-form\";\nimport { z } from \"zod\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\nimport {\n  ZFeed,\n  zNewFeedSchema,\n  zUpdateFeedSchema,\n} from \"@karakeep/shared/types/feeds\";\n\nimport ActionConfirmingDialog from \"../ui/action-confirming-dialog\";\nimport { Button, buttonVariants } from \"../ui/button\";\nimport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"../ui/dialog\";\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"../ui/table\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"../ui/tooltip\";\nimport { SettingsPage, SettingsSection } from \"./SettingsPage\";\n\nexport function FeedsEditorDialog() {\n  const api = useTRPC();\n  const { t } = useTranslation();\n  const [open, setOpen] = React.useState(false);\n  const queryClient = useQueryClient();\n\n  const form = useForm<z.infer<typeof zNewFeedSchema>>({\n    resolver: zodResolver(zNewFeedSchema),\n    defaultValues: {\n      name: \"\",\n      url: \"\",\n      enabled: true,\n      importTags: false,\n    },\n  });\n\n  React.useEffect(() => {\n    if (open) {\n      form.reset();\n    }\n  }, [open]);\n\n  const { mutateAsync: createFeed, isPending: isCreating } = useMutation(\n    api.feeds.create.mutationOptions({\n      onSuccess: () => {\n        toast({\n          description: \"Feed has been created!\",\n        });\n        queryClient.invalidateQueries(api.feeds.list.pathFilter());\n        setOpen(false);\n      },\n    }),\n  );\n\n  return (\n    <Dialog open={open} onOpenChange={setOpen}>\n      <DialogTrigger asChild>\n        <Button>\n          <CirclePlus className=\"mr-2 size-4\" />\n          {t(\"settings.feeds.add_a_subscription\")}\n        </Button>\n      </DialogTrigger>\n      <DialogContent>\n        <DialogHeader>\n          <DialogTitle>Subscribe to a new Feed</DialogTitle>\n        </DialogHeader>\n        <Form {...form}>\n          <form\n            className=\"flex flex-col gap-3\"\n            onSubmit={form.handleSubmit(async (value) => {\n              await createFeed(value);\n              form.resetField(\"name\");\n              form.resetField(\"url\");\n            })}\n          >\n            <FormField\n              control={form.control}\n              name=\"name\"\n              render={({ field }) => {\n                return (\n                  <FormItem className=\"flex-1\">\n                    <FormLabel>Name</FormLabel>\n                    <FormControl>\n                      <Input placeholder=\"Feed Name\" type=\"text\" {...field} />\n                    </FormControl>\n                    <FormMessage />\n                  </FormItem>\n                );\n              }}\n            />\n            <FormField\n              control={form.control}\n              name=\"url\"\n              render={({ field }) => {\n                return (\n                  <FormItem className=\"flex-1\">\n                    <FormLabel>URL</FormLabel>\n                    <FormControl>\n                      <Input placeholder=\"Feed URL\" type=\"text\" {...field} />\n                    </FormControl>\n                    <FormMessage />\n                  </FormItem>\n                );\n              }}\n            />\n            <FormField\n              control={form.control}\n              name=\"importTags\"\n              render={({ field }) => {\n                return (\n                  <FormItem className=\"flex flex-row items-center justify-between rounded-lg border p-3\">\n                    <div className=\"space-y-0.5\">\n                      <FormLabel>Import Tags</FormLabel>\n                      <div className=\"text-sm text-muted-foreground\">\n                        Automatically import categories from RSS feed as tags\n                      </div>\n                    </div>\n                    <FormControl>\n                      <Switch\n                        checked={field.value}\n                        onCheckedChange={field.onChange}\n                      />\n                    </FormControl>\n                  </FormItem>\n                );\n              }}\n            />\n          </form>\n        </Form>\n        <DialogFooter>\n          <DialogClose asChild>\n            <Button type=\"button\" variant=\"secondary\">\n              Close\n            </Button>\n          </DialogClose>\n          <ActionButton\n            onClick={form.handleSubmit(async (value) => {\n              await createFeed(value);\n            })}\n            loading={isCreating}\n            variant=\"default\"\n            className=\"items-center\"\n          >\n            <Plus className=\"mr-2 size-4\" />\n            Add\n          </ActionButton>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n\nexport function EditFeedDialog({ feed }: { feed: ZFeed }) {\n  const api = useTRPC();\n  const { t } = useTranslation();\n  const queryClient = useQueryClient();\n  const [open, setOpen] = React.useState(false);\n  React.useEffect(() => {\n    if (open) {\n      form.reset({\n        feedId: feed.id,\n        name: feed.name,\n        url: feed.url,\n        importTags: feed.importTags,\n      });\n    }\n  }, [open]);\n  const { mutateAsync: updateFeed, isPending: isUpdating } = useMutation(\n    api.feeds.update.mutationOptions({\n      onSuccess: () => {\n        toast({\n          description: \"Feed has been updated!\",\n        });\n        setOpen(false);\n        queryClient.invalidateQueries(api.feeds.list.pathFilter());\n      },\n    }),\n  );\n  const form = useForm<z.infer<typeof zUpdateFeedSchema>>({\n    resolver: zodResolver(zUpdateFeedSchema),\n    defaultValues: {\n      feedId: feed.id,\n      name: feed.name,\n      url: feed.url,\n      importTags: feed.importTags,\n    },\n  });\n  return (\n    <Dialog open={open} onOpenChange={setOpen}>\n      <Tooltip>\n        <TooltipTrigger asChild>\n          <DialogTrigger asChild>\n            <Button variant=\"ghost\">\n              <Edit className=\"size-4\" />\n            </Button>\n          </DialogTrigger>\n        </TooltipTrigger>\n        <TooltipContent>{t(\"actions.edit\")}</TooltipContent>\n      </Tooltip>\n      <DialogContent>\n        <DialogHeader>\n          <DialogTitle>Edit Feed</DialogTitle>\n        </DialogHeader>\n\n        <Form {...form}>\n          <form\n            className=\"flex flex-col gap-3\"\n            onSubmit={form.handleSubmit(async (value) => {\n              await updateFeed(value);\n            })}\n          >\n            <FormField\n              control={form.control}\n              name=\"feedId\"\n              render={({ field }) => {\n                return (\n                  <FormItem className=\"hidden\">\n                    <FormControl>\n                      <Input type=\"hidden\" {...field} />\n                    </FormControl>\n                    <FormMessage />\n                  </FormItem>\n                );\n              }}\n            />\n            <FormField\n              control={form.control}\n              name=\"name\"\n              render={({ field }) => {\n                return (\n                  <FormItem className=\"flex-1\">\n                    <FormLabel>{t(\"common.name\")}</FormLabel>\n                    <FormControl>\n                      <Input placeholder=\"Feed name\" type=\"text\" {...field} />\n                    </FormControl>\n                    <FormMessage />\n                  </FormItem>\n                );\n              }}\n            />\n            <FormField\n              control={form.control}\n              name=\"url\"\n              render={({ field }) => {\n                return (\n                  <FormItem className=\"flex-1\">\n                    <FormLabel>{t(\"common.url\")}</FormLabel>\n                    <FormControl>\n                      <Input placeholder=\"Feed url\" type=\"text\" {...field} />\n                    </FormControl>\n                    <FormMessage />\n                  </FormItem>\n                );\n              }}\n            />\n            <FormField\n              control={form.control}\n              name=\"importTags\"\n              render={({ field }) => {\n                return (\n                  <FormItem className=\"flex flex-row items-center justify-between rounded-lg border p-3\">\n                    <div className=\"space-y-0.5\">\n                      <FormLabel>Import Tags</FormLabel>\n                      <div className=\"text-sm text-muted-foreground\">\n                        Automatically import categories from RSS feed as tags\n                      </div>\n                    </div>\n                    <FormControl>\n                      <Switch\n                        checked={field.value}\n                        onCheckedChange={field.onChange}\n                      />\n                    </FormControl>\n                  </FormItem>\n                );\n              }}\n            />\n          </form>\n        </Form>\n        <DialogFooter>\n          <DialogClose asChild>\n            <Button type=\"button\" variant=\"secondary\">\n              {t(\"actions.close\")}\n            </Button>\n          </DialogClose>\n          <ActionButton\n            loading={isUpdating}\n            onClick={form.handleSubmit(async (value) => {\n              await updateFeed(value);\n            })}\n            type=\"submit\"\n            className=\"items-center\"\n          >\n            <Save className=\"mr-2 size-4\" />\n            {t(\"actions.save\")}\n          </ActionButton>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n\nexport function FeedRow({ feed }: { feed: ZFeed }) {\n  const api = useTRPC();\n  const { t } = useTranslation();\n  const queryClient = useQueryClient();\n  const { mutate: deleteFeed, isPending: isDeleting } = useMutation(\n    api.feeds.delete.mutationOptions({\n      onSuccess: () => {\n        toast({\n          description: \"Feed has been deleted!\",\n        });\n        queryClient.invalidateQueries(api.feeds.list.pathFilter());\n      },\n    }),\n  );\n\n  const { mutate: fetchNow, isPending: isFetching } = useMutation(\n    api.feeds.fetchNow.mutationOptions({\n      onSuccess: () => {\n        toast({\n          description: \"Feed fetch has been enqueued!\",\n        });\n        queryClient.invalidateQueries(api.feeds.list.pathFilter());\n      },\n    }),\n  );\n\n  const { mutate: updateFeedEnabled } = useMutation(\n    api.feeds.update.mutationOptions({\n      onSuccess: () => {\n        toast({\n          description: feed.enabled\n            ? t(\"settings.feeds.feed_disabled\")\n            : t(\"settings.feeds.feed_enabled\"),\n        });\n        queryClient.invalidateQueries(api.feeds.list.pathFilter());\n      },\n      onError: (error) => {\n        toast({\n          description: `Error: ${error.message}`,\n          variant: \"destructive\",\n        });\n      },\n    }),\n  );\n\n  const handleToggle = (checked: boolean) => {\n    updateFeedEnabled({ feedId: feed.id, enabled: checked });\n  };\n\n  return (\n    <TableRow>\n      <TableCell>\n        <Link\n          href={`/dashboard/feeds/${feed.id}`}\n          className={cn(buttonVariants({ variant: \"link\" }))}\n        >\n          {feed.name}\n        </Link>\n      </TableCell>\n      <TableCell\n        className=\"max-w-64 overflow-clip text-ellipsis\"\n        title={feed.url}\n      >\n        {feed.url}\n      </TableCell>\n      <TableCell>{feed.lastFetchedAt?.toLocaleString()}</TableCell>\n      <TableCell>\n        {feed.lastFetchedStatus === \"success\" ? (\n          <span title=\"Successful\">\n            <CheckCircle />\n          </span>\n        ) : feed.lastFetchedStatus === \"failure\" ? (\n          <span title=\"Failed\">\n            <XCircle />\n          </span>\n        ) : (\n          <span title=\"Pending\">\n            <CircleDashed name=\"Pending\" />\n          </span>\n        )}\n      </TableCell>\n      <TableCell className=\"flex items-center gap-2\">\n        <Switch checked={feed.enabled} onCheckedChange={handleToggle} />\n        <EditFeedDialog feed={feed} />\n        <Tooltip>\n          <TooltipTrigger asChild>\n            <ActionButton\n              loading={isFetching}\n              variant=\"ghost\"\n              className=\"items-center\"\n              onClick={() => fetchNow({ feedId: feed.id })}\n            >\n              <ArrowDownToLine className=\"size-4\" />\n            </ActionButton>\n          </TooltipTrigger>\n          <TooltipContent>{t(\"actions.fetch_now\")}</TooltipContent>\n        </Tooltip>\n        <ActionConfirmingDialog\n          title={`Delete Feed \"${feed.name}\"?`}\n          description={`Are you sure you want to delete the feed \"${feed.name}\"?`}\n          actionButton={() => (\n            <ActionButton\n              loading={isDeleting}\n              variant=\"destructive\"\n              onClick={() => deleteFeed({ feedId: feed.id })}\n              className=\"items-center\"\n              type=\"button\"\n            >\n              <Trash2 className=\"mr-2 size-4\" />\n              {t(\"actions.delete\")}\n            </ActionButton>\n          )}\n        >\n          <Button variant=\"ghostDestructive\" disabled={isDeleting}>\n            <Trash2 className=\"size-4\" />\n          </Button>\n        </ActionConfirmingDialog>\n      </TableCell>\n    </TableRow>\n  );\n}\n\nexport default function FeedSettings() {\n  const api = useTRPC();\n  const { t } = useTranslation();\n  const { data: feeds, isLoading } = useQuery(api.feeds.list.queryOptions());\n  return (\n    <SettingsPage\n      title={t(\"settings.feeds.rss_subscriptions\")}\n      action={<FeedsEditorDialog />}\n    >\n      <SettingsSection>\n        {isLoading && <FullPageSpinner />}\n        {feeds && feeds.feeds.length == 0 && (\n          <p className=\"rounded-md bg-muted p-3 text-center text-sm text-muted-foreground\">\n            You don&apos;t have any RSS subscriptions yet.\n          </p>\n        )}\n        {feeds && feeds.feeds.length > 0 && (\n          <Table>\n            <TableHeader>\n              <TableRow>\n                <TableHead>{t(\"common.name\")}</TableHead>\n                <TableHead>{t(\"common.url\")}</TableHead>\n                <TableHead>Last Fetch</TableHead>\n                <TableHead>Last Status</TableHead>\n                <TableHead>{t(\"common.actions\")}</TableHead>\n              </TableRow>\n            </TableHeader>\n            <TableBody>\n              {feeds.feeds.map((feed) => (\n                <FeedRow key={feed.id} feed={feed} />\n              ))}\n            </TableBody>\n          </Table>\n        )}\n      </SettingsSection>\n    </SettingsPage>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/settings/ImportExport.tsx",
    "content": "\"use client\";\n\nimport { useCallback, useEffect, useState } from \"react\";\nimport { Alert, AlertDescription, AlertTitle } from \"@/components/ui/alert\";\nimport { Button, buttonVariants } from \"@/components/ui/button\";\nimport FilePickerButton from \"@/components/ui/file-picker-button\";\nimport { Progress } from \"@/components/ui/progress\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { useBookmarkImport } from \"@/lib/hooks/useBookmarkImport\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { cn } from \"@/lib/utils\";\nimport { useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport { AlertCircle, Download, Loader2, Upload } from \"lucide-react\";\n\nimport { Card, CardContent } from \"../ui/card\";\nimport { ImportSessionsSection } from \"./ImportSessionsSection\";\nimport { SettingsPage, SettingsSection } from \"./SettingsPage\";\n\nfunction ImportCard({\n  text,\n  description,\n  children,\n}: {\n  text: string;\n  description: string;\n  children: React.ReactNode;\n}) {\n  return (\n    <Card className=\"transition-all hover:shadow-md\">\n      <CardContent className=\"flex items-center gap-3 p-4\">\n        <div className=\"rounded-full bg-primary/10 p-2\">\n          <Download className=\"h-5 w-5 text-primary\" />\n        </div>\n        <div className=\"flex-1\">\n          <h3 className=\"font-medium\">{text}</h3>\n          <p>{description}</p>\n        </div>\n        {children}\n      </CardContent>\n    </Card>\n  );\n}\n\nfunction ExportButton() {\n  const { t } = useTranslation();\n  const [format, setFormat] = useState<\"json\" | \"netscape\">(\"json\");\n  const queryClient = useQueryClient();\n  const { isFetching, refetch, error } = useQuery({\n    queryKey: [\"exportBookmarks\"],\n    queryFn: async () => {\n      const res = await fetch(`/api/bookmarks/export?format=${format}`);\n      if (!res.ok) {\n        const error = await res.json();\n        throw new Error(error?.error || \"Failed to export bookmarks\");\n      }\n      const match = res.headers\n        .get(\"Content-Disposition\")\n        ?.match(/filename\\*?=(?:UTF-8''|\")?([^\"]+)/i);\n      const filename = match\n        ? match[1]\n        : `karakeep-export-${new Date().toISOString()}.${format}`;\n      return { blob: res.blob(), filename };\n    },\n    enabled: false,\n  });\n\n  useEffect(() => {\n    if (error) {\n      toast({\n        description: error.message,\n        variant: \"destructive\",\n      });\n    }\n  }, [error]);\n\n  const onExport = useCallback(async () => {\n    const { data } = await refetch();\n    if (!data) return;\n    const { blob, filename } = data;\n    const url = window.URL.createObjectURL(await blob);\n    const a = document.createElement(\"a\");\n    a.href = url;\n    a.download = filename;\n    a.click();\n    window.URL.revokeObjectURL(url);\n    queryClient.setQueryData([\"exportBookmarks\"], () => null);\n  }, [refetch]);\n\n  return (\n    <Card className=\"transition-all hover:shadow-md\">\n      <CardContent className=\"flex items-center gap-3 p-4\">\n        <div className=\"rounded-full bg-primary/10 p-2\">\n          <Upload className=\"h-5 w-5 text-primary\" />\n        </div>\n        <div className=\"flex-1\">\n          <h3 className=\"font-medium\">Export File</h3>\n          <p>{t(\"settings.import.export_links_and_notes\")}</p>\n          <Select\n            value={format}\n            onValueChange={(value) => setFormat(value as \"json\" | \"netscape\")}\n          >\n            <SelectTrigger className=\"mt-2 w-[180px]\">\n              <SelectValue placeholder=\"Format\" />\n            </SelectTrigger>\n            <SelectContent>\n              <SelectItem value=\"json\">JSON (Karakeep format)</SelectItem>\n              <SelectItem value=\"netscape\">HTML (Netscape format)</SelectItem>\n            </SelectContent>\n          </Select>\n        </div>\n        <Button\n          className={cn(\n            buttonVariants({ variant: \"default\", size: \"sm\" }),\n            \"flex items-center gap-2\",\n          )}\n          onClick={onExport}\n          disabled={isFetching}\n        >\n          {isFetching && <Loader2 className=\"mr-2 animate-spin\" />}\n          <p>Export</p>\n        </Button>\n      </CardContent>\n    </Card>\n  );\n}\n\nexport function ImportExportRow() {\n  const { t } = useTranslation();\n  const { importProgress, quotaError, runUploadBookmarkFile } =\n    useBookmarkImport();\n\n  return (\n    <div className=\"flex flex-col gap-3\">\n      {quotaError && (\n        <Alert variant=\"destructive\" className=\"relative\">\n          <AlertCircle className=\"h-4 w-4\" />\n          <AlertTitle>Import Quota Exceeded</AlertTitle>\n          <AlertDescription>{quotaError}</AlertDescription>\n        </Alert>\n      )}\n      <div className=\"grid gap-4 md:grid-cols-2\">\n        <ImportCard\n          text=\"HTML File\"\n          description={t(\"settings.import.import_bookmarks_from_html_file\")}\n        >\n          <FilePickerButton\n            size={\"sm\"}\n            loading={false}\n            accept=\".html\"\n            multiple={false}\n            className=\"flex items-center gap-2\"\n            onFileSelect={(file) =>\n              runUploadBookmarkFile({ file, source: \"html\" })\n            }\n          >\n            <p>Import</p>\n          </FilePickerButton>\n        </ImportCard>\n        <ImportCard\n          text=\"Pocket\"\n          description={t(\"settings.import.import_bookmarks_from_pocket_export\")}\n        >\n          <FilePickerButton\n            size={\"sm\"}\n            loading={false}\n            accept=\".csv\"\n            multiple={false}\n            className=\"flex items-center gap-2\"\n            onFileSelect={(file) =>\n              runUploadBookmarkFile({ file, source: \"pocket\" })\n            }\n          >\n            <p>Import</p>\n          </FilePickerButton>\n        </ImportCard>\n        <ImportCard\n          text=\"Matter\"\n          description={t(\"settings.import.import_bookmarks_from_matter_export\")}\n        >\n          <FilePickerButton\n            size={\"sm\"}\n            loading={false}\n            accept=\".csv\"\n            multiple={false}\n            className=\"flex items-center gap-2\"\n            onFileSelect={(file) =>\n              runUploadBookmarkFile({ file, source: \"matter\" })\n            }\n          >\n            <p>Import</p>\n          </FilePickerButton>\n        </ImportCard>\n        <ImportCard\n          text=\"Omnivore\"\n          description={t(\n            \"settings.import.import_bookmarks_from_omnivore_export\",\n          )}\n        >\n          <FilePickerButton\n            size={\"sm\"}\n            loading={false}\n            accept=\".json\"\n            multiple={false}\n            className=\"flex items-center gap-2\"\n            onFileSelect={(file) =>\n              runUploadBookmarkFile({ file, source: \"omnivore\" })\n            }\n          >\n            <p>Import</p>\n          </FilePickerButton>\n        </ImportCard>\n        <ImportCard\n          text=\"Linkwarden\"\n          description={t(\n            \"settings.import.import_bookmarks_from_linkwarden_export\",\n          )}\n        >\n          <FilePickerButton\n            size={\"sm\"}\n            loading={false}\n            accept=\".json\"\n            multiple={false}\n            className=\"flex items-center gap-2\"\n            onFileSelect={(file) =>\n              runUploadBookmarkFile({ file, source: \"linkwarden\" })\n            }\n          >\n            <p>Import</p>\n          </FilePickerButton>\n        </ImportCard>\n        <ImportCard\n          text=\"Tab Session Manager\"\n          description={t(\n            \"settings.import.import_bookmarks_from_tab_session_manager_export\",\n          )}\n        >\n          <FilePickerButton\n            size={\"sm\"}\n            loading={false}\n            accept=\".json\"\n            multiple={false}\n            className=\"flex items-center gap-2\"\n            onFileSelect={(file) =>\n              runUploadBookmarkFile({ file, source: \"tab-session-manager\" })\n            }\n          >\n            <p>Import</p>\n          </FilePickerButton>\n        </ImportCard>\n        <ImportCard\n          text=\"mymind\"\n          description={t(\"settings.import.import_bookmarks_from_mymind_export\")}\n        >\n          <FilePickerButton\n            size={\"sm\"}\n            loading={false}\n            accept=\".csv\"\n            multiple={false}\n            className=\"flex items-center gap-2\"\n            onFileSelect={(file) =>\n              runUploadBookmarkFile({ file, source: \"mymind\" })\n            }\n          >\n            <p>Import</p>\n          </FilePickerButton>\n        </ImportCard>\n        <ImportCard\n          text=\"Instapaper\"\n          description={t(\n            \"settings.import.import_bookmarks_from_instapaper_export\",\n          )}\n        >\n          <FilePickerButton\n            size={\"sm\"}\n            loading={false}\n            accept=\".csv\"\n            multiple={false}\n            className=\"flex items-center gap-2\"\n            onFileSelect={(file) =>\n              runUploadBookmarkFile({ file, source: \"instapaper\" })\n            }\n          >\n            <p>Import</p>\n          </FilePickerButton>\n        </ImportCard>\n        <ImportCard\n          text=\"Karakeep\"\n          description={t(\n            \"settings.import.import_bookmarks_from_karakeep_export\",\n          )}\n        >\n          <FilePickerButton\n            size={\"sm\"}\n            loading={false}\n            accept=\".json\"\n            multiple={false}\n            className=\"flex items-center gap-2\"\n            onFileSelect={(file) =>\n              runUploadBookmarkFile({ file, source: \"karakeep\" })\n            }\n          >\n            <p>Import</p>\n          </FilePickerButton>\n        </ImportCard>\n        <ExportButton />\n      </div>\n      {importProgress && (\n        <div className=\"flex flex-col gap-2\">\n          <p className=\"shrink-0 text-sm\">\n            Processed {importProgress.done} of {importProgress.total} bookmarks\n          </p>\n          <div className=\"w-full\">\n            <Progress\n              value={(importProgress.done * 100) / importProgress.total}\n            />\n          </div>\n        </div>\n      )}\n    </div>\n  );\n}\n\nexport default function ImportExport() {\n  const { t } = useTranslation();\n  return (\n    <SettingsPage title={t(\"settings.import.import_export\")}>\n      <SettingsSection title={t(\"settings.import.import_export_bookmarks\")}>\n        <ImportExportRow />\n      </SettingsSection>\n\n      <ImportSessionsSection />\n    </SettingsPage>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/settings/ImportSessionCard.tsx",
    "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport ActionConfirmingDialog from \"@/components/ui/action-confirming-dialog\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardContent, CardHeader } from \"@/components/ui/card\";\nimport { Progress } from \"@/components/ui/progress\";\nimport {\n  useDeleteImportSession,\n  useImportSessionStats,\n  usePauseImportSession,\n  useResumeImportSession,\n} from \"@/lib/hooks/useImportSessions\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { formatDistanceToNow } from \"date-fns\";\nimport {\n  AlertCircle,\n  CheckCircle2,\n  ClipboardList,\n  Clock,\n  ExternalLink,\n  Loader2,\n  Pause,\n  Play,\n  Trash2,\n  Upload,\n} from \"lucide-react\";\n\nimport type {\n  ZImportSessionStatus,\n  ZImportSessionWithStats,\n} from \"@karakeep/shared/types/importSessions\";\nimport { switchCase } from \"@karakeep/shared/utils/switch\";\n\ninterface ImportSessionCardProps {\n  session: ZImportSessionWithStats;\n}\n\nfunction getStatusColor(status: string) {\n  switch (status) {\n    case \"staging\":\n      return \"bg-purple-500/10 text-purple-700 dark:text-purple-400\";\n    case \"pending\":\n      return \"bg-muted text-muted-foreground\";\n    case \"running\":\n      return \"bg-blue-500/10 text-blue-700 dark:text-blue-400\";\n    case \"paused\":\n      return \"bg-yellow-500/10 text-yellow-700 dark:text-yellow-400\";\n    case \"completed\":\n      return \"bg-green-500/10 text-green-700 dark:text-green-400\";\n    case \"failed\":\n      return \"bg-destructive/10 text-destructive\";\n    default:\n      return \"bg-muted text-muted-foreground\";\n  }\n}\n\nfunction getStatusIcon(status: string) {\n  switch (status) {\n    case \"staging\":\n      return <Upload className=\"h-4 w-4\" />;\n    case \"pending\":\n      return <Clock className=\"h-4 w-4\" />;\n    case \"running\":\n      return <Loader2 className=\"h-4 w-4 animate-spin\" />;\n    case \"paused\":\n      return <Pause className=\"h-4 w-4\" />;\n    case \"completed\":\n      return <CheckCircle2 className=\"h-4 w-4\" />;\n    case \"failed\":\n      return <AlertCircle className=\"h-4 w-4\" />;\n    default:\n      return <Clock className=\"h-4 w-4\" />;\n  }\n}\n\nexport function ImportSessionCard({ session }: ImportSessionCardProps) {\n  const { t } = useTranslation();\n  const { data: liveStats } = useImportSessionStats(session.id);\n  const deleteSession = useDeleteImportSession();\n  const pauseSession = usePauseImportSession();\n  const resumeSession = useResumeImportSession();\n\n  const statusLabels = (s: ZImportSessionStatus) =>\n    switchCase(s, {\n      staging: t(\"settings.import_sessions.status.staging\"),\n      pending: t(\"settings.import_sessions.status.pending\"),\n      running: t(\"settings.import_sessions.status.running\"),\n      paused: t(\"settings.import_sessions.status.paused\"),\n      completed: t(\"settings.import_sessions.status.completed\"),\n      failed: t(\"settings.import_sessions.status.failed\"),\n    });\n\n  // Use live stats if available, otherwise fallback to session stats\n  const stats = liveStats || session;\n  const progress =\n    stats.totalBookmarks > 0\n      ? ((stats.completedBookmarks + stats.failedBookmarks) /\n          stats.totalBookmarks) *\n        100\n      : 0;\n\n  const canDelete =\n    stats.status === \"completed\" ||\n    stats.status === \"failed\" ||\n    stats.status === \"paused\";\n\n  const canPause = stats.status === \"pending\" || stats.status === \"running\";\n\n  const canResume = stats.status === \"paused\";\n\n  return (\n    <Card className=\"transition-all hover:shadow-md\">\n      <CardHeader className=\"pb-3\">\n        <div className=\"flex items-start justify-between\">\n          <div className=\"flex-1\">\n            <h3 className=\"font-medium\">{session.name}</h3>\n            <p className=\"mt-1 text-sm text-accent-foreground\">\n              {t(\"settings.import_sessions.created_at\", {\n                time: formatDistanceToNow(session.createdAt, {\n                  addSuffix: true,\n                }),\n              })}\n            </p>\n          </div>\n          <div className=\"flex items-center gap-2\">\n            <Badge\n              className={`${getStatusColor(stats.status)} hover:bg-inherit`}\n            >\n              {getStatusIcon(stats.status)}\n              <span className=\"ml-1 capitalize\">\n                {statusLabels(stats.status)}\n              </span>\n            </Badge>\n          </div>\n        </div>\n      </CardHeader>\n\n      <CardContent className=\"pt-0\">\n        <div className=\"space-y-3\">\n          {/* Progress Section */}\n          <div className=\"space-y-3\">\n            <div className=\"flex items-center justify-between\">\n              <h4 className=\"text-sm font-medium text-muted-foreground\">\n                {t(\"settings.import_sessions.progress\")}\n              </h4>\n              <div className=\"flex items-center gap-2\">\n                <span className=\"text-sm font-medium\">\n                  {stats.completedBookmarks + stats.failedBookmarks} /{\" \"}\n                  {stats.totalBookmarks}\n                </span>\n                <Badge variant=\"outline\" className=\"text-xs\">\n                  {Math.round(progress)}%\n                </Badge>\n              </div>\n            </div>\n            {stats.totalBookmarks > 0 && (\n              <Progress value={progress} className=\"h-3\" />\n            )}\n          </div>\n\n          {/* Stats Breakdown */}\n          {stats.totalBookmarks > 0 && (\n            <div className=\"space-y-3\">\n              <div className=\"flex flex-wrap gap-2\">\n                {stats.pendingBookmarks > 0 && (\n                  <Badge\n                    variant=\"secondary\"\n                    className=\"bg-amber-500/10 text-amber-700 hover:bg-amber-500/10 dark:text-amber-400\"\n                  >\n                    <Clock className=\"mr-1.5 h-3 w-3\" />\n                    {t(\"settings.import_sessions.badges.pending\", {\n                      count: stats.pendingBookmarks,\n                    })}\n                  </Badge>\n                )}\n                {stats.processingBookmarks > 0 && (\n                  <Badge\n                    variant=\"secondary\"\n                    className=\"bg-blue-500/10 text-blue-700 hover:bg-blue-500/10 dark:text-blue-400\"\n                  >\n                    <Loader2 className=\"mr-1.5 h-3 w-3 animate-spin\" />\n                    {t(\"settings.import_sessions.badges.processing\", {\n                      count: stats.processingBookmarks,\n                    })}\n                  </Badge>\n                )}\n                {stats.completedBookmarks > 0 && (\n                  <Badge\n                    variant=\"secondary\"\n                    className=\"bg-green-500/10 text-green-700 hover:bg-green-500/10 dark:text-green-400\"\n                  >\n                    <CheckCircle2 className=\"mr-1.5 h-3 w-3\" />\n                    {t(\"settings.import_sessions.badges.completed\", {\n                      count: stats.completedBookmarks,\n                    })}\n                  </Badge>\n                )}\n                {stats.failedBookmarks > 0 && (\n                  <Badge\n                    variant=\"secondary\"\n                    className=\"bg-destructive/10 text-destructive hover:bg-destructive/10\"\n                  >\n                    <AlertCircle className=\"mr-1.5 h-3 w-3\" />\n                    {t(\"settings.import_sessions.badges.failed\", {\n                      count: stats.failedBookmarks,\n                    })}\n                  </Badge>\n                )}\n              </div>\n            </div>\n          )}\n\n          {/* Root List Link */}\n          {session.rootListId && (\n            <div className=\"rounded-lg border bg-muted/50 p-3 dark:bg-muted/20\">\n              <div className=\"flex items-center gap-2 text-sm\">\n                <ClipboardList className=\"h-4 w-4 text-muted-foreground\" />\n                <span className=\"font-medium text-muted-foreground\">\n                  {t(\"settings.import_sessions.imported_to\")}\n                </span>\n                <Link\n                  href={`/dashboard/lists/${session.rootListId}`}\n                  className=\"flex items-center gap-1 font-medium text-primary transition-colors hover:text-primary/80\"\n                  target=\"_blank\"\n                >\n                  {t(\"settings.import_sessions.view_list\")}\n                  <ExternalLink className=\"h-3 w-3\" />\n                </Link>\n              </div>\n            </div>\n          )}\n\n          {/* Message */}\n          {stats.message && (\n            <div className=\"rounded-lg border bg-muted/50 p-3 text-sm text-muted-foreground dark:bg-muted/20\">\n              {stats.message}\n            </div>\n          )}\n\n          {/* Actions */}\n          <div className=\"flex items-center justify-end pt-2\">\n            <div className=\"flex items-center gap-2\">\n              <Button variant=\"outline\" size=\"sm\" asChild>\n                <Link href={`/settings/import/${session.id}`}>\n                  <ExternalLink className=\"mr-1 h-4 w-4\" />\n                  {t(\"settings.import_sessions.view_details\")}\n                </Link>\n              </Button>\n              {canPause && (\n                <Button\n                  variant=\"outline\"\n                  size=\"sm\"\n                  onClick={() =>\n                    pauseSession.mutate({ importSessionId: session.id })\n                  }\n                  disabled={pauseSession.isPending}\n                >\n                  <Pause className=\"mr-1 h-4 w-4\" />\n                  {t(\"settings.import_sessions.pause_session\")}\n                </Button>\n              )}\n              {canResume && (\n                <Button\n                  variant=\"outline\"\n                  size=\"sm\"\n                  onClick={() =>\n                    resumeSession.mutate({ importSessionId: session.id })\n                  }\n                  disabled={resumeSession.isPending}\n                >\n                  <Play className=\"mr-1 h-4 w-4\" />\n                  {t(\"settings.import_sessions.resume_session\")}\n                </Button>\n              )}\n              {canDelete && (\n                <ActionConfirmingDialog\n                  title={t(\"settings.import_sessions.delete_dialog_title\")}\n                  description={\n                    <div>\n                      {t(\"settings.import_sessions.delete_dialog_description\", {\n                        name: session.name,\n                      })}\n                    </div>\n                  }\n                  actionButton={(setDialogOpen) => (\n                    <Button\n                      variant=\"destructive\"\n                      onClick={() => {\n                        deleteSession.mutateAsync({\n                          importSessionId: session.id,\n                        });\n                        setDialogOpen(false);\n                      }}\n                      disabled={deleteSession.isPending}\n                    >\n                      {t(\"settings.import_sessions.delete_session\")}\n                    </Button>\n                  )}\n                >\n                  <Button\n                    variant=\"destructiveOutline\"\n                    size=\"sm\"\n                    disabled={deleteSession.isPending}\n                  >\n                    <Trash2 className=\"mr-1 h-4 w-4\" />\n                    {t(\"actions.delete\")}\n                  </Button>\n                </ActionConfirmingDialog>\n              )}\n            </div>\n          </div>\n        </div>\n      </CardContent>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/settings/ImportSessionDetail.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport Link from \"next/link\";\nimport { useRouter } from \"next/navigation\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport ActionConfirmingDialog from \"@/components/ui/action-confirming-dialog\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Button } from \"@/components/ui/button\";\nimport { FullPageSpinner } from \"@/components/ui/full-page-spinner\";\nimport { Progress } from \"@/components/ui/progress\";\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"@/components/ui/table\";\nimport { Tabs, TabsList, TabsTrigger } from \"@/components/ui/tabs\";\nimport {\n  useDeleteImportSession,\n  useImportSessionResults,\n  useImportSessionStats,\n  usePauseImportSession,\n  useResumeImportSession,\n} from \"@/lib/hooks/useImportSessions\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { formatDistanceToNow } from \"date-fns\";\nimport {\n  AlertCircle,\n  ArrowLeft,\n  CheckCircle2,\n  Clock,\n  ExternalLink,\n  FileText,\n  Globe,\n  Loader2,\n  Paperclip,\n  Pause,\n  Play,\n  Trash2,\n  Upload,\n} from \"lucide-react\";\nimport { useInView } from \"react-intersection-observer\";\n\nimport type { ZImportSessionStatus } from \"@karakeep/shared/types/importSessions\";\nimport { switchCase } from \"@karakeep/shared/utils/switch\";\n\ntype FilterType =\n  | \"all\"\n  | \"accepted\"\n  | \"rejected\"\n  | \"skipped_duplicate\"\n  | \"pending\";\n\ntype SimpleTFunction = (\n  key: string,\n  options?: Record<string, unknown>,\n) => string;\n\ninterface ImportSessionResultItem {\n  id: string;\n  title: string | null;\n  url: string | null;\n  content: string | null;\n  type: string;\n  status: string;\n  result: string | null;\n  resultReason: string | null;\n  resultBookmarkId: string | null;\n}\n\nfunction getStatusColor(status: string) {\n  switch (status) {\n    case \"staging\":\n      return \"bg-purple-500/10 text-purple-700 dark:text-purple-400\";\n    case \"pending\":\n      return \"bg-muted text-muted-foreground\";\n    case \"running\":\n      return \"bg-blue-500/10 text-blue-700 dark:text-blue-400\";\n    case \"paused\":\n      return \"bg-yellow-500/10 text-yellow-700 dark:text-yellow-400\";\n    case \"completed\":\n      return \"bg-green-500/10 text-green-700 dark:text-green-400\";\n    case \"failed\":\n      return \"bg-destructive/10 text-destructive\";\n    default:\n      return \"bg-muted text-muted-foreground\";\n  }\n}\n\nfunction getStatusIcon(status: string) {\n  switch (status) {\n    case \"staging\":\n      return <Upload className=\"h-4 w-4\" />;\n    case \"pending\":\n      return <Clock className=\"h-4 w-4\" />;\n    case \"running\":\n      return <Loader2 className=\"h-4 w-4 animate-spin\" />;\n    case \"paused\":\n      return <Pause className=\"h-4 w-4\" />;\n    case \"completed\":\n      return <CheckCircle2 className=\"h-4 w-4\" />;\n    case \"failed\":\n      return <AlertCircle className=\"h-4 w-4\" />;\n    default:\n      return <Clock className=\"h-4 w-4\" />;\n  }\n}\n\nfunction getResultBadge(\n  status: string,\n  result: string | null,\n  t: (key: string) => string,\n) {\n  if (status === \"pending\") {\n    return (\n      <Badge\n        variant=\"secondary\"\n        className=\"bg-muted text-muted-foreground hover:bg-muted\"\n      >\n        <Clock className=\"mr-1 h-3 w-3\" />\n        {t(\"settings.import_sessions.detail.result_pending\")}\n      </Badge>\n    );\n  }\n  if (status === \"processing\") {\n    return (\n      <Badge\n        variant=\"secondary\"\n        className=\"bg-blue-500/10 text-blue-700 hover:bg-blue-500/10 dark:text-blue-400\"\n      >\n        <Loader2 className=\"mr-1 h-3 w-3 animate-spin\" />\n        {t(\"settings.import_sessions.detail.result_processing\")}\n      </Badge>\n    );\n  }\n  switch (result) {\n    case \"accepted\":\n      return (\n        <Badge\n          variant=\"secondary\"\n          className=\"bg-green-500/10 text-green-700 hover:bg-green-500/10 dark:text-green-400\"\n        >\n          <CheckCircle2 className=\"mr-1 h-3 w-3\" />\n          {t(\"settings.import_sessions.detail.result_accepted\")}\n        </Badge>\n      );\n    case \"rejected\":\n      return (\n        <Badge\n          variant=\"secondary\"\n          className=\"bg-destructive/10 text-destructive hover:bg-destructive/10\"\n        >\n          <AlertCircle className=\"mr-1 h-3 w-3\" />\n          {t(\"settings.import_sessions.detail.result_rejected\")}\n        </Badge>\n      );\n    case \"skipped_duplicate\":\n      return (\n        <Badge\n          variant=\"secondary\"\n          className=\"bg-amber-500/10 text-amber-700 hover:bg-amber-500/10 dark:text-amber-400\"\n        >\n          {t(\"settings.import_sessions.detail.result_skipped_duplicate\")}\n        </Badge>\n      );\n    default:\n      return (\n        <Badge variant=\"secondary\" className=\"bg-muted hover:bg-muted\">\n          —\n        </Badge>\n      );\n  }\n}\n\nfunction getTypeIcon(type: string) {\n  switch (type) {\n    case \"link\":\n      return <Globe className=\"h-3 w-3\" />;\n    case \"text\":\n      return <FileText className=\"h-3 w-3\" />;\n    case \"asset\":\n      return <Paperclip className=\"h-3 w-3\" />;\n    default:\n      return null;\n  }\n}\n\nfunction getTypeLabel(type: string, t: SimpleTFunction) {\n  switch (type) {\n    case \"link\":\n      return t(\"common.bookmark_types.link\");\n    case \"text\":\n      return t(\"common.bookmark_types.text\");\n    case \"asset\":\n      return t(\"common.bookmark_types.media\");\n    default:\n      return type;\n  }\n}\n\nfunction getTitleDisplay(\n  item: {\n    title: string | null;\n    url: string | null;\n    content: string | null;\n    type: string;\n  },\n  noTitleLabel: string,\n) {\n  if (item.title) {\n    return item.title;\n  }\n  if (item.type === \"text\" && item.content) {\n    return item.content.length > 80\n      ? item.content.substring(0, 80) + \"…\"\n      : item.content;\n  }\n  if (item.url) {\n    try {\n      const url = new URL(item.url);\n      const display = url.hostname + url.pathname;\n      return display.length > 60 ? display.substring(0, 60) + \"…\" : display;\n    } catch {\n      return item.url.length > 60 ? item.url.substring(0, 60) + \"…\" : item.url;\n    }\n  }\n  return noTitleLabel;\n}\n\nexport default function ImportSessionDetail({\n  sessionId,\n}: {\n  sessionId: string;\n}) {\n  const { t: tRaw } = useTranslation();\n  const t = tRaw as SimpleTFunction;\n  const router = useRouter();\n  const [filter, setFilter] = useState<FilterType>(\"all\");\n\n  const { data: stats, isLoading: isStatsLoading } =\n    useImportSessionStats(sessionId);\n  const {\n    data: resultsData,\n    isLoading: isResultsLoading,\n    fetchNextPage,\n    hasNextPage,\n    isFetchingNextPage,\n  } = useImportSessionResults(sessionId, filter);\n\n  const deleteSession = useDeleteImportSession();\n  const pauseSession = usePauseImportSession();\n  const resumeSession = useResumeImportSession();\n\n  const { ref: loadMoreRef, inView: loadMoreInView } = useInView();\n\n  useEffect(() => {\n    if (loadMoreInView && hasNextPage && !isFetchingNextPage) {\n      fetchNextPage();\n    }\n  }, [fetchNextPage, hasNextPage, isFetchingNextPage, loadMoreInView]);\n\n  if (isStatsLoading) {\n    return <FullPageSpinner />;\n  }\n\n  if (!stats) {\n    return null;\n  }\n\n  const items: ImportSessionResultItem[] =\n    resultsData?.pages.flatMap((page) => page.items) ?? [];\n\n  const progress =\n    stats.totalBookmarks > 0\n      ? ((stats.completedBookmarks + stats.failedBookmarks) /\n          stats.totalBookmarks) *\n        100\n      : 0;\n\n  const canDelete =\n    stats.status === \"completed\" ||\n    stats.status === \"failed\" ||\n    stats.status === \"paused\";\n  const canPause = stats.status === \"pending\" || stats.status === \"running\";\n  const canResume = stats.status === \"paused\";\n\n  const statusLabels = (s: ZImportSessionStatus) =>\n    switchCase(s, {\n      staging: t(\"settings.import_sessions.status.staging\"),\n      pending: t(\"settings.import_sessions.status.pending\"),\n      running: t(\"settings.import_sessions.status.running\"),\n      paused: t(\"settings.import_sessions.status.paused\"),\n      completed: t(\"settings.import_sessions.status.completed\"),\n      failed: t(\"settings.import_sessions.status.failed\"),\n    });\n\n  const handleDelete = () => {\n    deleteSession.mutateAsync({ importSessionId: sessionId }).then(() => {\n      router.push(\"/settings/import\");\n    });\n  };\n\n  return (\n    <div className=\"flex flex-col gap-6\">\n      {/* Back link */}\n      <Link\n        href=\"/settings/import\"\n        className=\"flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground\"\n      >\n        <ArrowLeft className=\"h-4 w-4\" />\n        {t(\"settings.import_sessions.detail.back_to_import\")}\n      </Link>\n\n      {/* Header */}\n      <div className=\"rounded-md border bg-background p-4\">\n        <div className=\"flex flex-col gap-4\">\n          <div className=\"flex items-start justify-between\">\n            <div className=\"flex-1\">\n              <h2 className=\"text-lg font-medium\">{stats.name}</h2>\n              <p className=\"mt-1 text-sm text-muted-foreground\">\n                {t(\"settings.import_sessions.created_at\", {\n                  time: formatDistanceToNow(stats.createdAt, {\n                    addSuffix: true,\n                  }),\n                })}\n              </p>\n            </div>\n            <Badge\n              className={`${getStatusColor(stats.status)} hover:bg-inherit`}\n            >\n              {getStatusIcon(stats.status)}\n              <span className=\"ml-1 capitalize\">\n                {statusLabels(stats.status)}\n              </span>\n            </Badge>\n          </div>\n\n          {/* Progress bar + stats */}\n          {stats.totalBookmarks > 0 && (\n            <div className=\"space-y-3\">\n              <div className=\"flex items-center justify-between\">\n                <h4 className=\"text-sm font-medium text-muted-foreground\">\n                  {t(\"settings.import_sessions.progress\")}\n                </h4>\n                <div className=\"flex items-center gap-2\">\n                  <span className=\"text-sm font-medium\">\n                    {stats.completedBookmarks + stats.failedBookmarks} /{\" \"}\n                    {stats.totalBookmarks}\n                  </span>\n                  <Badge variant=\"outline\" className=\"text-xs\">\n                    {Math.round(progress)}%\n                  </Badge>\n                </div>\n              </div>\n              <Progress value={progress} className=\"h-3\" />\n              <div className=\"flex flex-wrap gap-2\">\n                {stats.completedBookmarks > 0 && (\n                  <Badge\n                    variant=\"secondary\"\n                    className=\"bg-green-500/10 text-green-700 hover:bg-green-500/10 dark:text-green-400\"\n                  >\n                    <CheckCircle2 className=\"mr-1.5 h-3 w-3\" />\n                    {t(\"settings.import_sessions.badges.completed\", {\n                      count: stats.completedBookmarks,\n                    })}\n                  </Badge>\n                )}\n                {stats.failedBookmarks > 0 && (\n                  <Badge\n                    variant=\"secondary\"\n                    className=\"bg-destructive/10 text-destructive hover:bg-destructive/10\"\n                  >\n                    <AlertCircle className=\"mr-1.5 h-3 w-3\" />\n                    {t(\"settings.import_sessions.badges.failed\", {\n                      count: stats.failedBookmarks,\n                    })}\n                  </Badge>\n                )}\n                {stats.pendingBookmarks > 0 && (\n                  <Badge\n                    variant=\"secondary\"\n                    className=\"bg-amber-500/10 text-amber-700 hover:bg-amber-500/10 dark:text-amber-400\"\n                  >\n                    <Clock className=\"mr-1.5 h-3 w-3\" />\n                    {t(\"settings.import_sessions.badges.pending\", {\n                      count: stats.pendingBookmarks,\n                    })}\n                  </Badge>\n                )}\n                {stats.processingBookmarks > 0 && (\n                  <Badge\n                    variant=\"secondary\"\n                    className=\"bg-blue-500/10 text-blue-700 hover:bg-blue-500/10 dark:text-blue-400\"\n                  >\n                    <Loader2 className=\"mr-1.5 h-3 w-3 animate-spin\" />\n                    {t(\"settings.import_sessions.badges.processing\", {\n                      count: stats.processingBookmarks,\n                    })}\n                  </Badge>\n                )}\n              </div>\n            </div>\n          )}\n\n          {/* Message */}\n          {stats.message && (\n            <div className=\"rounded-lg border bg-muted/50 p-3 text-sm text-muted-foreground dark:bg-muted/20\">\n              {stats.message}\n            </div>\n          )}\n\n          {/* Action buttons */}\n          <div className=\"flex items-center justify-end\">\n            <div className=\"flex items-center gap-2\">\n              {canPause && (\n                <Button\n                  variant=\"outline\"\n                  size=\"sm\"\n                  onClick={() =>\n                    pauseSession.mutate({ importSessionId: sessionId })\n                  }\n                  disabled={pauseSession.isPending}\n                >\n                  <Pause className=\"mr-1 h-4 w-4\" />\n                  {t(\"settings.import_sessions.pause_session\")}\n                </Button>\n              )}\n              {canResume && (\n                <Button\n                  variant=\"outline\"\n                  size=\"sm\"\n                  onClick={() =>\n                    resumeSession.mutate({ importSessionId: sessionId })\n                  }\n                  disabled={resumeSession.isPending}\n                >\n                  <Play className=\"mr-1 h-4 w-4\" />\n                  {t(\"settings.import_sessions.resume_session\")}\n                </Button>\n              )}\n              {canDelete && (\n                <ActionConfirmingDialog\n                  title={t(\"settings.import_sessions.delete_dialog_title\")}\n                  description={\n                    <div>\n                      {t(\"settings.import_sessions.delete_dialog_description\", {\n                        name: stats.name,\n                      })}\n                    </div>\n                  }\n                  actionButton={(setDialogOpen) => (\n                    <Button\n                      variant=\"destructive\"\n                      onClick={() => {\n                        handleDelete();\n                        setDialogOpen(false);\n                      }}\n                      disabled={deleteSession.isPending}\n                    >\n                      {t(\"settings.import_sessions.delete_session\")}\n                    </Button>\n                  )}\n                >\n                  <Button\n                    variant=\"destructive\"\n                    size=\"sm\"\n                    disabled={deleteSession.isPending}\n                  >\n                    <Trash2 className=\"mr-1 h-4 w-4\" />\n                    {t(\"actions.delete\")}\n                  </Button>\n                </ActionConfirmingDialog>\n              )}\n            </div>\n          </div>\n        </div>\n      </div>\n\n      {/* Filter tabs + Results table */}\n      <div className=\"rounded-md border bg-background p-4\">\n        <Tabs\n          value={filter}\n          onValueChange={(v) => setFilter(v as FilterType)}\n          className=\"w-full\"\n        >\n          <TabsList className=\"mb-4 flex w-full flex-wrap\">\n            <TabsTrigger value=\"all\">\n              {t(\"settings.import_sessions.detail.filter_all\")}\n            </TabsTrigger>\n            <TabsTrigger value=\"accepted\">\n              {t(\"settings.import_sessions.detail.filter_accepted\")}\n            </TabsTrigger>\n            <TabsTrigger value=\"rejected\">\n              {t(\"settings.import_sessions.detail.filter_rejected\")}\n            </TabsTrigger>\n            <TabsTrigger value=\"skipped_duplicate\">\n              {t(\"settings.import_sessions.detail.filter_duplicates\")}\n            </TabsTrigger>\n            <TabsTrigger value=\"pending\">\n              {t(\"settings.import_sessions.detail.filter_pending\")}\n            </TabsTrigger>\n          </TabsList>\n        </Tabs>\n\n        {isResultsLoading ? (\n          <FullPageSpinner />\n        ) : items.length === 0 ? (\n          <p className=\"rounded-md bg-muted p-4 text-center text-sm text-muted-foreground\">\n            {t(\"settings.import_sessions.detail.no_results\")}\n          </p>\n        ) : (\n          <div className=\"flex flex-col gap-2\">\n            <Table>\n              <TableHeader>\n                <TableRow>\n                  <TableHead>\n                    {t(\"settings.import_sessions.detail.table_title\")}\n                  </TableHead>\n                  <TableHead className=\"w-[80px]\">\n                    {t(\"settings.import_sessions.detail.table_type\")}\n                  </TableHead>\n                  <TableHead className=\"w-[120px]\">\n                    {t(\"settings.import_sessions.detail.table_result\")}\n                  </TableHead>\n                  <TableHead>\n                    {t(\"settings.import_sessions.detail.table_reason\")}\n                  </TableHead>\n                  <TableHead className=\"w-[100px]\">\n                    {t(\"settings.import_sessions.detail.table_bookmark\")}\n                  </TableHead>\n                </TableRow>\n              </TableHeader>\n              <TableBody>\n                {items.map((item) => (\n                  <TableRow key={item.id}>\n                    <TableCell className=\"max-w-[300px] truncate font-medium\">\n                      {getTitleDisplay(\n                        item,\n                        t(\"settings.import_sessions.detail.no_title\"),\n                      )}\n                    </TableCell>\n                    <TableCell>\n                      <Badge\n                        variant=\"outline\"\n                        className=\"flex w-fit items-center gap-1 text-xs\"\n                      >\n                        {getTypeIcon(item.type)}\n                        {getTypeLabel(item.type, t)}\n                      </Badge>\n                    </TableCell>\n                    <TableCell>\n                      {getResultBadge(item.status, item.result, t)}\n                    </TableCell>\n                    <TableCell className=\"max-w-[200px] truncate text-sm text-muted-foreground\">\n                      {item.resultReason || \"—\"}\n                    </TableCell>\n                    <TableCell>\n                      {item.resultBookmarkId ? (\n                        <Link\n                          href={`/dashboard/preview/${item.resultBookmarkId}`}\n                          className=\"flex items-center gap-1 text-sm text-primary hover:text-primary/80\"\n                          prefetch={false}\n                        >\n                          <ExternalLink className=\"h-3 w-3\" />\n                          {t(\"settings.import_sessions.detail.view_bookmark\")}\n                        </Link>\n                      ) : (\n                        <span className=\"text-sm text-muted-foreground\">—</span>\n                      )}\n                    </TableCell>\n                  </TableRow>\n                ))}\n              </TableBody>\n            </Table>\n            {hasNextPage && (\n              <div className=\"flex justify-center\">\n                <ActionButton\n                  ref={loadMoreRef}\n                  ignoreDemoMode={true}\n                  loading={isFetchingNextPage}\n                  onClick={() => fetchNextPage()}\n                  variant=\"ghost\"\n                >\n                  {t(\"settings.import_sessions.detail.load_more\")}\n                </ActionButton>\n              </div>\n            )}\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/settings/ImportSessionsSection.tsx",
    "content": "\"use client\";\n\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport { useListImportSessions } from \"@/lib/hooks/useImportSessions\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { Package } from \"lucide-react\";\n\nimport { FullPageSpinner } from \"../ui/full-page-spinner\";\nimport { ImportSessionCard } from \"./ImportSessionCard\";\nimport { SettingsSection } from \"./SettingsPage\";\n\nexport function ImportSessionsSection() {\n  const { t } = useTranslation();\n  const { data: sessions, isLoading, error } = useListImportSessions();\n\n  if (isLoading) {\n    return (\n      <SettingsSection\n        title={t(\"settings.import_sessions.title\")}\n        description={t(\"settings.import_sessions.description\")}\n      >\n        <FullPageSpinner />\n      </SettingsSection>\n    );\n  }\n\n  if (error) {\n    return (\n      <SettingsSection\n        title={t(\"settings.import_sessions.title\")}\n        description={t(\"settings.import_sessions.description\")}\n      >\n        <Card>\n          <CardContent className=\"flex items-center justify-center py-8\">\n            <p className=\"text-gray-600\">\n              {t(\"settings.import_sessions.load_error\")}\n            </p>\n          </CardContent>\n        </Card>\n      </SettingsSection>\n    );\n  }\n\n  return (\n    <SettingsSection\n      title={t(\"settings.import_sessions.title\")}\n      description={t(\"settings.import_sessions.description\")}\n    >\n      {sessions && sessions.length > 0 ? (\n        <div className=\"space-y-4\">\n          {sessions.map((session) => (\n            <ImportSessionCard key={session.id} session={session} />\n          ))}\n        </div>\n      ) : (\n        <Card>\n          <CardContent className=\"flex flex-col items-center justify-center py-12\">\n            <Package className=\"mb-4 h-12 w-12 text-gray-400\" />\n            <p className=\"mb-2 text-center text-gray-600\">\n              {t(\"settings.import_sessions.no_sessions\")}\n            </p>\n            <p className=\"text-center text-sm text-gray-500\">\n              {t(\"settings.import_sessions.no_sessions_detail\")}\n            </p>\n          </CardContent>\n        </Card>\n      )}\n    </SettingsSection>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/settings/ReaderSettings.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { useClientConfig } from \"@/lib/clientConfig\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { useReaderSettings } from \"@/lib/readerSettings\";\nimport { AlertTriangle, ChevronDown, Laptop, RotateCcw } from \"lucide-react\";\n\nimport {\n  formatFontSize,\n  formatLineHeight,\n  READER_DEFAULTS,\n  READER_FONT_FAMILIES,\n  READER_SETTING_CONSTRAINTS,\n} from \"@karakeep/shared/types/readers\";\n\nimport { Alert, AlertDescription } from \"../ui/alert\";\nimport { Button } from \"../ui/button\";\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"../ui/card\";\nimport {\n  Collapsible,\n  CollapsibleContent,\n  CollapsibleTrigger,\n} from \"../ui/collapsible\";\nimport { Label } from \"../ui/label\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"../ui/select\";\nimport { Slider } from \"../ui/slider\";\n\nexport default function ReaderSettings() {\n  const { t } = useTranslation();\n  const clientConfig = useClientConfig();\n  const {\n    settings,\n    serverSettings,\n    localOverrides,\n    hasLocalOverrides,\n    clearServerDefaults,\n    clearLocalOverrides,\n    updateServerSetting,\n  } = useReaderSettings();\n\n  // Local state for collapsible\n  const [isOpen, setIsOpen] = useState(false);\n\n  // Local state for slider dragging (null = not dragging, use server value)\n  const [draggingFontSize, setDraggingFontSize] = useState<number | null>(null);\n  const [draggingLineHeight, setDraggingLineHeight] = useState<number | null>(\n    null,\n  );\n\n  const hasServerSettings =\n    serverSettings.fontSize !== null ||\n    serverSettings.lineHeight !== null ||\n    serverSettings.fontFamily !== null;\n\n  const handleClearDefaults = () => {\n    clearServerDefaults();\n    toast({ description: t(\"settings.info.reader_settings.defaults_cleared\") });\n  };\n\n  const handleClearLocalOverrides = () => {\n    clearLocalOverrides();\n    toast({\n      description: t(\"settings.info.reader_settings.local_overrides_cleared\"),\n    });\n  };\n\n  // Format local override for display\n  const formatLocalOverride = (\n    key: \"fontSize\" | \"lineHeight\" | \"fontFamily\",\n  ) => {\n    const value = localOverrides[key];\n    if (value === undefined) return null;\n    if (key === \"fontSize\") return formatFontSize(value as number);\n    if (key === \"lineHeight\") return formatLineHeight(value as number);\n    if (key === \"fontFamily\") {\n      switch (value) {\n        case \"serif\":\n          return t(\"settings.info.reader_settings.serif\");\n        case \"sans\":\n          return t(\"settings.info.reader_settings.sans\");\n        case \"mono\":\n          return t(\"settings.info.reader_settings.mono\");\n      }\n    }\n    return String(value);\n  };\n\n  return (\n    <Collapsible open={isOpen} onOpenChange={setIsOpen}>\n      <Card>\n        <CardHeader>\n          <CollapsibleTrigger className=\"flex w-full items-center justify-between [&[data-state=open]>svg]:rotate-180\">\n            <div className=\"flex flex-col items-start gap-1 text-left\">\n              <CardTitle className=\"text-lg\">\n                {t(\"settings.info.reader_settings.title\")}\n              </CardTitle>\n              <CardDescription>\n                {t(\"settings.info.reader_settings.description\")}\n              </CardDescription>\n            </div>\n            <ChevronDown className=\"h-5 w-5 shrink-0 transition-transform duration-200\" />\n          </CollapsibleTrigger>\n        </CardHeader>\n        <CollapsibleContent>\n          <CardContent className=\"space-y-6\">\n            {/* Local Overrides Warning */}\n            {hasLocalOverrides && (\n              <Alert>\n                <AlertTriangle className=\"h-4 w-4\" />\n                <AlertDescription className=\"flex flex-col gap-3\">\n                  <div>\n                    <p className=\"font-medium\">\n                      {t(\"settings.info.reader_settings.local_overrides_title\")}\n                    </p>\n                    <p className=\"mt-1 text-sm text-muted-foreground\">\n                      {t(\n                        \"settings.info.reader_settings.local_overrides_description\",\n                      )}\n                    </p>\n                    <ul className=\"mt-2 text-sm text-muted-foreground\">\n                      {localOverrides.fontFamily !== undefined && (\n                        <li>\n                          {t(\"settings.info.reader_settings.font_family\")}:{\" \"}\n                          {formatLocalOverride(\"fontFamily\")}\n                        </li>\n                      )}\n                      {localOverrides.fontSize !== undefined && (\n                        <li>\n                          {t(\"settings.info.reader_settings.font_size\")}:{\" \"}\n                          {formatLocalOverride(\"fontSize\")}\n                        </li>\n                      )}\n                      {localOverrides.lineHeight !== undefined && (\n                        <li>\n                          {t(\"settings.info.reader_settings.line_height\")}:{\" \"}\n                          {formatLocalOverride(\"lineHeight\")}\n                        </li>\n                      )}\n                    </ul>\n                  </div>\n                  <Button\n                    variant=\"outline\"\n                    size=\"sm\"\n                    onClick={handleClearLocalOverrides}\n                    className=\"w-fit\"\n                  >\n                    <Laptop className=\"mr-2 h-4 w-4\" />\n                    {t(\"settings.info.reader_settings.clear_local_overrides\")}\n                  </Button>\n                </AlertDescription>\n              </Alert>\n            )}\n\n            {/* Font Family */}\n            <div className=\"space-y-2\">\n              <Label className=\"text-sm font-medium\">\n                {t(\"settings.info.reader_settings.font_family\")}\n              </Label>\n              <Select\n                disabled={!!clientConfig.demoMode}\n                value={serverSettings.fontFamily ?? \"not-set\"}\n                onValueChange={(value) => {\n                  if (value !== \"not-set\") {\n                    updateServerSetting({\n                      fontFamily: value as \"serif\" | \"sans\" | \"mono\",\n                    });\n                  }\n                }}\n              >\n                <SelectTrigger className=\"h-11\">\n                  <SelectValue\n                    placeholder={t(\"settings.info.reader_settings.not_set\")}\n                  />\n                </SelectTrigger>\n                <SelectContent>\n                  <SelectItem value=\"not-set\" disabled>\n                    {t(\"settings.info.reader_settings.not_set\")} (\n                    {t(\"common.default\")}: {READER_DEFAULTS.fontFamily})\n                  </SelectItem>\n                  <SelectItem value=\"serif\">\n                    {t(\"settings.info.reader_settings.serif\")}\n                  </SelectItem>\n                  <SelectItem value=\"sans\">\n                    {t(\"settings.info.reader_settings.sans\")}\n                  </SelectItem>\n                  <SelectItem value=\"mono\">\n                    {t(\"settings.info.reader_settings.mono\")}\n                  </SelectItem>\n                </SelectContent>\n              </Select>\n              {serverSettings.fontFamily === null && (\n                <p className=\"text-xs text-muted-foreground\">\n                  {t(\"settings.info.reader_settings.using_default\")}:{\" \"}\n                  {READER_DEFAULTS.fontFamily}\n                </p>\n              )}\n            </div>\n\n            {/* Font Size */}\n            <div className=\"space-y-2\">\n              <div className=\"flex items-center justify-between\">\n                <Label className=\"text-sm font-medium\">\n                  {t(\"settings.info.reader_settings.font_size\")}\n                </Label>\n                <span className=\"text-sm text-muted-foreground\">\n                  {formatFontSize(draggingFontSize ?? settings.fontSize)}\n                  {serverSettings.fontSize === null &&\n                    draggingFontSize === null &&\n                    ` (${t(\"common.default\").toLowerCase()})`}\n                </span>\n              </div>\n              <Slider\n                disabled={!!clientConfig.demoMode}\n                value={[draggingFontSize ?? settings.fontSize]}\n                onValueChange={([value]) => setDraggingFontSize(value)}\n                onValueCommit={([value]) => {\n                  updateServerSetting({ fontSize: value });\n                  setDraggingFontSize(null);\n                }}\n                max={READER_SETTING_CONSTRAINTS.fontSize.max}\n                min={READER_SETTING_CONSTRAINTS.fontSize.min}\n                step={READER_SETTING_CONSTRAINTS.fontSize.step}\n              />\n            </div>\n\n            {/* Line Height */}\n            <div className=\"space-y-2\">\n              <div className=\"flex items-center justify-between\">\n                <Label className=\"text-sm font-medium\">\n                  {t(\"settings.info.reader_settings.line_height\")}\n                </Label>\n                <span className=\"text-sm text-muted-foreground\">\n                  {formatLineHeight(draggingLineHeight ?? settings.lineHeight)}\n                  {serverSettings.lineHeight === null &&\n                    draggingLineHeight === null &&\n                    ` (${t(\"common.default\").toLowerCase()})`}\n                </span>\n              </div>\n              <Slider\n                disabled={!!clientConfig.demoMode}\n                value={[draggingLineHeight ?? settings.lineHeight]}\n                onValueChange={([value]) => setDraggingLineHeight(value)}\n                onValueCommit={([value]) => {\n                  updateServerSetting({ lineHeight: value });\n                  setDraggingLineHeight(null);\n                }}\n                max={READER_SETTING_CONSTRAINTS.lineHeight.max}\n                min={READER_SETTING_CONSTRAINTS.lineHeight.min}\n                step={READER_SETTING_CONSTRAINTS.lineHeight.step}\n              />\n            </div>\n\n            {/* Clear Defaults Button */}\n            {hasServerSettings && (\n              <Button\n                variant=\"outline\"\n                onClick={handleClearDefaults}\n                className=\"w-full\"\n                disabled={!!clientConfig.demoMode}\n              >\n                <RotateCcw className=\"mr-2 h-4 w-4\" />\n                {t(\"settings.info.reader_settings.clear_defaults\")}\n              </Button>\n            )}\n\n            {/* Preview */}\n            <div className=\"rounded-lg border p-4\">\n              <p className=\"mb-2 text-sm font-medium text-muted-foreground\">\n                {t(\"settings.info.reader_settings.preview\")}\n              </p>\n              <p\n                style={{\n                  fontFamily: READER_FONT_FAMILIES[settings.fontFamily],\n                  fontSize: `${draggingFontSize ?? settings.fontSize}px`,\n                  lineHeight: draggingLineHeight ?? settings.lineHeight,\n                }}\n              >\n                {t(\"settings.info.reader_settings.preview_text\")}\n                <br />\n                {t(\"settings.info.reader_settings.preview_text\")}\n                <br />\n                {t(\"settings.info.reader_settings.preview_text\")}\n              </p>\n            </div>\n          </CardContent>\n        </CollapsibleContent>\n      </Card>\n    </Collapsible>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/settings/RegenerateApiKey.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@/components/ui/dialog\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { useMutation } from \"@tanstack/react-query\";\nimport { RefreshCcw } from \"lucide-react\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\nimport ApiKeySuccess from \"./ApiKeySuccess\";\n\nexport default function RegenerateApiKey({\n  id,\n  name,\n}: {\n  id: string;\n  name: string;\n}) {\n  const api = useTRPC();\n  const { t } = useTranslation();\n  const router = useRouter();\n\n  const [key, setKey] = useState<string | undefined>(undefined);\n  const [dialogOpen, setDialogOpen] = useState<boolean>(false);\n\n  const mutator = useMutation(\n    api.apiKeys.regenerate.mutationOptions({\n      onSuccess: (resp) => {\n        setKey(resp.key);\n        router.refresh();\n      },\n      onError: () => {\n        toast({\n          description: t(\"common.something_went_wrong\"),\n          variant: \"destructive\",\n        });\n        setDialogOpen(false);\n      },\n    }),\n  );\n\n  const handleRegenerate = () => {\n    mutator.mutate({ id });\n  };\n\n  return (\n    <Dialog\n      open={dialogOpen}\n      onOpenChange={(o) => {\n        setDialogOpen(o);\n        setKey(undefined);\n      }}\n    >\n      <DialogTrigger asChild>\n        <Button variant=\"ghost\" size=\"sm\" title=\"Regenerate\">\n          <RefreshCcw className=\"h-4 w-4\" />\n        </Button>\n      </DialogTrigger>\n      <DialogContent>\n        <DialogHeader>\n          <DialogTitle>\n            {key\n              ? t(\"settings.api_keys.key_regenerated\")\n              : t(\"settings.api_keys.regenerate_api_key\")}\n          </DialogTitle>\n          {!key && (\n            <DialogDescription>\n              {t(\"settings.api_keys.regenerate_warning\", { name })}\n            </DialogDescription>\n          )}\n        </DialogHeader>\n        {key ? (\n          <ApiKeySuccess\n            apiKey={key}\n            message={t(\"settings.api_keys.key_regenerated_please_copy\")}\n          />\n        ) : (\n          <p className=\"text-sm\">\n            {t(\"settings.api_keys.regenerate_confirmation\")}\n          </p>\n        )}\n        <DialogFooter className=\"sm:justify-end\">\n          {!key ? (\n            <>\n              <DialogClose asChild>\n                <Button type=\"button\" variant=\"outline\">\n                  {t(\"actions.cancel\")}\n                </Button>\n              </DialogClose>\n              <ActionButton\n                variant=\"destructive\"\n                onClick={handleRegenerate}\n                loading={mutator.isPending}\n              >\n                {t(\"actions.regenerate\")}\n              </ActionButton>\n            </>\n          ) : (\n            <DialogClose asChild>\n              <Button type=\"button\" variant=\"outline\">\n                {t(\"actions.close\")}\n              </Button>\n            </DialogClose>\n          )}\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/settings/SettingsPage.tsx",
    "content": "import { cn } from \"@/lib/utils\";\n\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"../ui/card\";\n\nexport function SettingsPage({\n  title,\n  description,\n  action,\n  children,\n}: {\n  title: string;\n  description?: string;\n  action?: React.ReactNode;\n  children: React.ReactNode;\n}) {\n  return (\n    <div className=\"space-y-5\">\n      <div className=\"flex items-center justify-between gap-4\">\n        <div>\n          <h1 className=\"text-2xl font-semibold tracking-tight\">{title}</h1>\n          {description && (\n            <p className=\"mt-1 text-sm text-muted-foreground\">{description}</p>\n          )}\n        </div>\n        {action && <div className=\"shrink-0\">{action}</div>}\n      </div>\n      {children}\n    </div>\n  );\n}\n\nexport function SettingsSection({\n  title,\n  description,\n  action,\n  children,\n  variant = \"default\",\n}: {\n  title?: string;\n  description?: string;\n  action?: React.ReactNode;\n  children: React.ReactNode;\n  variant?: \"default\" | \"danger\";\n}) {\n  return (\n    <Card\n      className={variant === \"danger\" ? \"border-destructive/20\" : undefined}\n    >\n      {(title || description || action) && (\n        <CardHeader>\n          <div className=\"flex items-center justify-between gap-4\">\n            <div className=\"space-y-1\">\n              {title && (\n                <CardTitle\n                  className={cn(\n                    \"text-lg\",\n                    variant === \"danger\" && \"text-destructive\",\n                  )}\n                >\n                  {title}\n                </CardTitle>\n              )}\n              {description && <CardDescription>{description}</CardDescription>}\n            </div>\n            {action && <div className=\"shrink-0\">{action}</div>}\n          </div>\n        </CardHeader>\n      )}\n      <CardContent\n        className={cn(\"space-y-4\", !(title || description || action) && \"pt-6\")}\n      >\n        {children}\n      </CardContent>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/settings/SubscriptionSettings.tsx",
    "content": "\"use client\";\n\nimport { useEffect } from \"react\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { useMutation, useQuery } from \"@tanstack/react-query\";\nimport { Loader2 } from \"lucide-react\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\nimport { Alert, AlertDescription } from \"../ui/alert\";\nimport { Badge } from \"../ui/badge\";\nimport { Button } from \"../ui/button\";\nimport { Skeleton } from \"../ui/skeleton\";\nimport { SettingsSection } from \"./SettingsPage\";\n\nexport default function SubscriptionSettings() {\n  const api = useTRPC();\n  const { t } = useTranslation();\n  const {\n    data: subscriptionStatus,\n    refetch,\n    isLoading: isQueryLoading,\n  } = useQuery(api.subscriptions.getSubscriptionStatus.queryOptions());\n\n  const { data: subscriptionPrice } = useQuery(\n    api.subscriptions.getSubscriptionPrice.queryOptions(),\n  );\n\n  const { mutate: syncStripeState } = useMutation(\n    api.subscriptions.syncWithStripe.mutationOptions({\n      onSuccess: () => {\n        refetch();\n      },\n    }),\n  );\n  const createCheckoutSession = useMutation(\n    api.subscriptions.createCheckoutSession.mutationOptions({\n      onSuccess: (resp) => {\n        if (resp.url) {\n          window.location.href = resp.url;\n        }\n      },\n      onError: () => {\n        toast({\n          description: t(\"common.something_went_wrong\"),\n          variant: \"destructive\",\n        });\n      },\n    }),\n  );\n  const createPortalSession = useMutation(\n    api.subscriptions.createPortalSession.mutationOptions({\n      onSuccess: (resp) => {\n        if (resp.url) {\n          window.location.href = resp.url;\n        }\n      },\n      onError: () => {\n        toast({\n          description: t(\"common.something_went_wrong\"),\n          variant: \"destructive\",\n        });\n      },\n    }),\n  );\n\n  const isLoading =\n    createCheckoutSession.isPending || createPortalSession.isPending;\n\n  useEffect(() => {\n    syncStripeState();\n  }, []);\n\n  const formatDate = (date: Date | null) => {\n    if (!date) return \"N/A\";\n    return new Intl.DateTimeFormat(\"en-US\", {\n      year: \"numeric\",\n      month: \"long\",\n      day: \"numeric\",\n    }).format(date);\n  };\n\n  const getStatusBadge = (status: \"free\" | \"paid\") => {\n    switch (status) {\n      case \"paid\":\n        return (\n          <Badge variant=\"default\" className=\"bg-green-500\">\n            {t(\"settings.subscription.paid\")}\n          </Badge>\n        );\n      case \"free\":\n        return (\n          <Badge variant=\"outline\">{t(\"settings.subscription.free\")}</Badge>\n        );\n    }\n  };\n\n  return (\n    <SettingsSection\n      title={t(\"settings.subscription.subscription\")}\n      description={t(\"settings.subscription.manage_subscription\")}\n    >\n      {isQueryLoading ? (\n        <div className=\"space-y-6\">\n          <div className=\"grid gap-4 md:grid-cols-2\">\n            <div className=\"space-y-2\">\n              <Skeleton className=\"h-4 w-24\" />\n              <div className=\"flex items-center gap-2\">\n                <Skeleton className=\"h-5 w-16\" />\n                <Skeleton className=\"h-5 w-12\" />\n              </div>\n            </div>\n            <div className=\"space-y-2\">\n              <Skeleton className=\"h-4 w-28\" />\n              <Skeleton className=\"h-4 w-40\" />\n            </div>\n          </div>\n          <div className=\"space-y-4\">\n            <div className=\"rounded-lg border p-4\">\n              <div className=\"flex items-center justify-between\">\n                <div className=\"space-y-2\">\n                  <Skeleton className=\"h-6 w-24\" />\n                  <Skeleton className=\"h-4 w-64\" />\n                  <Skeleton className=\"h-6 w-20\" />\n                </div>\n                <Skeleton className=\"h-10 w-32\" />\n              </div>\n            </div>\n          </div>\n        </div>\n      ) : (\n        <>\n          <div className=\"grid gap-4 md:grid-cols-2\">\n            <div className=\"space-y-2\">\n              <label className=\"text-sm font-medium\">\n                {t(\"settings.subscription.current_plan\")}\n              </label>\n              <div className=\"flex items-center gap-2\">\n                {subscriptionStatus?.tier &&\n                  getStatusBadge(subscriptionStatus.tier)}\n              </div>\n            </div>\n\n            {subscriptionStatus?.hasActiveSubscription && (\n              <>\n                <div className=\"space-y-2\">\n                  <label className=\"text-sm font-medium\">\n                    {t(\"settings.subscription.billing_period\")}\n                  </label>\n                  <div className=\"text-sm text-muted-foreground\">\n                    {formatDate(subscriptionStatus.startDate)} -{\" \"}\n                    {formatDate(subscriptionStatus.endDate)}\n                  </div>\n                </div>\n              </>\n            )}\n          </div>\n\n          <div className=\"space-y-4\">\n            {!subscriptionStatus?.hasActiveSubscription ? (\n              <div className=\"space-y-4\">\n                <div className=\"rounded-lg border p-4\">\n                  <div className=\"flex items-center justify-between\">\n                    <div>\n                      <h3 className=\"flex items-center gap-2 font-semibold\">\n                        {t(\"settings.subscription.paid_plan\")}\n                      </h3>\n                      <p className=\"text-sm text-muted-foreground\">\n                        {t(\"settings.subscription.unlock_bigger_quota\")}\n                      </p>\n                      {subscriptionPrice && subscriptionPrice.amount ? (\n                        <span className=\"flex items-baseline gap-2\">\n                          <p className=\"mt-2 text-lg font-bold uppercase\">\n                            {subscriptionPrice.amount / 100}{\" \"}\n                            {subscriptionPrice.currency}\n                          </p>\n                          <span className=\"text-xs italic text-muted-foreground\">\n                            (excl. VAT)\n                          </span>\n                        </span>\n                      ) : (\n                        <Skeleton className=\"h-4 w-24\" />\n                      )}\n                    </div>\n                    <Button\n                      onClick={() => createCheckoutSession.mutate()}\n                      disabled={isLoading}\n                      size=\"lg\"\n                      className=\"shadow-md transition-all hover:scale-[1.02] hover:shadow-lg active:scale-[0.98]\"\n                    >\n                      {isLoading && (\n                        <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n                      )}\n                      {t(\"settings.subscription.subscribe_now\")}\n                    </Button>\n                  </div>\n                </div>\n              </div>\n            ) : (\n              <div className=\"space-y-4\">\n                <div className=\"flex gap-2\">\n                  <Button\n                    onClick={() => createPortalSession.mutate()}\n                    disabled={isLoading}\n                    variant=\"outline\"\n                  >\n                    {isLoading && (\n                      <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />\n                    )}\n                    {t(\"settings.subscription.manage_billing\")}\n                  </Button>\n                </div>\n\n                {subscriptionStatus.cancelAtPeriodEnd && (\n                  <Alert>\n                    <AlertDescription>\n                      {t(\"settings.subscription.subscription_canceled\", {\n                        date: formatDate(subscriptionStatus.endDate),\n                      })}\n                    </AlertDescription>\n                  </Alert>\n                )}\n              </div>\n            )}\n          </div>\n        </>\n      )}\n    </SettingsSection>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/settings/UserAvatar.tsx",
    "content": "\"use client\";\n\nimport type { ChangeEvent } from \"react\";\nimport { useRef } from \"react\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport ActionConfirmingDialog from \"@/components/ui/action-confirming-dialog\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { UserAvatar as UserAvatarImage } from \"@/components/ui/user-avatar\";\nimport useUpload from \"@/lib/hooks/upload-file\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { Upload, User, X } from \"lucide-react\";\n\nimport {\n  useUpdateUserAvatar,\n  useWhoAmI,\n} from \"@karakeep/shared-react/hooks/users\";\n\nimport { Button } from \"../ui/button\";\nimport { SettingsSection } from \"./SettingsPage\";\n\nexport default function UserAvatar() {\n  const { t } = useTranslation();\n  const fileInputRef = useRef<HTMLInputElement>(null);\n  const whoami = useWhoAmI();\n  const image = whoami.data?.image ?? null;\n\n  const updateAvatar = useUpdateUserAvatar({\n    onError: () => {\n      toast({\n        description: t(\"common.something_went_wrong\"),\n        variant: \"destructive\",\n      });\n    },\n  });\n\n  const upload = useUpload({\n    onSuccess: async (resp) => {\n      try {\n        await updateAvatar.mutateAsync({ assetId: resp.assetId });\n        toast({\n          description: t(\"settings.info.avatar.updated\"),\n        });\n      } catch {\n        // handled in onError\n      }\n    },\n    onError: (err) => {\n      toast({\n        description: err.error,\n        variant: \"destructive\",\n      });\n    },\n  });\n\n  const isBusy = upload.isPending || updateAvatar.isPending;\n\n  const handleSelectFile = () => {\n    fileInputRef.current?.click();\n  };\n\n  const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {\n    const file = event.target.files?.[0];\n    if (!file) {\n      return;\n    }\n    upload.mutate(file);\n    event.target.value = \"\";\n  };\n\n  return (\n    <SettingsSection\n      title={t(\"settings.info.avatar.title\")}\n      description={t(\"settings.info.avatar.description\")}\n    >\n      <div className=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n        <div className=\"flex items-center gap-4\">\n          <div className=\"flex size-16 items-center justify-center overflow-hidden rounded-full border bg-muted\">\n            <UserAvatarImage\n              image={image}\n              name={t(\"settings.info.avatar.title\")}\n              fallback={<User className=\"h-7 w-7\" />}\n              className=\"h-full w-full\"\n            />\n          </div>\n          <input\n            ref={fileInputRef}\n            type=\"file\"\n            accept=\"image/*\"\n            className=\"hidden\"\n            onChange={handleFileChange}\n          />\n          <ActionButton\n            type=\"button\"\n            variant=\"secondary\"\n            onClick={handleSelectFile}\n            loading={upload.isPending}\n            disabled={isBusy}\n          >\n            <Upload className=\"mr-2 h-4 w-4\" />\n            {image\n              ? t(\"settings.info.avatar.change\")\n              : t(\"settings.info.avatar.upload\")}\n          </ActionButton>\n        </div>\n        <ActionConfirmingDialog\n          title={t(\"settings.info.avatar.remove_confirm_title\")}\n          description={\n            <p>{t(\"settings.info.avatar.remove_confirm_description\")}</p>\n          }\n          actionButton={(setDialogOpen) => (\n            <ActionButton\n              type=\"button\"\n              variant=\"destructive\"\n              loading={updateAvatar.isPending}\n              onClick={() =>\n                updateAvatar.mutate(\n                  { assetId: null },\n                  {\n                    onSuccess: () => {\n                      toast({\n                        description: t(\"settings.info.avatar.removed\"),\n                      });\n                      setDialogOpen(false);\n                    },\n                  },\n                )\n              }\n            >\n              {t(\"settings.info.avatar.remove\")}\n            </ActionButton>\n          )}\n        >\n          <Button type=\"button\" variant=\"outline\" disabled={!image || isBusy}>\n            <X className=\"mr-2 h-4 w-4\" />\n            {t(\"settings.info.avatar.remove\")}\n          </Button>\n        </ActionConfirmingDialog>\n      </div>\n    </SettingsSection>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/settings/UserDetails.tsx",
    "content": "import { Input } from \"@/components/ui/input\";\nimport { useTranslation } from \"@/lib/i18n/server\";\nimport { api } from \"@/server/api/client\";\nimport { Mail } from \"lucide-react\";\n\nimport { Label } from \"../ui/label\";\nimport { SettingsSection } from \"./SettingsPage\";\n\nexport default async function UserDetails() {\n  // oxlint-disable-next-line rules-of-hooks\n  const { t } = await useTranslation();\n  const whoami = await api.users.whoami();\n\n  return (\n    <SettingsSection title={t(\"settings.info.basic_details\")}>\n      <div className=\"grid gap-6 md:grid-cols-2\">\n        <div className=\"space-y-2\">\n          <Label htmlFor=\"name\" className=\"text-sm font-medium\">\n            {t(\"common.name\")}\n          </Label>\n          <Input\n            id=\"name\"\n            defaultValue={whoami.name ?? \"\"}\n            className=\"h-11\"\n            disabled\n          />\n        </div>\n        <div className=\"space-y-2\">\n          <Label\n            htmlFor=\"email\"\n            className=\"flex items-center gap-2 text-sm font-medium\"\n          >\n            <Mail className=\"h-4 w-4\" />\n            {t(\"common.email\")}\n          </Label>\n          <div className=\"relative\">\n            <Input\n              id=\"email\"\n              type=\"email\"\n              defaultValue={whoami.email ?? \"\"}\n              className=\"h-11\"\n              disabled\n            />\n          </div>\n        </div>\n      </div>\n    </SettingsSection>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/settings/UserOptions.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { useClientConfig } from \"@/lib/clientConfig\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { useInterfaceLang } from \"@/lib/userLocalSettings/bookmarksLayout\";\nimport { updateInterfaceLang } from \"@/lib/userLocalSettings/userLocalSettings\";\nimport { useUserSettings } from \"@/lib/userSettings\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { Archive, Bookmark, Clock } from \"lucide-react\";\nimport { useForm } from \"react-hook-form\";\nimport { z } from \"zod\";\n\nimport { useUpdateUserSettings } from \"@karakeep/shared-react/hooks/users\";\nimport { langNameMappings } from \"@karakeep/shared/langs\";\nimport {\n  ZUserSettings,\n  zUserSettingsSchema,\n} from \"@karakeep/shared/types/users\";\n\nimport { Form, FormField } from \"../ui/form\";\nimport { Label } from \"../ui/label\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"../ui/select\";\nimport { SettingsSection } from \"./SettingsPage\";\n\nconst LanguageSelect = () => {\n  const lang = useInterfaceLang();\n  return (\n    <Select\n      value={lang}\n      onValueChange={async (val) => {\n        await updateInterfaceLang(val);\n      }}\n    >\n      <SelectTrigger className=\"h-11\">\n        <SelectValue />\n      </SelectTrigger>\n      <SelectContent>\n        {Object.entries(langNameMappings).map(([lang, name]) => (\n          <SelectItem key={lang} value={lang}>\n            {name}\n          </SelectItem>\n        ))}\n      </SelectContent>\n    </Select>\n  );\n};\n\nexport default function UserOptions() {\n  const { t } = useTranslation();\n  const clientConfig = useClientConfig();\n  const data = useUserSettings();\n  const { mutate } = useUpdateUserSettings({\n    onSuccess: () => {\n      toast({\n        description: t(\"settings.info.user_settings.user_settings_updated\"),\n      });\n    },\n    onError: () => {\n      toast({\n        description: t(\"common.something_went_wrong\"),\n        variant: \"destructive\",\n      });\n    },\n  });\n  const [timezones, setTimezones] = useState<\n    { label: string; value: string }[] | null\n  >(null);\n\n  const bookmarkClickActionTranslation: Record<\n    ZUserSettings[\"bookmarkClickAction\"],\n    string\n  > = {\n    open_original_link: t(\n      \"settings.info.user_settings.bookmark_click_action.open_external_url\",\n    ),\n    expand_bookmark_preview: t(\n      \"settings.info.user_settings.bookmark_click_action.open_bookmark_details\",\n    ),\n  };\n\n  const archiveDisplayBehaviourTranslation: Record<\n    ZUserSettings[\"archiveDisplayBehaviour\"],\n    string\n  > = {\n    show: t(\"settings.info.user_settings.archive_display_behaviour.show\"),\n    hide: t(\"settings.info.user_settings.archive_display_behaviour.hide\"),\n  };\n\n  // Get all supported timezones and format them nicely\n  useEffect(() => {\n    try {\n      const browserTimezones = Intl.supportedValuesOf(\"timeZone\");\n      setTimezones(\n        browserTimezones\n          .map((tz) => {\n            // Create a more readable label by replacing underscores with spaces\n            // and showing the current time offset\n            const now = new Date();\n            const formatter = new Intl.DateTimeFormat(\"en\", {\n              timeZone: tz,\n              timeZoneName: \"short\",\n            });\n            const parts = formatter.formatToParts(now);\n            const timeZoneName =\n              parts.find((part) => part.type === \"timeZoneName\")?.value || \"\";\n\n            // Format the timezone name for display\n            const displayName = tz.replace(/_/g, \" \").replace(\"/\", \" / \");\n            const label = timeZoneName\n              ? `${displayName} (${timeZoneName})`\n              : displayName;\n\n            return { value: tz, label };\n          })\n          .sort((a, b) => a.label.localeCompare(b.label)),\n      );\n    } catch {\n      setTimezones(null);\n    }\n  }, []);\n\n  const form = useForm<z.infer<typeof zUserSettingsSchema>>({\n    resolver: zodResolver(zUserSettingsSchema),\n    defaultValues: data,\n  });\n\n  // When the actual user setting is loaded, reset the form to the current value\n  useEffect(() => {\n    form.reset(data);\n  }, [data]);\n\n  return (\n    <Form {...form}>\n      <SettingsSection title={t(\"settings.info.options\")}>\n        <div className=\"space-y-2\">\n          <Label className=\"text-sm font-medium\">\n            {t(\"settings.info.interface_lang\")}\n          </Label>\n          <LanguageSelect />\n        </div>\n\n        <FormField\n          control={form.control}\n          name=\"timezone\"\n          render={({ field }) => (\n            <div className=\"space-y-2\">\n              <Label className=\"flex items-center gap-2 text-sm font-medium\">\n                <Clock className=\"h-4 w-4\" />\n                Timezone\n              </Label>\n              <Select\n                disabled={!!clientConfig.demoMode || timezones === null}\n                value={field.value}\n                onValueChange={(value) => {\n                  mutate({\n                    timezone: value,\n                  });\n                }}\n              >\n                <SelectTrigger className=\"h-11\">\n                  <SelectValue>\n                    {timezones?.find(\n                      (tz: { value: string; label: string }) =>\n                        tz.value === field.value,\n                    )?.label || field.value}\n                  </SelectValue>\n                </SelectTrigger>\n                <SelectContent>\n                  {timezones?.map((tz: { value: string; label: string }) => (\n                    <SelectItem key={tz.value} value={tz.value}>\n                      {tz.label}\n                    </SelectItem>\n                  ))}\n                </SelectContent>\n              </Select>\n            </div>\n          )}\n        />\n\n        <div className=\"grid grid-cols-2 gap-6\">\n          <FormField\n            control={form.control}\n            name=\"bookmarkClickAction\"\n            render={({ field }) => (\n              <div className=\"space-y-2\">\n                <Label className=\"flex items-center gap-2 text-sm font-medium\">\n                  <Bookmark className=\"h-4 w-4\" />\n                  {t(\"settings.info.user_settings.bookmark_click_action.title\")}\n                </Label>\n                <Select\n                  disabled={!!clientConfig.demoMode}\n                  value={field.value}\n                  onValueChange={(value) => {\n                    mutate({\n                      bookmarkClickAction:\n                        value as ZUserSettings[\"bookmarkClickAction\"],\n                    });\n                  }}\n                >\n                  <SelectTrigger className=\"h-11\">\n                    <SelectValue>\n                      {bookmarkClickActionTranslation[field.value]}\n                    </SelectValue>\n                  </SelectTrigger>\n                  <SelectContent>\n                    {Object.entries(bookmarkClickActionTranslation).map(\n                      ([value, label]) => (\n                        <SelectItem key={value} value={value}>\n                          {label}\n                        </SelectItem>\n                      ),\n                    )}\n                  </SelectContent>\n                </Select>\n              </div>\n            )}\n          />\n\n          <FormField\n            control={form.control}\n            name=\"archiveDisplayBehaviour\"\n            render={({ field }) => (\n              <div className=\"space-y-2\">\n                <Label className=\"flex items-center gap-2 text-sm font-medium\">\n                  <Archive className=\"h-4 w-4\" />\n                  {t(\n                    \"settings.info.user_settings.archive_display_behaviour.title\",\n                  )}\n                </Label>\n                <Select\n                  disabled={!!clientConfig.demoMode}\n                  value={field.value}\n                  onValueChange={(value) => {\n                    mutate({\n                      archiveDisplayBehaviour:\n                        value as ZUserSettings[\"archiveDisplayBehaviour\"],\n                    });\n                  }}\n                >\n                  <SelectTrigger className=\"h-11\">\n                    <SelectValue>\n                      {archiveDisplayBehaviourTranslation[field.value]}\n                    </SelectValue>\n                  </SelectTrigger>\n                  <SelectContent>\n                    {Object.entries(archiveDisplayBehaviourTranslation).map(\n                      ([value, label]) => (\n                        <SelectItem key={value} value={value}>\n                          {label}\n                        </SelectItem>\n                      ),\n                    )}\n                  </SelectContent>\n                </Select>\n              </div>\n            )}\n          />\n        </div>\n      </SettingsSection>\n    </Form>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/settings/WebhookEventSelector.tsx",
    "content": "import { Button } from \"@/components/ui/button\";\nimport {\n  Command,\n  CommandEmpty,\n  CommandGroup,\n  CommandInput,\n  CommandItem,\n  CommandList,\n} from \"@/components/ui/command\";\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\";\nimport { Check, ChevronsUpDown } from \"lucide-react\";\n\nimport {\n  ZWebhookEvent,\n  zWebhookEventSchema,\n} from \"@karakeep/shared/types/webhooks\";\n\nexport function WebhookEventSelector({\n  value,\n  onChange,\n}: {\n  value: ZWebhookEvent[];\n  onChange: (value: ZWebhookEvent[]) => void;\n}) {\n  return (\n    <Popover>\n      <PopoverTrigger asChild>\n        <Button\n          variant=\"outline\"\n          role=\"combobox\"\n          className=\"w-full justify-between\"\n        >\n          {value.length > 0 ? value.join(\", \") : \"Select events\"}\n          <ChevronsUpDown className=\"ml-2 h-4 w-4 shrink-0 opacity-50\" />\n        </Button>\n      </PopoverTrigger>\n      <PopoverContent>\n        <Command>\n          <CommandInput placeholder=\"Search events...\" />\n          <CommandList>\n            <CommandEmpty>No events found.</CommandEmpty>\n            <CommandGroup>\n              {zWebhookEventSchema.options.map((eventType) => (\n                <CommandItem\n                  key={eventType}\n                  value={eventType}\n                  onSelect={() => {\n                    const newEvents = value.includes(eventType)\n                      ? value.filter((e) => e !== eventType)\n                      : [...value, eventType];\n                    onChange(newEvents);\n                  }}\n                >\n                  {eventType}\n                  {value?.includes(eventType) && (\n                    <Check className=\"ml-auto h-4 w-4\" />\n                  )}\n                </CommandItem>\n              ))}\n            </CommandGroup>\n          </CommandList>\n        </Command>\n      </PopoverContent>\n    </Popover>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/settings/WebhookSettings.tsx",
    "content": "\"use client\";\n\nimport React from \"react\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport {\n  Form,\n  FormControl,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@/components/ui/form\";\nimport { FullPageSpinner } from \"@/components/ui/full-page-spinner\";\nimport { Input } from \"@/components/ui/input\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\nimport {\n  Edit,\n  KeyRound,\n  Plus,\n  PlusCircle,\n  Save,\n  Trash2,\n  X,\n} from \"lucide-react\";\nimport { useForm } from \"react-hook-form\";\nimport { z } from \"zod\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\nimport {\n  zNewWebhookSchema,\n  zUpdateWebhookSchema,\n  ZWebhook,\n} from \"@karakeep/shared/types/webhooks\";\n\nimport ActionConfirmingDialog from \"../ui/action-confirming-dialog\";\nimport { Button } from \"../ui/button\";\nimport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"../ui/dialog\";\nimport {\n  Table,\n  TableBody,\n  TableCell,\n  TableHead,\n  TableHeader,\n  TableRow,\n} from \"../ui/table\";\nimport { SettingsPage, SettingsSection } from \"./SettingsPage\";\nimport { WebhookEventSelector } from \"./WebhookEventSelector\";\n\nexport function WebhooksEditorDialog() {\n  const api = useTRPC();\n  const { t } = useTranslation();\n  const [open, setOpen] = React.useState(false);\n  const queryClient = useQueryClient();\n\n  const form = useForm<z.infer<typeof zNewWebhookSchema>>({\n    resolver: zodResolver(zNewWebhookSchema),\n    defaultValues: {\n      url: \"\",\n      events: [],\n      token: \"\",\n    },\n  });\n\n  React.useEffect(() => {\n    if (open) {\n      form.reset();\n    }\n  }, [open]);\n\n  const { mutateAsync: createWebhook, isPending: isCreating } = useMutation(\n    api.webhooks.create.mutationOptions({\n      onSuccess: () => {\n        toast({\n          description: \"Webhook has been created!\",\n        });\n        queryClient.invalidateQueries(api.webhooks.list.pathFilter());\n        setOpen(false);\n      },\n    }),\n  );\n\n  return (\n    <Dialog open={open} onOpenChange={setOpen}>\n      <DialogTrigger asChild>\n        <Button>\n          <PlusCircle className=\"mr-2 size-4\" />\n          {t(\"settings.webhooks.create_webhook\")}\n        </Button>\n      </DialogTrigger>\n      <DialogContent>\n        <DialogHeader>\n          <DialogTitle>{t(\"settings.webhooks.create_webhook\")}</DialogTitle>\n        </DialogHeader>\n        <Form {...form}>\n          <form\n            className=\"flex flex-col gap-3\"\n            onSubmit={form.handleSubmit(async (value) => {\n              await createWebhook(value);\n              form.resetField(\"url\");\n              form.resetField(\"events\");\n            })}\n          >\n            <FormField\n              control={form.control}\n              name=\"url\"\n              render={({ field }) => {\n                return (\n                  <FormItem className=\"flex-1\">\n                    <FormLabel>URL</FormLabel>\n                    <FormControl>\n                      <Input placeholder=\"Webhook URL\" type=\"text\" {...field} />\n                    </FormControl>\n                    <FormMessage />\n                  </FormItem>\n                );\n              }}\n            />\n            <FormField\n              control={form.control}\n              name=\"token\"\n              render={({ field }) => (\n                <FormItem className=\"flex-1\">\n                  <FormLabel>{t(\"settings.webhooks.auth_token\")}</FormLabel>\n                  <FormControl>\n                    <Input\n                      type=\"password\"\n                      placeholder=\"Authentication token\"\n                      {...field}\n                    />\n                  </FormControl>\n                  <FormMessage />\n                </FormItem>\n              )}\n            />\n            <FormField\n              control={form.control}\n              name=\"events\"\n              render={({ field }) => (\n                <FormItem className=\"flex-1\">\n                  <FormLabel>Events</FormLabel>\n                  <WebhookEventSelector\n                    value={field.value}\n                    onChange={field.onChange}\n                  />\n                  <FormMessage />\n                </FormItem>\n              )}\n            />\n          </form>\n        </Form>\n        <DialogFooter>\n          <DialogClose asChild>\n            <Button type=\"button\" variant=\"secondary\">\n              Close\n            </Button>\n          </DialogClose>\n          <ActionButton\n            onClick={form.handleSubmit(async (value) => {\n              await createWebhook(value);\n            })}\n            loading={isCreating}\n            variant=\"default\"\n            className=\"items-center\"\n          >\n            <Plus className=\"mr-2 size-4\" />\n            Add\n          </ActionButton>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n\nexport function EditWebhookDialog({ webhook }: { webhook: ZWebhook }) {\n  const api = useTRPC();\n  const { t } = useTranslation();\n  const queryClient = useQueryClient();\n  const [open, setOpen] = React.useState(false);\n  React.useEffect(() => {\n    if (open) {\n      form.reset({\n        webhookId: webhook.id,\n        url: webhook.url,\n        events: webhook.events,\n      });\n    }\n  }, [open]);\n  const { mutateAsync: updateWebhook, isPending: isUpdating } = useMutation(\n    api.webhooks.update.mutationOptions({\n      onSuccess: () => {\n        toast({\n          description: \"Webhook has been updated!\",\n        });\n        setOpen(false);\n        queryClient.invalidateQueries(api.webhooks.list.pathFilter());\n      },\n    }),\n  );\n  const updateSchema = zUpdateWebhookSchema.required({\n    events: true,\n    url: true,\n  });\n  const form = useForm<z.infer<typeof updateSchema>>({\n    resolver: zodResolver(updateSchema),\n    defaultValues: {\n      webhookId: webhook.id,\n      url: webhook.url,\n      events: webhook.events,\n    },\n  });\n  return (\n    <Dialog open={open} onOpenChange={setOpen}>\n      <DialogTrigger asChild>\n        <Button variant=\"secondary\">\n          <Edit className=\"mr-2 size-4\" />\n          {t(\"actions.edit\")}\n        </Button>\n      </DialogTrigger>\n      <DialogContent>\n        <DialogHeader>\n          <DialogTitle>{t(\"settings.webhooks.edit_webhook\")}</DialogTitle>\n        </DialogHeader>\n\n        <Form {...form}>\n          <form\n            className=\"flex flex-col gap-3\"\n            onSubmit={form.handleSubmit(async (value) => {\n              await updateWebhook(value);\n            })}\n          >\n            <FormField\n              control={form.control}\n              name=\"webhookId\"\n              render={({ field }) => {\n                return (\n                  <FormItem className=\"hidden\">\n                    <FormControl>\n                      <Input type=\"hidden\" {...field} />\n                    </FormControl>\n                    <FormMessage />\n                  </FormItem>\n                );\n              }}\n            />\n            <FormField\n              control={form.control}\n              name=\"url\"\n              render={({ field }) => {\n                return (\n                  <FormItem className=\"flex-1\">\n                    <FormLabel>{t(\"common.url\")}</FormLabel>\n                    <FormControl>\n                      <Input placeholder=\"Webhook URL\" type=\"text\" {...field} />\n                    </FormControl>\n                    <FormMessage />\n                  </FormItem>\n                );\n              }}\n            />\n            <FormField\n              control={form.control}\n              name=\"events\"\n              render={({ field }) => (\n                <FormItem className=\"flex-1\">\n                  <FormLabel>{t(\"settings.webhooks.events.title\")}</FormLabel>\n                  <WebhookEventSelector\n                    value={field.value}\n                    onChange={field.onChange}\n                  />\n                  <FormMessage />\n                </FormItem>\n              )}\n            />\n          </form>\n        </Form>\n        <DialogFooter>\n          <DialogClose asChild>\n            <Button type=\"button\" variant=\"secondary\">\n              {t(\"actions.close\")}\n            </Button>\n          </DialogClose>\n          <ActionButton\n            loading={isUpdating}\n            onClick={form.handleSubmit(async (value) => {\n              await updateWebhook(value);\n            })}\n            type=\"submit\"\n            className=\"items-center\"\n          >\n            <Save className=\"mr-2 size-4\" />\n            {t(\"actions.save\")}\n          </ActionButton>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n\nexport function EditTokenDialog({ webhook }: { webhook: ZWebhook }) {\n  const api = useTRPC();\n  const { t } = useTranslation();\n  const queryClient = useQueryClient();\n  const [open, setOpen] = React.useState(false);\n  React.useEffect(() => {\n    if (open) {\n      form.reset({\n        webhookId: webhook.id,\n        token: \"\",\n      });\n    }\n  }, [open]);\n\n  const updateSchema = zUpdateWebhookSchema\n    .pick({\n      webhookId: true,\n      token: true,\n    })\n    .required({\n      token: true,\n    });\n\n  const form = useForm<z.infer<typeof updateSchema>>({\n    resolver: zodResolver(updateSchema),\n    defaultValues: {\n      webhookId: webhook.id,\n      token: \"\",\n    },\n  });\n\n  const { mutateAsync: updateWebhook, isPending: isUpdating } = useMutation(\n    api.webhooks.update.mutationOptions({\n      onSuccess: () => {\n        toast({\n          description: \"Webhook token has been updated!\",\n        });\n        setOpen(false);\n        queryClient.invalidateQueries(api.webhooks.list.pathFilter());\n      },\n    }),\n  );\n\n  return (\n    <Dialog open={open} onOpenChange={setOpen}>\n      <DialogTrigger asChild>\n        <Button variant=\"secondary\">\n          <KeyRound className=\"mr-2 size-4\" />\n          {webhook.hasToken\n            ? t(\"settings.webhooks.edit_auth_token\")\n            : t(\"settings.webhooks.add_auth_token\")}\n        </Button>\n      </DialogTrigger>\n      <DialogContent>\n        <DialogHeader>\n          <DialogTitle>{t(\"settings.webhooks.edit_auth_token\")}</DialogTitle>\n        </DialogHeader>\n        <Form {...form}>\n          <form\n            className=\"flex flex-col gap-3\"\n            onSubmit={form.handleSubmit(async (value) => {\n              await updateWebhook(value);\n            })}\n          >\n            <FormField\n              control={form.control}\n              name=\"webhookId\"\n              render={({ field }) => (\n                <FormItem className=\"hidden\">\n                  <FormControl>\n                    <Input type=\"hidden\" {...field} />\n                  </FormControl>\n                </FormItem>\n              )}\n            />\n            <FormField\n              control={form.control}\n              name=\"token\"\n              render={({ field }) => (\n                <FormItem className=\"flex-1\">\n                  <FormLabel>{t(\"settings.webhooks.auth_token\")}</FormLabel>\n                  <FormControl>\n                    <Input\n                      type=\"password\"\n                      placeholder=\"Authentication token\"\n                      {...field}\n                      value={field.value ?? \"\"}\n                    />\n                  </FormControl>\n                  <FormMessage />\n                </FormItem>\n              )}\n            />\n          </form>\n        </Form>\n        <DialogFooter>\n          <DialogClose asChild>\n            <Button type=\"button\" variant=\"secondary\">\n              {t(\"actions.close\")}\n            </Button>\n          </DialogClose>\n          <ActionButton\n            variant=\"destructive\"\n            loading={isUpdating}\n            onClick={form.handleSubmit(async (value) => {\n              await updateWebhook({\n                webhookId: value.webhookId,\n                token: null,\n              });\n            })}\n            className=\"items-center\"\n          >\n            <Trash2 className=\"mr-2 size-4\" />\n            {t(\"actions.delete\")}\n          </ActionButton>\n          <ActionButton\n            loading={isUpdating}\n            onClick={form.handleSubmit(async (value) => {\n              await updateWebhook(value);\n            })}\n            type=\"submit\"\n            className=\"items-center\"\n          >\n            <Save className=\"mr-2 size-4\" />\n            {t(\"actions.save\")}\n          </ActionButton>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n\nexport function WebhookRow({ webhook }: { webhook: ZWebhook }) {\n  const api = useTRPC();\n  const { t } = useTranslation();\n  const queryClient = useQueryClient();\n  const { mutate: deleteWebhook, isPending: isDeleting } = useMutation(\n    api.webhooks.delete.mutationOptions({\n      onSuccess: () => {\n        toast({\n          description: \"Webhook has been deleted!\",\n        });\n        queryClient.invalidateQueries(api.webhooks.list.pathFilter());\n      },\n    }),\n  );\n\n  return (\n    <TableRow>\n      <TableCell>{webhook.url}</TableCell>\n      <TableCell>{webhook.events.join(\", \")}</TableCell>\n      <TableCell>{webhook.hasToken ? \"*******\" : <X />}</TableCell>\n      <TableCell className=\"flex items-center gap-2\">\n        <EditWebhookDialog webhook={webhook} />\n        <EditTokenDialog webhook={webhook} />\n        <ActionConfirmingDialog\n          title={t(\"settings.webhooks.delete_webhook\")}\n          description={t(\"settings.webhooks.delete_webhook_confirmation\")}\n          actionButton={() => (\n            <ActionButton\n              loading={isDeleting}\n              variant=\"destructive\"\n              onClick={() => deleteWebhook({ webhookId: webhook.id })}\n              className=\"items-center\"\n              type=\"button\"\n            >\n              <Trash2 className=\"mr-2 size-4\" />\n              {t(\"actions.delete\")}\n            </ActionButton>\n          )}\n        >\n          <Button variant=\"destructive\" disabled={isDeleting}>\n            <Trash2 className=\"mr-2 size-4\" />\n            {t(\"actions.delete\")}\n          </Button>\n        </ActionConfirmingDialog>\n      </TableCell>\n    </TableRow>\n  );\n}\n\nexport default function WebhookSettings() {\n  const api = useTRPC();\n  const { t } = useTranslation();\n  const { data: webhooks, isLoading } = useQuery(\n    api.webhooks.list.queryOptions(),\n  );\n  return (\n    <SettingsPage\n      title={t(\"settings.webhooks.webhooks\")}\n      description={t(\"settings.webhooks.description\")}\n      action={<WebhooksEditorDialog />}\n    >\n      <SettingsSection>\n        {isLoading && <FullPageSpinner />}\n        {webhooks && webhooks.webhooks.length == 0 && (\n          <p className=\"rounded-md bg-muted p-3 text-center text-sm text-muted-foreground\">\n            You don&apos;t have any webhooks configured yet.\n          </p>\n        )}\n        {webhooks && webhooks.webhooks.length > 0 && (\n          <Table className=\"table-auto\">\n            <TableHeader>\n              <TableRow>\n                <TableHead>{t(\"common.url\")}</TableHead>\n                <TableHead>{t(\"settings.webhooks.events.title\")}</TableHead>\n                <TableHead>{t(\"settings.webhooks.auth_token\")}</TableHead>\n                <TableHead>{t(\"common.actions\")}</TableHead>\n              </TableRow>\n            </TableHeader>\n            <TableBody>\n              {webhooks.webhooks.map((webhook) => (\n                <WebhookRow key={webhook.id} webhook={webhook} />\n              ))}\n            </TableBody>\n          </Table>\n        )}\n      </SettingsSection>\n    </SettingsPage>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/shared/sidebar/MobileSidebar.tsx",
    "content": "import { useTranslation } from \"@/lib/i18n/server\";\nimport { TFunction } from \"i18next\";\n\nimport MobileSidebarItem from \"./ModileSidebarItem\";\nimport { TSidebarItem } from \"./TSidebarItem\";\n\nexport default async function MobileSidebar({\n  items,\n}: {\n  items: (t: TFunction) => TSidebarItem[];\n}) {\n  // oxlint-disable-next-line rules-of-hooks\n  const { t } = await useTranslation();\n  return (\n    <aside className=\"w-full overflow-x-auto\">\n      <ul className=\"flex justify-between space-x-2 border-b-black px-5 py-2 pt-5\">\n        {items(t).map((item) => (\n          <MobileSidebarItem\n            key={item.name}\n            logo={item.icon}\n            path={item.path}\n          />\n        ))}\n      </ul>\n    </aside>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/shared/sidebar/ModileSidebarItem.tsx",
    "content": "\"use client\";\n\nimport Link from \"next/link\";\nimport { usePathname } from \"next/navigation\";\nimport { haptic } from \"@/lib/haptic\";\nimport { cn } from \"@/lib/utils\";\n\nexport default function MobileSidebarItem({\n  logo,\n  path,\n}: {\n  logo: React.ReactNode;\n  path: string;\n}) {\n  const currentPath = usePathname();\n  return (\n    <li\n      className={cn(\n        \"flex w-full rounded-lg hover:bg-background\",\n        path == currentPath ? \"bg-background\" : \"\",\n      )}\n    >\n      <Link onClick={haptic} href={path} className=\"m-auto px-3 py-2\">\n        {logo}\n      </Link>\n    </li>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/shared/sidebar/Sidebar.tsx",
    "content": "import { useTranslation } from \"@/lib/i18n/server\";\nimport { TFunction } from \"i18next\";\n\nimport serverConfig from \"@karakeep/shared/config\";\n\nimport SidebarItem from \"./SidebarItem\";\nimport SidebarVersion from \"./SidebarVersion\";\nimport { TSidebarItem } from \"./TSidebarItem\";\n\nexport default async function Sidebar({\n  items,\n  extraSections,\n}: {\n  items: (t: TFunction) => TSidebarItem[];\n  extraSections?: React.ReactNode;\n}) {\n  // oxlint-disable-next-line rules-of-hooks\n  const { t } = await useTranslation();\n\n  return (\n    <aside className=\"flex h-[calc(100vh-64px)] w-60 flex-col gap-5 border-r p-4\">\n      <div>\n        <ul className=\"space-y-2 text-sm\">\n          {items(t).map((item) => (\n            <SidebarItem\n              key={item.name}\n              logo={item.icon}\n              name={item.name}\n              path={item.path}\n            />\n          ))}\n        </ul>\n      </div>\n      {extraSections}\n      <SidebarVersion\n        serverVersion={serverConfig.serverVersion}\n        changeLogVersion={serverConfig.changelogVersion}\n      />\n    </aside>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/shared/sidebar/SidebarItem.tsx",
    "content": "\"use client\";\n\nimport React from \"react\";\nimport Link from \"next/link\";\nimport { usePathname } from \"next/navigation\";\nimport { cn } from \"@/lib/utils\";\n\nexport default function SidebarItem({\n  name,\n  logo,\n  path,\n  className,\n  linkClassName,\n  style,\n  collapseButton,\n  right = null,\n  dropHighlight = false,\n  onDrop,\n  onDragOver,\n  onDragEnter,\n  onDragLeave,\n}: {\n  name: string;\n  logo: React.ReactNode;\n  path: string;\n  style?: React.CSSProperties;\n  className?: string;\n  linkClassName?: string;\n  right?: React.ReactNode;\n  collapseButton?: React.ReactNode;\n  dropHighlight?: boolean;\n  onDrop?: React.DragEventHandler;\n  onDragOver?: React.DragEventHandler;\n  onDragEnter?: React.DragEventHandler;\n  onDragLeave?: React.DragEventHandler;\n}) {\n  const currentPath = usePathname();\n  return (\n    <li\n      className={cn(\n        \"relative flex justify-between rounded-lg text-sm transition-colors hover:bg-accent\",\n        path == currentPath\n          ? \"bg-accent/50 text-foreground\"\n          : \"text-muted-foreground\",\n        dropHighlight && \"bg-accent ring-2 ring-primary\",\n        className,\n      )}\n      style={style}\n      onDrop={onDrop}\n      onDragOver={onDragOver}\n      onDragEnter={onDragEnter}\n      onDragLeave={onDragLeave}\n    >\n      <div className=\"flex-1\">\n        {collapseButton}\n        <Link\n          href={path}\n          className={cn(\n            \"flex items-center gap-x-2 rounded-[inherit] px-3 py-2\",\n            linkClassName,\n          )}\n        >\n          {logo}\n          <span title={name} className=\"line-clamp-1 break-all\">\n            {name}\n          </span>\n        </Link>\n      </div>\n      {right}\n    </li>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/shared/sidebar/SidebarLayout.tsx",
    "content": "import { Suspense } from \"react\";\nimport ErrorFallback from \"@/components/dashboard/ErrorFallback\";\nimport Header from \"@/components/dashboard/header/Header\";\nimport DemoModeBanner from \"@/components/DemoModeBanner\";\nimport { Separator } from \"@/components/ui/separator\";\nimport LoadingSpinner from \"@/components/ui/spinner\";\nimport ValidAccountCheck from \"@/components/utils/ValidAccountCheck\";\nimport { ErrorBoundary } from \"react-error-boundary\";\n\nimport serverConfig from \"@karakeep/shared/config\";\n\nexport default function SidebarLayout({\n  children,\n  mobileSidebar,\n  sidebar,\n  modal,\n}: {\n  children: React.ReactNode;\n  mobileSidebar: React.ReactNode;\n  sidebar: React.ReactNode;\n  modal?: React.ReactNode;\n}) {\n  return (\n    <div className=\"sm:fixed sm:inset-0 sm:overflow-hidden\">\n      <Header />\n      <div className=\"flex min-h-[calc(100vh-64px)] w-full flex-col sm:h-[calc(100dvh-64px)] sm:flex-row sm:overflow-hidden\">\n        <ValidAccountCheck />\n        <div className=\"hidden flex-none sm:flex\">{sidebar}</div>\n        <main className=\"flex-1 bg-muted sm:min-h-0 sm:overflow-y-auto\">\n          {serverConfig.demoMode && <DemoModeBanner />}\n          <div className=\"block w-full sm:hidden\">\n            {mobileSidebar}\n            <Separator />\n          </div>\n          {modal}\n          <div className=\"min-h-30 container p-4\">\n            <ErrorBoundary fallback={<ErrorFallback />}>\n              <Suspense fallback={<LoadingSpinner />}>{children}</Suspense>\n            </ErrorBoundary>\n          </div>\n        </main>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/shared/sidebar/SidebarVersion.tsx",
    "content": "\"use client\";\n\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\nimport Link from \"next/link\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\";\nimport { MarkdownReadonly } from \"@/components/ui/markdown/markdown-readonly\";\nimport { useClientConfig } from \"@/lib/clientConfig\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { AlertCircle, Loader2 } from \"lucide-react\";\nimport { z } from \"zod\";\n\nconst GITHUB_OWNER_REPO = \"karakeep-app/karakeep\";\nconst GITHUB_REPO_URL = `https://github.com/${GITHUB_OWNER_REPO}`;\nconst GITHUB_RELEASE_URL = `${GITHUB_REPO_URL}/releases/tag/`;\nconst RELEASE_API_URL = `https://api.github.com/repos/${GITHUB_OWNER_REPO}/releases/tags/`;\nconst LOCAL_STORAGE_KEY = \"karakeep:whats-new:last-seen-version\";\nconst RELEASE_NOTES_STALE_TIME = 1000 * 60 * 10; // 10 minutes\n\nconst zGitHubReleaseSchema = z.object({\n  body: z.string().optional(),\n  tag_name: z.string(),\n  name: z.string(),\n});\n\nfunction isStableRelease(version?: string) {\n  if (!version) {\n    return false;\n  }\n  const normalized = version.toLowerCase();\n  if (\n    normalized.includes(\"nightly\") ||\n    normalized.includes(\"beta\") ||\n    normalized.includes(\"0.0.1\")\n  ) {\n    return false;\n  }\n  return true;\n}\n\ninterface SidebarVersionProps {\n  // The actual version of the server\n  serverVersion?: string;\n  // The version that should be displayed in the changelog\n  changeLogVersion?: string;\n}\n\nexport default function SidebarVersion({\n  serverVersion,\n  changeLogVersion,\n}: SidebarVersionProps) {\n  const { disableNewReleaseCheck } = useClientConfig();\n  const { t } = useTranslation();\n\n  const effectiveChangelogVersion = changeLogVersion ?? serverVersion;\n  const stableRelease = isStableRelease(effectiveChangelogVersion);\n  const displayVersion = serverVersion ?? \"unknown\";\n  const changelogDisplayVersion = effectiveChangelogVersion ?? displayVersion;\n  const versionLabel = `Karakeep v${displayVersion}`;\n  const releasePageUrl = useMemo(() => {\n    if (\n      !effectiveChangelogVersion ||\n      !isStableRelease(effectiveChangelogVersion)\n    ) {\n      return GITHUB_REPO_URL;\n    }\n    return `${GITHUB_RELEASE_URL}v${effectiveChangelogVersion}`;\n  }, [effectiveChangelogVersion]);\n\n  const [open, setOpen] = useState(false);\n  const [shouldNotify, setShouldNotify] = useState(false);\n\n  const releaseNotesQuery = useQuery<string>({\n    queryKey: [\"sidebar-release-notes\", effectiveChangelogVersion],\n    queryFn: async ({ signal }) => {\n      if (!effectiveChangelogVersion) {\n        return \"\";\n      }\n\n      const response = await fetch(\n        `${RELEASE_API_URL}v${effectiveChangelogVersion}`,\n        {\n          signal,\n        },\n      );\n\n      if (!response.ok) {\n        throw new Error(\"Failed to load release notes\");\n      }\n\n      const json = await response.json();\n      const data = zGitHubReleaseSchema.parse(json);\n      return data.body ?? \"\";\n    },\n    enabled:\n      open &&\n      stableRelease &&\n      !disableNewReleaseCheck &&\n      Boolean(effectiveChangelogVersion),\n    staleTime: RELEASE_NOTES_STALE_TIME,\n    retry: 1,\n    refetchOnWindowFocus: false,\n  });\n\n  const isLoadingReleaseNotes =\n    releaseNotesQuery.isLoading && !releaseNotesQuery.data;\n\n  const releaseNotesErrorMessage = useMemo(() => {\n    const queryError = releaseNotesQuery.error;\n    if (!queryError) {\n      return null;\n    }\n\n    const errorName =\n      queryError instanceof Error\n        ? queryError.name\n        : typeof (queryError as { name?: unknown })?.name === \"string\"\n          ? String((queryError as { name?: unknown }).name)\n          : undefined;\n\n    if (\n      errorName === \"AbortError\" ||\n      errorName === \"CanceledError\" ||\n      errorName === \"CancelledError\"\n    ) {\n      return null;\n    }\n\n    return t(\"version.unable_to_load_release_notes\");\n  }, [releaseNotesQuery.error, t]);\n\n  useEffect(() => {\n    if (\n      !stableRelease ||\n      !effectiveChangelogVersion ||\n      disableNewReleaseCheck\n    ) {\n      setShouldNotify(false);\n      return;\n    }\n\n    try {\n      const seenVersion = window.localStorage.getItem(LOCAL_STORAGE_KEY);\n      setShouldNotify(seenVersion !== effectiveChangelogVersion);\n    } catch (error) {\n      console.warn(\"Failed to read localStorage:\", error);\n      setShouldNotify(true);\n    }\n  }, [effectiveChangelogVersion, stableRelease, disableNewReleaseCheck]);\n\n  const markReleaseAsSeen = useCallback(() => {\n    if (!effectiveChangelogVersion) return;\n    try {\n      window.localStorage.setItem(LOCAL_STORAGE_KEY, effectiveChangelogVersion);\n    } catch (error) {\n      console.warn(\"Failed to write to localStorage:\", error);\n      // Ignore failures, we still clear the notification for the session\n    }\n    setShouldNotify(false);\n  }, [effectiveChangelogVersion]);\n\n  const handleOpenChange = useCallback(\n    (nextOpen: boolean) => {\n      setOpen((prev) => {\n        if (prev && !nextOpen) {\n          markReleaseAsSeen();\n        }\n        return nextOpen;\n      });\n    },\n    [markReleaseAsSeen],\n  );\n\n  if (!stableRelease || disableNewReleaseCheck) {\n    return (\n      <Link\n        href={releasePageUrl}\n        target=\"_blank\"\n        rel=\"noopener noreferrer\"\n        className=\"mt-auto flex items-center border-t pt-2 text-sm text-gray-400 hover:underline\"\n      >\n        {versionLabel}\n      </Link>\n    );\n  }\n\n  return (\n    <>\n      <div className=\"mt-auto border-t pt-2\">\n        <button\n          type=\"button\"\n          onClick={() => setOpen(true)}\n          aria-label={\n            shouldNotify ? t(\"version.new_release_available\") : undefined\n          }\n          className=\"flex w-full items-center justify-between text-left text-sm text-gray-400 transition hover:text-foreground hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\"\n        >\n          <span aria-hidden={shouldNotify}>{versionLabel}</span>\n          {shouldNotify && (\n            <span className=\"inline-flex items-center gap-2 rounded-full bg-primary/10 px-2 py-1 text-xs font-semibold text-primary\">\n              <span className=\"sr-only\">\n                {t(\"version.new_release_available\")}\n              </span>\n              <span className=\"relative flex size-2\" aria-hidden=\"true\">\n                <span className=\"absolute inline-flex size-full animate-ping rounded-full bg-primary opacity-75\" />\n                <span className=\"relative inline-flex size-2 rounded-full bg-primary\" />\n              </span>\n            </span>\n          )}\n        </button>\n      </div>\n      <Dialog open={open} onOpenChange={handleOpenChange}>\n        <DialogContent className=\"max-w-3xl\">\n          <DialogHeader>\n            <DialogTitle>\n              {t(\"version.whats_new_title\", {\n                version: changelogDisplayVersion,\n              })}\n            </DialogTitle>\n            <DialogDescription>\n              {t(\"version.release_notes_description\")}\n            </DialogDescription>\n          </DialogHeader>\n          <div className=\"max-h-[60vh] overflow-y-auto pr-2\">\n            {isLoadingReleaseNotes ? (\n              <div className=\"flex items-center justify-center gap-2 py-10 text-muted-foreground\">\n                <Loader2 className=\"size-5 animate-spin\" aria-hidden=\"true\" />\n                <span>{t(\"version.loading_release_notes\")}</span>\n              </div>\n            ) : releaseNotesErrorMessage ? (\n              <div className=\"flex items-center gap-2 rounded-md border border-destructive/40 bg-destructive/10 p-3 text-sm text-destructive\">\n                <AlertCircle className=\"size-4\" aria-hidden=\"true\" />\n                <span>{releaseNotesErrorMessage}</span>\n              </div>\n            ) : releaseNotesQuery.data !== undefined ? (\n              releaseNotesQuery.data.trim() ? (\n                <MarkdownReadonly className=\"prose-sm\">\n                  {releaseNotesQuery.data}\n                </MarkdownReadonly>\n              ) : (\n                <p className=\"text-sm text-muted-foreground\">\n                  {t(\"version.no_release_notes\")}\n                </p>\n              )\n            ) : null}\n          </div>\n          <div className=\"flex flex-col gap-2 text-sm text-muted-foreground sm:flex-row sm:items-center sm:justify-between\">\n            <span>{t(\"version.release_notes_synced\")}</span>\n            <Button asChild variant=\"link\" size=\"sm\" className=\"px-0\">\n              <Link\n                href={releasePageUrl}\n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n              >\n                {t(\"version.view_on_github\")}\n              </Link>\n            </Button>\n          </div>\n        </DialogContent>\n      </Dialog>\n    </>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/shared/sidebar/TSidebarItem.ts",
    "content": "export interface TSidebarItem {\n  name: string;\n  icon: React.ReactElement;\n  path: string;\n}\n"
  },
  {
    "path": "apps/web/components/signin/CredentialsForm.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport Link from \"next/link\";\nimport { useRouter, useSearchParams } from \"next/navigation\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport { Alert, AlertTitle } from \"@/components/ui/alert\";\nimport {\n  Form,\n  FormControl,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@/components/ui/form\";\nimport { Input } from \"@/components/ui/input\";\nimport { signIn } from \"@/lib/auth/client\";\nimport { useClientConfig } from \"@/lib/clientConfig\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { AlertCircle, Lock } from \"lucide-react\";\nimport { useForm } from \"react-hook-form\";\nimport { z } from \"zod\";\n\nconst signInSchema = z.object({\n  email: z.string().email(),\n  password: z.string(),\n});\n\nconst SIGNIN_FAILED = \"Incorrect email or password\";\nconst OAUTH_FAILED = \"OAuth login failed: \";\n\nconst VERIFY_EMAIL_ERROR = \"Please verify your email address before signing in\";\n\nexport default function CredentialsForm() {\n  const [signinError, setSigninError] = useState(\"\");\n  const router = useRouter();\n  const searchParams = useSearchParams();\n  const clientConfig = useClientConfig();\n\n  const oAuthError = searchParams.get(\"error\");\n  if (oAuthError && !signinError) {\n    setSigninError(`${OAUTH_FAILED} ${oAuthError}`);\n  }\n\n  const form = useForm<z.infer<typeof signInSchema>>({\n    resolver: zodResolver(signInSchema),\n    defaultValues: {\n      email: \"\",\n      password: \"\",\n    },\n  });\n\n  if (clientConfig.auth.disablePasswordAuth) {\n    return (\n      <div className=\"space-y-4\">\n        {signinError && (\n          <Alert variant=\"destructive\">\n            <AlertCircle className=\"h-4 w-4\" />\n            <AlertTitle>{signinError}</AlertTitle>\n          </Alert>\n        )}\n        <Alert>\n          <Lock className=\"h-4 w-4\" />\n          <AlertTitle>\n            Password authentication is currently disabled.\n          </AlertTitle>\n        </Alert>\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"space-y-6\">\n      <Form {...form}>\n        <form\n          onSubmit={form.handleSubmit(async (value) => {\n            const resp = await signIn(\"credentials\", {\n              redirect: false,\n              email: value.email.trim(),\n              password: value.password,\n            });\n            if (!resp || !resp?.ok || resp.error) {\n              if (resp?.error === \"CredentialsSignin\") {\n                setSigninError(SIGNIN_FAILED);\n              } else if (resp?.error === VERIFY_EMAIL_ERROR) {\n                router.replace(\n                  `/check-email?email=${encodeURIComponent(value.email.trim())}`,\n                );\n              } else {\n                setSigninError(resp?.error ?? SIGNIN_FAILED);\n              }\n              return;\n            }\n            router.replace(\"/\");\n          })}\n          className=\"space-y-4\"\n        >\n          {signinError && (\n            <Alert variant=\"destructive\">\n              <AlertCircle className=\"h-4 w-4\" />\n              <AlertTitle>{signinError}</AlertTitle>\n            </Alert>\n          )}\n\n          <FormField\n            control={form.control}\n            name=\"email\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Email</FormLabel>\n                <FormControl>\n                  <Input\n                    type=\"email\"\n                    placeholder=\"Enter your email\"\n                    {...field}\n                  />\n                </FormControl>\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n\n          <FormField\n            control={form.control}\n            name=\"password\"\n            render={({ field }) => (\n              <FormItem>\n                <FormLabel>Password</FormLabel>\n                <FormControl>\n                  <Input\n                    type=\"password\"\n                    placeholder=\"Enter your password\"\n                    {...field}\n                  />\n                </FormControl>\n                <FormMessage />\n              </FormItem>\n            )}\n          />\n\n          <ActionButton\n            ignoreDemoMode\n            type=\"submit\"\n            loading={form.formState.isSubmitting}\n            className=\"w-full\"\n          >\n            Sign In\n          </ActionButton>\n\n          <div className=\"text-center\">\n            <Link\n              href=\"/forgot-password\"\n              className=\"text-sm text-muted-foreground underline hover:text-primary\"\n            >\n              Forgot your password?\n            </Link>\n          </div>\n        </form>\n      </Form>\n\n      <div className=\"text-center\">\n        <p className=\"text-sm text-gray-600\">\n          Don&apos;t have an account?{\" \"}\n          <Link\n            href=\"/signup\"\n            className=\"font-medium text-blue-600 hover:text-blue-500\"\n          >\n            Sign up\n          </Link>\n        </p>\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/signin/ForgotPasswordForm.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport { Alert, AlertDescription } from \"@/components/ui/alert\";\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\";\nimport {\n  Form,\n  FormControl,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@/components/ui/form\";\nimport { Input } from \"@/components/ui/input\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { useMutation } from \"@tanstack/react-query\";\nimport { TRPCClientError } from \"@trpc/client\";\nimport { AlertCircle, CheckCircle } from \"lucide-react\";\nimport { useForm } from \"react-hook-form\";\nimport { z } from \"zod\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\nconst forgotPasswordSchema = z.object({\n  email: z.string().email(\"Please enter a valid email address\"),\n});\n\nexport default function ForgotPasswordForm() {\n  const api = useTRPC();\n  const [isSubmitted, setIsSubmitted] = useState(false);\n  const [errorMessage, setErrorMessage] = useState(\"\");\n  const router = useRouter();\n\n  const form = useForm<z.infer<typeof forgotPasswordSchema>>({\n    resolver: zodResolver(forgotPasswordSchema),\n  });\n\n  const forgotPasswordMutation = useMutation(\n    api.users.forgotPassword.mutationOptions(),\n  );\n\n  const onSubmit = async (values: z.infer<typeof forgotPasswordSchema>) => {\n    try {\n      setErrorMessage(\"\");\n      await forgotPasswordMutation.mutateAsync(values);\n      setIsSubmitted(true);\n    } catch (error) {\n      if (error instanceof TRPCClientError) {\n        setErrorMessage(error.message);\n      } else {\n        setErrorMessage(\"An unexpected error occurred. Please try again.\");\n      }\n    }\n  };\n\n  return (\n    <Card className=\"w-full\">\n      <CardHeader className=\"text-center\">\n        <CardTitle className=\"text-2xl font-bold\">\n          {isSubmitted ? \"Check your email\" : \"Forgot your password?\"}\n        </CardTitle>\n        <CardDescription>\n          {isSubmitted\n            ? \"If an account with that email exists, we've sent you a password reset link.\"\n            : \"Enter your email address and we'll send you a link to reset your password.\"}\n        </CardDescription>\n      </CardHeader>\n      <CardContent className=\"space-y-6\">\n        {isSubmitted ? (\n          <>\n            <div className=\"flex items-center justify-center\">\n              <CheckCircle className=\"h-12 w-12 text-green-600\" />\n            </div>\n            <Alert>\n              <AlertDescription className=\"text-center\">\n                If an account with that email exists, we&apos;ve sent you a\n                password reset link.\n              </AlertDescription>\n            </Alert>\n            <ActionButton\n              variant=\"outline\"\n              loading={false}\n              onClick={() => router.push(\"/signin\")}\n              className=\"w-full\"\n            >\n              Back to Sign In\n            </ActionButton>\n          </>\n        ) : (\n          <>\n            <Form {...form}>\n              <form\n                onSubmit={form.handleSubmit(onSubmit)}\n                className=\"space-y-4\"\n              >\n                {errorMessage && (\n                  <Alert variant=\"destructive\">\n                    <AlertCircle className=\"h-4 w-4\" />\n                    <AlertDescription>{errorMessage}</AlertDescription>\n                  </Alert>\n                )}\n\n                <FormField\n                  control={form.control}\n                  name=\"email\"\n                  render={({ field }) => (\n                    <FormItem>\n                      <FormLabel>Email</FormLabel>\n                      <FormControl>\n                        <Input\n                          type=\"email\"\n                          placeholder=\"Enter your email address\"\n                          {...field}\n                        />\n                      </FormControl>\n                      <FormMessage />\n                    </FormItem>\n                  )}\n                />\n\n                <ActionButton\n                  type=\"submit\"\n                  loading={form.formState.isSubmitting}\n                  className=\"w-full\"\n                >\n                  Send Reset Link\n                </ActionButton>\n              </form>\n            </Form>\n\n            <div className=\"text-center\">\n              <ActionButton\n                variant=\"ghost\"\n                loading={false}\n                onClick={() => router.push(\"/signin\")}\n                className=\"w-full\"\n              >\n                Back to Sign In\n              </ActionButton>\n            </div>\n          </>\n        )}\n      </CardContent>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/signin/OAuthAutoRedirect.tsx",
    "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { useSearchParams } from \"next/navigation\";\nimport { signIn } from \"@/lib/auth/client\";\nimport { useClientConfig } from \"@/lib/clientConfig\";\n\nexport default function OAuthAutoRedirect({\n  oauthProviderId,\n}: {\n  oauthProviderId: string;\n}) {\n  const clientConfig = useClientConfig();\n  const searchParams = useSearchParams();\n  const hasError = searchParams.has(\"error\");\n  const callbackUrl = searchParams.get(\"callbackUrl\") ?? \"/\";\n\n  const shouldRedirect =\n    clientConfig.auth.oauthAutoRedirect &&\n    clientConfig.auth.disablePasswordAuth &&\n    !!oauthProviderId &&\n    !hasError;\n\n  const [isRedirecting, setIsRedirecting] = useState(shouldRedirect);\n\n  useEffect(() => {\n    if (shouldRedirect) {\n      signIn(oauthProviderId, {\n        callbackUrl,\n      });\n    } else {\n      setIsRedirecting(false);\n    }\n  }, [shouldRedirect, oauthProviderId, callbackUrl]);\n\n  if (isRedirecting) {\n    return (\n      <div className=\"flex justify-center p-8\">\n        <span className=\"text-muted-foreground\">\n          Redirecting to login provider...\n        </span>\n      </div>\n    );\n  }\n  return null;\n}\n"
  },
  {
    "path": "apps/web/components/signin/ResetPasswordForm.tsx",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport { Alert, AlertDescription } from \"@/components/ui/alert\";\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\";\nimport {\n  Form,\n  FormControl,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@/components/ui/form\";\nimport { Input } from \"@/components/ui/input\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { useMutation } from \"@tanstack/react-query\";\nimport { TRPCClientError } from \"@trpc/client\";\nimport { AlertCircle, CheckCircle } from \"lucide-react\";\nimport { useForm } from \"react-hook-form\";\nimport { z } from \"zod\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\nimport { zResetPasswordSchema } from \"@karakeep/shared/types/users\";\n\nconst resetPasswordSchema = z\n  .object({\n    confirmPassword: z.string(),\n  })\n  .merge(zResetPasswordSchema.pick({ newPassword: true }))\n  .refine((data) => data.newPassword === data.confirmPassword, {\n    message: \"Passwords don't match\",\n    path: [\"confirmPassword\"],\n  });\n\ninterface ResetPasswordFormProps {\n  token: string;\n}\n\nexport default function ResetPasswordForm({ token }: ResetPasswordFormProps) {\n  const api = useTRPC();\n  const [isSuccess, setIsSuccess] = useState(false);\n  const [errorMessage, setErrorMessage] = useState(\"\");\n  const router = useRouter();\n\n  const form = useForm<z.infer<typeof resetPasswordSchema>>({\n    resolver: zodResolver(resetPasswordSchema),\n  });\n\n  const resetPasswordMutation = useMutation(\n    api.users.resetPassword.mutationOptions(),\n  );\n\n  const onSubmit = async (values: z.infer<typeof resetPasswordSchema>) => {\n    try {\n      setErrorMessage(\"\");\n      await resetPasswordMutation.mutateAsync({\n        token,\n        newPassword: values.newPassword,\n      });\n      setIsSuccess(true);\n    } catch (error) {\n      if (error instanceof TRPCClientError) {\n        setErrorMessage(error.message);\n      } else {\n        setErrorMessage(\"An unexpected error occurred. Please try again.\");\n      }\n    }\n  };\n\n  return (\n    <Card className=\"w-full\">\n      <CardHeader className=\"text-center\">\n        <CardTitle className=\"text-2xl font-bold\">\n          {isSuccess ? \"Password reset successful\" : \"Reset your password\"}\n        </CardTitle>\n        <CardDescription>\n          {isSuccess\n            ? \"Your password has been successfully reset. You can now sign in with your new password.\"\n            : \"Enter your new password below.\"}\n        </CardDescription>\n      </CardHeader>\n      <CardContent className=\"space-y-6\">\n        {isSuccess ? (\n          <>\n            <div className=\"flex items-center justify-center\">\n              <CheckCircle className=\"h-12 w-12 text-green-600\" />\n            </div>\n            <Alert>\n              <AlertDescription className=\"text-center\">\n                Your password has been successfully reset. You can now sign in\n                with your new password.\n              </AlertDescription>\n            </Alert>\n            <ActionButton\n              loading={false}\n              onClick={() => router.push(\"/signin\")}\n              className=\"w-full\"\n            >\n              Go to Sign In\n            </ActionButton>\n          </>\n        ) : (\n          <>\n            <Form {...form}>\n              <form\n                onSubmit={form.handleSubmit(onSubmit)}\n                className=\"space-y-4\"\n              >\n                {errorMessage && (\n                  <Alert variant=\"destructive\">\n                    <AlertCircle className=\"h-4 w-4\" />\n                    <AlertDescription>{errorMessage}</AlertDescription>\n                  </Alert>\n                )}\n\n                <FormField\n                  control={form.control}\n                  name=\"newPassword\"\n                  render={({ field }) => (\n                    <FormItem>\n                      <FormLabel>New Password</FormLabel>\n                      <FormControl>\n                        <Input\n                          type=\"password\"\n                          placeholder=\"Enter your new password\"\n                          {...field}\n                        />\n                      </FormControl>\n                      <FormMessage />\n                    </FormItem>\n                  )}\n                />\n\n                <FormField\n                  control={form.control}\n                  name=\"confirmPassword\"\n                  render={({ field }) => (\n                    <FormItem>\n                      <FormLabel>Confirm New Password</FormLabel>\n                      <FormControl>\n                        <Input\n                          type=\"password\"\n                          placeholder=\"Confirm your new password\"\n                          {...field}\n                        />\n                      </FormControl>\n                      <FormMessage />\n                    </FormItem>\n                  )}\n                />\n\n                <ActionButton\n                  type=\"submit\"\n                  loading={form.formState.isSubmitting}\n                  className=\"w-full\"\n                >\n                  Reset Password\n                </ActionButton>\n              </form>\n            </Form>\n\n            <div className=\"text-center\">\n              <ActionButton\n                variant=\"ghost\"\n                loading={false}\n                onClick={() => router.push(\"/signin\")}\n                className=\"w-full\"\n              >\n                Back to Sign In\n              </ActionButton>\n            </div>\n          </>\n        )}\n      </CardContent>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/signin/SignInForm.tsx",
    "content": "import { Alert, AlertDescription } from \"@/components/ui/alert\";\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\";\nimport { authOptions } from \"@/server/auth\";\nimport { Info } from \"lucide-react\";\n\nimport serverConfig from \"@karakeep/shared/config\";\n\nimport CredentialsForm from \"./CredentialsForm\";\nimport OAuthAutoRedirect from \"./OAuthAutoRedirect\";\nimport SignInProviderButton from \"./SignInProviderButton\";\n\nexport default async function SignInForm() {\n  const providers = authOptions.providers;\n  let providerValues;\n  if (providers) {\n    providerValues = Object.values(providers).filter(\n      // Credentials are handled manually by the sign in form\n      (p) => p.id != \"credentials\",\n    );\n  }\n\n  return (\n    <div className=\"w-full\">\n      {/* Auto-redirect to OAuth provider if configured */}\n      {providerValues && providerValues.length > 0 && (\n        <OAuthAutoRedirect oauthProviderId={providerValues[0].id} />\n      )}\n      <Card className=\"w-full\">\n        <CardHeader className=\"text-center\">\n          <CardTitle className=\"text-2xl font-bold\">Welcome Back</CardTitle>\n          <CardDescription>Sign in to your Karakeep account</CardDescription>\n        </CardHeader>\n        <CardContent className=\"space-y-6\">\n          {serverConfig.demoMode && (\n            <Alert>\n              <Info className=\"h-4 w-4\" />\n              <AlertDescription>\n                <div className=\"space-y-1\">\n                  <p className=\"font-semibold\">Demo Mode</p>\n                  <p>Email: {serverConfig.demoMode.email}</p>\n                  <p>Password: {serverConfig.demoMode.password}</p>\n                </div>\n              </AlertDescription>\n            </Alert>\n          )}\n\n          <CredentialsForm />\n\n          {providerValues && providerValues.length > 0 && (\n            <>\n              <div className=\"flex w-full items-center\">\n                <div className=\"flex-1 grow border-t border-gray-200\"></div>\n                <span className=\"bg-white px-3 text-sm text-gray-500\">Or</span>\n                <div className=\"flex-1 grow border-t border-gray-200\"></div>\n              </div>\n              <div className=\"space-y-2\">\n                {providerValues.map((provider) => (\n                  <SignInProviderButton\n                    key={provider.id}\n                    provider={{ id: provider.id, name: provider.name }}\n                  />\n                ))}\n              </div>\n            </>\n          )}\n        </CardContent>\n      </Card>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/signin/SignInProviderButton.tsx",
    "content": "\"use client\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { signIn } from \"@/lib/auth/client\";\n\nexport default function SignInProviderButton({\n  provider,\n}: {\n  provider: {\n    id: string;\n    name: string;\n  };\n}) {\n  return (\n    <Button\n      onClick={() =>\n        signIn(provider.id, {\n          callbackUrl: \"/\",\n        })\n      }\n      className=\"w-full\"\n    >\n      Sign in with {provider.name}\n    </Button>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/signup/SignUpForm.tsx",
    "content": "\"use client\";\n\nimport type { TurnstileInstance } from \"@marsidev/react-turnstile\";\nimport { useRef, useState } from \"react\";\nimport Link from \"next/link\";\nimport { useRouter } from \"next/navigation\";\nimport { ActionButton } from \"@/components/ui/action-button\";\nimport { Alert, AlertDescription } from \"@/components/ui/alert\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\";\nimport {\n  Form,\n  FormControl,\n  FormField,\n  FormItem,\n  FormLabel,\n  FormMessage,\n} from \"@/components/ui/form\";\nimport { Input } from \"@/components/ui/input\";\nimport { signIn } from \"@/lib/auth/client\";\nimport { useClientConfig } from \"@/lib/clientConfig\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { Turnstile } from \"@marsidev/react-turnstile\";\nimport { useMutation } from \"@tanstack/react-query\";\nimport { TRPCClientError } from \"@trpc/client\";\nimport { AlertCircle, UserX } from \"lucide-react\";\nimport { useForm } from \"react-hook-form\";\nimport { z } from \"zod\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\nimport { zSignUpSchema } from \"@karakeep/shared/types/users\";\nimport { isMobileAppRedirect } from \"@karakeep/shared/utils/redirectUrl\";\n\nconst VERIFY_EMAIL_ERROR = \"Please verify your email address before signing in\";\n\ninterface SignUpFormProps {\n  redirectUrl: string;\n}\n\nexport default function SignUpForm({ redirectUrl }: SignUpFormProps) {\n  const api = useTRPC();\n  const form = useForm<z.infer<typeof zSignUpSchema>>({\n    resolver: zodResolver(zSignUpSchema),\n    defaultValues: {\n      email: \"\",\n      name: \"\",\n      password: \"\",\n      confirmPassword: \"\",\n      turnstileToken: \"\",\n    },\n  });\n  const [errorMessage, setErrorMessage] = useState(\"\");\n  const router = useRouter();\n  const clientConfig = useClientConfig();\n  const turnstileSiteKey = clientConfig.turnstile?.siteKey;\n  const turnstileRef = useRef<TurnstileInstance>(null);\n\n  const createUserMutation = useMutation(api.users.create.mutationOptions());\n\n  if (\n    clientConfig.auth.disableSignups ||\n    clientConfig.auth.disablePasswordAuth\n  ) {\n    return (\n      <Card className=\"w-full\">\n        <CardHeader className=\"text-center\">\n          <CardTitle className=\"text-2xl font-bold\">\n            Sign Up Unavailable\n          </CardTitle>\n          <CardDescription>\n            Account registration is currently disabled\n          </CardDescription>\n        </CardHeader>\n        <CardContent className=\"space-y-6\">\n          <div className=\"space-y-4\">\n            <Alert>\n              <UserX className=\"h-4 w-4\" />\n              <AlertDescription>\n                Signups are currently disabled. Please contact an administrator\n                for access.\n              </AlertDescription>\n            </Alert>\n            <Button asChild className=\"w-full\">\n              <Link href=\"/signin\">Back to Sign In</Link>\n            </Button>\n          </div>\n        </CardContent>\n      </Card>\n    );\n  }\n\n  return (\n    <Card className=\"w-full\">\n      <CardHeader className=\"text-center\">\n        <CardTitle className=\"text-2xl font-bold\">\n          Create Your Account\n        </CardTitle>\n        <CardDescription>\n          Join Karakeep to start organizing your bookmarks\n        </CardDescription>\n      </CardHeader>\n      <CardContent className=\"space-y-6\">\n        <Form {...form}>\n          <form\n            onSubmit={form.handleSubmit(async (value) => {\n              if (turnstileSiteKey && !value.turnstileToken) {\n                form.setError(\"turnstileToken\", {\n                  type: \"manual\",\n                  message: \"Please complete the verification challenge\",\n                });\n                return;\n              }\n              form.clearErrors(\"turnstileToken\");\n              try {\n                await createUserMutation.mutateAsync({\n                  ...value,\n                  redirectUrl,\n                });\n              } catch (e) {\n                if (e instanceof TRPCClientError) {\n                  setErrorMessage(e.message);\n                }\n                // Reset turnstile widget on error to get a new token\n                if (turnstileSiteKey) {\n                  turnstileRef.current?.reset();\n                  form.setValue(\"turnstileToken\", \"\");\n                }\n                return;\n              }\n              const resp = await signIn(\"credentials\", {\n                redirect: false,\n                email: value.email.trim(),\n                password: value.password,\n              });\n              if (!resp || !resp.ok || resp.error) {\n                if (resp?.error === VERIFY_EMAIL_ERROR) {\n                  router.replace(\n                    `/check-email?email=${encodeURIComponent(value.email.trim())}&redirectUrl=${encodeURIComponent(redirectUrl)}`,\n                  );\n                } else {\n                  setErrorMessage(\n                    resp?.error ?? \"Hit an unexpected error while signing in\",\n                  );\n                }\n                // Reset turnstile widget on error to get a new token\n                if (turnstileSiteKey) {\n                  turnstileRef.current?.reset();\n                  form.setValue(\"turnstileToken\", \"\");\n                }\n                return;\n              }\n              if (isMobileAppRedirect(redirectUrl)) {\n                window.location.href = redirectUrl;\n              } else {\n                router.replace(redirectUrl);\n              }\n            })}\n            className=\"space-y-4\"\n          >\n            {errorMessage && (\n              <Alert variant=\"destructive\">\n                <AlertCircle className=\"h-4 w-4\" />\n                <AlertDescription>{errorMessage}</AlertDescription>\n              </Alert>\n            )}\n\n            <FormField\n              control={form.control}\n              name=\"name\"\n              render={({ field }) => (\n                <FormItem>\n                  <FormLabel>Full Name</FormLabel>\n                  <FormControl>\n                    <Input\n                      type=\"text\"\n                      placeholder=\"Enter your full name\"\n                      {...field}\n                    />\n                  </FormControl>\n                  <FormMessage />\n                </FormItem>\n              )}\n            />\n\n            <FormField\n              control={form.control}\n              name=\"email\"\n              render={({ field }) => (\n                <FormItem>\n                  <FormLabel>Email</FormLabel>\n                  <FormControl>\n                    <Input\n                      type=\"email\"\n                      placeholder=\"Enter your email\"\n                      {...field}\n                    />\n                  </FormControl>\n                  <FormMessage />\n                </FormItem>\n              )}\n            />\n\n            <FormField\n              control={form.control}\n              name=\"password\"\n              render={({ field }) => (\n                <FormItem>\n                  <FormLabel>Password</FormLabel>\n                  <FormControl>\n                    <Input\n                      type=\"password\"\n                      placeholder=\"Create a password\"\n                      {...field}\n                    />\n                  </FormControl>\n                  <FormMessage />\n                </FormItem>\n              )}\n            />\n\n            <FormField\n              control={form.control}\n              name=\"confirmPassword\"\n              render={({ field }) => (\n                <FormItem>\n                  <FormLabel>Confirm Password</FormLabel>\n                  <FormControl>\n                    <Input\n                      type=\"password\"\n                      placeholder=\"Confirm your password\"\n                      {...field}\n                    />\n                  </FormControl>\n                  <FormMessage />\n                </FormItem>\n              )}\n            />\n\n            {turnstileSiteKey && (\n              <FormField\n                control={form.control}\n                name=\"turnstileToken\"\n                render={({ field }) => (\n                  <FormItem>\n                    <FormLabel>Verification</FormLabel>\n                    <FormControl>\n                      <Turnstile\n                        ref={turnstileRef}\n                        siteKey={turnstileSiteKey}\n                        onSuccess={(token) => {\n                          field.onChange(token);\n                          form.clearErrors(\"turnstileToken\");\n                        }}\n                        onExpire={() => field.onChange(\"\")}\n                        onError={() => {\n                          field.onChange(\"\");\n                          form.setError(\"turnstileToken\", {\n                            type: \"manual\",\n                            message:\n                              \"Verification failed, please reload the challenge\",\n                          });\n                        }}\n                      />\n                    </FormControl>\n                    <FormMessage />\n                  </FormItem>\n                )}\n              />\n            )}\n\n            <ActionButton\n              type=\"submit\"\n              loading={\n                form.formState.isSubmitting || createUserMutation.isPending\n              }\n              className=\"w-full\"\n            >\n              Sign up\n            </ActionButton>\n\n            {(clientConfig.legal.termsOfServiceUrl ||\n              clientConfig.legal.privacyPolicyUrl) && (\n              <p className=\"text-center text-xs text-muted-foreground\">\n                By clicking on &apos;Sign up&apos; above, you are agreeing to\n                the{\" \"}\n                {clientConfig.legal.termsOfServiceUrl && (\n                  <Link\n                    href={clientConfig.legal.termsOfServiceUrl}\n                    target=\"_blank\"\n                    rel=\"noopener noreferrer\"\n                    className=\"underline hover:text-foreground\"\n                  >\n                    Terms of Service\n                  </Link>\n                )}\n                {clientConfig.legal.termsOfServiceUrl &&\n                  clientConfig.legal.privacyPolicyUrl &&\n                  \" and \"}\n                {clientConfig.legal.privacyPolicyUrl && (\n                  <Link\n                    href={clientConfig.legal.privacyPolicyUrl}\n                    target=\"_blank\"\n                    rel=\"noopener noreferrer\"\n                    className=\"underline hover:text-foreground\"\n                  >\n                    Privacy Policy\n                  </Link>\n                )}\n                .\n              </p>\n            )}\n          </form>\n        </Form>\n\n        <div className=\"text-center\">\n          <p className=\"text-sm text-gray-600\">\n            Already have an account?{\" \"}\n            <Link\n              href=\"/signin\"\n              className=\"font-medium text-blue-600 hover:text-blue-500\"\n            >\n              Sign in\n            </Link>\n          </p>\n        </div>\n      </CardContent>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/subscription/QuotaProgress.tsx",
    "content": "\"use client\";\n\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { Database, HardDrive } from \"lucide-react\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"../ui/card\";\nimport { Progress } from \"../ui/progress\";\n\nfunction formatBytes(bytes: number): string {\n  if (bytes === 0) return \"0 B\";\n  const k = 1024;\n  const sizes = [\"B\", \"KB\", \"MB\", \"GB\"];\n  const i = Math.floor(Math.log(bytes) / Math.log(k));\n  return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + \" \" + sizes[i];\n}\n\nfunction formatNumber(num: number): string {\n  if (num >= 1000000) {\n    return (num / 1000000).toFixed(1) + \"M\";\n  }\n  if (num >= 1000) {\n    return (num / 1000).toFixed(1) + \"K\";\n  }\n  return num.toString();\n}\n\ninterface QuotaProgressItemProps {\n  title: string;\n  icon: React.ReactNode;\n  used: number;\n  quota: number | null;\n  unlimited: boolean;\n  formatter: (value: number) => string;\n  description: string;\n}\n\nfunction QuotaProgressItem({\n  title,\n  icon,\n  used,\n  quota,\n  unlimited,\n  formatter,\n  description,\n}: QuotaProgressItemProps) {\n  const { t } = useTranslation();\n  const percentage =\n    unlimited || !quota ? 0 : Math.min((used / quota) * 100, 100);\n  const isNearLimit = percentage > 80;\n  const isAtLimit = percentage >= 100;\n\n  return (\n    <div className=\"space-y-3\">\n      <div className=\"flex items-center gap-2\">\n        {icon}\n        <h4 className=\"font-medium\">{title}</h4>\n      </div>\n\n      <div className=\"space-y-2\">\n        <div className=\"flex justify-between text-sm\">\n          <span className=\"text-muted-foreground\">{description}</span>\n          <span className={isAtLimit ? \"font-medium text-destructive\" : \"\"}>\n            {formatter(used)}{\" \"}\n            {unlimited\n              ? \"\"\n              : `/ ${quota !== null && quota !== undefined ? formatter(quota) : \"∞\"}`}\n          </span>\n        </div>\n\n        {!unlimited && quota && (\n          <Progress\n            value={percentage}\n            className={`h-2 ${\n              isAtLimit\n                ? \"[&>div]:bg-destructive\"\n                : isNearLimit\n                  ? \"[&>div]:bg-orange-500\"\n                  : \"\"\n            }`}\n          />\n        )}\n\n        {unlimited && (\n          <div className=\"text-xs text-muted-foreground\">\n            {t(\"settings.subscription.unlimited_usage\")}\n          </div>\n        )}\n\n        {isAtLimit && (\n          <div className=\"text-xs text-destructive\">\n            {t(\"settings.subscription.quota_limit_reached\")}\n          </div>\n        )}\n\n        {isNearLimit && !isAtLimit && (\n          <div className=\"text-xs text-orange-600\">\n            {t(\"settings.subscription.approaching_quota_limit\")}\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n\nexport function QuotaProgress() {\n  const api = useTRPC();\n  const { t } = useTranslation();\n  const { data: quotaUsage, isLoading } = useQuery(\n    api.subscriptions.getQuotaUsage.queryOptions(),\n  );\n\n  if (isLoading) {\n    return (\n      <Card>\n        <CardHeader>\n          <CardTitle>{t(\"settings.subscription.usage_quotas\")}</CardTitle>\n          <CardDescription>\n            {t(\"settings.subscription.loading_usage\")}\n          </CardDescription>\n        </CardHeader>\n        <CardContent>\n          <div className=\"space-y-4\">\n            <div className=\"animate-pulse space-y-2\">\n              <div className=\"h-4 w-1/3 rounded bg-muted\"></div>\n              <div className=\"h-2 rounded bg-muted\"></div>\n            </div>\n            <div className=\"animate-pulse space-y-2\">\n              <div className=\"h-4 w-1/3 rounded bg-muted\"></div>\n              <div className=\"h-2 rounded bg-muted\"></div>\n            </div>\n          </div>\n        </CardContent>\n      </Card>\n    );\n  }\n\n  if (!quotaUsage) {\n    return null;\n  }\n\n  return (\n    <Card>\n      <CardHeader>\n        <CardTitle>{t(\"settings.subscription.usage_quotas\")}</CardTitle>\n        <CardDescription>\n          {t(\"settings.subscription.track_usage\")}\n        </CardDescription>\n      </CardHeader>\n      <CardContent className=\"space-y-6\">\n        <QuotaProgressItem\n          title=\"Bookmarks\"\n          icon={<Database className=\"h-4 w-4\" />}\n          used={quotaUsage.bookmarks.used}\n          quota={quotaUsage.bookmarks.quota}\n          unlimited={quotaUsage.bookmarks.unlimited}\n          formatter={formatNumber}\n          description={t(\"settings.subscription.total_bookmarks_saved\")}\n        />\n\n        <QuotaProgressItem\n          title=\"Storage\"\n          icon={<HardDrive className=\"h-4 w-4\" />}\n          used={quotaUsage.storage.used}\n          quota={quotaUsage.storage.quota}\n          unlimited={quotaUsage.storage.unlimited}\n          formatter={formatBytes}\n          description={t(\"settings.subscription.assets_file_storage\")}\n        />\n      </CardContent>\n    </Card>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/theme-provider.tsx",
    "content": "\"use client\";\n\nimport type { ThemeProviderProps } from \"next-themes\";\nimport * as React from \"react\";\nimport { ThemeProvider as NextThemesProvider, useTheme } from \"next-themes\";\n\nexport function ThemeProvider({ children, ...props }: ThemeProviderProps) {\n  return (\n    <NextThemesProvider scriptProps={{ \"data-cfasync\": \"false\" }} {...props}>\n      {children}\n    </NextThemesProvider>\n  );\n}\n\nexport function useToggleTheme() {\n  const { theme, setTheme } = useTheme();\n  if (theme == \"dark\") {\n    return () => setTheme(\"light\");\n  } else {\n    return () => setTheme(\"dark\");\n  }\n}\n"
  },
  {
    "path": "apps/web/components/ui/action-button.tsx",
    "content": "import React from \"react\";\nimport { useClientConfig } from \"@/lib/clientConfig\";\n\nimport type { ButtonProps } from \"./button\";\nimport { Button } from \"./button\";\nimport LoadingSpinner from \"./spinner\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipPortal,\n  TooltipTrigger,\n} from \"./tooltip\";\n\ninterface ActionButtonProps extends ButtonProps {\n  loading: boolean;\n  spinner?: React.ReactNode;\n  ignoreDemoMode?: boolean;\n}\n\nconst ActionButton = React.forwardRef<HTMLButtonElement, ActionButtonProps>(\n  (\n    { children, loading, spinner, disabled, ignoreDemoMode = false, ...props },\n    ref,\n  ) => {\n    const clientConfig = useClientConfig();\n    spinner ||= <LoadingSpinner />;\n    if (!ignoreDemoMode && clientConfig.demoMode) {\n      disabled = true;\n    } else if (disabled !== undefined) {\n      disabled ||= loading;\n    } else if (loading) {\n      disabled = true;\n    }\n    return (\n      <Button ref={ref} {...props} disabled={disabled}>\n        {loading ? spinner : children}\n      </Button>\n    );\n  },\n);\nActionButton.displayName = \"ActionButton\";\n\nconst ActionButtonWithTooltip = React.forwardRef<\n  HTMLButtonElement,\n  ActionButtonProps & { tooltip: string; delayDuration?: number }\n>(({ tooltip, delayDuration, ...props }, ref) => {\n  return (\n    <Tooltip delayDuration={delayDuration}>\n      <TooltipTrigger asChild>\n        <ActionButton ref={ref} {...props} />\n      </TooltipTrigger>\n      <TooltipPortal>\n        <TooltipContent>{tooltip}</TooltipContent>\n      </TooltipPortal>\n    </Tooltip>\n  );\n});\nActionButtonWithTooltip.displayName = \"ActionButtonWithTooltip\";\n\nexport { ActionButton, ActionButtonWithTooltip };\nexport type { ActionButtonProps };\n"
  },
  {
    "path": "apps/web/components/ui/action-confirming-dialog.tsx",
    "content": "import { useState } from \"react\";\nimport {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@/components/ui/dialog\";\nimport { useTranslation } from \"@/lib/i18n/client\";\n\nimport { Button } from \"./button\";\n\nexport default function ActionConfirmingDialog({\n  title,\n  description,\n  actionButton,\n  children,\n  open: userIsOpen,\n  setOpen: userSetOpen,\n}: {\n  open?: boolean;\n  setOpen?: (v: boolean) => void;\n  title: React.ReactNode;\n  description: React.ReactNode;\n  actionButton: (setDialogOpen: (open: boolean) => void) => React.ReactNode;\n  children?: React.ReactNode;\n}) {\n  const { t } = useTranslation();\n  const [customIsOpen, setCustomIsOpen] = useState(false);\n  const [isDialogOpen, setDialogOpen] = [\n    userIsOpen ?? customIsOpen,\n    userSetOpen ?? setCustomIsOpen,\n  ];\n  return (\n    <Dialog open={isDialogOpen} onOpenChange={setDialogOpen}>\n      {children && <DialogTrigger asChild>{children}</DialogTrigger>}\n      <DialogContent>\n        <DialogHeader>\n          <DialogTitle>{title}</DialogTitle>\n        </DialogHeader>\n        {description}\n        <DialogFooter className=\"sm:justify-end\">\n          <DialogClose asChild>\n            <Button type=\"button\" variant=\"secondary\">\n              {t(\"actions.close\")}\n            </Button>\n          </DialogClose>\n          {actionButton(setDialogOpen)}\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/ui/alert.tsx",
    "content": "import type { VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { cva } from \"class-variance-authority\";\n\nconst alertVariants = cva(\n  \"relative w-full rounded-lg border p-4 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-background text-foreground\",\n        destructive:\n          \"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nconst Alert = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>\n>(({ className, variant, ...props }, ref) => (\n  <div\n    ref={ref}\n    role=\"alert\"\n    className={cn(alertVariants({ variant }), className)}\n    {...props}\n  />\n));\nAlert.displayName = \"Alert\";\n\nconst AlertTitle = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLHeadingElement>\n>(({ className, ...props }, ref) => (\n  // eslint-disable-next-line jsx-a11y/heading-has-content\n  <h5\n    ref={ref}\n    className={cn(\"mb-1 font-medium leading-none tracking-tight\", className)}\n    {...props}\n  />\n));\nAlertTitle.displayName = \"AlertTitle\";\n\nconst AlertDescription = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"text-sm [&_p]:leading-relaxed\", className)}\n    {...props}\n  />\n));\nAlertDescription.displayName = \"AlertDescription\";\n\nexport { Alert, AlertTitle, AlertDescription };\n"
  },
  {
    "path": "apps/web/components/ui/avatar.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport * as AvatarPrimitive from \"@radix-ui/react-avatar\";\n\nconst Avatar = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Root\n    ref={ref}\n    className={cn(\n      \"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full\",\n      className,\n    )}\n    {...props}\n  />\n));\nAvatar.displayName = AvatarPrimitive.Root.displayName;\n\nconst AvatarImage = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Image>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Image\n    ref={ref}\n    className={cn(\"aspect-square h-full w-full\", className)}\n    {...props}\n  />\n));\nAvatarImage.displayName = AvatarPrimitive.Image.displayName;\n\nconst AvatarFallback = React.forwardRef<\n  React.ElementRef<typeof AvatarPrimitive.Fallback>,\n  React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>\n>(({ className, ...props }, ref) => (\n  <AvatarPrimitive.Fallback\n    ref={ref}\n    className={cn(\n      \"flex h-full w-full items-center justify-center rounded-full bg-black text-white\",\n      className,\n    )}\n    {...props}\n  />\n));\nAvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;\n\nexport { Avatar, AvatarImage, AvatarFallback };\n"
  },
  {
    "path": "apps/web/components/ui/back-button.tsx",
    "content": "\"use client\";\n\nimport { useRouter } from \"next/navigation\";\n\nimport type { ButtonProps } from \"./button\";\nimport { Button } from \"./button\";\n\nexport function BackButton({ ...props }: ButtonProps) {\n  const router = useRouter();\n  return <Button {...props} onClick={() => router.back()} />;\n}\n"
  },
  {
    "path": "apps/web/components/ui/badge.tsx",
    "content": "import type { VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { cva } from \"class-variance-authority\";\n\nconst badgeVariants = cva(\n  \"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2\",\n  {\n    variants: {\n      variant: {\n        default:\n          \"border-transparent bg-primary text-primary-foreground hover:bg-primary/80\",\n        secondary:\n          \"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n        destructive:\n          \"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80\",\n        outline: \"text-foreground\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nexport interface BadgeProps\n  extends\n    React.HTMLAttributes<HTMLDivElement>,\n    VariantProps<typeof badgeVariants> {}\n\nfunction Badge({ className, variant, ...props }: BadgeProps) {\n  return (\n    <div className={cn(badgeVariants({ variant }), className)} {...props} />\n  );\n}\n\nexport { Badge, badgeVariants };\n"
  },
  {
    "path": "apps/web/components/ui/button.tsx",
    "content": "import * as React from \"react\";\n\nimport type { ButtonProps } from \"@karakeep/shared-react/components/ui/button\";\nimport { Button } from \"@karakeep/shared-react/components/ui/button\";\n\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipPortal,\n  TooltipTrigger,\n} from \"./tooltip\";\n\nexport {\n  Button,\n  buttonVariants,\n  type ButtonProps,\n} from \"@karakeep/shared-react/components/ui/button\";\n\nconst ButtonWithTooltip = React.forwardRef<\n  HTMLButtonElement,\n  ButtonProps & { tooltip: string; delayDuration?: number }\n>(({ tooltip, delayDuration, ...props }, ref) => {\n  return (\n    <Tooltip delayDuration={delayDuration}>\n      <TooltipTrigger asChild>\n        <Button ref={ref} {...props} />\n      </TooltipTrigger>\n      <TooltipPortal>\n        <TooltipContent>{tooltip}</TooltipContent>\n      </TooltipPortal>\n    </Tooltip>\n  );\n});\nButtonWithTooltip.displayName = \"ButtonWithTooltip\";\n\nexport { ButtonWithTooltip };\n"
  },
  {
    "path": "apps/web/components/ui/calendar.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Button, buttonVariants } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  ChevronDownIcon,\n  ChevronLeftIcon,\n  ChevronRightIcon,\n} from \"lucide-react\";\nimport { DayButton, DayPicker, getDefaultClassNames } from \"react-day-picker\";\n\nfunction Calendar({\n  className,\n  classNames,\n  showOutsideDays = true,\n  captionLayout = \"label\",\n  buttonVariant = \"ghost\",\n  formatters,\n  components,\n  ...props\n}: React.ComponentProps<typeof DayPicker> & {\n  buttonVariant?: React.ComponentProps<typeof Button>[\"variant\"];\n}) {\n  const defaultClassNames = getDefaultClassNames();\n\n  return (\n    <DayPicker\n      showOutsideDays={showOutsideDays}\n      className={cn(\n        \"group/calendar bg-background p-3 [--cell-size:2rem] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent\",\n        String.raw`rtl:**:[.rdp-button\\_next>svg]:rotate-180`,\n        String.raw`rtl:**:[.rdp-button\\_previous>svg]:rotate-180`,\n        className,\n      )}\n      captionLayout={captionLayout}\n      formatters={{\n        formatMonthDropdown: (date) =>\n          date.toLocaleString(\"default\", { month: \"short\" }),\n        ...formatters,\n      }}\n      classNames={{\n        root: cn(\"w-fit\", defaultClassNames.root),\n        months: cn(\n          \"relative flex flex-col gap-4 md:flex-row\",\n          defaultClassNames.months,\n        ),\n        month: cn(\"flex w-full flex-col gap-4\", defaultClassNames.month),\n        nav: cn(\n          \"absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1\",\n          defaultClassNames.nav,\n        ),\n        button_previous: cn(\n          buttonVariants({ variant: buttonVariant }),\n          \"h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50\",\n          defaultClassNames.button_previous,\n        ),\n        button_next: cn(\n          buttonVariants({ variant: buttonVariant }),\n          \"h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50\",\n          defaultClassNames.button_next,\n        ),\n        month_caption: cn(\n          \"flex h-[--cell-size] w-full items-center justify-center px-[--cell-size]\",\n          defaultClassNames.month_caption,\n        ),\n        dropdowns: cn(\n          \"flex h-[--cell-size] w-full items-center justify-center gap-1.5 text-sm font-medium\",\n          defaultClassNames.dropdowns,\n        ),\n        dropdown_root: cn(\n          \"has-focus:border-ring shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative rounded-md border border-input\",\n          defaultClassNames.dropdown_root,\n        ),\n        dropdown: cn(\"absolute inset-0 opacity-0\", defaultClassNames.dropdown),\n        caption_label: cn(\n          \"select-none font-medium\",\n          captionLayout === \"label\"\n            ? \"text-sm\"\n            : \"flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground\",\n          defaultClassNames.caption_label,\n        ),\n        table: \"w-full border-collapse\",\n        weekdays: cn(\"flex\", defaultClassNames.weekdays),\n        weekday: cn(\n          \"flex-1 select-none rounded-md text-[0.8rem] font-normal text-muted-foreground\",\n          defaultClassNames.weekday,\n        ),\n        week: cn(\"mt-2 flex w-full\", defaultClassNames.week),\n        week_number_header: cn(\n          \"w-[--cell-size] select-none\",\n          defaultClassNames.week_number_header,\n        ),\n        week_number: cn(\n          \"select-none text-[0.8rem] text-muted-foreground\",\n          defaultClassNames.week_number,\n        ),\n        day: cn(\n          \"group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md\",\n          defaultClassNames.day,\n        ),\n        range_start: cn(\n          \"rounded-l-md bg-accent\",\n          defaultClassNames.range_start,\n        ),\n        range_middle: cn(\"rounded-none\", defaultClassNames.range_middle),\n        range_end: cn(\"rounded-r-md bg-accent\", defaultClassNames.range_end),\n        today: cn(\n          \"rounded-md bg-accent text-accent-foreground data-[selected=true]:rounded-none\",\n          defaultClassNames.today,\n        ),\n        outside: cn(\n          \"text-muted-foreground aria-selected:text-muted-foreground\",\n          defaultClassNames.outside,\n        ),\n        disabled: cn(\n          \"text-muted-foreground opacity-50\",\n          defaultClassNames.disabled,\n        ),\n        hidden: cn(\"invisible\", defaultClassNames.hidden),\n        ...classNames,\n      }}\n      components={{\n        Root: ({ className, rootRef, ...props }) => {\n          return (\n            <div\n              data-slot=\"calendar\"\n              ref={rootRef}\n              className={cn(className)}\n              {...props}\n            />\n          );\n        },\n        Chevron: ({ className, orientation, ...props }) => {\n          if (orientation === \"left\") {\n            return (\n              <ChevronLeftIcon className={cn(\"size-4\", className)} {...props} />\n            );\n          }\n\n          if (orientation === \"right\") {\n            return (\n              <ChevronRightIcon\n                className={cn(\"size-4\", className)}\n                {...props}\n              />\n            );\n          }\n\n          return (\n            <ChevronDownIcon className={cn(\"size-4\", className)} {...props} />\n          );\n        },\n        DayButton: CalendarDayButton,\n        WeekNumber: ({ children, ...props }) => {\n          return (\n            <td {...props}>\n              <div className=\"flex size-[--cell-size] items-center justify-center text-center\">\n                {children}\n              </div>\n            </td>\n          );\n        },\n        ...components,\n      }}\n      {...props}\n    />\n  );\n}\n\nfunction CalendarDayButton({\n  className,\n  day,\n  modifiers,\n  ...props\n}: React.ComponentProps<typeof DayButton>) {\n  const defaultClassNames = getDefaultClassNames();\n\n  const ref = React.useRef<HTMLButtonElement>(null);\n  React.useEffect(() => {\n    if (modifiers.focused) ref.current?.focus();\n  }, [modifiers.focused]);\n\n  return (\n    <Button\n      ref={ref}\n      variant=\"ghost\"\n      size=\"icon\"\n      data-day={day.date.toLocaleDateString()}\n      data-selected-single={\n        modifiers.selected &&\n        !modifiers.range_start &&\n        !modifiers.range_end &&\n        !modifiers.range_middle\n      }\n      data-range-start={modifiers.range_start}\n      data-range-end={modifiers.range_end}\n      data-range-middle={modifiers.range_middle}\n      className={cn(\n        \"flex aspect-square h-auto w-full min-w-[--cell-size] flex-col gap-1 font-normal leading-none data-[range-end=true]:rounded-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-end=true]:bg-primary data-[range-middle=true]:bg-accent data-[range-start=true]:bg-primary data-[selected-single=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:text-accent-foreground data-[range-start=true]:text-primary-foreground data-[selected-single=true]:text-primary-foreground group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring/50 [&>span]:text-xs [&>span]:opacity-70\",\n        defaultClassNames.day,\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport { Calendar, CalendarDayButton };\n"
  },
  {
    "path": "apps/web/components/ui/card.tsx",
    "content": "import * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst Card = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\n      \"rounded-lg border bg-card text-card-foreground shadow-sm\",\n      className,\n    )}\n    {...props}\n  />\n));\nCard.displayName = \"Card\";\n\nconst CardHeader = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"flex flex-col space-y-1.5 p-6\", className)}\n    {...props}\n  />\n));\nCardHeader.displayName = \"CardHeader\";\n\nconst CardTitle = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLHeadingElement>\n>(({ className, ...props }, ref) => (\n  // eslint-disable-next-line jsx-a11y/heading-has-content\n  <h3\n    ref={ref}\n    className={cn(\n      \"text-2xl font-semibold leading-none tracking-tight\",\n      className,\n    )}\n    {...props}\n  />\n));\nCardTitle.displayName = \"CardTitle\";\n\nconst CardDescription = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => (\n  <p\n    ref={ref}\n    className={cn(\"text-sm text-muted-foreground\", className)}\n    {...props}\n  />\n));\nCardDescription.displayName = \"CardDescription\";\n\nconst CardContent = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div ref={ref} className={cn(\"p-6 pt-0\", className)} {...props} />\n));\nCardContent.displayName = \"CardContent\";\n\nconst CardFooter = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => (\n  <div\n    ref={ref}\n    className={cn(\"flex items-center p-6 pt-0\", className)}\n    {...props}\n  />\n));\nCardFooter.displayName = \"CardFooter\";\n\nexport {\n  Card,\n  CardHeader,\n  CardFooter,\n  CardTitle,\n  CardDescription,\n  CardContent,\n};\n"
  },
  {
    "path": "apps/web/components/ui/collapsible.tsx",
    "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport * as CollapsiblePrimitive from \"@radix-ui/react-collapsible\";\nimport { ChevronRight, Triangle } from \"lucide-react\";\n\nconst Collapsible = CollapsiblePrimitive.Root;\n\nconst CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;\n\nconst CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;\n\nfunction CollapsibleTriggerTriangle({\n  open,\n  className,\n}: {\n  open: boolean;\n  className?: string;\n}) {\n  return (\n    <CollapsibleTrigger asChild>\n      <Triangle\n        className={cn(\n          \"fill-foreground\",\n          !open ? \"rotate-90\" : \"rotate-180\",\n          className,\n        )}\n      />\n    </CollapsibleTrigger>\n  );\n}\n\nfunction CollapsibleTriggerChevron({\n  open,\n  className,\n}: {\n  open: boolean;\n  className?: string;\n}) {\n  return (\n    <CollapsibleTrigger asChild>\n      <ChevronRight\n        className={cn(!open ? \"rotate-0\" : \"rotate-90\", className)}\n      />\n    </CollapsibleTrigger>\n  );\n}\n\nexport {\n  Collapsible,\n  CollapsibleTrigger,\n  CollapsibleContent,\n  CollapsibleTriggerTriangle,\n  CollapsibleTriggerChevron,\n};\n"
  },
  {
    "path": "apps/web/components/ui/command.tsx",
    "content": "\"use client\";\n\nimport type { DialogProps } from \"@radix-ui/react-dialog\";\nimport * as React from \"react\";\nimport { Dialog, DialogContent } from \"@/components/ui/dialog\";\nimport { cn } from \"@/lib/utils\";\nimport { Command as CommandPrimitive } from \"cmdk\";\nimport { Search } from \"lucide-react\";\n\nconst Command = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive\n    ref={ref}\n    className={cn(\n      \"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground\",\n      className,\n    )}\n    {...props}\n  />\n));\nCommand.displayName = CommandPrimitive.displayName;\n\nconst CommandDialog = ({ children, ...props }: DialogProps) => {\n  return (\n    <Dialog {...props}>\n      <DialogContent className=\"overflow-hidden p-0 shadow-lg\">\n        <Command className=\"[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5\">\n          {children}\n        </Command>\n      </DialogContent>\n    </Dialog>\n  );\n};\n\nconst CommandInput = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Input>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>\n>(({ className, ...props }, ref) => (\n  // https://github.com/shadcn-ui/ui/issues/3366\n  // eslint-disable-next-line react/no-unknown-property\n  <div className=\"flex items-center border-b px-3\" cmdk-input-wrapper=\"\">\n    <Search className=\"mr-2 h-4 w-4 shrink-0 opacity-50\" />\n    <CommandPrimitive.Input\n      ref={ref}\n      className={cn(\n        \"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50\",\n        className,\n      )}\n      {...props}\n    />\n  </div>\n));\n\nCommandInput.displayName = CommandPrimitive.Input.displayName;\n\nconst CommandList = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.List>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive.List\n    ref={ref}\n    className={cn(\"max-h-[300px] overflow-y-auto overflow-x-hidden\", className)}\n    {...props}\n  />\n));\n\nCommandList.displayName = CommandPrimitive.List.displayName;\n\nconst CommandEmpty = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Empty>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>\n>((props, ref) => (\n  <CommandPrimitive.Empty\n    ref={ref}\n    className=\"py-6 text-center text-sm\"\n    {...props}\n  />\n));\n\nCommandEmpty.displayName = CommandPrimitive.Empty.displayName;\n\nconst CommandGroup = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Group>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive.Group\n    ref={ref}\n    className={cn(\n      \"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground\",\n      className,\n    )}\n    {...props}\n  />\n));\n\nCommandGroup.displayName = CommandPrimitive.Group.displayName;\n\nconst CommandSeparator = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive.Separator\n    ref={ref}\n    className={cn(\"-mx-1 h-px bg-border\", className)}\n    {...props}\n  />\n));\nCommandSeparator.displayName = CommandPrimitive.Separator.displayName;\n\nconst CommandItem = React.forwardRef<\n  React.ElementRef<typeof CommandPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>\n>(({ className, ...props }, ref) => (\n  <CommandPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0\",\n      className,\n    )}\n    {...props}\n  />\n));\n\nCommandItem.displayName = CommandPrimitive.Item.displayName;\n\nconst CommandShortcut = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLSpanElement>) => {\n  return (\n    <span\n      className={cn(\n        \"ml-auto text-xs tracking-widest text-muted-foreground\",\n        className,\n      )}\n      {...props}\n    />\n  );\n};\nCommandShortcut.displayName = \"CommandShortcut\";\n\nexport {\n  Command,\n  CommandDialog,\n  CommandInput,\n  CommandList,\n  CommandEmpty,\n  CommandGroup,\n  CommandItem,\n  CommandShortcut,\n  CommandSeparator,\n};\n"
  },
  {
    "path": "apps/web/components/ui/copy-button.tsx",
    "content": "import React, { useEffect, useState } from \"react\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { cn } from \"@/lib/utils\";\nimport { Check, Copy } from \"lucide-react\";\n\nimport { Button } from \"./button\";\n\nexport default function CopyBtn({\n  className,\n  getStringToCopy,\n}: {\n  className?: string;\n  getStringToCopy: () => string;\n}) {\n  const [copyOk, setCopyOk] = React.useState(false);\n  const [disabled, setDisabled] = React.useState(false);\n  useEffect(() => {\n    if (!navigator || !navigator.clipboard) {\n      setDisabled(true);\n    }\n  });\n\n  const handleClick = async () => {\n    await navigator.clipboard.writeText(getStringToCopy());\n    setCopyOk(true);\n    setTimeout(() => {\n      setCopyOk(false);\n    }, 2000);\n  };\n\n  return (\n    <button\n      className={className}\n      onClick={handleClick}\n      disabled={disabled}\n      title={disabled ? \"Copying is only available over https\" : undefined}\n    >\n      {copyOk ? <Check /> : <Copy />}\n    </button>\n  );\n}\n\nexport function CopyBtnV2({\n  className,\n  getStringToCopy,\n}: {\n  className?: string;\n  getStringToCopy: () => string;\n}) {\n  const [copied, setCopied] = useState(false);\n\n  const handleCopy = async (url: string) => {\n    try {\n      await navigator.clipboard.writeText(url);\n      setCopied(true);\n      setTimeout(() => setCopied(false), 2000);\n    } catch {\n      toast({\n        description:\n          \"Failed to copy link. Browsers only support copying to the clipboard from https pages.\",\n        variant: \"destructive\",\n      });\n    }\n  };\n\n  return (\n    <Button\n      size=\"sm\"\n      variant=\"outline\"\n      onClick={() => handleCopy(getStringToCopy())}\n      className={cn(\"shrink-0\", className)}\n    >\n      {copied ? <Check className=\"h-4 w-4\" /> : <Copy className=\"h-4 w-4\" />}\n    </Button>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/ui/dialog.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport { X } from \"lucide-react\";\n\nconst Dialog = DialogPrimitive.Root;\n\nconst DialogTrigger = DialogPrimitive.Trigger;\n\nconst DialogPortal = DialogPrimitive.Portal;\n\nconst DialogClose = DialogPrimitive.Close;\n\nconst DialogOverlay = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Overlay>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Overlay\n    ref={ref}\n    className={cn(\n      \"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0\",\n      className,\n    )}\n    {...props}\n  />\n));\nDialogOverlay.displayName = DialogPrimitive.Overlay.displayName;\n\nconst DialogContent = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {\n    hideCloseBtn?: boolean;\n  }\n>(({ className, children, hideCloseBtn = false, ...props }, ref) => (\n  <DialogPortal>\n    <DialogOverlay />\n    <DialogPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      {!hideCloseBtn && (\n        <DialogPrimitive.Close className=\"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground\">\n          <X className=\"size-4\" />\n          <span className=\"sr-only\">Close</span>\n        </DialogPrimitive.Close>\n      )}\n    </DialogPrimitive.Content>\n  </DialogPortal>\n));\nDialogContent.displayName = DialogPrimitive.Content.displayName;\n\nconst DialogHeader = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col space-y-1.5 text-center sm:text-left\",\n      className,\n    )}\n    {...props}\n  />\n);\nDialogHeader.displayName = \"DialogHeader\";\n\nconst DialogFooter = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) => (\n  <div\n    className={cn(\n      \"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2\",\n      className,\n    )}\n    {...props}\n  />\n);\nDialogFooter.displayName = \"DialogFooter\";\n\nconst DialogTitle = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Title>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Title\n    ref={ref}\n    className={cn(\n      \"text-lg font-semibold leading-none tracking-tight\",\n      className,\n    )}\n    {...props}\n  />\n));\nDialogTitle.displayName = DialogPrimitive.Title.displayName;\n\nconst DialogDescription = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Description\n    ref={ref}\n    className={cn(\"text-sm text-muted-foreground\", className)}\n    {...props}\n  />\n));\nDialogDescription.displayName = DialogPrimitive.Description.displayName;\n\nexport {\n  Dialog,\n  DialogPortal,\n  DialogOverlay,\n  DialogClose,\n  DialogTrigger,\n  DialogContent,\n  DialogHeader,\n  DialogFooter,\n  DialogTitle,\n  DialogDescription,\n};\n"
  },
  {
    "path": "apps/web/components/ui/dropdown-menu.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\";\nimport { Check, ChevronRight, Circle } from \"lucide-react\";\n\nconst DropdownMenu = DropdownMenuPrimitive.Root;\n\nconst DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;\n\nconst DropdownMenuGroup = DropdownMenuPrimitive.Group;\n\nconst DropdownMenuPortal = DropdownMenuPrimitive.Portal;\n\nconst DropdownMenuSub = DropdownMenuPrimitive.Sub;\n\nconst DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;\n\nconst DropdownMenuSubTrigger = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {\n    inset?: boolean;\n  }\n>(({ className, inset, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubTrigger\n    ref={ref}\n    className={cn(\n      \"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent\",\n      inset && \"pl-8\",\n      className,\n    )}\n    {...props}\n  >\n    {children}\n    <ChevronRight className=\"ml-auto size-4\" />\n  </DropdownMenuPrimitive.SubTrigger>\n));\nDropdownMenuSubTrigger.displayName =\n  DropdownMenuPrimitive.SubTrigger.displayName;\n\nconst DropdownMenuSubContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.SubContent\n    ref={ref}\n    className={cn(\n      \"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n      className,\n    )}\n    {...props}\n  />\n));\nDropdownMenuSubContent.displayName =\n  DropdownMenuPrimitive.SubContent.displayName;\n\nconst DropdownMenuContent = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n  <DropdownMenuPrimitive.Portal>\n    <DropdownMenuPrimitive.Content\n      ref={ref}\n      sideOffset={sideOffset}\n      className={cn(\n        \"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        className,\n      )}\n      {...props}\n    />\n  </DropdownMenuPrimitive.Portal>\n));\nDropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;\n\nconst DropdownMenuItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {\n    inset?: boolean;\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      inset && \"pl-8\",\n      className,\n    )}\n    {...props}\n  />\n));\nDropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;\n\nconst DropdownMenuCheckboxItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>\n>(({ className, children, checked, ...props }, ref) => (\n  <DropdownMenuPrimitive.CheckboxItem\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className,\n    )}\n    checked={checked}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex size-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <Check className=\"size-4\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.CheckboxItem>\n));\nDropdownMenuCheckboxItem.displayName =\n  DropdownMenuPrimitive.CheckboxItem.displayName;\n\nconst DropdownMenuRadioItem = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>\n>(({ className, children, ...props }, ref) => (\n  <DropdownMenuPrimitive.RadioItem\n    ref={ref}\n    className={cn(\n      \"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className,\n    )}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex size-3.5 items-center justify-center\">\n      <DropdownMenuPrimitive.ItemIndicator>\n        <Circle className=\"size-2 fill-current\" />\n      </DropdownMenuPrimitive.ItemIndicator>\n    </span>\n    {children}\n  </DropdownMenuPrimitive.RadioItem>\n));\nDropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;\n\nconst DropdownMenuLabel = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {\n    inset?: boolean;\n  }\n>(({ className, inset, ...props }, ref) => (\n  <DropdownMenuPrimitive.Label\n    ref={ref}\n    className={cn(\n      \"px-2 py-1.5 text-sm font-semibold\",\n      inset && \"pl-8\",\n      className,\n    )}\n    {...props}\n  />\n));\nDropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;\n\nconst DropdownMenuSeparator = React.forwardRef<\n  React.ElementRef<typeof DropdownMenuPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <DropdownMenuPrimitive.Separator\n    ref={ref}\n    className={cn(\"-mx-1 my-1 h-px bg-muted\", className)}\n    {...props}\n  />\n));\nDropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;\n\nconst DropdownMenuShortcut = ({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLSpanElement>) => {\n  return (\n    <span\n      className={cn(\"ml-auto text-xs tracking-widest opacity-60\", className)}\n      {...props}\n    />\n  );\n};\nDropdownMenuShortcut.displayName = \"DropdownMenuShortcut\";\n\nexport {\n  DropdownMenu,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuCheckboxItem,\n  DropdownMenuRadioItem,\n  DropdownMenuLabel,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuGroup,\n  DropdownMenuPortal,\n  DropdownMenuSub,\n  DropdownMenuSubContent,\n  DropdownMenuSubTrigger,\n  DropdownMenuRadioGroup,\n};\n"
  },
  {
    "path": "apps/web/components/ui/field.tsx",
    "content": "\"use client\";\n\nimport type { VariantProps } from \"class-variance-authority\";\nimport { useMemo } from \"react\";\nimport { Label } from \"@/components/ui/label\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { cn } from \"@/lib/utils\";\nimport { cva } from \"class-variance-authority\";\n\nfunction FieldSet({ className, ...props }: React.ComponentProps<\"fieldset\">) {\n  return (\n    <fieldset\n      data-slot=\"field-set\"\n      className={cn(\n        \"flex flex-col gap-6\",\n        \"has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction FieldLegend({\n  className,\n  variant = \"legend\",\n  ...props\n}: React.ComponentProps<\"legend\"> & { variant?: \"legend\" | \"label\" }) {\n  return (\n    <legend\n      data-slot=\"field-legend\"\n      data-variant={variant}\n      className={cn(\n        \"mb-3 font-medium\",\n        \"data-[variant=legend]:text-base\",\n        \"data-[variant=label]:text-sm\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction FieldGroup({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"field-group\"\n      className={cn(\n        \"group/field-group @container/field-group flex w-full flex-col gap-7 data-[slot=checkbox-group]:gap-3 [&>[data-slot=field-group]]:gap-4\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nconst fieldVariants = cva(\n  \"group/field flex w-full gap-3 data-[invalid=true]:text-destructive\",\n  {\n    variants: {\n      orientation: {\n        vertical: [\"flex-col [&>*]:w-full [&>.sr-only]:w-auto\"],\n        horizontal: [\n          \"flex-row items-center\",\n          \"[&>[data-slot=field-label]]:flex-auto\",\n          \"has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px has-[>[data-slot=field-content]]:items-start\",\n        ],\n        responsive: [\n          \"@md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto flex-col [&>*]:w-full [&>.sr-only]:w-auto\",\n          \"@md/field-group:[&>[data-slot=field-label]]:flex-auto\",\n          \"@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px\",\n        ],\n      },\n    },\n    defaultVariants: {\n      orientation: \"vertical\",\n    },\n  },\n);\n\nfunction Field({\n  className,\n  orientation = \"vertical\",\n  ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof fieldVariants>) {\n  return (\n    <div\n      role=\"group\"\n      data-slot=\"field\"\n      data-orientation={orientation}\n      className={cn(fieldVariants({ orientation }), className)}\n      {...props}\n    />\n  );\n}\n\nfunction FieldContent({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"field-content\"\n      className={cn(\n        \"group/field-content flex flex-1 flex-col gap-1.5 leading-snug\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction FieldLabel({\n  className,\n  ...props\n}: React.ComponentProps<typeof Label>) {\n  return (\n    <Label\n      data-slot=\"field-label\"\n      className={cn(\n        \"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50\",\n        \"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col has-[>[data-slot=field]]:rounded-md has-[>[data-slot=field]]:border [&>[data-slot=field]]:p-4\",\n        \"has-data-[state=checked]:bg-primary/5 has-data-[state=checked]:border-primary dark:has-data-[state=checked]:bg-primary/10\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction FieldTitle({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"field-label\"\n      className={cn(\n        \"flex w-fit items-center gap-2 text-sm font-medium leading-snug group-data-[disabled=true]/field:opacity-50\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction FieldDescription({ className, ...props }: React.ComponentProps<\"p\">) {\n  return (\n    <p\n      data-slot=\"field-description\"\n      className={cn(\n        \"text-sm font-normal leading-normal text-muted-foreground group-has-[[data-orientation=horizontal]]/field:text-balance\",\n        \"nth-last-2:-mt-1 last:mt-0 [[data-variant=legend]+&]:-mt-1.5\",\n        \"[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction FieldSeparator({\n  children,\n  className,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  children?: React.ReactNode;\n}) {\n  return (\n    <div\n      data-slot=\"field-separator\"\n      data-content={!!children}\n      className={cn(\n        \"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2\",\n        className,\n      )}\n      {...props}\n    >\n      <Separator className=\"absolute inset-0 top-1/2\" />\n      {children && (\n        <span\n          className=\"relative mx-auto block w-fit bg-background px-2 text-muted-foreground\"\n          data-slot=\"field-separator-content\"\n        >\n          {children}\n        </span>\n      )}\n    </div>\n  );\n}\n\nfunction FieldError({\n  className,\n  children,\n  errors,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  errors?: ({ message?: string } | undefined)[];\n}) {\n  const content = useMemo(() => {\n    if (children) {\n      return children;\n    }\n\n    if (!errors) {\n      return null;\n    }\n\n    if (errors?.length === 1 && errors[0]?.message) {\n      return errors[0].message;\n    }\n\n    return (\n      <ul className=\"ml-4 flex list-disc flex-col gap-1\">\n        {errors.map(\n          (error, index) =>\n            error?.message && <li key={index}>{error.message}</li>,\n        )}\n      </ul>\n    );\n  }, [children, errors]);\n\n  if (!content) {\n    return null;\n  }\n\n  return (\n    <div\n      role=\"alert\"\n      data-slot=\"field-error\"\n      className={cn(\"text-sm font-normal text-destructive\", className)}\n      {...props}\n    >\n      {content}\n    </div>\n  );\n}\n\nexport {\n  Field,\n  FieldLabel,\n  FieldDescription,\n  FieldError,\n  FieldGroup,\n  FieldLegend,\n  FieldSeparator,\n  FieldSet,\n  FieldContent,\n  FieldTitle,\n};\n"
  },
  {
    "path": "apps/web/components/ui/file-picker-button.tsx",
    "content": "import React, { ChangeEvent, useRef } from \"react\";\n\nimport { ActionButton, ActionButtonProps } from \"./action-button\";\n\ninterface FilePickerButtonProps extends Omit<ActionButtonProps, \"onClick\"> {\n  onFileSelect?: (file: File) => void;\n  accept?: string;\n  multiple?: boolean;\n}\n\nconst FilePickerButton: React.FC<FilePickerButtonProps> = ({\n  onFileSelect,\n  accept,\n  multiple = false,\n  ...buttonProps\n}) => {\n  const fileInputRef = useRef<HTMLInputElement>(null);\n\n  const handleButtonClick = () => {\n    fileInputRef.current?.click();\n  };\n\n  const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {\n    const files = event.target.files;\n    if (files && files.length > 0) {\n      if (onFileSelect) {\n        if (multiple) {\n          Array.from(files).forEach(onFileSelect);\n        } else {\n          onFileSelect(files[0]);\n        }\n      }\n    }\n  };\n\n  return (\n    <div>\n      <ActionButton onClick={handleButtonClick} {...buttonProps} />\n      <input\n        type=\"file\"\n        ref={fileInputRef}\n        onChange={handleFileChange}\n        className=\"hidden\"\n        accept={accept}\n        multiple={multiple}\n      />\n    </div>\n  );\n};\n\nexport default FilePickerButton;\n"
  },
  {
    "path": "apps/web/components/ui/form.tsx",
    "content": "import type * as LabelPrimitive from \"@radix-ui/react-label\";\nimport type { ControllerProps, FieldPath, FieldValues } from \"react-hook-form\";\nimport * as React from \"react\";\nimport { Label } from \"@/components/ui/label\";\nimport { cn } from \"@/lib/utils\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { Controller, FormProvider, useFormContext } from \"react-hook-form\";\n\nconst Form = FormProvider;\n\ninterface FormFieldContextValue<\n  TFieldValues extends FieldValues = FieldValues,\n  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n> {\n  name: TName;\n}\n\nconst FormFieldContext = React.createContext<FormFieldContextValue>(\n  {} as FormFieldContextValue,\n);\n\nconst FormField = <\n  TFieldValues extends FieldValues = FieldValues,\n  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,\n>({\n  ...props\n}: ControllerProps<TFieldValues, TName>) => {\n  return (\n    <FormFieldContext.Provider value={{ name: props.name }}>\n      <Controller {...props} />\n    </FormFieldContext.Provider>\n  );\n};\n\nconst useFormField = () => {\n  const fieldContext = React.useContext(FormFieldContext);\n  const itemContext = React.useContext(FormItemContext);\n  const { getFieldState, formState } = useFormContext();\n\n  const fieldState = getFieldState(fieldContext.name, formState);\n\n  if (!fieldContext) {\n    throw new Error(\"useFormField should be used within <FormField>\");\n  }\n\n  const { id } = itemContext;\n\n  return {\n    id,\n    name: fieldContext.name,\n    formItemId: `${id}-form-item`,\n    formDescriptionId: `${id}-form-item-description`,\n    formMessageId: `${id}-form-item-message`,\n    ...fieldState,\n  };\n};\n\ninterface FormItemContextValue {\n  id: string;\n}\n\nconst FormItemContext = React.createContext<FormItemContextValue>(\n  {} as FormItemContextValue,\n);\n\nconst FormItem = React.forwardRef<\n  HTMLDivElement,\n  React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => {\n  const id = React.useId();\n\n  return (\n    <FormItemContext.Provider value={{ id }}>\n      <div ref={ref} className={cn(\"space-y-2\", className)} {...props} />\n    </FormItemContext.Provider>\n  );\n});\nFormItem.displayName = \"FormItem\";\n\nconst FormLabel = React.forwardRef<\n  React.ElementRef<typeof LabelPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>\n>(({ className, ...props }, ref) => {\n  const { error, formItemId } = useFormField();\n\n  return (\n    <Label\n      ref={ref}\n      className={cn(error && \"text-destructive\", className)}\n      htmlFor={formItemId}\n      {...props}\n    />\n  );\n});\nFormLabel.displayName = \"FormLabel\";\n\nconst FormControl = React.forwardRef<\n  React.ElementRef<typeof Slot>,\n  React.ComponentPropsWithoutRef<typeof Slot>\n>(({ ...props }, ref) => {\n  const { error, formItemId, formDescriptionId, formMessageId } =\n    useFormField();\n\n  return (\n    <Slot\n      ref={ref}\n      id={formItemId}\n      aria-describedby={\n        !error\n          ? `${formDescriptionId}`\n          : `${formDescriptionId} ${formMessageId}`\n      }\n      aria-invalid={!!error}\n      {...props}\n    />\n  );\n});\nFormControl.displayName = \"FormControl\";\n\nconst FormDescription = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => {\n  const { formDescriptionId } = useFormField();\n\n  return (\n    <p\n      ref={ref}\n      id={formDescriptionId}\n      className={cn(\"text-sm text-muted-foreground\", className)}\n      {...props}\n    />\n  );\n});\nFormDescription.displayName = \"FormDescription\";\n\nconst FormMessage = React.forwardRef<\n  HTMLParagraphElement,\n  React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, children, ...props }, ref) => {\n  const { error, formMessageId } = useFormField();\n  const body = error ? String(error?.message) : children;\n\n  if (!body) {\n    return null;\n  }\n\n  return (\n    <p\n      ref={ref}\n      id={formMessageId}\n      className={cn(\"text-sm font-medium text-destructive\", className)}\n      {...props}\n    >\n      {body}\n    </p>\n  );\n});\nFormMessage.displayName = \"FormMessage\";\n\nexport {\n  useFormField,\n  Form,\n  FormItem,\n  FormLabel,\n  FormControl,\n  FormDescription,\n  FormMessage,\n  FormField,\n};\n"
  },
  {
    "path": "apps/web/components/ui/full-page-spinner.tsx",
    "content": "import Spinner from \"./spinner\";\n\nexport function FullPageSpinner() {\n  return (\n    <div className=\"flex size-full\">\n      <div className=\"m-auto\">\n        <Spinner />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/ui/info-tooltip.tsx",
    "content": "import {\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\nimport { HelpCircle, Info } from \"lucide-react\";\n\nexport default function InfoTooltip({\n  className,\n  children,\n  size,\n  variant = \"tip\",\n}: {\n  className?: string;\n  size?: number;\n  children?: React.ReactNode;\n  variant?: \"tip\" | \"explain\";\n}) {\n  return (\n    <Tooltip>\n      <TooltipTrigger asChild>\n        {variant === \"tip\" ? (\n          <Info\n            className={cn(\"z-10 cursor-pointer text-[#494949]\", className)}\n            size={size}\n          />\n        ) : (\n          <HelpCircle className={cn(\"cursor-pointer\", className)} size={size} />\n        )}\n      </TooltipTrigger>\n      <TooltipContent>{children}</TooltipContent>\n    </Tooltip>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/ui/input.tsx",
    "content": "import * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {\n  startIcon?: React.ReactNode;\n  endIcon?: React.ReactNode;\n}\n\nconst Input = React.forwardRef<HTMLInputElement, InputProps>(\n  ({ className, type, startIcon, endIcon, ...props }, ref) => {\n    return (\n      <div className=\"relative w-full\">\n        {startIcon && (\n          <div className=\"absolute left-2 top-1/2 -translate-y-1/2 transform\">\n            {startIcon}\n          </div>\n        )}\n        <input\n          type={type}\n          className={cn(\n            \"flex h-10 w-full rounded-md border border-input bg-background px-4 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50\",\n            startIcon ? \"pl-8\" : \"\",\n            endIcon ? \"pr-8\" : \"\",\n            className,\n          )}\n          ref={ref}\n          {...props}\n        />\n        {endIcon && (\n          <div className=\"absolute right-3 top-1/2 -translate-y-1/2 transform\">\n            {endIcon}\n          </div>\n        )}\n      </div>\n    );\n  },\n);\nInput.displayName = \"Input\";\n\nexport { Input };\n"
  },
  {
    "path": "apps/web/components/ui/kbd.tsx",
    "content": "import { cn } from \"@/lib/utils\";\n\nfunction Kbd({ className, ...props }: React.ComponentProps<\"kbd\">) {\n  return (\n    <kbd\n      data-slot=\"kbd\"\n      className={cn(\n        \"pointer-events-none inline-flex h-5 w-fit min-w-5 select-none items-center justify-center gap-1 rounded-sm bg-muted px-1 font-sans text-xs font-medium text-muted-foreground\",\n        \"[&_svg:not([class*='size-'])]:size-3\",\n        \"[[data-slot=tooltip-content]_&]:bg-background/20 [[data-slot=tooltip-content]_&]:text-background dark:[[data-slot=tooltip-content]_&]:bg-background/10\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction KbdGroup({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <kbd\n      data-slot=\"kbd-group\"\n      className={cn(\"inline-flex items-center gap-1\", className)}\n      {...props}\n    />\n  );\n}\n\nexport { Kbd, KbdGroup };\n"
  },
  {
    "path": "apps/web/components/ui/label.tsx",
    "content": "\"use client\";\n\nimport type { VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport * as LabelPrimitive from \"@radix-ui/react-label\";\nimport { cva } from \"class-variance-authority\";\n\nconst labelVariants = cva(\n  \"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\",\n);\n\nconst Label = React.forwardRef<\n  React.ElementRef<typeof LabelPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &\n    VariantProps<typeof labelVariants>\n>(({ className, ...props }, ref) => (\n  <LabelPrimitive.Root\n    ref={ref}\n    className={cn(labelVariants(), className)}\n    {...props}\n  />\n));\nLabel.displayName = LabelPrimitive.Root.displayName;\n\nexport { Label };\n"
  },
  {
    "path": "apps/web/components/ui/markdown/markdown-editor.tsx",
    "content": "import { memo, useMemo, useState } from \"react\";\nimport ToolbarPlugin from \"@/components/ui/markdown/plugins/toolbar-plugin\";\nimport { MarkdownEditorTheme } from \"@/components/ui/markdown/theme\";\nimport {\n  CodeHighlightNode,\n  CodeNode,\n  registerCodeHighlighting,\n} from \"@lexical/code\";\nimport { LinkNode } from \"@lexical/link\";\nimport { ListItemNode, ListNode } from \"@lexical/list\";\nimport {\n  $convertFromMarkdownString,\n  $convertToMarkdownString,\n  TRANSFORMERS,\n} from \"@lexical/markdown\";\nimport { AutoFocusPlugin } from \"@lexical/react/LexicalAutoFocusPlugin\";\nimport {\n  InitialConfigType,\n  LexicalComposer,\n} from \"@lexical/react/LexicalComposer\";\nimport { ContentEditable } from \"@lexical/react/LexicalContentEditable\";\nimport { LexicalErrorBoundary } from \"@lexical/react/LexicalErrorBoundary\";\nimport { HistoryPlugin } from \"@lexical/react/LexicalHistoryPlugin\";\nimport { HorizontalRuleNode } from \"@lexical/react/LexicalHorizontalRuleNode\";\nimport { ListPlugin } from \"@lexical/react/LexicalListPlugin\";\nimport { MarkdownShortcutPlugin } from \"@lexical/react/LexicalMarkdownShortcutPlugin\";\nimport { OnChangePlugin } from \"@lexical/react/LexicalOnChangePlugin\";\nimport { PlainTextPlugin } from \"@lexical/react/LexicalPlainTextPlugin\";\nimport { RichTextPlugin } from \"@lexical/react/LexicalRichTextPlugin\";\nimport { TabIndentationPlugin } from \"@lexical/react/LexicalTabIndentationPlugin\";\nimport { HeadingNode, QuoteNode } from \"@lexical/rich-text\";\nimport { $getRoot, EditorState, LexicalEditor } from \"lexical\";\n\nfunction onError(error: Error) {\n  console.error(error);\n}\n\nconst EDITOR_NODES = [\n  HeadingNode,\n  ListNode,\n  ListItemNode,\n  QuoteNode,\n  LinkNode,\n  CodeNode,\n  HorizontalRuleNode,\n  CodeHighlightNode,\n];\n\ninterface MarkdownEditorProps {\n  children: string;\n  onSave?: (markdown: string) => void;\n  isSaving?: boolean;\n}\n\nconst MarkdownEditor = memo(\n  ({ children: initialMarkdown, onSave, isSaving }: MarkdownEditorProps) => {\n    const [isRawMarkdownMode, setIsRawMarkdownMode] = useState(false);\n    const [rawMarkdown, setRawMarkdown] = useState(initialMarkdown);\n\n    const initialConfig: InitialConfigType = useMemo(\n      () => ({\n        namespace: \"editor\",\n        onError,\n        theme: MarkdownEditorTheme,\n        nodes: EDITOR_NODES,\n        editorState: (editor: LexicalEditor) => {\n          registerCodeHighlighting(editor);\n          $convertFromMarkdownString(initialMarkdown, TRANSFORMERS);\n        },\n      }),\n      [initialMarkdown],\n    );\n\n    const handleOnChange = (editorState: EditorState) => {\n      editorState.read(() => {\n        let markdownString;\n        if (isRawMarkdownMode) {\n          markdownString = $getRoot()?.getFirstChild()?.getTextContent() ?? \"\";\n        } else {\n          markdownString = $convertToMarkdownString(TRANSFORMERS);\n        }\n        setRawMarkdown(markdownString);\n      });\n    };\n\n    return (\n      <LexicalComposer initialConfig={initialConfig}>\n        <div className=\"flex h-full flex-col justify-stretch\">\n          <ToolbarPlugin\n            isRawMarkdownMode={isRawMarkdownMode}\n            setIsRawMarkdownMode={setIsRawMarkdownMode}\n            onSave={onSave && (() => onSave(rawMarkdown))}\n            isSaving={!!isSaving}\n          />\n          {isRawMarkdownMode ? (\n            <PlainTextPlugin\n              contentEditable={\n                <ContentEditable className=\"h-full w-full min-w-full overflow-auto p-2\" />\n              }\n              ErrorBoundary={LexicalErrorBoundary}\n            />\n          ) : (\n            <RichTextPlugin\n              contentEditable={\n                <ContentEditable className=\"prose h-full w-full min-w-full overflow-auto p-2 dark:prose-invert prose-p:m-0\" />\n              }\n              ErrorBoundary={LexicalErrorBoundary}\n            />\n          )}\n        </div>\n        <HistoryPlugin />\n        <AutoFocusPlugin />\n        <TabIndentationPlugin />\n        <MarkdownShortcutPlugin transformers={TRANSFORMERS} />\n        <OnChangePlugin onChange={handleOnChange} />\n        <ListPlugin />\n      </LexicalComposer>\n    );\n  },\n);\n// needed for linter because of memo\nMarkdownEditor.displayName = \"MarkdownEditor\";\n\nexport default MarkdownEditor;\n"
  },
  {
    "path": "apps/web/components/ui/markdown/markdown-readonly.tsx",
    "content": "import React from \"react\";\nimport CopyBtn from \"@/components/ui/copy-button\";\nimport { cn } from \"@/lib/utils\";\nimport Markdown from \"react-markdown\";\nimport { Prism as SyntaxHighlighter } from \"react-syntax-highlighter\";\nimport { dracula } from \"react-syntax-highlighter/dist/cjs/styles/prism\";\nimport remarkBreaks from \"remark-breaks\";\nimport remarkGfm from \"remark-gfm\";\n\nfunction PreWithCopyBtn({ className, ...props }: React.ComponentProps<\"pre\">) {\n  const ref = React.useRef<HTMLPreElement>(null);\n  return (\n    <span className=\"group relative\">\n      <CopyBtn\n        className=\"absolute right-1 top-1 m-1 hidden text-white group-hover:block\"\n        getStringToCopy={() => {\n          return ref.current?.textContent ?? \"\";\n        }}\n      />\n      <pre ref={ref} className={cn(className, \"\")} {...props} />\n    </span>\n  );\n}\n\nexport function MarkdownReadonly({\n  children: markdown,\n  className,\n  onSave,\n}: {\n  children: string;\n  className?: string;\n  onSave?: (markdown: string) => void;\n}) {\n  /**\n   * This method is triggered when a checkbox is toggled from the masonry view\n   * It finds the index of the clicked checkbox inside of the note\n   * It then finds the corresponding markdown and changes it accordingly\n   */\n  const handleTodoClick = (e: React.ChangeEvent<HTMLInputElement>) => {\n    e.preventDefault();\n    const parent = e.target.closest(\".prose\");\n    if (!parent) return;\n    const allCheckboxes = parent.querySelectorAll(\".todo-checkbox\");\n    let checkboxIndex = 0;\n    allCheckboxes.forEach((cb, i) => {\n      if (cb === e.target) checkboxIndex = i;\n    });\n    let i = 0;\n    const todoPattern = /^(\\s*[-*+]\\s*\\[)( |x|X)(\\])/gm;\n    const newMarkdown = markdown.replace(\n      todoPattern,\n      (match, prefix: string, state: string, suffix: string) => {\n        const currentIndex = i++;\n        if (currentIndex !== checkboxIndex) {\n          return match;\n        }\n        const isDone = state.toLowerCase() === \"x\";\n        const nextState = isDone ? \" \" : \"x\";\n        return `${prefix}${nextState}${suffix}`;\n      },\n    );\n    if (onSave) {\n      onSave(newMarkdown);\n    }\n  };\n\n  return (\n    <Markdown\n      remarkPlugins={[remarkGfm, remarkBreaks]}\n      className={cn(\"prose dark:prose-invert\", className)}\n      components={{\n        input: (props) =>\n          props.type === \"checkbox\" ? (\n            <input\n              checked={props.checked}\n              onChange={handleTodoClick}\n              type=\"checkbox\"\n              className=\"todo-checkbox\"\n            />\n          ) : (\n            <input {...props} readOnly />\n          ),\n        pre({ ...props }) {\n          return <PreWithCopyBtn {...props} />;\n        },\n        code({ className, children, ...props }) {\n          const match = /language-(\\w+)/.exec(className ?? \"\");\n          return match ? (\n            <SyntaxHighlighter\n              PreTag=\"div\"\n              language={match[1]}\n              {...props}\n              style={dracula}\n            >\n              {String(children).replace(/\\n$/, \"\")}\n            </SyntaxHighlighter>\n          ) : (\n            <code className={className} {...props}>\n              {children}\n            </code>\n          );\n        },\n      }}\n    >\n      {markdown}\n    </Markdown>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/ui/markdown/plugins/toolbar-plugin.tsx",
    "content": "import { useCallback, useEffect, useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport {\n  $convertFromMarkdownString,\n  $convertToMarkdownString,\n  TRANSFORMERS,\n} from \"@lexical/markdown\";\nimport { useLexicalComposerContext } from \"@lexical/react/LexicalComposerContext\";\nimport { mergeRegister } from \"@lexical/utils\";\nimport {\n  $createParagraphNode,\n  $createTextNode,\n  $getRoot,\n  $getSelection,\n  $isRangeSelection,\n  FORMAT_TEXT_COMMAND,\n  LexicalCommand,\n  SELECTION_CHANGE_COMMAND,\n  TextFormatType,\n} from \"lexical\";\nimport {\n  Bold,\n  Code,\n  Highlighter,\n  Italic,\n  LucideIcon,\n  Save,\n  Strikethrough,\n} from \"lucide-react\";\n\nimport { ActionButton } from \"../../action-button\";\nimport InfoTooltip from \"../../info-tooltip\";\nimport { Label } from \"../../label\";\nimport { Switch } from \"../../switch\";\n\nconst LowPriority = 1;\n\nfunction MarkdownToolTip() {\n  const { t } = useTranslation();\n  return (\n    <InfoTooltip size={15}>\n      <table className=\"w-full table-auto text-left text-sm\">\n        <thead>\n          <tr>\n            <th>{t(\"editor.text_toolbar.markdown_shortcuts.label\")}</th>\n          </tr>\n        </thead>\n        <tbody>\n          <tr className=\"border-b\">\n            <td className=\"py-2\">\n              {t(\"editor.text_toolbar.markdown_shortcuts.heading.label\")}\n            </td>\n            <td className=\"py-2\">\n              {t(\"editor.text_toolbar.markdown_shortcuts.heading.example\")}\n            </td>\n          </tr>\n          <tr className=\"border-b\">\n            <td className=\"py-2\">\n              {t(\"editor.text_toolbar.markdown_shortcuts.bold.label\")}\n            </td>\n            <td className=\"py-2\">\n              {t(\"editor.text_toolbar.markdown_shortcuts.bold.example\")}\n            </td>\n          </tr>\n          <tr className=\"border-b\">\n            <td className=\"py-2\">\n              {t(\"editor.text_toolbar.markdown_shortcuts.italic.label\")}\n            </td>\n            <td className=\"py-2\">\n              {t(\"editor.text_toolbar.markdown_shortcuts.italic.example\")}\n            </td>\n          </tr>\n          <tr className=\"border-b\">\n            <td className=\"py-2\">\n              {t(\"editor.text_toolbar.markdown_shortcuts.blockquote.label\")}\n            </td>\n            <td className=\"py-2\">\n              {t(\"editor.text_toolbar.markdown_shortcuts.blockquote.example\")}\n            </td>\n          </tr>\n          <tr className=\"border-b\">\n            <td className=\"py-2\">\n              {t(\"editor.text_toolbar.markdown_shortcuts.ordered_list.label\")}\n            </td>\n            <td className=\"py-2\">\n              {t(\"editor.text_toolbar.markdown_shortcuts.ordered_list.example\")}\n            </td>\n          </tr>\n          <tr className=\"border-b\">\n            <td className=\"py-2\">\n              {t(\"editor.text_toolbar.markdown_shortcuts.unordered_list.label\")}\n            </td>\n            <td className=\"py-2\">\n              {t(\n                \"editor.text_toolbar.markdown_shortcuts.unordered_list.example\",\n              )}\n            </td>\n          </tr>\n          <tr className=\"border-b\">\n            <td className=\"py-2\">\n              {t(\"editor.text_toolbar.markdown_shortcuts.inline_code.label\")}\n            </td>\n            <td className=\"py-2\">\n              {t(\"editor.text_toolbar.markdown_shortcuts.inline_code.example\")}\n            </td>\n          </tr>\n          <tr className=\"border-b\">\n            <td className=\"py-2\">\n              {t(\"editor.text_toolbar.markdown_shortcuts.block_code.label\")}\n            </td>\n            <td className=\"py-2\">\n              {t(\"editor.text_toolbar.markdown_shortcuts.block_code.example\")}\n            </td>\n          </tr>\n        </tbody>\n      </table>\n    </InfoTooltip>\n  );\n}\n\nexport default function ToolbarPlugin({\n  isRawMarkdownMode = false,\n  setIsRawMarkdownMode,\n  onSave,\n  isSaving,\n}: {\n  isRawMarkdownMode: boolean;\n  setIsRawMarkdownMode: (value: boolean) => void;\n  onSave?: () => void;\n  isSaving: boolean;\n}) {\n  const { t } = useTranslation();\n  const [editor] = useLexicalComposerContext();\n  const [editorToolbarState, setEditorToolbarState] = useState<{\n    isBold: boolean;\n    isItalic: boolean;\n    isStrikethrough: boolean;\n    isHighlight: boolean;\n    isCode: boolean;\n  }>({\n    isBold: false,\n    isItalic: false,\n    isStrikethrough: false,\n    isHighlight: false,\n    isCode: false,\n  });\n\n  const $updateToolbar = useCallback(() => {\n    const selection = $getSelection();\n    if ($isRangeSelection(selection)) {\n      setEditorToolbarState({\n        isBold: selection.hasFormat(\"bold\"),\n        isItalic: selection.hasFormat(\"italic\"),\n        isStrikethrough: selection.hasFormat(\"strikethrough\"),\n        isHighlight: selection.hasFormat(\"highlight\"),\n        isCode: selection.hasFormat(\"code\"),\n      });\n    }\n  }, []);\n\n  useEffect(() => {\n    return mergeRegister(\n      editor.registerUpdateListener(({ editorState }) => {\n        editorState.read(() => {\n          $updateToolbar();\n        });\n      }),\n      editor.registerCommand(\n        SELECTION_CHANGE_COMMAND,\n        (_payload, _newEditor) => {\n          $updateToolbar();\n          return false;\n        },\n        LowPriority,\n      ),\n    );\n  }, [editor, $updateToolbar]);\n\n  const formatButtons: {\n    command: LexicalCommand<TextFormatType>;\n    format: TextFormatType;\n    isActive?: boolean;\n    icon: LucideIcon;\n    label: string;\n  }[] = [\n    {\n      command: FORMAT_TEXT_COMMAND,\n      format: \"bold\",\n      icon: Bold,\n      isActive: editorToolbarState.isBold,\n      label: t(\"editor.text_toolbar.bold\"),\n    },\n    {\n      command: FORMAT_TEXT_COMMAND,\n      format: \"italic\",\n      icon: Italic,\n      isActive: editorToolbarState.isItalic,\n      label: t(\"editor.text_toolbar.italic\"),\n    },\n    {\n      command: FORMAT_TEXT_COMMAND,\n      format: \"strikethrough\",\n      icon: Strikethrough,\n      isActive: editorToolbarState.isStrikethrough,\n      label: t(\"editor.text_toolbar.strikethrough\"),\n    },\n    {\n      command: FORMAT_TEXT_COMMAND,\n      format: \"code\",\n      icon: Code,\n      isActive: editorToolbarState.isCode,\n      label: t(\"editor.text_toolbar.code\"),\n    },\n    {\n      command: FORMAT_TEXT_COMMAND,\n      format: \"highlight\",\n      icon: Highlighter,\n      isActive: editorToolbarState.isHighlight,\n      label: t(\"editor.text_toolbar.highlight\"),\n    },\n  ];\n\n  const handleRawMarkdownToggle = useCallback(() => {\n    editor.update(() => {\n      console.log(isRawMarkdownMode);\n      const root = $getRoot();\n      const firstChild = root.getFirstChild();\n      if (isRawMarkdownMode) {\n        if (firstChild) {\n          $convertFromMarkdownString(firstChild.getTextContent(), TRANSFORMERS);\n        }\n        setIsRawMarkdownMode(false);\n      } else {\n        const markdown = $convertToMarkdownString(TRANSFORMERS);\n        const pNode = $createParagraphNode();\n        pNode.append($createTextNode(markdown));\n        root.clear().append(pNode);\n        setIsRawMarkdownMode(true);\n      }\n    });\n  }, [editor, isRawMarkdownMode]);\n\n  return (\n    <div className=\"mb-1 flex items-center justify-between rounded-t-lg p-1\">\n      <div className=\"flex\">\n        {formatButtons.map(\n          ({ command, format, icon: Icon, isActive, label }) => (\n            <Button\n              key={format}\n              disabled={isRawMarkdownMode}\n              size={\"sm\"}\n              onClick={() => {\n                editor.dispatchCommand(command, format);\n              }}\n              variant={isActive ? \"default\" : \"ghost\"}\n              aria-label={label}\n            >\n              <Icon className=\"h-4 w-4\" />\n            </Button>\n          ),\n        )}\n      </div>\n      <div className=\"flex items-center gap-2\">\n        <div className=\"flex items-center space-x-2\">\n          <Switch\n            id=\"editor-raw-markdown\"\n            onCheckedChange={handleRawMarkdownToggle}\n            checked={isRawMarkdownMode}\n          />\n          <Label htmlFor=\"editor-raw-markdown\">Raw Markdown</Label>\n        </div>\n        {onSave && (\n          <ActionButton\n            loading={isSaving}\n            className=\"flex items-center gap-2\"\n            size={\"sm\"}\n            onClick={() => {\n              onSave?.();\n            }}\n          >\n            <Save className=\"size-4\" />\n            Save\n          </ActionButton>\n        )}\n        <MarkdownToolTip />\n      </div>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/ui/markdown/theme.ts",
    "content": "export const MarkdownEditorTheme = {\n  code: \"bg-[#282A36] text-[#F8F8F2] font-mono block px-4 py-2 my-2 text-sm overflow-x-auto relative rounded-md shadow-sm\",\n  codeHighlight: {\n    atrule: \"text-[#8BE9FD]\",\n    attr: \"text-[#8BE9FD]\",\n    boolean: \"text-[#FF79C6]\",\n    builtin: \"text-[#50FA7B]\",\n    cdata: \"text-[#6272A4]\",\n    char: \"text-[#50FA7B]\",\n    class: \"text-[#FF79C6]\",\n    \"class-name\": \"text-[#FF79C6]\",\n    comment: \"text-[#6272A4]\",\n    constant: \"text-[#FF79C6]\",\n    deleted: \"text-[#FF5555]\",\n    doctype: \"text-[#6272A4]\",\n    entity: \"text-[#FFB86C]\",\n    function: \"text-[#50FA7B]\",\n    important: \"text-[#F1FA8C]\",\n    inserted: \"text-[#50FA7B]\",\n    keyword: \"text-[#FF79C6]\",\n    namespace: \"text-[#F1FA8C]\",\n    number: \"text-[#BD93F9]\",\n    operator: \"text-[#FFB86C]\",\n    prolog: \"text-[#6272A4]\",\n    property: \"text-[#FFB86C]\",\n    punctuation: \"text-[#F8F8F2]\",\n    regex: \"text-[#FF5555]\",\n    selector: \"text-[#50FA7B]\",\n    string: \"text-[#F1FA8C]\",\n    symbol: \"text-[#FF79C6]\",\n    tag: \"text-[#FF79C6]\",\n    url: \"text-[#8BE9FD]\",\n    variable: \"text-[#F1FA8C]\",\n  },\n};\n"
  },
  {
    "path": "apps/web/components/ui/multiple-choice-dialog.tsx",
    "content": "import { useState } from \"react\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@/components/ui/dialog\";\n\nexport default function MultipleChoiceDialog({\n  open: userIsOpen,\n  setOpen: userSetOpen,\n  onOpenChange,\n  title,\n  description,\n  actionButtons,\n  children,\n}: {\n  open?: boolean;\n  setOpen?: (v: boolean) => void;\n  onOpenChange?: (open: boolean) => void;\n  title: React.ReactNode;\n  description: React.ReactNode;\n  actionButtons: ((\n    setDialogOpen: (open: boolean) => void,\n  ) => React.ReactNode)[];\n  children?: React.ReactNode;\n}) {\n  const [customIsOpen, setCustomIsOpen] = useState(false);\n  const [isDialogOpen, setDialogOpen] = [\n    userIsOpen ?? customIsOpen,\n    userSetOpen ?? setCustomIsOpen,\n  ];\n  return (\n    <Dialog\n      open={isDialogOpen}\n      onOpenChange={(isOpen) => {\n        onOpenChange?.(isOpen);\n        setDialogOpen(isOpen);\n      }}\n    >\n      {children && <DialogTrigger asChild>{children}</DialogTrigger>}\n      <DialogContent>\n        <DialogHeader>\n          <DialogTitle>{title}</DialogTitle>\n        </DialogHeader>\n        {description}\n        <DialogFooter className=\"sm:justify-end\">\n          {actionButtons.map((actionButton) => actionButton(setDialogOpen))}\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/ui/popover.tsx",
    "content": "export {\n  Popover,\n  PopoverTrigger,\n  PopoverContent,\n} from \"@karakeep/shared-react/components/ui/popover\";\n"
  },
  {
    "path": "apps/web/components/ui/progress.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport * as ProgressPrimitive from \"@radix-ui/react-progress\";\n\nconst Progress = React.forwardRef<\n  React.ElementRef<typeof ProgressPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>\n>(({ className, value, ...props }, ref) => (\n  <ProgressPrimitive.Root\n    ref={ref}\n    className={cn(\n      \"relative h-4 w-full overflow-hidden rounded-full bg-secondary\",\n      className,\n    )}\n    {...props}\n  >\n    <ProgressPrimitive.Indicator\n      className=\"h-full w-full flex-1 bg-primary transition-all\"\n      style={{ transform: `translateX(-${100 - (value ?? 0)}%)` }}\n    />\n  </ProgressPrimitive.Root>\n));\nProgress.displayName = ProgressPrimitive.Root.displayName;\n\nexport { Progress };\n"
  },
  {
    "path": "apps/web/components/ui/radio-group.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport * as RadioGroupPrimitive from \"@radix-ui/react-radio-group\";\nimport { Circle } from \"lucide-react\";\n\nconst RadioGroup = React.forwardRef<\n  React.ElementRef<typeof RadioGroupPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>\n>(({ className, ...props }, ref) => {\n  return (\n    <RadioGroupPrimitive.Root\n      className={cn(\"grid gap-2\", className)}\n      {...props}\n      ref={ref}\n    />\n  );\n});\nRadioGroup.displayName = RadioGroupPrimitive.Root.displayName;\n\nconst RadioGroupItem = React.forwardRef<\n  React.ElementRef<typeof RadioGroupPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>\n>(({ className, ...props }, ref) => {\n  return (\n    <RadioGroupPrimitive.Item\n      ref={ref}\n      className={cn(\n        \"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\",\n        className,\n      )}\n      {...props}\n    >\n      <RadioGroupPrimitive.Indicator className=\"flex items-center justify-center\">\n        <Circle className=\"h-2.5 w-2.5 fill-current text-current\" />\n      </RadioGroupPrimitive.Indicator>\n    </RadioGroupPrimitive.Item>\n  );\n});\nRadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;\n\nexport { RadioGroup, RadioGroupItem };\n"
  },
  {
    "path": "apps/web/components/ui/scroll-area.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport * as ScrollAreaPrimitive from \"@radix-ui/react-scroll-area\";\n\nconst ScrollArea = React.forwardRef<\n  React.ElementRef<typeof ScrollAreaPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>\n>(({ className, children, ...props }, ref) => (\n  <ScrollAreaPrimitive.Root\n    ref={ref}\n    className={cn(\"relative overflow-hidden\", className)}\n    {...props}\n  >\n    <ScrollAreaPrimitive.Viewport className=\"size-full rounded-[inherit]\">\n      {children}\n    </ScrollAreaPrimitive.Viewport>\n    <ScrollBar />\n    <ScrollAreaPrimitive.Corner />\n  </ScrollAreaPrimitive.Root>\n));\nScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;\n\nconst ScrollBar = React.forwardRef<\n  React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,\n  React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>\n>(({ className, orientation = \"vertical\", ...props }, ref) => (\n  <ScrollAreaPrimitive.ScrollAreaScrollbar\n    ref={ref}\n    orientation={orientation}\n    className={cn(\n      \"flex touch-none select-none transition-colors\",\n      orientation === \"vertical\" &&\n        \"h-full w-2.5 border-l border-l-transparent p-[1px]\",\n      orientation === \"horizontal\" &&\n        \"h-2.5 flex-col border-t border-t-transparent p-[1px]\",\n      className,\n    )}\n    {...props}\n  >\n    <ScrollAreaPrimitive.ScrollAreaThumb className=\"relative flex-1 rounded-full bg-border\" />\n  </ScrollAreaPrimitive.ScrollAreaScrollbar>\n));\nScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;\n\nexport { ScrollArea, ScrollBar };\n"
  },
  {
    "path": "apps/web/components/ui/select.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport * as SelectPrimitive from \"@radix-ui/react-select\";\nimport { Check, ChevronDown, ChevronUp } from \"lucide-react\";\n\nconst Select = SelectPrimitive.Root;\n\nconst SelectGroup = SelectPrimitive.Group;\n\nconst SelectValue = SelectPrimitive.Value;\n\nconst SelectTrigger = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Trigger\n    ref={ref}\n    className={cn(\n      \"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1\",\n      className,\n    )}\n    {...props}\n  >\n    {children}\n    <SelectPrimitive.Icon asChild>\n      <ChevronDown className=\"size-4 opacity-50\" />\n    </SelectPrimitive.Icon>\n  </SelectPrimitive.Trigger>\n));\nSelectTrigger.displayName = SelectPrimitive.Trigger.displayName;\n\nconst SelectScrollUpButton = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollUpButton\n    ref={ref}\n    className={cn(\n      \"flex cursor-default items-center justify-center py-1\",\n      className,\n    )}\n    {...props}\n  >\n    <ChevronUp className=\"size-4\" />\n  </SelectPrimitive.ScrollUpButton>\n));\nSelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;\n\nconst SelectScrollDownButton = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.ScrollDownButton\n    ref={ref}\n    className={cn(\n      \"flex cursor-default items-center justify-center py-1\",\n      className,\n    )}\n    {...props}\n  >\n    <ChevronDown className=\"size-4\" />\n  </SelectPrimitive.ScrollDownButton>\n));\nSelectScrollDownButton.displayName =\n  SelectPrimitive.ScrollDownButton.displayName;\n\nconst SelectContent = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>\n>(({ className, children, position = \"popper\", ...props }, ref) => (\n  <SelectPrimitive.Portal>\n    <SelectPrimitive.Content\n      ref={ref}\n      className={cn(\n        \"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        position === \"popper\" &&\n          \"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1\",\n        className,\n      )}\n      position={position}\n      {...props}\n    >\n      <SelectScrollUpButton />\n      <SelectPrimitive.Viewport\n        className={cn(\n          \"p-1\",\n          position === \"popper\" &&\n            \"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]\",\n        )}\n      >\n        {children}\n      </SelectPrimitive.Viewport>\n      <SelectScrollDownButton />\n    </SelectPrimitive.Content>\n  </SelectPrimitive.Portal>\n));\nSelectContent.displayName = SelectPrimitive.Content.displayName;\n\nconst SelectLabel = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Label>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Label\n    ref={ref}\n    className={cn(\"py-1.5 pl-8 pr-2 text-sm font-semibold\", className)}\n    {...props}\n  />\n));\nSelectLabel.displayName = SelectPrimitive.Label.displayName;\n\nconst SelectItem = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Item>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>\n>(({ className, children, ...props }, ref) => (\n  <SelectPrimitive.Item\n    ref={ref}\n    className={cn(\n      \"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n      className,\n    )}\n    {...props}\n  >\n    <span className=\"absolute left-2 flex size-3.5 items-center justify-center\">\n      <SelectPrimitive.ItemIndicator>\n        <Check className=\"size-4\" />\n      </SelectPrimitive.ItemIndicator>\n    </span>\n\n    <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>\n  </SelectPrimitive.Item>\n));\nSelectItem.displayName = SelectPrimitive.Item.displayName;\n\nconst SelectSeparator = React.forwardRef<\n  React.ElementRef<typeof SelectPrimitive.Separator>,\n  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>\n>(({ className, ...props }, ref) => (\n  <SelectPrimitive.Separator\n    ref={ref}\n    className={cn(\"-mx-1 my-1 h-px bg-muted\", className)}\n    {...props}\n  />\n));\nSelectSeparator.displayName = SelectPrimitive.Separator.displayName;\n\nexport {\n  Select,\n  SelectGroup,\n  SelectValue,\n  SelectTrigger,\n  SelectContent,\n  SelectLabel,\n  SelectItem,\n  SelectSeparator,\n  SelectScrollUpButton,\n  SelectScrollDownButton,\n};\n"
  },
  {
    "path": "apps/web/components/ui/separator.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport * as SeparatorPrimitive from \"@radix-ui/react-separator\";\n\nconst Separator = React.forwardRef<\n  React.ElementRef<typeof SeparatorPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>\n>(\n  (\n    { className, orientation = \"horizontal\", decorative = true, ...props },\n    ref,\n  ) => (\n    <SeparatorPrimitive.Root\n      ref={ref}\n      decorative={decorative}\n      orientation={orientation}\n      className={cn(\n        \"shrink-0 bg-border\",\n        orientation === \"horizontal\" ? \"h-[1px] w-full\" : \"h-full w-[1px]\",\n        className,\n      )}\n      {...props}\n    />\n  ),\n);\nSeparator.displayName = SeparatorPrimitive.Root.displayName;\n\nexport { Separator };\n"
  },
  {
    "path": "apps/web/components/ui/skeleton.tsx",
    "content": "import { cn } from \"@/lib/utils\";\n\nfunction Skeleton({\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement>) {\n  return (\n    <div\n      className={cn(\"animate-pulse rounded-md bg-muted\", className)}\n      {...props}\n    />\n  );\n}\n\nexport { Skeleton };\n"
  },
  {
    "path": "apps/web/components/ui/slider.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport * as SliderPrimitive from \"@radix-ui/react-slider\";\n\nconst Slider = React.forwardRef<\n  React.ElementRef<typeof SliderPrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>\n>(({ className, ...props }, ref) => (\n  <SliderPrimitive.Root\n    ref={ref}\n    className={cn(\n      \"relative flex w-full touch-none select-none items-center\",\n      className,\n    )}\n    {...props}\n  >\n    <SliderPrimitive.Track className=\"relative h-2 w-full grow overflow-hidden rounded-full bg-secondary\">\n      <SliderPrimitive.Range className=\"absolute h-full bg-primary\" />\n    </SliderPrimitive.Track>\n    <SliderPrimitive.Thumb className=\"block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50\" />\n  </SliderPrimitive.Root>\n));\nSlider.displayName = SliderPrimitive.Root.displayName;\n\nexport { Slider };\n"
  },
  {
    "path": "apps/web/components/ui/sonner.tsx",
    "content": "\"use client\";\n\nimport {\n  CircleCheck,\n  Info,\n  LoaderCircle,\n  OctagonX,\n  TriangleAlert,\n} from \"lucide-react\";\nimport { useTheme } from \"next-themes\";\nimport { Toaster as Sonner, toast } from \"sonner\";\n\ntype ToasterProps = React.ComponentProps<typeof Sonner>;\n\nconst Toaster = ({ ...props }: ToasterProps) => {\n  const { theme = \"system\" } = useTheme();\n\n  return (\n    <Sonner\n      theme={theme as ToasterProps[\"theme\"]}\n      className=\"toaster group\"\n      icons={{\n        success: <CircleCheck className=\"h-4 w-4\" />,\n        info: <Info className=\"h-4 w-4\" />,\n        warning: <TriangleAlert className=\"h-4 w-4\" />,\n        error: <OctagonX className=\"h-4 w-4\" />,\n        loading: <LoaderCircle className=\"h-4 w-4 animate-spin\" />,\n      }}\n      toastOptions={{\n        classNames: {\n          toast:\n            \"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg\",\n          description: \"group-[.toast]:text-muted-foreground\",\n          actionButton:\n            \"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground\",\n          cancelButton:\n            \"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground\",\n        },\n      }}\n      {...props}\n    />\n  );\n};\n\n/**\n * Compat layer for migrating from old toaster to sonner\n * @deprecated Use sonner's natie toast instead\n */\nconst legacyToast = ({\n  title,\n  description,\n  variant,\n}: {\n  title?: React.ReactNode;\n  description?: React.ReactNode;\n  variant?: \"destructive\" | \"default\";\n}) => {\n  let toastTitle = title;\n  let toastDescription: React.ReactNode | undefined = description;\n  if (!title) {\n    toastTitle = description;\n    toastDescription = undefined;\n  }\n  if (variant === \"destructive\") {\n    toast.error(toastTitle, { description: toastDescription });\n  } else {\n    toast(toastTitle, { description: toastDescription });\n  }\n};\n\nexport { Toaster, legacyToast as toast };\n"
  },
  {
    "path": "apps/web/components/ui/spinner.tsx",
    "content": "import { cn } from \"@/lib/utils\";\n\nexport default function LoadingSpinner({ className }: { className?: string }) {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"24\"\n      height=\"24\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      className={cn(\"animate-spin\", className)}\n    >\n      <path d=\"M21 12a9 9 0 1 1-6.219-8.56\" />\n    </svg>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/ui/switch.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport * as SwitchPrimitives from \"@radix-ui/react-switch\";\n\nconst Switch = React.forwardRef<\n  React.ElementRef<typeof SwitchPrimitives.Root>,\n  React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>\n>(({ className, ...props }, ref) => (\n  <SwitchPrimitives.Root\n    className={cn(\n      \"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input\",\n      className,\n    )}\n    {...props}\n    ref={ref}\n  >\n    <SwitchPrimitives.Thumb\n      className={cn(\n        \"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0\",\n      )}\n    />\n  </SwitchPrimitives.Root>\n));\nSwitch.displayName = SwitchPrimitives.Root.displayName;\n\nexport { Switch };\n"
  },
  {
    "path": "apps/web/components/ui/table.tsx",
    "content": "import * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nconst Table = React.forwardRef<\n  HTMLTableElement,\n  React.HTMLAttributes<HTMLTableElement>\n>(({ className, ...props }, ref) => (\n  <div className=\"relative w-full overflow-auto\">\n    <table\n      ref={ref}\n      className={cn(\"w-full caption-bottom text-sm\", className)}\n      {...props}\n    />\n  </div>\n));\nTable.displayName = \"Table\";\n\nconst TableHeader = React.forwardRef<\n  HTMLTableSectionElement,\n  React.HTMLAttributes<HTMLTableSectionElement>\n>(({ className, ...props }, ref) => (\n  <thead ref={ref} className={cn(\"[&_tr]:border-b\", className)} {...props} />\n));\nTableHeader.displayName = \"TableHeader\";\n\nconst TableBody = React.forwardRef<\n  HTMLTableSectionElement,\n  React.HTMLAttributes<HTMLTableSectionElement>\n>(({ className, ...props }, ref) => (\n  <tbody\n    ref={ref}\n    className={cn(\"[&_tr:last-child]:border-0\", className)}\n    {...props}\n  />\n));\nTableBody.displayName = \"TableBody\";\n\nconst TableFooter = React.forwardRef<\n  HTMLTableSectionElement,\n  React.HTMLAttributes<HTMLTableSectionElement>\n>(({ className, ...props }, ref) => (\n  <tfoot\n    ref={ref}\n    className={cn(\n      \"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0\",\n      className,\n    )}\n    {...props}\n  />\n));\nTableFooter.displayName = \"TableFooter\";\n\nconst TableRow = React.forwardRef<\n  HTMLTableRowElement,\n  React.HTMLAttributes<HTMLTableRowElement>\n>(({ className, ...props }, ref) => (\n  <tr\n    ref={ref}\n    className={cn(\n      \"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted\",\n      className,\n    )}\n    {...props}\n  />\n));\nTableRow.displayName = \"TableRow\";\n\nconst TableHead = React.forwardRef<\n  HTMLTableCellElement,\n  React.ThHTMLAttributes<HTMLTableCellElement>\n>(({ className, ...props }, ref) => (\n  <th\n    ref={ref}\n    className={cn(\n      \"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0\",\n      className,\n    )}\n    {...props}\n  />\n));\nTableHead.displayName = \"TableHead\";\n\nconst TableCell = React.forwardRef<\n  HTMLTableCellElement,\n  React.TdHTMLAttributes<HTMLTableCellElement>\n>(({ className, ...props }, ref) => (\n  <td\n    ref={ref}\n    className={cn(\"p-4 align-middle [&:has([role=checkbox])]:pr-0\", className)}\n    {...props}\n  />\n));\nTableCell.displayName = \"TableCell\";\n\nconst TableCaption = React.forwardRef<\n  HTMLTableCaptionElement,\n  React.HTMLAttributes<HTMLTableCaptionElement>\n>(({ className, ...props }, ref) => (\n  <caption\n    ref={ref}\n    className={cn(\"mt-4 text-sm text-muted-foreground\", className)}\n    {...props}\n  />\n));\nTableCaption.displayName = \"TableCaption\";\n\nexport {\n  Table,\n  TableHeader,\n  TableBody,\n  TableFooter,\n  TableHead,\n  TableRow,\n  TableCell,\n  TableCaption,\n};\n"
  },
  {
    "path": "apps/web/components/ui/tabs.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\";\n\nconst Tabs = TabsPrimitive.Root;\n\nconst TabsList = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.List>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.List\n    ref={ref}\n    className={cn(\n      \"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground\",\n      className,\n    )}\n    {...props}\n  />\n));\nTabsList.displayName = TabsPrimitive.List.displayName;\n\nconst TabsTrigger = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.Trigger>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.Trigger\n    ref={ref}\n    className={cn(\n      \"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm\",\n      className,\n    )}\n    {...props}\n  />\n));\nTabsTrigger.displayName = TabsPrimitive.Trigger.displayName;\n\nconst TabsContent = React.forwardRef<\n  React.ElementRef<typeof TabsPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>\n>(({ className, ...props }, ref) => (\n  <TabsPrimitive.Content\n    ref={ref}\n    className={cn(\n      \"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n      className,\n    )}\n    {...props}\n  />\n));\nTabsContent.displayName = TabsPrimitive.Content.displayName;\n\nexport { Tabs, TabsList, TabsTrigger, TabsContent };\n"
  },
  {
    "path": "apps/web/components/ui/textarea.tsx",
    "content": "export {\n  Textarea,\n  type TextareaProps,\n} from \"@karakeep/shared-react/components/ui/textarea\";\n"
  },
  {
    "path": "apps/web/components/ui/toast.tsx",
    "content": "import type { VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport * as ToastPrimitives from \"@radix-ui/react-toast\";\nimport { cva } from \"class-variance-authority\";\nimport { X } from \"lucide-react\";\n\nconst ToastProvider = ToastPrimitives.Provider;\n\nconst ToastViewport = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Viewport>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Viewport\n    ref={ref}\n    className={cn(\n      \"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]\",\n      className,\n    )}\n    {...props}\n  />\n));\nToastViewport.displayName = ToastPrimitives.Viewport.displayName;\n\nconst toastVariants = cva(\n  \"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full\",\n  {\n    variants: {\n      variant: {\n        default: \"border bg-background text-foreground\",\n        destructive:\n          \"destructive group border-destructive bg-destructive text-destructive-foreground\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\nconst Toast = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Root>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &\n    VariantProps<typeof toastVariants>\n>(({ className, variant, ...props }, ref) => {\n  return (\n    <ToastPrimitives.Root\n      ref={ref}\n      className={cn(toastVariants({ variant }), className)}\n      {...props}\n    />\n  );\n});\nToast.displayName = ToastPrimitives.Root.displayName;\n\nconst ToastAction = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Action>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Action\n    ref={ref}\n    className={cn(\n      \"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive\",\n      className,\n    )}\n    {...props}\n  />\n));\nToastAction.displayName = ToastPrimitives.Action.displayName;\n\nconst ToastClose = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Close>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Close\n    ref={ref}\n    className={cn(\n      \"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600\",\n      className,\n    )}\n    toast-close=\"\"\n    {...props}\n  >\n    <X className=\"size-4\" />\n  </ToastPrimitives.Close>\n));\nToastClose.displayName = ToastPrimitives.Close.displayName;\n\nconst ToastTitle = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Title>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Title\n    ref={ref}\n    className={cn(\"text-sm font-semibold\", className)}\n    {...props}\n  />\n));\nToastTitle.displayName = ToastPrimitives.Title.displayName;\n\nconst ToastDescription = React.forwardRef<\n  React.ElementRef<typeof ToastPrimitives.Description>,\n  React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>\n>(({ className, ...props }, ref) => (\n  <ToastPrimitives.Description\n    ref={ref}\n    className={cn(\"text-sm opacity-90\", className)}\n    {...props}\n  />\n));\nToastDescription.displayName = ToastPrimitives.Description.displayName;\n\ntype ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;\n\ntype ToastActionElement = React.ReactElement<typeof ToastAction>;\n\nexport {\n  type ToastProps,\n  type ToastActionElement,\n  ToastProvider,\n  ToastViewport,\n  Toast,\n  ToastTitle,\n  ToastDescription,\n  ToastClose,\n  ToastAction,\n};\n"
  },
  {
    "path": "apps/web/components/ui/toggle.tsx",
    "content": "\"use client\";\n\nimport type { VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport * as TogglePrimitive from \"@radix-ui/react-toggle\";\nimport { cva } from \"class-variance-authority\";\n\nconst toggleVariants = cva(\n  \"inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-transparent\",\n        outline:\n          \"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground\",\n      },\n      size: {\n        default: \"h-10 px-3\",\n        sm: \"h-9 px-2.5\",\n        lg: \"h-11 px-5\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nconst Toggle = React.forwardRef<\n  React.ElementRef<typeof TogglePrimitive.Root>,\n  React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &\n    VariantProps<typeof toggleVariants>\n>(({ className, variant, size, ...props }, ref) => (\n  <TogglePrimitive.Root\n    ref={ref}\n    className={cn(toggleVariants({ variant, size, className }))}\n    {...props}\n  />\n));\n\nToggle.displayName = TogglePrimitive.Root.displayName;\n\nexport { Toggle, toggleVariants };\n"
  },
  {
    "path": "apps/web/components/ui/tooltip.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\";\n\nconst TooltipProvider = TooltipPrimitive.Provider;\n\nconst Tooltip = TooltipPrimitive.Root;\n\nconst TooltipTrigger = TooltipPrimitive.Trigger;\nconst TooltipPortal = TooltipPrimitive.Portal;\n\nconst TooltipContent = React.forwardRef<\n  React.ElementRef<typeof TooltipPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>\n>(({ className, sideOffset = 4, ...props }, ref) => (\n  <TooltipPrimitive.Content\n    ref={ref}\n    sideOffset={sideOffset}\n    className={cn(\n      \"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md 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\",\n      className,\n    )}\n    {...props}\n  />\n));\nTooltipContent.displayName = TooltipPrimitive.Content.displayName;\n\nexport {\n  Tooltip,\n  TooltipTrigger,\n  TooltipContent,\n  TooltipProvider,\n  TooltipPortal,\n};\n"
  },
  {
    "path": "apps/web/components/ui/user-avatar.tsx",
    "content": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport { Avatar, AvatarFallback, AvatarImage } from \"@/components/ui/avatar\";\nimport { cn } from \"@/lib/utils\";\n\nimport { getAssetUrl } from \"@karakeep/shared/utils/assetUtils\";\n\ninterface UserAvatarProps {\n  image?: string | null;\n  name?: string | null;\n  className?: string;\n  imgClassName?: string;\n  fallbackClassName?: string;\n  fallback?: React.ReactNode;\n}\n\nconst isExternalUrl = (value: string) =>\n  value.startsWith(\"http://\") || value.startsWith(\"https://\");\n\nexport function UserAvatar({\n  image,\n  name,\n  className,\n  imgClassName,\n  fallbackClassName,\n  fallback,\n}: UserAvatarProps) {\n  const avatarUrl = useMemo(() => {\n    if (!image) {\n      return null;\n    }\n    return isExternalUrl(image) ? image : getAssetUrl(image);\n  }, [image]);\n\n  const fallbackContent = fallback ?? name?.charAt(0) ?? \"U\";\n\n  return (\n    <Avatar className={className}>\n      {avatarUrl && (\n        <AvatarImage\n          src={avatarUrl}\n          alt={name ?? \"User\"}\n          className={cn(\"object-cover\", imgClassName)}\n        />\n      )}\n      <AvatarFallback className={cn(\"text-sm font-medium\", fallbackClassName)}>\n        {fallbackContent}\n      </AvatarFallback>\n    </Avatar>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/utils/BookmarkAlreadyExistsToast.tsx",
    "content": "import Link from \"next/link\";\nimport { ExternalLink } from \"lucide-react\";\n\nexport default function BookmarkAlreadyExistsToast({\n  bookmarkId,\n}: {\n  bookmarkId: string;\n}) {\n  return (\n    <div className=\"flex items-center gap-1\">\n      Bookmark already exists.\n      <Link\n        className=\"flex underline-offset-4 hover:underline\"\n        href={`/dashboard/preview/${bookmarkId}`}\n      >\n        Open <ExternalLink className=\"ml-1 size-4\" />\n      </Link>\n    </div>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/utils/ValidAccountCheck.tsx",
    "content": "\"use client\";\n\nimport { useEffect } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport { useQuery } from \"@tanstack/react-query\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\n/**\n * This component is used to address a confusion when the JWT token exists but the user no longer exists in the database.\n * So this component synchronusly checks if the user is still valid and if not, signs out the user.\n */\nexport default function ValidAccountCheck() {\n  const api = useTRPC();\n  const router = useRouter();\n  const { error } = useQuery(\n    api.users.whoami.queryOptions(undefined, {\n      retry: (_failureCount, error) => {\n        if (error.data?.code === \"UNAUTHORIZED\") {\n          return false;\n        }\n        return true;\n      },\n    }),\n  );\n  useEffect(() => {\n    if (error?.data?.code === \"UNAUTHORIZED\") {\n      router.push(\"/logout\");\n    }\n  }, [error]);\n\n  return <></>;\n}\n"
  },
  {
    "path": "apps/web/components/utils/useShowArchived.tsx",
    "content": "import { useCallback } from \"react\";\nimport { useUserSettings } from \"@/lib/userSettings\";\nimport { parseAsBoolean, useQueryState } from \"nuqs\";\n\nexport function useShowArchived() {\n  const userSettings = useUserSettings();\n  const [showArchived, setShowArchived] = useQueryState(\n    \"includeArchived\",\n    parseAsBoolean\n      .withOptions({\n        shallow: false,\n      })\n      .withDefault(userSettings.archiveDisplayBehaviour === \"show\"),\n  );\n\n  const onClickShowArchived = useCallback(() => {\n    setShowArchived((prev) => !prev);\n  }, [setShowArchived]);\n\n  return {\n    showArchived,\n    onClickShowArchived,\n  };\n}\n"
  },
  {
    "path": "apps/web/components/wrapped/ShareButton.tsx",
    "content": "\"use client\";\n\nimport { RefObject, useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Download, Loader2, Share2 } from \"lucide-react\";\nimport { domToPng } from \"modern-screenshot\";\n\ninterface ShareButtonProps {\n  contentRef: RefObject<HTMLDivElement | null>;\n  fileName?: string;\n}\n\nexport function ShareButton({\n  contentRef,\n  fileName = \"karakeep-wrapped-2025.png\",\n}: ShareButtonProps) {\n  const [isGenerating, setIsGenerating] = useState(false);\n\n  const handleShare = async () => {\n    if (!contentRef.current) return;\n\n    setIsGenerating(true);\n\n    try {\n      // Capture the content as PNG data URL\n      const dataUrl = await domToPng(contentRef.current, {\n        scale: 2, // Higher resolution\n        quality: 1,\n        debug: false,\n        width: contentRef.current.scrollWidth, // Capture full width\n        height: contentRef.current.scrollHeight, // Capture full height including scrolled content\n        drawImageInterval: 100, // Add delay for rendering\n      });\n\n      // Convert data URL to blob\n      const response = await fetch(dataUrl);\n      const blob = await response.blob();\n\n      // Try native share API first (works well on mobile)\n      if (\n        typeof navigator.share !== \"undefined\" &&\n        typeof navigator.canShare !== \"undefined\"\n      ) {\n        const file = new File([blob], fileName, { type: \"image/png\" });\n        if (navigator.canShare({ files: [file] })) {\n          await navigator.share({\n            files: [file],\n            title: \"My 2025 Karakeep Wrapped\",\n            text: \"Check out my 2025 Karakeep Wrapped!\",\n          });\n          return;\n        }\n      }\n\n      // Fallback: download the image\n      const a = document.createElement(\"a\");\n      a.href = dataUrl;\n      a.download = fileName;\n      document.body.appendChild(a);\n      a.click();\n      document.body.removeChild(a);\n    } catch (error) {\n      console.error(\"Failed to capture or share image:\", error);\n    } finally {\n      setIsGenerating(false);\n    }\n  };\n\n  const isNativeShareAvailable =\n    typeof navigator.share !== \"undefined\" &&\n    typeof navigator.canShare !== \"undefined\";\n\n  return (\n    <Button\n      onClick={handleShare}\n      disabled={isGenerating}\n      size=\"icon\"\n      variant=\"ghost\"\n      className=\"h-10 w-10 rounded-full bg-white/10 text-slate-100 hover:bg-white/20\"\n      aria-label={isNativeShareAvailable ? \"Share\" : \"Download\"}\n      title={isNativeShareAvailable ? \"Share\" : \"Download\"}\n    >\n      {isGenerating ? (\n        <Loader2 className=\"h-4 w-4 animate-spin\" />\n      ) : isNativeShareAvailable ? (\n        <Share2 className=\"h-4 w-4\" />\n      ) : (\n        <Download className=\"h-4 w-4\" />\n      )}\n    </Button>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/wrapped/WrappedContent.tsx",
    "content": "\"use client\";\n\nimport { forwardRef } from \"react\";\nimport { Badge } from \"@/components/ui/badge\";\nimport { Card } from \"@/components/ui/card\";\nimport {\n  BookOpen,\n  Calendar,\n  Chrome,\n  Clock,\n  Code,\n  FileText,\n  Globe,\n  Hash,\n  Heart,\n  Highlighter,\n  Link,\n  Rss,\n  Smartphone,\n  Upload,\n  Zap,\n} from \"lucide-react\";\nimport { z } from \"zod\";\n\nimport { zBookmarkSourceSchema } from \"@karakeep/shared/types/bookmarks\";\nimport { zWrappedStatsResponseSchema } from \"@karakeep/shared/types/users\";\n\ntype WrappedStats = z.infer<typeof zWrappedStatsResponseSchema>;\ntype BookmarkSource = z.infer<typeof zBookmarkSourceSchema>;\n\ninterface WrappedContentProps {\n  stats: WrappedStats;\n  userName?: string;\n}\n\nconst dayNames = [\n  \"Sunday\",\n  \"Monday\",\n  \"Tuesday\",\n  \"Wednesday\",\n  \"Thursday\",\n  \"Friday\",\n  \"Saturday\",\n];\nconst monthNames = [\n  \"Jan\",\n  \"Feb\",\n  \"Mar\",\n  \"Apr\",\n  \"May\",\n  \"Jun\",\n  \"Jul\",\n  \"Aug\",\n  \"Sep\",\n  \"Oct\",\n  \"Nov\",\n  \"Dec\",\n];\n\nfunction formatSourceName(source: BookmarkSource | null): string {\n  if (!source) return \"Unknown\";\n  const sourceMap: Record<BookmarkSource, string> = {\n    api: \"API\",\n    web: \"Web\",\n    extension: \"Browser Extension\",\n    cli: \"CLI\",\n    mobile: \"Mobile App\",\n    singlefile: \"SingleFile\",\n    rss: \"RSS Feed\",\n    import: \"Import\",\n  };\n  return sourceMap[source];\n}\n\nfunction getSourceIcon(source: BookmarkSource | null, className = \"h-5 w-5\") {\n  const iconProps = { className };\n  switch (source) {\n    case \"api\":\n      return <Zap {...iconProps} />;\n    case \"web\":\n      return <Globe {...iconProps} />;\n    case \"extension\":\n      return <Chrome {...iconProps} />;\n    case \"cli\":\n      return <Code {...iconProps} />;\n    case \"mobile\":\n      return <Smartphone {...iconProps} />;\n    case \"singlefile\":\n      return <FileText {...iconProps} />;\n    case \"rss\":\n      return <Rss {...iconProps} />;\n    case \"import\":\n      return <Upload {...iconProps} />;\n    default:\n      return <Globe {...iconProps} />;\n  }\n}\n\nexport const WrappedContent = forwardRef<HTMLDivElement, WrappedContentProps>(\n  ({ stats, userName }, ref) => {\n    const maxMonthlyCount = Math.max(\n      ...stats.monthlyActivity.map((m) => m.count),\n    );\n\n    return (\n      <div\n        ref={ref}\n        className=\"min-h-screen w-full overflow-auto bg-slate-950 bg-[radial-gradient(1200px_600px_at_20%_-10%,rgba(16,185,129,0.18),transparent),radial-gradient(900px_500px_at_90%_10%,rgba(14,116,144,0.2),transparent)] p-6 text-slate-100 md:p-8\"\n      >\n        <div className=\"mx-auto max-w-5xl space-y-4\">\n          {/* Header */}\n          <div className=\"flex flex-col gap-2 md:flex-row md:items-end md:justify-between\">\n            <div>\n              <h1 className=\"text-2xl font-semibold md:text-3xl\">\n                Your {stats.year} Wrapped\n              </h1>\n              <p className=\"mt-1 text-xs text-slate-300 md:text-sm\">\n                A Year in Karakeep\n              </p>\n              {userName && (\n                <p className=\"mt-2 text-sm text-slate-400\">{userName}</p>\n              )}\n            </div>\n          </div>\n\n          <div className=\"grid gap-3 md:grid-cols-2 lg:grid-cols-3\">\n            <Card className=\"flex flex-col items-center justify-center border border-white/10 bg-white/5 p-4 text-center text-slate-100 backdrop-blur-sm\">\n              <p className=\"text-xs text-slate-300\">You saved</p>\n              <p className=\"my-2 text-3xl font-semibold md:text-4xl\">\n                {stats.totalBookmarks}\n              </p>\n              <p className=\"text-xs text-slate-300\">\n                {stats.totalBookmarks === 1 ? \"item\" : \"items\"} this year\n              </p>\n            </Card>\n            {/* First Bookmark */}\n            {stats.firstBookmark && (\n              <Card className=\"border border-white/10 bg-white/5 p-4 text-slate-100 backdrop-blur-sm\">\n                <div className=\"flex h-full flex-col\">\n                  <div className=\"mb-3 flex items-center gap-2\">\n                    <Calendar className=\"h-4 w-4 flex-shrink-0 text-emerald-300\" />\n                    <p className=\"text-[10px] uppercase tracking-wide text-slate-400\">\n                      First Bookmark of {stats.year}\n                    </p>\n                  </div>\n                  <div className=\"flex-1\">\n                    <p className=\"text-2xl font-bold text-slate-100\">\n                      {new Date(\n                        stats.firstBookmark.createdAt,\n                      ).toLocaleDateString(\"en-US\", {\n                        month: \"long\",\n                        day: \"numeric\",\n                      })}\n                    </p>\n                    {stats.firstBookmark.title && (\n                      <p className=\"mt-2 line-clamp-2 text-base leading-relaxed text-slate-300\">\n                        &ldquo;{stats.firstBookmark.title}&rdquo;\n                      </p>\n                    )}\n                  </div>\n                </div>\n              </Card>\n            )}\n\n            {/* Activity + Peak */}\n            <Card className=\"border border-white/10 bg-white/5 p-4 text-slate-100 backdrop-blur-sm\">\n              <h2 className=\"mb-2 flex items-center gap-2 text-sm font-semibold uppercase tracking-wide text-slate-300\">\n                <Clock className=\"h-4 w-4\" />\n                Activity Highlights\n              </h2>\n              <div className=\"grid gap-2 text-sm\">\n                {stats.mostActiveDay && (\n                  <div>\n                    <p className=\"text-xs text-slate-400\">Most Active Day</p>\n                    <p className=\"text-base font-semibold\">\n                      {new Date(stats.mostActiveDay.date).toLocaleDateString(\n                        \"en-US\",\n                        {\n                          month: \"short\",\n                          day: \"numeric\",\n                        },\n                      )}\n                    </p>\n                    <p className=\"text-xs text-slate-400\">\n                      {stats.mostActiveDay.count}{\" \"}\n                      {stats.mostActiveDay.count === 1 ? \"save\" : \"saves\"}\n                    </p>\n                  </div>\n                )}\n                <div className=\"grid grid-cols-2 gap-2\">\n                  <div>\n                    <p className=\"text-xs text-slate-400\">Peak Hour</p>\n                    <p className=\"text-base font-semibold\">\n                      {stats.peakHour === 0\n                        ? \"12 AM\"\n                        : stats.peakHour < 12\n                          ? `${stats.peakHour} AM`\n                          : stats.peakHour === 12\n                            ? \"12 PM\"\n                            : `${stats.peakHour - 12} PM`}\n                    </p>\n                  </div>\n                  <div>\n                    <p className=\"text-xs text-slate-400\">Peak Day</p>\n                    <p className=\"text-base font-semibold\">\n                      {dayNames[stats.peakDayOfWeek]}\n                    </p>\n                  </div>\n                </div>\n              </div>\n            </Card>\n\n            {/* Top Lists */}\n            {(stats.topDomains.length > 0 || stats.topTags.length > 0) && (\n              <Card className=\"border border-white/10 bg-white/5 p-4 text-slate-100 backdrop-blur-sm md:col-span-2 lg:col-span-2\">\n                <h2 className=\"mb-2 text-sm font-semibold uppercase tracking-wide text-slate-300\">\n                  Top Lists\n                </h2>\n                <div className=\"grid gap-3 md:grid-cols-2\">\n                  {stats.topDomains.length > 0 && (\n                    <div>\n                      <h3 className=\"mb-1 flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-slate-400\">\n                        <Globe className=\"h-3.5 w-3.5\" />\n                        Sites\n                      </h3>\n                      <div className=\"space-y-1.5 text-sm\">\n                        {stats.topDomains.map((domain, index) => (\n                          <div\n                            key={domain.domain}\n                            className=\"flex items-center justify-between\"\n                          >\n                            <div className=\"flex items-center gap-2\">\n                              <div className=\"flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-white/10 text-[10px] font-semibold text-slate-200\">\n                                {index + 1}\n                              </div>\n                              <span className=\"text-slate-100\">\n                                {domain.domain}\n                              </span>\n                            </div>\n                            <Badge className=\"bg-white/10 text-[10px] text-slate-200\">\n                              {domain.count}\n                            </Badge>\n                          </div>\n                        ))}\n                      </div>\n                    </div>\n                  )}\n                  {stats.topTags.length > 0 && (\n                    <div>\n                      <h3 className=\"mb-1 flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-slate-400\">\n                        <Hash className=\"h-3.5 w-3.5\" />\n                        Tags\n                      </h3>\n                      <div className=\"space-y-1.5 text-sm\">\n                        {stats.topTags.map((tag, index) => (\n                          <div\n                            key={tag.name}\n                            className=\"flex items-center justify-between\"\n                          >\n                            <div className=\"flex items-center gap-2\">\n                              <div className=\"flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-white/10 text-[10px] font-semibold text-slate-200\">\n                                {index + 1}\n                              </div>\n                              <span className=\"text-slate-100\">{tag.name}</span>\n                            </div>\n                            <Badge className=\"bg-white/10 text-[10px] text-slate-200\">\n                              {tag.count}\n                            </Badge>\n                          </div>\n                        ))}\n                      </div>\n                    </div>\n                  )}\n                </div>\n              </Card>\n            )}\n\n            {/* Bookmarks by Source */}\n            {stats.bookmarksBySource.length > 0 && (\n              <Card className=\"border border-white/10 bg-white/5 p-4 text-slate-100 backdrop-blur-sm\">\n                <h2 className=\"mb-3 text-sm font-semibold uppercase tracking-wide text-slate-300\">\n                  How You Save\n                </h2>\n                <div className=\"space-y-1.5 text-sm\">\n                  {stats.bookmarksBySource.map((source) => (\n                    <div\n                      key={source.source || \"unknown\"}\n                      className=\"flex items-center justify-between\"\n                    >\n                      <div className=\"flex items-center gap-2 text-slate-100\">\n                        {getSourceIcon(source.source, \"h-4 w-4\")}\n                        <span>{formatSourceName(source.source)}</span>\n                      </div>\n                      <Badge className=\"bg-white/10 text-[10px] text-slate-200\">\n                        {source.count}\n                      </Badge>\n                    </div>\n                  ))}\n                </div>\n              </Card>\n            )}\n\n            {/* Monthly Activity */}\n            <Card className=\"border border-white/10 bg-white/5 p-4 text-slate-100 backdrop-blur-sm md:col-span-2 lg:col-span-3\">\n              <h2 className=\"mb-3 flex items-center gap-2 text-sm font-semibold uppercase tracking-wide text-slate-300\">\n                <Calendar className=\"h-4 w-4\" />\n                Your Year in Saves\n              </h2>\n              <div className=\"grid gap-2 text-xs md:grid-cols-2 lg:grid-cols-3\">\n                {stats.monthlyActivity.map((month) => (\n                  <div key={month.month} className=\"flex items-center gap-2\">\n                    <div className=\"w-7 text-right text-[10px] text-slate-400\">\n                      {monthNames[month.month - 1]}\n                    </div>\n                    <div className=\"relative h-2 flex-1 overflow-hidden rounded-full bg-white/10\">\n                      <div\n                        className=\"h-full rounded-full bg-emerald-300/70\"\n                        style={{\n                          width: `${maxMonthlyCount > 0 ? (month.count / maxMonthlyCount) * 100 : 0}%`,\n                        }}\n                      />\n                    </div>\n                    <div className=\"w-7 text-[10px] text-slate-300\">\n                      {month.count}\n                    </div>\n                  </div>\n                ))}\n              </div>\n            </Card>\n\n            {/* Summary Stats */}\n            <Card className=\"border border-white/10 bg-white/5 p-4 text-slate-100 backdrop-blur-sm md:col-span-2 lg:col-span-3\">\n              <div className=\"grid gap-3 text-center sm:grid-cols-3\">\n                <div className=\"rounded-lg bg-white/5 p-3\">\n                  <Heart className=\"mx-auto mb-1 h-4 w-4 text-rose-200\" />\n                  <p className=\"text-lg font-semibold\">\n                    {stats.totalFavorites}\n                  </p>\n                  <p className=\"text-[10px] text-slate-400\">Favorites</p>\n                </div>\n                <div className=\"rounded-lg bg-white/5 p-3\">\n                  <Hash className=\"mx-auto mb-1 h-4 w-4 text-amber-200\" />\n                  <p className=\"text-lg font-semibold\">{stats.totalTags}</p>\n                  <p className=\"text-[10px] text-slate-400\">Tags Created</p>\n                </div>\n                <div className=\"rounded-lg bg-white/5 p-3\">\n                  <Highlighter className=\"mx-auto mb-1 h-4 w-4 text-emerald-200\" />\n                  <p className=\"text-lg font-semibold\">\n                    {stats.totalHighlights}\n                  </p>\n                  <p className=\"text-[10px] text-slate-400\">Highlights</p>\n                </div>\n              </div>\n              <div className=\"mt-3 grid gap-3 text-center sm:grid-cols-3\">\n                <div className=\"rounded-lg bg-white/5 p-3\">\n                  <Link className=\"mx-auto mb-1 h-4 w-4 text-slate-200\" />\n                  <p className=\"text-lg font-semibold\">\n                    {stats.bookmarksByType.link}\n                  </p>\n                  <p className=\"text-[10px] text-slate-400\">Links</p>\n                </div>\n                <div className=\"rounded-lg bg-white/5 p-3\">\n                  <FileText className=\"mx-auto mb-1 h-4 w-4 text-slate-200\" />\n                  <p className=\"text-lg font-semibold\">\n                    {stats.bookmarksByType.text}\n                  </p>\n                  <p className=\"text-[10px] text-slate-400\">Notes</p>\n                </div>\n                <div className=\"rounded-lg bg-white/5 p-3\">\n                  <BookOpen className=\"mx-auto mb-1 h-4 w-4 text-slate-200\" />\n                  <p className=\"text-lg font-semibold\">\n                    {stats.bookmarksByType.asset}\n                  </p>\n                  <p className=\"text-[10px] text-slate-400\">Assets</p>\n                </div>\n              </div>\n            </Card>\n          </div>\n\n          {/* Footer */}\n          <div className=\"pb-4 pt-1 text-center text-[10px] text-slate-500\">\n            Made with Karakeep\n          </div>\n        </div>\n      </div>\n    );\n  },\n);\n\nWrappedContent.displayName = \"WrappedContent\";\n"
  },
  {
    "path": "apps/web/components/wrapped/WrappedModal.tsx",
    "content": "\"use client\";\n\nimport { useRef } from \"react\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogOverlay,\n  DialogTitle,\n} from \"@/components/ui/dialog\";\nimport * as VisuallyHidden from \"@radix-ui/react-visually-hidden\";\nimport { useQuery } from \"@tanstack/react-query\";\nimport { Loader2, X } from \"lucide-react\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\nimport { ShareButton } from \"./ShareButton\";\nimport { WrappedContent } from \"./WrappedContent\";\n\ninterface WrappedModalProps {\n  open: boolean;\n  onClose: () => void;\n}\n\nexport function WrappedModal({ open, onClose }: WrappedModalProps) {\n  const api = useTRPC();\n  const contentRef = useRef<HTMLDivElement | null>(null);\n  const { data: stats, isLoading } = useQuery(\n    api.users.wrapped.queryOptions(undefined, {\n      enabled: open,\n    }),\n  );\n  const { data: whoami } = useQuery(\n    api.users.whoami.queryOptions(undefined, {\n      enabled: open,\n    }),\n  );\n\n  return (\n    <Dialog open={open} onOpenChange={onClose}>\n      <DialogOverlay className=\"z-50\" />\n      <DialogContent\n        className=\"max-w-screen h-screen max-h-screen w-screen overflow-hidden rounded-none border-none p-0\"\n        hideCloseBtn={true}\n      >\n        <VisuallyHidden.Root>\n          <DialogTitle>Your 2025 Wrapped</DialogTitle>\n        </VisuallyHidden.Root>\n        <div className=\"fixed right-4 top-4 z-50 flex items-center gap-2\">\n          {/* Share button overlay */}\n          {stats && !isLoading && <ShareButton contentRef={contentRef} />}\n          {/* Close button overlay */}\n          <button\n            onClick={onClose}\n            className=\"rounded-full bg-white/10 p-2 backdrop-blur-sm transition-colors hover:bg-white/20\"\n            aria-label=\"Close\"\n            title=\"Close\"\n          >\n            <X className=\"h-5 w-5 text-white\" />\n          </button>\n        </div>\n\n        {/* Content */}\n        {isLoading ? (\n          <div className=\"flex h-full items-center justify-center bg-slate-950 bg-[radial-gradient(1200px_600px_at_20%_-10%,rgba(16,185,129,0.18),transparent),radial-gradient(900px_500px_at_90%_10%,rgba(14,116,144,0.2),transparent)]\">\n            <div className=\"text-center text-white\">\n              <Loader2 className=\"mx-auto mb-4 h-12 w-12 animate-spin\" />\n              <p className=\"text-xl\">Loading your Wrapped...</p>\n            </div>\n          </div>\n        ) : stats ? (\n          <WrappedContent\n            ref={contentRef}\n            stats={stats}\n            userName={whoami?.name || undefined}\n          />\n        ) : (\n          <div className=\"flex h-full items-center justify-center bg-slate-950 bg-[radial-gradient(1200px_600px_at_20%_-10%,rgba(16,185,129,0.18),transparent),radial-gradient(900px_500px_at_90%_10%,rgba(14,116,144,0.2),transparent)]\">\n            <div className=\"text-center text-white\">\n              <p className=\"text-xl\">Failed to load your Wrapped stats</p>\n              <button\n                onClick={onClose}\n                className=\"mt-4 rounded-lg bg-white/20 px-6 py-2 backdrop-blur-sm hover:bg-white/30\"\n              >\n                Close\n              </button>\n            </div>\n          </div>\n        )}\n      </DialogContent>\n    </Dialog>\n  );\n}\n"
  },
  {
    "path": "apps/web/components/wrapped/index.ts",
    "content": "export { WrappedModal } from \"./WrappedModal\";\nexport { WrappedContent } from \"./WrappedContent\";\nexport { ShareButton } from \"./ShareButton\";\n"
  },
  {
    "path": "apps/web/components.json",
    "content": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"default\",\n  \"rsc\": true,\n  \"tsx\": true,\n  \"tailwind\": {\n    \"config\": \"tailwind.config.ts\",\n    \"css\": \"app/globals.css\",\n    \"baseColor\": \"slate\",\n    \"cssVariables\": true,\n    \"prefix\": \"\"\n  },\n  \"iconLibrary\": \"lucide\",\n  \"aliases\": {\n    \"components\": \"@/components\",\n    \"utils\": \"@/lib/utils\"\n  },\n  \"registries\": {\n    \"@ncdai\": \"https://chanhdai.com/r/{name}.json\"\n  }\n}\n"
  },
  {
    "path": "apps/web/instrumentation.node.ts",
    "content": "import { initTracing } from \"@karakeep/shared-server\";\n\ninitTracing(\"web\");\n"
  },
  {
    "path": "apps/web/instrumentation.ts",
    "content": "export async function register() {\n  if (process.env.NEXT_RUNTIME === \"nodejs\") {\n    await import(\"./instrumentation.node\");\n  }\n}\n"
  },
  {
    "path": "apps/web/lib/attachments.tsx",
    "content": "import {\n  Archive,\n  Camera,\n  FileCode,\n  FileText,\n  Image,\n  Paperclip,\n  SquareUser,\n  Upload,\n  Video,\n} from \"lucide-react\";\n\nimport { ZAssetType } from \"@karakeep/shared/types/bookmarks\";\n\nexport const ASSET_TYPE_TO_ICON: Record<ZAssetType, React.ReactNode> = {\n  screenshot: <Camera className=\"size-4\" />,\n  pdf: <FileText className=\"size-4\" />,\n  assetScreenshot: <Camera className=\"size-4\" />,\n  fullPageArchive: <Archive className=\"size-4\" />,\n  precrawledArchive: <Archive className=\"size-4\" />,\n  bannerImage: <Image className=\"size-4\" />,\n  video: <Video className=\"size-4\" />,\n  bookmarkAsset: <Paperclip className=\"size-4\" />,\n  linkHtmlContent: <FileCode className=\"size-4\" />,\n  userUploaded: <Upload className=\"size-4\" />,\n  avatar: <SquareUser className=\"size-4\" />,\n  unknown: <Paperclip className=\"size-4\" />,\n};\n"
  },
  {
    "path": "apps/web/lib/auth/client.ts",
    "content": "\"use client\";\n\n/**\n * Centralized client-side auth utilities.\n * This module re-exports next-auth/react functions to allow for easier\n * future migration to a different auth provider.\n */\n\nexport { SessionProvider, signIn, signOut, useSession } from \"next-auth/react\";\n\nexport type { Session } from \"next-auth\";\n"
  },
  {
    "path": "apps/web/lib/bookmark-drag.ts",
    "content": "/**\n * MIME type used in HTML5 drag-and-drop dataTransfer to identify\n * bookmark card drags (as opposed to file drops).\n */\nexport const BOOKMARK_DRAG_MIME = \"application/x-karakeep-bookmark\";\n"
  },
  {
    "path": "apps/web/lib/bulkActions.ts",
    "content": "// reference article https://refine.dev/blog/zustand-react-state/#build-a-to-do-app-using-zustand\nimport { create } from \"zustand\";\n\nimport type { ZBookmark } from \"@karakeep/shared/types/bookmarks\";\nimport { ZBookmarkList } from \"@karakeep/shared/types/lists\";\n\ninterface BookmarkState {\n  selectedBookmarks: ZBookmark[];\n  visibleBookmarks: ZBookmark[];\n  isBulkEditEnabled: boolean;\n  setIsBulkEditEnabled: (isEnabled: boolean) => void;\n  toggleBookmark: (bookmark: ZBookmark) => void;\n  setVisibleBookmarks: (visibleBookmarks: ZBookmark[]) => void;\n  selectAll: () => void;\n  unSelectAll: () => void;\n  isEverythingSelected: () => boolean;\n  setListContext: (listContext: ZBookmarkList | undefined) => void;\n  listContext: ZBookmarkList | undefined;\n}\n\nconst useBulkActionsStore = create<BookmarkState>((set, get) => ({\n  selectedBookmarks: [],\n  visibleBookmarks: [],\n  isBulkEditEnabled: false,\n  listContext: undefined,\n\n  toggleBookmark: (bookmark: ZBookmark) => {\n    const selectedBookmarks = get().selectedBookmarks;\n    const isBookmarkAlreadySelected = selectedBookmarks.some(\n      (b) => b.id === bookmark.id,\n    );\n    if (isBookmarkAlreadySelected) {\n      set({\n        selectedBookmarks: selectedBookmarks.filter(\n          (b) => b.id !== bookmark.id,\n        ),\n      });\n    } else {\n      set({ selectedBookmarks: [...selectedBookmarks, bookmark] });\n    }\n  },\n\n  selectAll: () => {\n    set({ selectedBookmarks: get().visibleBookmarks });\n  },\n  unSelectAll: () => {\n    set({ selectedBookmarks: [] });\n  },\n\n  isEverythingSelected: () => {\n    return get().selectedBookmarks.length === get().visibleBookmarks.length;\n  },\n\n  setIsBulkEditEnabled: (isEnabled) => {\n    set({ isBulkEditEnabled: isEnabled });\n    set({ selectedBookmarks: [] });\n  },\n\n  setVisibleBookmarks: (visibleBookmarks: ZBookmark[]) => {\n    set({\n      visibleBookmarks,\n    });\n  },\n  setListContext: (listContext: ZBookmarkList | undefined) => {\n    set({ listContext });\n  },\n}));\n\nexport default useBulkActionsStore;\n"
  },
  {
    "path": "apps/web/lib/bulkTagActions.ts",
    "content": "import { create } from \"zustand\";\n\ninterface TagState {\n  selectedTagIds: string[];\n  visibleTagIds: string[];\n  isBulkEditEnabled: boolean;\n  setIsBulkEditEnabled: (isEnabled: boolean) => void;\n  toggleTag: (tagId: string) => void;\n  setVisibleTagIds: (visibleTagIds: string[]) => void;\n  selectAll: () => void;\n  unSelectAll: () => void;\n  isEverythingSelected: () => boolean;\n  isTagSelected: (tagId: string) => boolean;\n}\n\nconst useBulkTagActionsStore = create<TagState>((set, get) => ({\n  selectedTagIds: [],\n  visibleTagIds: [],\n  isBulkEditEnabled: false,\n\n  toggleTag: (tagId: string) => {\n    const selectedTagIds = get().selectedTagIds;\n    set({\n      selectedTagIds: selectedTagIds.includes(tagId)\n        ? selectedTagIds.filter((id) => id !== tagId)\n        : [...selectedTagIds, tagId],\n    });\n  },\n\n  selectAll: () => {\n    set({ selectedTagIds: get().visibleTagIds });\n  },\n  unSelectAll: () => {\n    set({ selectedTagIds: [] });\n  },\n\n  isEverythingSelected: () => {\n    return get().selectedTagIds.length === get().visibleTagIds.length;\n  },\n\n  setIsBulkEditEnabled: (isEnabled) => {\n    set({\n      isBulkEditEnabled: isEnabled,\n      selectedTagIds: [],\n    });\n  },\n\n  setVisibleTagIds: (visibleTagIds: string[]) => {\n    set({\n      visibleTagIds,\n      selectedTagIds: get().selectedTagIds.filter((id) =>\n        visibleTagIds.includes(id),\n      ),\n    });\n  },\n  isTagSelected: (tagId: string) => {\n    return get().selectedTagIds.includes(tagId);\n  },\n}));\n\nexport default useBulkTagActionsStore;\n"
  },
  {
    "path": "apps/web/lib/clientConfig.tsx",
    "content": "import { createContext, useContext } from \"react\";\n\nimport type { ClientConfig } from \"@karakeep/shared/config\";\n\nexport const ClientConfigCtx = createContext<ClientConfig>({\n  publicUrl: \"\",\n  publicApiUrl: \"\",\n  demoMode: undefined,\n  auth: {\n    disableSignups: false,\n    disablePasswordAuth: false,\n    oauthAutoRedirect: false,\n  },\n  turnstile: null,\n  inference: {\n    isConfigured: false,\n    inferredTagLang: \"english\",\n    enableAutoTagging: false,\n    enableAutoSummarization: false,\n  },\n  legal: {\n    termsOfServiceUrl: undefined,\n    privacyPolicyUrl: undefined,\n  },\n  serverVersion: undefined,\n  disableNewReleaseCheck: true,\n});\n\nexport function useClientConfig() {\n  return useContext(ClientConfigCtx);\n}\n"
  },
  {
    "path": "apps/web/lib/drag-and-drop.ts",
    "content": "import React from \"react\";\nimport { DraggableEvent } from \"react-draggable\";\n\nexport interface DraggingState {\n  isDragging: boolean;\n  initialX: number;\n  initialY: number;\n}\n\nexport function useDragAndDrop(\n  dragTargetIdAttribute: string,\n  onDragOver: (dragTargetId: string) => void,\n  setDraggingState?: React.Dispatch<React.SetStateAction<DraggingState>>,\n) {\n  function findTargetId(element: HTMLElement): string | null {\n    let currentElement: HTMLElement | null = element;\n    while (currentElement) {\n      const listId = currentElement.getAttribute(dragTargetIdAttribute);\n      if (listId) {\n        return listId;\n      }\n      currentElement = currentElement.parentElement;\n    }\n    return null;\n  }\n\n  const handleDragStart = React.useCallback(\n    (e: DraggableEvent) => {\n      const rect = (e.target as HTMLElement).getBoundingClientRect();\n      setDraggingState?.({\n        isDragging: true,\n        initialX: rect.x,\n        initialY: rect.y,\n      });\n    },\n    [setDraggingState],\n  );\n\n  const handleDragEnd = React.useCallback(\n    (e: DraggableEvent) => {\n      const { target } = e;\n      const dragTargetId = findTargetId(target as HTMLElement);\n\n      if (dragTargetId) {\n        /*          As Draggable tries to setState when the\n          component is unmounted, it is needed to\n          push onCombine to the event loop queue.\n          onCombine would be run after setState on\n          Draggable so it would fix the issue until\n          they fix it on their end.\n      */\n        setTimeout(() => {\n          onDragOver(dragTargetId);\n        }, 0);\n      }\n      setDraggingState?.({\n        isDragging: false,\n        initialX: 0,\n        initialY: 0,\n      });\n    },\n    [onDragOver],\n  );\n\n  return {\n    handleDragStart,\n    handleDragEnd,\n  };\n}\n"
  },
  {
    "path": "apps/web/lib/haptic.ts",
    "content": "export const supportsHaptic =\n  typeof window !== \"undefined\"\n    ? window.matchMedia(\"(pointer: coarse)\").matches\n    : false;\n\n/**\n * Type guard to check if navigator supports vibrate API\n */\nfunction hasVibrate(\n  nav: Navigator,\n): nav is Navigator & { vibrate: (pattern: number | number[]) => boolean } {\n  return \"vibrate\" in nav && typeof nav.vibrate === \"function\";\n}\n\n/**\n * Trigger haptic feedback on mobile devices.\n *\n * Uses Vibration API on Android/modern browsers, and iOS checkbox trick on iOS.\n *\n * @example\n * import { haptic } from \"@/lib/haptic\"\n *\n * <Button onClick={haptic}>Haptic</Button>\n */\nexport function haptic() {\n  try {\n    if (!supportsHaptic) return;\n\n    if (hasVibrate(navigator)) {\n      navigator.vibrate(50);\n      return;\n    }\n\n    // iOS haptic trick via checkbox switch element\n    const label = document.createElement(\"label\");\n    label.ariaHidden = \"true\";\n    label.style.display = \"none\";\n\n    const input = document.createElement(\"input\");\n    input.type = \"checkbox\";\n    input.setAttribute(\"switch\", \"\");\n    label.appendChild(input);\n\n    try {\n      document.head.appendChild(label);\n      label.click();\n    } finally {\n      document.head.removeChild(label);\n    }\n  } catch {\n    // ignore\n  }\n}\n"
  },
  {
    "path": "apps/web/lib/hooks/bookmark-search.ts",
    "content": "import { useEffect, useMemo, useRef } from \"react\";\nimport { usePathname, useRouter, useSearchParams } from \"next/navigation\";\nimport { useSortOrderStore } from \"@/lib/store/useSortOrderStore\";\nimport { keepPreviousData, useInfiniteQuery } from \"@tanstack/react-query\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\nimport { parseSearchQuery } from \"@karakeep/shared/searchQueryParser\";\n\nimport { useInSearchPageStore } from \"../store/useInSearchPageStore\";\n\nfunction useSearchQuery() {\n  const searchParams = useSearchParams();\n  const searchQuery = searchParams.get(\"q\") ?? \"\";\n  const pathname = usePathname();\n  const lastQuery = useRef(searchQuery);\n\n  // Only update the effective search query when on the search page.\n  // This prevents the query from resetting when intercepting routes\n  // change the URL (e.g., opening a bookmark preview dialog).\n  if (pathname.startsWith(\"/dashboard/search\")) {\n    lastQuery.current = searchQuery;\n  }\n\n  const effectiveQuery = lastQuery.current;\n  const parsed = useMemo(\n    () => parseSearchQuery(effectiveQuery),\n    [effectiveQuery],\n  );\n  return { searchQuery: effectiveQuery, parsedSearchQuery: parsed };\n}\n\nexport function useDoBookmarkSearch() {\n  const router = useRouter();\n  const { searchQuery, parsedSearchQuery } = useSearchQuery();\n  const isInSearchPage = useInSearchPageStore((val) => val.inSearchPage);\n  const timeoutId = useRef<NodeJS.Timeout>(null);\n\n  useEffect(() => {\n    return () => {\n      if (!timeoutId.current) {\n        return;\n      }\n      clearTimeout(timeoutId.current);\n    };\n  }, [timeoutId]);\n\n  const doSearch = (val: string) => {\n    timeoutId.current = null;\n    router.replace(`/dashboard/search?q=${encodeURIComponent(val)}`);\n  };\n\n  const debounceSearch = (val: string) => {\n    if (timeoutId.current) {\n      clearTimeout(timeoutId.current);\n    }\n    timeoutId.current = setTimeout(() => {\n      doSearch(val);\n    }, 10);\n  };\n\n  return {\n    doSearch,\n    debounceSearch,\n    searchQuery,\n    parsedSearchQuery,\n    isInSearchPage,\n  };\n}\n\nexport function useBookmarkSearch() {\n  const api = useTRPC();\n  const { searchQuery } = useSearchQuery();\n  const sortOrder = useSortOrderStore((state) => state.sortOrder);\n\n  const {\n    data,\n    isPending,\n    isPlaceholderData,\n    error,\n    hasNextPage,\n    fetchNextPage,\n    isFetchingNextPage,\n    refetch,\n  } = useInfiniteQuery(\n    api.bookmarks.searchBookmarks.infiniteQueryOptions(\n      {\n        text: searchQuery,\n        sortOrder,\n      },\n      {\n        placeholderData: keepPreviousData,\n        gcTime: 0,\n        initialCursor: null,\n        getNextPageParam: (lastPage) => lastPage.nextCursor,\n      },\n    ),\n  );\n\n  useEffect(() => {\n    refetch();\n  }, [refetch, sortOrder]);\n\n  if (error) {\n    throw error;\n  }\n\n  return {\n    error,\n    data,\n    isPending,\n    isPlaceholderData,\n    hasNextPage,\n    fetchNextPage,\n    isFetchingNextPage,\n  };\n}\n"
  },
  {
    "path": "apps/web/lib/hooks/relative-time.ts",
    "content": "import { useEffect, useState } from \"react\";\nimport { formatDistanceToNow } from \"date-fns\";\n\nexport default function useRelativeTime(date: Date) {\n  const [state, setState] = useState({\n    fromNow: \"\",\n    localCreatedAt: \"\",\n  });\n\n  // This is to avoid hydration errors when server and clients are in different timezones\n  useEffect(() => {\n    setState({\n      fromNow: formatDistanceToNow(date, { addSuffix: true }),\n      localCreatedAt: date.toLocaleString(),\n    });\n  }, [date]);\n\n  return state;\n}\n"
  },
  {
    "path": "apps/web/lib/hooks/upload-file.ts",
    "content": "import { useMutation } from \"@tanstack/react-query\";\n\nimport {\n  ZUploadError,\n  zUploadErrorSchema,\n  ZUploadResponse,\n  zUploadResponseSchema,\n} from \"@karakeep/shared/types/uploads\";\n\nexport default function useUpload({\n  onSuccess,\n  onError,\n}: {\n  onError?: (e: ZUploadError, req: File) => void;\n  onSuccess?: (resp: ZUploadResponse, req: File) => Promise<void>;\n}) {\n  return useMutation({\n    mutationFn: async (file: File) => {\n      const formData = new FormData();\n      formData.append(\"file\", file);\n      const resp = await fetch(\"/api/assets\", {\n        method: \"POST\",\n        body: formData,\n      });\n      if (!resp.ok) {\n        throw new Error(await resp.text());\n      }\n      return zUploadResponseSchema.parse(await resp.json());\n    },\n    onSuccess: onSuccess,\n    onError: (error, req) => {\n      const err = zUploadErrorSchema.parse(JSON.parse(error.message));\n      if (onError) {\n        onError(err, req);\n      }\n    },\n  });\n}\n"
  },
  {
    "path": "apps/web/lib/hooks/useBookmarkImport.ts",
    "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport { toast } from \"@/components/ui/sonner\";\nimport { useTranslation } from \"@/lib/i18n/client\";\nimport { useMutation, useQueryClient } from \"@tanstack/react-query\";\n\nimport { useCreateBookmarkList } from \"@karakeep/shared-react/hooks/lists\";\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\nimport {\n  importBookmarksFromFile,\n  ImportSource,\n  parseImportFile,\n} from \"@karakeep/shared/import-export\";\n\nimport { useCreateImportSession } from \"./useImportSessions\";\n\nexport interface ImportProgress {\n  done: number;\n  total: number;\n}\n\nexport function useBookmarkImport() {\n  const { t } = useTranslation();\n  const api = useTRPC();\n\n  const [importProgress, setImportProgress] = useState<ImportProgress | null>(\n    null,\n  );\n  const [quotaError, setQuotaError] = useState<string | null>(null);\n\n  const queryClient = useQueryClient();\n  const { mutateAsync: createImportSession } = useCreateImportSession();\n  const { mutateAsync: createList } = useCreateBookmarkList();\n  const { mutateAsync: stageImportedBookmarks } = useMutation(\n    api.importSessions.stageImportedBookmarks.mutationOptions(),\n  );\n  const { mutateAsync: finalizeImportStaging } = useMutation(\n    api.importSessions.finalizeImportStaging.mutationOptions(),\n  );\n\n  const uploadBookmarkFileMutation = useMutation({\n    mutationFn: async ({\n      file,\n      source,\n    }: {\n      file: File;\n      source: ImportSource;\n    }) => {\n      // Clear any previous quota error\n      setQuotaError(null);\n\n      // First, parse the file to count bookmarks\n      const textContent = await file.text();\n      const parsedImport = parseImportFile(source, textContent);\n      const bookmarkCount = parsedImport.bookmarks.length;\n\n      // Check quota before proceeding\n      if (bookmarkCount > 0) {\n        const quotaUsage = await queryClient.fetchQuery(\n          api.subscriptions.getQuotaUsage.queryOptions(),\n        );\n\n        if (\n          !quotaUsage.bookmarks.unlimited &&\n          quotaUsage.bookmarks.quota !== null\n        ) {\n          const remaining =\n            quotaUsage.bookmarks.quota - quotaUsage.bookmarks.used;\n\n          if (remaining < bookmarkCount) {\n            const errorMsg = `Cannot import ${bookmarkCount} bookmarks. You have ${remaining} bookmark${remaining === 1 ? \"\" : \"s\"} remaining in your quota of ${quotaUsage.bookmarks.quota}.`;\n            setQuotaError(errorMsg);\n            throw new Error(errorMsg);\n          }\n        }\n      }\n\n      // Proceed with import if quota check passes\n      const result = await importBookmarksFromFile(\n        {\n          file,\n          source,\n          rootListName: t(\"settings.import.imported_bookmarks\"),\n          deps: {\n            createImportSession,\n            createList,\n            stageImportedBookmarks,\n            finalizeImportStaging: async (sessionId: string) => {\n              await finalizeImportStaging({ importSessionId: sessionId });\n            },\n          },\n          onProgress: (done, total) => setImportProgress({ done, total }),\n        },\n        {\n          // Use a custom parser to avoid re-parsing the file\n          parsers: {\n            [source]: () => parsedImport,\n          },\n        },\n      );\n      return result;\n    },\n    onSuccess: async (result) => {\n      setImportProgress(null);\n\n      if (result.counts.total === 0) {\n        toast({ description: \"No bookmarks found in the file.\" });\n        return;\n      }\n\n      toast({\n        description: `Staged ${result.counts.total} bookmarks for import. Background processing will start automatically.`,\n        variant: \"default\",\n      });\n    },\n    onError: (error) => {\n      setImportProgress(null);\n      toast({\n        description: error.message,\n        variant: \"destructive\",\n      });\n    },\n  });\n\n  return {\n    importProgress,\n    quotaError,\n    clearQuotaError: () => setQuotaError(null),\n    runUploadBookmarkFile: uploadBookmarkFileMutation.mutateAsync,\n    isImporting: uploadBookmarkFileMutation.isPending,\n  };\n}\n"
  },
  {
    "path": "apps/web/lib/hooks/useDialogFormReset.ts",
    "content": "import type { FieldValues, UseFormReturn } from \"react-hook-form\";\nimport { useEffect, useRef } from \"react\";\n\n/**\n * Custom hook to handle form reset behavior in dialogs\n * Only resets the form when the dialog transitions from closed to open,\n * preventing loss of unsaved changes when external data updates occur\n *\n * @param open - Dialog open state\n * @param form - React Hook Form instance\n * @param resetData - Data to reset the form with\n */\nexport function useDialogFormReset<T extends FieldValues>(\n  open: boolean,\n  form: UseFormReturn<T>,\n  resetData: T,\n) {\n  const prevOpenRef = useRef(open);\n\n  useEffect(() => {\n    // Only reset form when transitioning from closed to open, not on data updates\n    if (open && !prevOpenRef.current) {\n      form.reset(resetData);\n    }\n    prevOpenRef.current = open;\n  }, [open, form, resetData]);\n}\n"
  },
  {
    "path": "apps/web/lib/hooks/useImportSessions.ts",
    "content": "\"use client\";\n\nimport { toast } from \"@/components/ui/sonner\";\nimport {\n  useInfiniteQuery,\n  useMutation,\n  useQuery,\n  useQueryClient,\n} from \"@tanstack/react-query\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\n\nexport function useCreateImportSession() {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n\n  return useMutation(\n    api.importSessions.createImportSession.mutationOptions({\n      onSuccess: () => {\n        queryClient.invalidateQueries(\n          api.importSessions.listImportSessions.pathFilter(),\n        );\n      },\n      onError: (error) => {\n        toast({\n          description: error.message || \"Failed to create import session\",\n          variant: \"destructive\",\n        });\n      },\n    }),\n  );\n}\n\nexport function useListImportSessions() {\n  const api = useTRPC();\n  return useQuery(\n    api.importSessions.listImportSessions.queryOptions(\n      {},\n      {\n        select: (data) => data.sessions,\n      },\n    ),\n  );\n}\n\nexport function useImportSessionStats(importSessionId: string) {\n  const api = useTRPC();\n  return useQuery(\n    api.importSessions.getImportSessionStats.queryOptions(\n      {\n        importSessionId,\n      },\n      {\n        refetchInterval: (q) =>\n          !q.state.data ||\n          ![\"completed\", \"failed\"].includes(q.state.data.status)\n            ? 5000\n            : false, // Refetch every 5 seconds to show progress\n        enabled: !!importSessionId,\n      },\n    ),\n  );\n}\n\nexport function useDeleteImportSession() {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n\n  return useMutation(\n    api.importSessions.deleteImportSession.mutationOptions({\n      onSuccess: () => {\n        queryClient.invalidateQueries(\n          api.importSessions.listImportSessions.pathFilter(),\n        );\n        toast({\n          description: \"Import session deleted successfully\",\n          variant: \"default\",\n        });\n      },\n      onError: (error) => {\n        toast({\n          description: error.message || \"Failed to delete import session\",\n          variant: \"destructive\",\n        });\n      },\n    }),\n  );\n}\n\nexport function usePauseImportSession() {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n\n  return useMutation(\n    api.importSessions.pauseImportSession.mutationOptions({\n      onSuccess: () => {\n        queryClient.invalidateQueries(\n          api.importSessions.listImportSessions.pathFilter(),\n        );\n        toast({\n          description: \"Import session paused\",\n          variant: \"default\",\n        });\n      },\n      onError: (error) => {\n        toast({\n          description: error.message || \"Failed to pause import session\",\n          variant: \"destructive\",\n        });\n      },\n    }),\n  );\n}\n\nexport function useResumeImportSession() {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n\n  return useMutation(\n    api.importSessions.resumeImportSession.mutationOptions({\n      onSuccess: () => {\n        queryClient.invalidateQueries(\n          api.importSessions.listImportSessions.pathFilter(),\n        );\n        toast({\n          description: \"Import session resumed\",\n          variant: \"default\",\n        });\n      },\n      onError: (error) => {\n        toast({\n          description: error.message || \"Failed to resume import session\",\n          variant: \"destructive\",\n        });\n      },\n    }),\n  );\n}\n\nexport function useImportSessionResults(\n  importSessionId: string,\n  filter: \"all\" | \"accepted\" | \"rejected\" | \"skipped_duplicate\" | \"pending\",\n) {\n  const api = useTRPC();\n  return useInfiniteQuery(\n    api.importSessions.getImportSessionResults.infiniteQueryOptions(\n      { importSessionId, filter, limit: 50 },\n      { getNextPageParam: (lastPage) => lastPage.nextCursor },\n    ),\n  );\n}\n"
  },
  {
    "path": "apps/web/lib/i18n/client.ts",
    "content": "\"use client\";\n\nimport i18next from \"i18next\";\nimport resourcesToBackend from \"i18next-resources-to-backend\";\nimport {\n  initReactI18next,\n  Trans as TransOrg,\n  useTranslation as useTranslationOrg,\n} from \"react-i18next\";\n\nimport { getOptions, languages } from \"./settings\";\n\nconst runsOnServerSide = typeof window === \"undefined\";\n\ni18next\n  .use(initReactI18next)\n  .use(\n    resourcesToBackend(\n      (language: string, namespace: string) =>\n        import(`./locales/${language}/${namespace}.json`),\n    ),\n  )\n  .init({\n    ...getOptions(),\n    lng: undefined, // let detect the language on client side\n    debug: false,\n    interpolation: {\n      escapeValue: false, // not needed for react as it escapes by default\n    },\n    preload: runsOnServerSide ? languages : [],\n  });\n\nexport const useTranslation = useTranslationOrg;\nexport const Trans = TransOrg;\nexport const i18n = i18next;\n"
  },
  {
    "path": "apps/web/lib/i18n/locales/ar/translation.json",
    "content": "{\n  \"common\": {\n    \"url\": \"الرابط\",\n    \"name\": \"الاسم\",\n    \"email\": \"البريد الإلكتروني\",\n    \"password\": \"كلمة المرور\",\n    \"action\": \"إجراء\",\n    \"actions\": \"إجراءات\",\n    \"created_at\": \"أنشئ في\",\n    \"key\": \"المفتاح\",\n    \"role\": \"الدور\",\n    \"roles\": {\n      \"user\": \"مستخدم\",\n      \"admin\": \"مدير\"\n    },\n    \"something_went_wrong\": \"حدث خطأ ما\",\n    \"experimental\": \"تجريبي\",\n    \"search\": \"بحث\",\n    \"tags\": \"وسوم\",\n    \"note\": \"ملاحظة\",\n    \"attachments\": \"مرفقات\",\n    \"highlights\": \"تمييزات\",\n    \"source\": \"المصدر\",\n    \"screenshot\": \"لقطة شاشة\",\n    \"video\": \"فيديو\",\n    \"archive\": \"أرشيف\",\n    \"home\": \"الرئيسية\",\n    \"bookmark_types\": {\n      \"title\": \"نوع الإشارة المرجعية\",\n      \"link\": \"رابط\",\n      \"text\": \"نص\",\n      \"media\": \"وسائط متعددة\"\n    },\n    \"type\": \"نوع\",\n    \"size\": \"حجم\",\n    \"title\": \"العنوان\",\n    \"description\": \"الوصف\",\n    \"summary\": \"ملخص\",\n    \"updated_at\": \"تم التحديث في\",\n    \"quota\": \"حصة\",\n    \"bookmarks\": \"الإشارات المرجعية\",\n    \"storage\": \"تخزين\",\n    \"pdf\": \"نسخة PDF مؤرشفة\",\n    \"default\": \"افتراضي\",\n    \"id\": \"معرف\",\n    \"last_used\": \"آخر استخدام\"\n  },\n  \"layouts\": {\n    \"masonry\": \"متعدد الأعمدة\",\n    \"grid\": \"شبكة\",\n    \"list\": \"قائمة\",\n    \"compact\": \"مضغوط\"\n  },\n  \"actions\": {\n    \"change_layout\": \"تغيير التخطيط\",\n    \"archive\": \"أرشفة\",\n    \"unarchive\": \"إلغاء الأرشفة\",\n    \"favorite\": \"إضافة للمفضلة\",\n    \"unfavorite\": \"إزالة من المفضلة\",\n    \"delete\": \"حذف\",\n    \"refresh\": \"تحديث\",\n    \"recrawl\": \"إعادة الاستكشاف\",\n    \"download_full_page_archive\": \"تحميل أرشيف الصفحة الكامل\",\n    \"edit_tags\": \"تحرير الوسوم\",\n    \"add_to_list\": \"إضافة إلى القائمة\",\n    \"select_all\": \"تحديد الكل\",\n    \"unselect_all\": \"إلغاء تحديد الكل\",\n    \"copy_link\": \"نسخ الرابط\",\n    \"close_bulk_edit\": \"إغلاق التحرير الجماعي\",\n    \"bulk_edit\": \"تحرير جماعي\",\n    \"manage_lists\": \"إدارة القوائم\",\n    \"remove_from_list\": \"إزالة من القائمة\",\n    \"save\": \"حفظ\",\n    \"add\": \"إضافة\",\n    \"edit\": \"تحرير\",\n    \"create\": \"إنشاء\",\n    \"fetch_now\": \"جلب الآن\",\n    \"summarize_with_ai\": \"تلخيص باستخدام الذكاء الاصطناعي\",\n    \"edit_title\": \"تحرير العنوان\",\n    \"sign_out\": \"تسجيل الخروج\",\n    \"close\": \"إغلاق\",\n    \"merge\": \"دمج\",\n    \"cancel\": \"إلغاء\",\n    \"apply_all\": \"تطبيق الكل\",\n    \"ignore\": \"تجاهل\",\n    \"sort\": {\n      \"title\": \"ترتيب\",\n      \"newest_first\": \"الأحدث أولاً\",\n      \"oldest_first\": \"الأقدم أولاً\",\n      \"relevant_first\": \"الأكثر صلة أولاً\"\n    },\n    \"open_editor\": \"فتح المحرر\",\n    \"toggle_show_archived\": \"اعرض المؤرشفة\",\n    \"confirm\": \"تأكيد\",\n    \"regenerate\": \"تجديد\",\n    \"load_more\": \"المزيد\",\n    \"edit_notes\": \"تحرير الملاحظات\",\n    \"preserve_as_pdf\": \"حفظ كملف PDF\",\n    \"offline_copies\": \"نسخ غير متصلة بالإنترنت\",\n    \"preserve_offline_archive\": \"احتفظ بالأرشيفات غير المتصلة بالإنترنت\",\n    \"download_full_page_archive_file\": \"تنزيل ملف الأرشيف\",\n    \"download_pdf_file\": \"تنزيل ملف PDF\",\n    \"remove\": \"إزالة\",\n    \"more\": \"المزيد\",\n    \"replace_banner\": \"استبدال البانر\",\n    \"add_banner\": \"إضافة بانر\",\n    \"download\": \"تنزيل\"\n  },\n  \"highlights\": {\n    \"no_highlights\": \"ليس لديك أي تمييزات بعد.\"\n  },\n  \"settings\": {\n    \"back_to_app\": \"العودة إلى التطبيق\",\n    \"user_settings\": \"إعدادات المستخدم\",\n    \"info\": {\n      \"user_info\": \"معلومات المستخدم\",\n      \"basic_details\": \"التفاصيل الأساسية\",\n      \"change_password\": \"تغيير كلمة المرور\",\n      \"current_password\": \"كلمة المرور الحالية\",\n      \"new_password\": \"كلمة المرور الجديدة\",\n      \"confirm_new_password\": \"تأكيد كلمة المرور الجديدة\",\n      \"options\": \"خيارات\",\n      \"interface_lang\": \"لغة الواجهة\",\n      \"user_settings\": {\n        \"user_settings_updated\": \"تم تحديث إعدادات المستخدم!\",\n        \"bookmark_click_action\": {\n          \"title\": \"إجراء النقر على الإشارة المرجعية\",\n          \"open_external_url\": \"افتح عنوان URL الأصلي\",\n          \"open_bookmark_details\": \"افتح تفاصيل الإشارة المرجعية\"\n        },\n        \"archive_display_behaviour\": {\n          \"title\": \"الإشارات المرجعية المؤرشفة\",\n          \"show\": \"اعرض الإشارات المرجعية المؤرشفة في العلامات والقوائم\",\n          \"hide\": \"إخفاء الإشارات المرجعية المؤرشفة في العلامات والقوائم\"\n        }\n      },\n      \"reader_settings\": {\n        \"local_overrides_title\": \"إعدادات خاصة بالجهاز مُفعلة\",\n        \"using_default\": \"استخدام الإعدادات الافتراضية للعميل\",\n        \"clear_override_hint\": \"امسح تجاوز الجهاز لاستخدام الإعداد العام ({{value}})\",\n        \"font_size\": \"حجم الخط\",\n        \"font_family\": \"نوع الخط\",\n        \"preview_inline\": \"(معاينة)\",\n        \"tooltip_preview\": \"تغييرات المعاينة غير المحفوظة\",\n        \"save_to_all_devices\": \"كل الأجهزة\",\n        \"tooltip_local\": \"إعدادات الجهاز تختلف عن الإعدادات العامة\",\n        \"reset_preview\": \"إعادة ضبط المعاينة\",\n        \"mono\": \"Monospace\",\n        \"line_height\": \"ارتفاع السطر\",\n        \"tooltip_default\": \"إعدادات القراءة\",\n        \"title\": \"إعدادات القارئ\",\n        \"serif\": \"Serif\",\n        \"preview\": \"معاينة\",\n        \"not_set\": \"غير مضبوط\",\n        \"clear_local_overrides\": \"مسح إعدادات الجهاز\",\n        \"preview_text\": \"الـ quick brown fox jumps over the lazy dog. ستظهر نصوص عرض القارئ بهذه الطريقة.\",\n        \"local_overrides_cleared\": \"تم مسح إعدادات الجهاز المخصصة\",\n        \"local_overrides_description\": \"يحتوي هذا الجهاز على إعدادات قارئ مختلفة عن الإعدادات الافتراضية العامة:\",\n        \"clear_defaults\": \"مسح كل الإعدادات الافتراضية\",\n        \"description\": \"اضبط إعدادات النص الافتراضية لعرض القارئ. تتم مزامنة هذه الإعدادات عبر جميع أجهزتك.\",\n        \"defaults_cleared\": \"تم مسح الإعدادات الافتراضية للقارئ\",\n        \"save_hint\": \"احفظ الإعدادات لهذا الجهاز فقط أو قم بمزامنتها عبر جميع الأجهزة\",\n        \"save_as_default\": \"حفظ كافتراضي\",\n        \"save_to_device\": \"هذا الجهاز\",\n        \"sans\": \"Sans Serif\",\n        \"tooltip_preview_and_local\": \"تغييرات المعاينة غير المحفوظة؛ إعدادات الجهاز تختلف عن الإعدادات العامة\",\n        \"adjust_hint\": \"اضبط الإعدادات أعلاه لمعاينة التغييرات\"\n      },\n      \"avatar\": {\n        \"upload\": \"ارفع الصورة الرمزية\",\n        \"change\": \"غير الصورة الرمزية\",\n        \"remove_confirm_title\": \"تشيل الصورة الرمزية؟\",\n        \"updated\": \"تم تحديث الصورة الرمزية\",\n        \"removed\": \"تمت إزالة الصورة الرمزية\",\n        \"description\": \"ارفع صورة مربعة عشان تستخدمها كصورة رمزية.\",\n        \"remove_confirm_description\": \"ده هيمسح صورة ملفك الشخصي الحالية.\",\n        \"title\": \"صورة الملف الشخصي\",\n        \"remove\": \"شيل الصورة الرمزية\"\n      }\n    },\n    \"ai\": {\n      \"ai_settings\": \"إعدادات الذكاء الاصطناعي\",\n      \"tagging_rules\": \"قواعد التوسيم\",\n      \"tagging_rule_description\": \"سيتم استخدام الإرشادات التي تضيفها هنا كقواعد للنموذج عند إنشاء الوسوم. يمكنك مراجعة الإرشادات النهائية في قسم المعاينة.\",\n      \"prompt_preview\": \"معاينة الإرشادات\",\n      \"text_prompt\": \"إرشادات النص\",\n      \"images_prompt\": \"إرشادات الصور\",\n      \"summarization_prompt\": \"إرشادات التلخيص\",\n      \"all_tagging\": \"التوسيم الشامل\",\n      \"text_tagging\": \"توسيم النصوص\",\n      \"image_tagging\": \"توسيم الصور\",\n      \"summarization\": \"التلخيص\",\n      \"tag_style\": \"نمط العلامة\",\n      \"auto_summarization_description\": \"إنشاء ملخصات تلقائيًا لعلاماتك المرجعية باستخدام الذكاء الاصطناعي.\",\n      \"auto_tagging\": \"وضع العلامات التلقائي\",\n      \"titlecase_spaces\": \"أحرف استهلالية مع مسافات\",\n      \"lowercase_underscores\": \"أحرف صغيرة مع شرطات سفلية\",\n      \"inference_language\": \"لُغة الاستنتاج\",\n      \"titlecase_hyphens\": \"أحرف استهلالية مع واصلات\",\n      \"lowercase_hyphens\": \"أحرف صغيرة مع واصلات\",\n      \"lowercase_spaces\": \"أحرف صغيرة مع مسافات\",\n      \"inference_language_description\": \"اختر اللغة الخاصة بالعلامات والملخصات التي تم إنشاؤها بواسطة الذكاء الاصطناعي.\",\n      \"tag_style_description\": \"اختر كيف ينبغي تنسيق علاماتك التي تم إنشاؤها تلقائيًا.\",\n      \"auto_tagging_description\": \"إنشاء علامات تلقائيًا لعلاماتك المرجعية باستخدام الذكاء الاصطناعي.\",\n      \"camelCase\": \"camelCase\",\n      \"auto_summarization\": \"التلخيص التلقائي\",\n      \"no_preference\": \"لا يوجد تفضيل\",\n      \"curated_tags\": \"العلامات المنسقة\",\n      \"curated_tags_description\": \"بشكل اختياري، قم بتقييد وضع علامات الذكاء الاصطناعي لاستخدام العلامات من هذه القائمة فقط. عندما لا يتم تحديد أي علامات، يقوم الذكاء الاصطناعي بإنشاء علامات بحرية.\",\n      \"curated_tags_updated\": \"تم تحديث العلامات المنسقة بنجاح!\",\n      \"curated_tags_update_failed\": \"فشل تحديث العلامات المنسقة\"\n    },\n    \"feeds\": {\n      \"rss_subscriptions\": \"اشتراكات RSS\",\n      \"add_a_subscription\": \"إضافة اشتراك\",\n      \"feed_enabled\": \"تمكين موجز RSS\",\n      \"feed_disabled\": \"تم تعطيل موجز RSS\"\n    },\n    \"webhooks\": {\n      \"webhooks\": \"واجهات الربط\",\n      \"description\": \"يمكنك استخدام واجهات الربط لتنفيذ إجراءات عند إنشاء الإشارات المرجعية أو تحديثها أو معالجتها\",\n      \"events\": {\n        \"title\": \"الأحداث\",\n        \"crawled\": \"تم الاستكشاف\",\n        \"created\": \"تم الإنشاء\",\n        \"edited\": \"تم التعديل\"\n      },\n      \"auth_token\": \"رمز التفويض\",\n      \"add_auth_token\": \"إضافة رمز تفويض\",\n      \"edit_auth_token\": \"تعديل رمز التفويض\",\n      \"create_webhook\": \"إنشاء واجهة ربط\",\n      \"delete_webhook\": \"حذف واجهة الربط\",\n      \"delete_webhook_confirmation\": \"هل تريد حذف واجهة الربط هذه؟\",\n      \"edit_webhook\": \"تعديل واجهة الربط\",\n      \"webhook_url\": \"رابط واجهة الربط\"\n    },\n    \"import\": {\n      \"import_export\": \"استيراد / تصدير\",\n      \"import_export_bookmarks\": \"استيراد / تصدير الإشارات المرجعية\",\n      \"import_bookmarks_from_html_file\": \"استيراد إشارات مرجعية من ملف HTML\",\n      \"import_bookmarks_from_pocket_export\": \"استيراد إشارات مرجعية من تصدير Pocket\",\n      \"import_bookmarks_from_matter_export\": \"استيراد إشارات مرجعية من تصدير Matter\",\n      \"import_bookmarks_from_omnivore_export\": \"استيراد إشارات مرجعية من تصدير Omnivore\",\n      \"import_bookmarks_from_linkwarden_export\": \"استيراد إشارات مرجعية من تصدير Linkwarden\",\n      \"import_bookmarks_from_karakeep_export\": \"استيراد إشارات مرجعية من تصدير Karakeep\",\n      \"export_links_and_notes\": \"تصدير الروابط والملاحظات\",\n      \"imported_bookmarks\": \"الإشارات المرجعية المستوردة\",\n      \"import_bookmarks_from_tab_session_manager_export\": \"استيراد الإشارات المرجعية من مدير جلسة علامات التبويب\",\n      \"import_bookmarks_from_mymind_export\": \"استيراد الإشارات المرجعية من تصدير mymind\",\n      \"import_bookmarks_from_instapaper_export\": \"استيراد الإشارات المرجعية من ملف تصدير Instapaper\"\n    },\n    \"api_keys\": {\n      \"api_keys\": \"مفاتيح API\",\n      \"new_api_key\": \"مفتاح API جديد\",\n      \"new_api_key_desc\": \"أعط مفتاح API اسماً فريداً\",\n      \"key_success\": \"تم إنشاء المفتاح بنجاح\",\n      \"key_success_please_copy\": \"يرجى نسخ المفتاح وتخزينه في مكان آمن. لن تتمكن من الوصول إليه مرة أخرى بعد إغلاق هذا الحوار.\",\n      \"regenerate_api_key\": \"تجديد مفتاح API\",\n      \"key_regenerated\": \"تم تجديد المفتاح بنجاح\",\n      \"key_regenerated_please_copy\": \"يرجى نسخ المفتاح الجديد وتخزينه في مكان آمن. تم إلغاء المفتاح القديم ولن يعمل بعد الآن.\",\n      \"regenerate_warning\": \"هل أنت متأكد أنك تريد تجديد مفتاح API لـ \\\"{{name}}\\\"?\",\n      \"regenerate_confirmation\": \"سيؤدي ذلك إلى إلغاء المفتاح الحالي وإنشاء مفتاح جديد. ستتوقف أي تطبيقات تستخدم المفتاح الحالي عن العمل.\"\n    },\n    \"broken_links\": {\n      \"broken_links\": \"روابط معطلة\",\n      \"last_crawled_at\": \"آخر استكشاف في\",\n      \"crawling_status\": \"حالة الاستكشاف\",\n      \"crawling_failed\": \"فشل الاستكشاف\"\n    },\n    \"manage_assets\": {\n      \"asset_type\": \"نوع الأصل\",\n      \"bookmark_link\": \"رابط الإشارة المرجعية\",\n      \"delete_asset\": \"حذف الأصل\",\n      \"delete_asset_confirmation\": \"هل أنت متأكد أنك تريد حذف هذا الأصل؟\",\n      \"asset_link\": \"رابط الأصل\",\n      \"manage_assets\": \"إدارة الأصول\",\n      \"no_assets\": \"ليس لديك أي أصول حتى الآن.\"\n    },\n    \"rules\": {\n      \"enter_rule_name\": \"أدخل اسم القاعدة\",\n      \"conditions_types\": {\n        \"or\": \"أي مما يلي صحيح\",\n        \"always\": \"دائمًا\",\n        \"url_contains\": \"عنوان URL يحتوي على\",\n        \"imported_from_feed\": \"تم الاستيراد من الخلاصة\",\n        \"bookmark_type_is\": \"نوع الإشارة المرجعية هو\",\n        \"has_tag\": \"يحتوي على وسم\",\n        \"is_favourited\": \"مفضل\",\n        \"is_archived\": \"مؤرشف\",\n        \"and\": \"كل ما يلي صحيح\",\n        \"url_does_not_contain\": \"عنوان URL لا يحتوي على\",\n        \"title_contains\": \"العنوان يتضمن\",\n        \"title_does_not_contain\": \"العنوان لا يتضمن\"\n      },\n      \"actions_types\": {\n        \"add_tag\": \"إضافة علامة\",\n        \"remove_tag\": \"إزالة الوسم\",\n        \"add_to_list\": \"أضف إلى القائمة\",\n        \"remove_from_list\": \"إزالة من القائمة\",\n        \"download_full_page_archive\": \"تنزيل أرشيف الصفحة الكاملة\",\n        \"favourite_bookmark\": \"إشارة مرجعية مفضلة\",\n        \"archive_bookmark\": \"أرشفة الإشارة المرجعية\"\n      },\n      \"rules\": \"محرك القواعد\",\n      \"rule_name\": \"اسم القاعدة\",\n      \"description\": \"يمكنك استخدام القواعد لتشغيل الإجراءات عند إطلاق حدث.\",\n      \"ceate_rule\": \"إنشاء قاعدة\",\n      \"edit_rule\": \"تعديل القاعدة\",\n      \"save_rule\": \"حفظ القاعدة\",\n      \"delete_rule\": \"حذف القاعدة\",\n      \"delete_rule_confirmation\": \"هل أنت متأكد أنك تريد حذف هذه القاعدة؟\",\n      \"whenever\": \"كلما ...\",\n      \"if\": \"إذا ...\",\n      \"describe_what_this_rule_does\": \"صف ما تفعله هذه القاعدة\",\n      \"rule_has_been_created\": \"تم إنشاء القاعدة!\",\n      \"rule_has_been_updated\": \"تم تحديث القاعدة!\",\n      \"rule_has_been_deleted\": \"تم حذف القاعدة!\",\n      \"no_rules_created_yet\": \"لم يتم إنشاء أي قواعد حتى الآن\",\n      \"create_your_first_rule\": \"أنشئ قاعدتك الأولى لأتمتة سير عملك\",\n      \"events_types\": {\n        \"bookmark_added\": \"تمت إضافة إشارة مرجعية\",\n        \"tag_added\": \"تمت إضافة هذه العلامة إلى إشارة مرجعية\",\n        \"tag_removed\": \"تمت إزالة هذا الوسم من إشارة مرجعية\",\n        \"added_to_list\": \"تمت إضافة إشارة مرجعية إلى هذه القائمة\",\n        \"removed_from_list\": \"تمت إزالة إشارة مرجعية من هذه القائمة\",\n        \"favourited\": \"تم تفضيل إشارة مرجعية\",\n        \"archived\": \"تم أرشفة إشارة مرجعية\"\n      }\n    },\n    \"stats\": {\n      \"usage_statistics\": \"إحصائيات الاستخدام\",\n      \"insights_description\": \"نظرة ثاقبة على عاداتك في وضع الإشارات المرجعية ومجموعتك\",\n      \"failed_to_load\": \"فشل تحميل الإحصائيات\",\n      \"overview\": {\n        \"total_bookmarks\": \"إجمالي الإشارات المرجعية\",\n        \"all_saved_items\": \"جميع العناصر المحفوظة\",\n        \"favorites\": \"المفضلة\",\n        \"starred_bookmarks\": \"الإشارات المرجعية المميزة بنجمة\",\n        \"archived\": \"مؤرشف\",\n        \"archived_items\": \"العناصر المؤرشفة\",\n        \"tags\": \"العلامات\",\n        \"unique_tags_created\": \"تم إنشاء علامات فريدة\",\n        \"lists\": \"قوائم\",\n        \"bookmark_collections\": \"مجموعات الإشارات المرجعية\",\n        \"highlights\": \"أبرز النقاط\",\n        \"text_highlights\": \"أبرز النقاط النصية\",\n        \"storage_used\": \"التخزين المستخدم\",\n        \"total_asset_storage\": \"إجمالي مساحة تخزين الأصول\",\n        \"this_month\": \"هذا الشهر\",\n        \"bookmarks_added\": \"تمت إضافة الإشارات المرجعية\"\n      },\n      \"bookmark_types\": {\n        \"title\": \"أنواع الإشارات المرجعية\",\n        \"links\": \"روابط\",\n        \"text_notes\": \"ملاحظات نصية\",\n        \"assets\": \"الأصول\"\n      },\n      \"recent_activity\": {\n        \"title\": \"النشاط الأخير\",\n        \"this_week\": \"هذا الأسبوع\",\n        \"this_month\": \"هذا الشهر\",\n        \"this_year\": \"هذا العام\"\n      },\n      \"top_domains\": {\n        \"title\": \"أهم النطاقات\",\n        \"no_domains_found\": \"لم يتم العثور على نطاقات\"\n      },\n      \"most_used_tags\": {\n        \"title\": \"العلامات الأكثر استخدامًا\",\n        \"no_tags_found\": \"لم يتم العثور على علامات\"\n      },\n      \"activity_patterns\": {\n        \"activity_by_hour\": \"النشاط حسب الساعة\",\n        \"activity_by_day\": \"النشاط حسب اليوم\"\n      },\n      \"storage_breakdown\": {\n        \"title\": \"تفصيل التخزين\"\n      },\n      \"bookmark_sources\": {\n        \"title\": \"مصادر الإشارات المرجعية\",\n        \"empty\": \"لا توجد بيانات مصدر متاحة\"\n      }\n    },\n    \"subscription\": {\n      \"subscription\": \"اشتراك\",\n      \"manage_subscription\": \"إدارة معلومات الاشتراك والفوترة الخاصة بك\",\n      \"current_plan\": \"الخطة الحالية\",\n      \"billing_period\": \"فترة الفوترة\",\n      \"paid_plan\": \"خطة مدفوعة\",\n      \"unlock_bigger_quota\": \"افتح حصة أكبر وادعم المشروع\",\n      \"subscribe_now\": \"اشترك الآن\",\n      \"manage_billing\": \"إدارة الفوترة\",\n      \"subscription_canceled\": \"تم إلغاء اشتراكك وسينتهي في {{date}}. يمكنك إعادة الاشتراك في أي وقت.\",\n      \"usage_quotas\": \"الاستخدام والحصص\",\n      \"track_usage\": \"تتبع استخدامك الحالي مقابل حدود خطتك\",\n      \"total_bookmarks_saved\": \"إجمالي الإشارات المرجعية المحفوظة\",\n      \"assets_file_storage\": \"الأصول وتخزين الملفات\",\n      \"unlimited_usage\": \"استخدام غير محدود\",\n      \"quota_limit_reached\": \"تم الوصول إلى حد الحصة\",\n      \"approaching_quota_limit\": \"الاقتراب من حد الحصة\",\n      \"loading_usage\": \"جارٍ تحميل معلومات الاستخدام...\",\n      \"free\": \"مجاني\",\n      \"paid\": \"مدفوعة\"\n    },\n    \"import_sessions\": {\n      \"title\": \"استيراد الجلسات\",\n      \"description\": \"اعرض وجّه جلسات الاستيراد المجمّع الخاصة بك وإدارتها. يتم إنشاء الجلسات تلقائيًا عند استيراد الإشارات المرجعية.\",\n      \"load_error\": \"فشل تحميل جلسات الاستيراد\",\n      \"no_sessions\": \"لا توجد جلسات استيراد حتى الآن\",\n      \"no_sessions_detail\": \"ستظهر جلسات الاستيراد هنا تلقائيًا عند استيراد الإشارات المرجعية\",\n      \"created_at\": \"أنشئ {{time}}\",\n      \"progress\": \"التقدّم\",\n      \"status\": {\n        \"pending\": \"معلّق\",\n        \"in_progress\": \"قيد التقدّم\",\n        \"completed\": \"اكتمل\",\n        \"failed\": \"فشل\",\n        \"processing\": \"جارٍ المعالجة\",\n        \"staging\": \"التجهيز\",\n        \"running\": \"قيد التشغيل\",\n        \"paused\": \"متوقف مؤقتًا\"\n      },\n      \"badges\": {\n        \"pending\": \"{{count}} معلّق\",\n        \"processing\": \"{{count}} قيد المعالجة\",\n        \"completed\": \"{{count}} اكتمل\",\n        \"failed\": \"{{count}} فشل\"\n      },\n      \"imported_to\": \"تم الاستيراد إلى:\",\n      \"view_list\": \"عرض اللائحة\",\n      \"delete_dialog_title\": \"حذف جلسة الاستيراد\",\n      \"delete_dialog_description\": \"هل أنت متأكد أنك تريد حذف \\\" {{name}} \\\"؟ لا يمكن التراجع عن هذا الإجراء. لن يتم حذف الإشارات المرجعية نفسها.\",\n      \"delete_session\": \"حذف الجلسة\",\n      \"pause_session\": \"إيقاف مؤقت\",\n      \"resume_session\": \"استئناف التشغيل\",\n      \"view_details\": \"عرض التفاصيل\",\n      \"detail\": {\n        \"page_title\": \"استيراد تفاصيل الجلسة\",\n        \"back_to_import\": \"العودة إلى الاستيراد\",\n        \"filter_all\": \"الكل\",\n        \"filter_accepted\": \"مقبولة\",\n        \"filter_rejected\": \"مرفوضة\",\n        \"filter_duplicates\": \"مكررات\",\n        \"filter_pending\": \"قيد الانتظار\",\n        \"table_title\": \"العنوان / URL\",\n        \"table_type\": \"النوع\",\n        \"table_result\": \"النتيجة\",\n        \"table_reason\": \"السبب\",\n        \"table_bookmark\": \"إشارة مرجعية\",\n        \"result_accepted\": \"مقبولة\",\n        \"result_rejected\": \"مرفوضة\",\n        \"result_skipped_duplicate\": \"مكرر\",\n        \"result_pending\": \"قيد الانتظار\",\n        \"result_processing\": \"معالجة\",\n        \"no_results\": \"لم يتم العثور على نتائج لهذا الفلتر.\",\n        \"view_bookmark\": \"عرض الإشارة المرجعية\",\n        \"load_more\": \"تحميل المزيد\",\n        \"no_title\": \"لا يوجد عنوان\"\n      }\n    },\n    \"backups\": {\n      \"backups\": \"النُسخ الاحتياطية\",\n      \"page_title\": \"النُسخ الاحتياطية\",\n      \"page_description\": \"أنشئ وأدر نُسخًا احتياطية لعلاماتك المرجعية تلقائيًا. النُسخ الاحتياطية مُضغوطة ومخزنة بشكل آمن.\",\n      \"configuration\": {\n        \"title\": \"إعدادات النسخ الاحتياطي\",\n        \"enable_automatic_backups\": \"تمكين النسخ الاحتياطي التلقائي\",\n        \"enable_automatic_backups_description\": \"أنشئ نُسخًا احتياطية لعلاماتك المرجعية تلقائيًا\",\n        \"backup_frequency\": \"تكرار النسخ الاحتياطي\",\n        \"backup_frequency_description\": \"كم مرة يتم فيها إنشاء نسخ احتياطية\",\n        \"retention_period\": \"فترة الاستبقاء (بالأيام)\",\n        \"retention_period_description\": \"عدد الأيام التي يجب الاحتفاظ فيها بالنُسخ الاحتياطية قبل حذفها\",\n        \"frequency\": {\n          \"daily\": \"يوميًا\",\n          \"weekly\": \"أسبوعيًا\"\n        },\n        \"select_frequency\": \"اختر التكرار\",\n        \"save_settings\": \"حفظ الإعدادات\"\n      },\n      \"list\": {\n        \"title\": \"نُسخك الاحتياطية\",\n        \"create_backup_now\": \"إنشاء نسخة احتياطية الآن\",\n        \"no_backups\": \"ليس لديك أي نُسخ احتياطية حتى الآن. قم بتمكين النُسخ الاحتياطية التلقائية أو أنشئ واحدة يدويًا.\",\n        \"table\": {\n          \"created_at\": \"تاريخ الإنشاء\",\n          \"bookmarks\": \"العلامات المرجعية\",\n          \"size\": \"الحجم\",\n          \"status\": \"الحالة\",\n          \"actions\": \"الإجراءات\"\n        },\n        \"status\": {\n          \"success\": \"نجاح\",\n          \"failed\": \"فشل\",\n          \"pending\": \"معلقة\"\n        },\n        \"actions\": {\n          \"download_backup\": \"تنزيل نسخة احتياطية\",\n          \"delete_backup\": \"حذف النسخة الاحتياطية\"\n        }\n      },\n      \"dialogs\": {\n        \"delete_backup_title\": \"حذف النسخة الاحتياطية؟\",\n        \"delete_backup_description\": \"هل أنت متأكد أنك تريد حذف هذه النسخة الاحتياطية؟ لا يمكن التراجع عن هذا الإجراء.\"\n      },\n      \"toasts\": {\n        \"backup_queued\": \"تمت إضافة مهمة النسخ الاحتياطي إلى قائمة الانتظار! ستتم معالجتها قريبًا.\",\n        \"backup_deleted\": \"تم حذف النسخة الاحتياطية!\"\n      }\n    }\n  },\n  \"admin\": {\n    \"admin_settings\": \"إعدادات المشرف\",\n    \"server_stats\": {\n      \"server_stats\": \"إحصائيات الخادم\",\n      \"total_users\": \"إجمالي المستخدمين\",\n      \"total_bookmarks\": \"إجمالي الإشارات المرجعية\",\n      \"server_version\": \"إصدار الخادم\"\n    },\n    \"background_jobs\": {\n      \"background_jobs\": \"المهام الخلفية\",\n      \"crawler_jobs\": \"مهام الاستكشاف\",\n      \"indexing_jobs\": \"مهام الفهرسة\",\n      \"inference_jobs\": \"مهام الاستدلال\",\n      \"tidy_assets_jobs\": \"مهام تنظيم الوسائط\",\n      \"job\": \"مهمة\",\n      \"queued\": \"في قائمة الانتظار\",\n      \"pending\": \"قيد الانتظار\",\n      \"failed\": \"فشلت\",\n      \"feed_jobs\": \"وظائف موجز RSS\",\n      \"video_jobs\": \"وظائف تنزيل الفيديو\",\n      \"webhook_jobs\": \"وظائف Webhook\",\n      \"asset_preprocessing_jobs\": \"وظائف المعالجة المسبقة للأصول\",\n      \"jobs\": {\n        \"crawler\": {\n          \"title\": \"وظائف الزحف\",\n          \"description\": \"الزحف على الويب واستخراج المحتوى من عناوين URL\"\n        },\n        \"inference\": {\n          \"title\": \"وظائف الاستدلال\",\n          \"description\": \"وضع العلامات وتلخيص المحتوى المدعوم بالذكاء الاصطناعي\"\n        },\n        \"indexing\": {\n          \"title\": \"وظائف الفهرسة\",\n          \"description\": \"تحديثات فهرس البحث\"\n        },\n        \"asset_preprocessing\": {\n          \"title\": \"وظائف المعالجة المسبقة للأصول\",\n          \"description\": \"معالجة الصور والمستندات (لقطات الشاشة، واستخراج النصوص، إلخ.)\"\n        },\n        \"tidy_assets\": {\n          \"title\": \"وظائف تنظيف الأصول\",\n          \"description\": \"تنظيف الأصول وتحسين التخزين\"\n        },\n        \"video\": {\n          \"title\": \"وظائف تنزيل الفيديو\",\n          \"description\": \"استخراج الفيديو وتنزيله\"\n        },\n        \"webhook\": {\n          \"title\": \"وظائف Webhook\",\n          \"description\": \"إشعارات Webhook الخارجية\"\n        },\n        \"feed\": {\n          \"title\": \"وظائف تغذية RSS\",\n          \"description\": \"معالجة موجز RSS وتحديثات المحتوى\"\n        },\n        \"admin_maintenance\": {\n          \"title\": \"مهام صيانة المسؤول\",\n          \"description\": \"تنظيف إداري وصيانة للأصول\"\n        }\n      },\n      \"monitor_and_manage\": \"مراقبة وإدارة قوائم انتظار الوظائف الخلفية ومهام معالجة النظام\",\n      \"active\": \"نشط\",\n      \"available_actions\": \"الإجراءات المتاحة\",\n      \"status\": {\n        \"title\": \"فهم حالات الوظائف\",\n        \"queued\": {\n          \"title\": \"في الانتظار\",\n          \"description\": \"الوظائف في انتظار دورها للمعالجة. ستبدأ تلقائيًا عندما تتوفر الموارد.\"\n        },\n        \"unprocessed\": {\n          \"title\": \"لم تتم معالجته\",\n          \"description\": \"الإشارات المرجعية التي لم تتم معالجتها بعد. من المرجح أنها قيد الانتظار للمعالجة بالفعل، وإذا لم يكن الأمر كذلك، فقد تحتاج إلى إعادة إدخالها يدويًا.\"\n        },\n        \"failed\": {\n          \"title\": \"فشل\",\n          \"description\": \"علامات مرجعية واجهت أخطاء أثناء المعالجة. قد تحتاج هذه إلى اهتمام يدوي.\"\n        }\n      },\n      \"actions\": {\n        \"recrawl_failed_links_only\": \"إعادة زحف الروابط الفاشلة فقط\",\n        \"recrawl_all_links\": \"إعادة زحف جميع الروابط\",\n        \"without_inference\": \"بدون استدلال\",\n        \"regenerate_ai_tags_for_failed_bookmarks_only\": \"إعادة إنشاء علامات الذكاء الاصطناعي فقط للمعلومات المرجعية التي فشلت\",\n        \"regenerate_ai_tags_for_all_bookmarks\": \"إعادة إنشاء علامات الذكاء الاصطناعي لجميع الإشارات المرجعية\",\n        \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"إعادة إنشاء ملخصات الذكاء الاصطناعي للإشارات المرجعية الفاشلة فقط\",\n        \"regenerate_ai_summaries_for_all_bookmarks\": \"إعادة إنشاء ملخصات الذكاء الاصطناعي لجميع الإشارات المرجعية\",\n        \"reindex_all_bookmarks\": \"إعادة فهرسة جميع الإشارات المرجعية\",\n        \"clean_assets\": \"تنظيف الأصول المتدلية وإعادة مزامنة البيانات الوصفية\",\n        \"reprocess_assets_fix_mode\": \"إعادة معالجة الأصول غير المعالجة\",\n        \"migrate_large_link_html_content\": \"نقل محتوى HTML كبير مضمّن إلى الأصول\",\n        \"recrawl_pending_links_only\": \"إعادة فهرسة الروابط المعلّقة فقط\",\n        \"regenerate_ai_tags_for_pending_bookmarks_only\": \"إعادة إنشاء وسوم الذكاء الاصطناعي للإشارات المرجعية المعلّقة فقط\",\n        \"regenerate_ai_summaries_for_pending_bookmarks_only\": \"إعادة إنشاء ملخصات الذكاء الاصطناعي للإشارات المرجعية المعلّقة فقط\"\n      }\n    },\n    \"actions\": {\n      \"recrawl_failed_links_only\": \"إعادة استكشاف الروابط الفاشلة فقط\",\n      \"recrawl_all_links\": \"إعادة استكشاف جميع الروابط\",\n      \"without_inference\": \"بدون استدلال\",\n      \"regenerate_ai_tags_for_failed_bookmarks_only\": \"إعادة إنشاء علامات الذكاء الاصطناعي للإشارات المرجعية الفاشلة فقط\",\n      \"regenerate_ai_tags_for_all_bookmarks\": \"إعادة إنشاء علامات الذكاء الاصطناعي لجميع الإشارات المرجعية\",\n      \"reindex_all_bookmarks\": \"إعادة فهرسة جميع الإشارات المرجعية\",\n      \"compact_assets\": \"ضغط الوسائط\",\n      \"reprocess_assets_fix_mode\": \"إعادة معالجة الوسائط (وضع الإصلاح)\",\n      \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"أعد إنشاء ملخصات الذكاء الاصطناعي للإشارات المرجعية الفاشلة فقط\",\n      \"regenerate_ai_summaries_for_all_bookmarks\": \"أعد إنشاء ملخصات الذكاء الاصطناعي لجميع الإشارات المرجعية\"\n    },\n    \"users_list\": {\n      \"users_list\": \"قائمة المستخدمين\",\n      \"create_user\": \"إنشاء مستخدم\",\n      \"change_role\": \"تغيير الدور\",\n      \"reset_password\": \"إعادة تعيين كلمة المرور\",\n      \"delete_user\": \"حذف المستخدم\",\n      \"num_bookmarks\": \"عدد الإشارات المرجعية\",\n      \"asset_sizes\": \"أحجام الوسائط\",\n      \"local_user\": \"مستخدم محلي\",\n      \"confirm_password\": \"تأكيد كلمة المرور\",\n      \"delete_user_confirm_description\": \"هل أنت متأكد أنك تريد حذف المستخدم \\\"{{name}}؟\\\"\",\n      \"unlimited\": \"غير محدود\"\n    },\n    \"service_connections\": {\n      \"title\": \"اتصالات الخدمة\",\n      \"description\": \"راقب حالة التبعيات الخاصة بالنظام الخارجي واتصاله\",\n      \"search_engine\": \"محرك البحث\",\n      \"browser\": \"المتصفح\",\n      \"queue_system\": \"نظام الانتظار\",\n      \"status\": {\n        \"not_configured\": \"غير مهيأ\",\n        \"connected\": \"متصل\",\n        \"disconnected\": \"قطع الاتصال\"\n      }\n    },\n    \"admin_tools\": {\n      \"admin_tools\": \"أدوات المشرف\",\n      \"bookmark_debugger\": \"تصحيح أخطاء الإشارات المرجعية\",\n      \"bookmark_id\": \"معرف الإشارة المرجعية\",\n      \"bookmark_id_placeholder\": \"أدخل معرف الإشارة المرجعية\",\n      \"lookup\": \"بحث\",\n      \"debug_info\": \"معلومات التصحيح\",\n      \"basic_info\": \"معلومات أساسية\",\n      \"status\": \"الحالة\",\n      \"content\": \"المحتوى\",\n      \"html_preview\": \"معاينة HTML (أول 1000 حرف)\",\n      \"summary\": \"ملخص\",\n      \"url\": \"عنوان URL\",\n      \"source_url\": \"عنوان URL للمصدر\",\n      \"asset_type\": \"نوع الأصل\",\n      \"file_name\": \"اسم الملف\",\n      \"owner_user_id\": \"معرف المستخدم المالك\",\n      \"tagging_status\": \"حالة وضع العلامات\",\n      \"summarization_status\": \"حالة التلخيص\",\n      \"crawl_status\": \"حالة الزحف\",\n      \"crawl_status_code\": \"رمز حالة HTTP\",\n      \"crawled_at\": \"تمت الزحف في\",\n      \"recrawl\": \"إعادة الزحف\",\n      \"reindex\": \"إعادة الفهرسة\",\n      \"retag\": \"إعادة الوسم\",\n      \"resummarize\": \"إعادة التلخيص\",\n      \"bookmark_not_found\": \"لم يتم العثور على إشارة مرجعية\",\n      \"action_success\": \"اكتمل الإجراء بنجاح\",\n      \"action_failed\": \"فشل الإجراء\",\n      \"recrawl_queued\": \"تمت إضافة مهمة إعادة الزحف إلى قائمة الانتظار\",\n      \"reindex_queued\": \"تمت إضافة مهمة إعادة الفهرسة إلى قائمة الانتظار\",\n      \"retag_queued\": \"تمت إضافة مهمة إعادة وضع العلامات إلى قائمة الانتظار\",\n      \"resummarize_queued\": \"تمت إضافة مهمة إعادة التلخيص إلى قائمة الانتظار\",\n      \"view\": \"عرض\",\n      \"fetch_error\": \"خطأ في جلب الإشارة المرجعية\"\n    }\n  },\n  \"options\": {\n    \"dark_mode\": \"الوضع الداكن\",\n    \"light_mode\": \"الوضع الفاتح\",\n    \"apps_extensions\": \"التطبيقات والإضافات\",\n    \"documentation\": \"الوثائق\",\n    \"follow_us_on_x\": \"تابعنا على X\"\n  },\n  \"lists\": {\n    \"all_lists\": \"جميع القوائم\",\n    \"favourites\": \"المفضلة\",\n    \"new_list\": \"قائمة جديدة\",\n    \"edit_list\": \"تحرير القائمة\",\n    \"new_nested_list\": \"قائمة متداخلة جديدة\",\n    \"parent_list\": \"القائمة الأم\",\n    \"no_parent\": \"بدون أم\",\n    \"list_type\": \"نوع القائمة\",\n    \"manual_list\": \"قائمة يدوية\",\n    \"smart_list\": \"قائمة ذكية\",\n    \"search_query\": \"استعلام البحث\",\n    \"search_query_help\": \"تعرف المزيد عن لغة استعلام البحث.\",\n    \"merge_list\": \"دمج القائمة\",\n    \"destination_list\": \"القائمة الوجهة\",\n    \"delete_after_merge\": \"حذف القائمة الأصلية بعد الدمج\",\n    \"no_destination\": \"لا توجد وجهة\",\n    \"description\": \"الوصف (اختياري)\",\n    \"rss\": {\n      \"title\": \"موجز RSS\",\n      \"description\": \"تمكين موجز RSS لهذه القائمة\",\n      \"feed_url\": \"عنوان URL لموجز RSS\"\n    },\n    \"share_list\": \"شارك القائمة\",\n    \"public_list\": {\n      \"title\": \"قائمة عامة\",\n      \"description\": \"السماح للآخرين بعرض هذه القائمة\",\n      \"share_link\": \"شارك الرابط\"\n    },\n    \"delete_list\": {\n      \"title\": \"حذف القائمة\",\n      \"description\": \"لا يؤدي حذف قائمة ما إلى حذف أي إشارات مرجعية في هذه القائمة.\",\n      \"delete_children\": \"حذف القوائم الفرعية (بشكل متكرر)\",\n      \"delete_children_description\": \"إذا لم يتم تحديده، فستصبح جميع القوائم الفرعية المباشرة قوائم جذر\"\n    },\n    \"shared\": \"مشترك\",\n    \"collaborators\": {\n      \"manage\": \"إدارة المتعاونين\",\n      \"view\": \"عرض المتعاونين\",\n      \"collaborators\": \"المتعاونين\",\n      \"add\": \"إضافة متعاون\",\n      \"current\": \"المتعاونون الحاليون\",\n      \"enter_email\": \"أدخل عنوان البريد الإلكتروني\",\n      \"please_enter_email\": \"يرجى إدخال عنوان بريد إلكتروني\",\n      \"added_successfully\": \"تمت إضافة المتعاون بنجاح\",\n      \"failed_to_add\": \"فشل في إضافة متعاون\",\n      \"removed\": \"تمت إزالة المتعاون\",\n      \"failed_to_remove\": \"فشل في إزالة المتعاون\",\n      \"role_updated\": \"تم تحديث الدور\",\n      \"failed_to_update_role\": \"فشل في تحديث الدور\",\n      \"viewer\": \"عارض\",\n      \"editor\": \"محرر\",\n      \"owner\": \"مالك\",\n      \"viewer_description\": \"يمكنه عرض الإشارات المرجعية في القائمة\",\n      \"editor_description\": \"يمكنه إضافة وإزالة الإشارات المرجعية\",\n      \"no_collaborators\": \"لا يوجد متعاونون حتى الآن. أضف شخصًا لبدء التعاون!\",\n      \"no_collaborators_readonly\": \"لا يوجد متعاونين لهذه القائمة.\",\n      \"people_with_access\": \"الأشخاص الذين لديهم صلاحية الوصول إلى هذه القائمة\",\n      \"add_or_remove\": \"إضافة أو إزالة الأشخاص الذين يمكنهم الوصول إلى هذه القائمة\",\n      \"invitation_sent\": \"تم إرسال الدعوة بنجاح\",\n      \"invitation_revoked\": \"تم إلغاء الدعوة\",\n      \"failed_to_revoke\": \"فشل إلغاء الدعوة\",\n      \"pending\": \"معلقة\",\n      \"revoke\": \"إلغاء\",\n      \"declined\": \"مرفوضة\"\n    },\n    \"leave_list\": {\n      \"title\": \"مغادرة القائمة\",\n      \"confirm_message\": \"هل أنت متأكد أنك تريد مغادرة {{icon}} {{name}}؟\",\n      \"warning\": \"لن تتمكن بعد ذلك من عرض أو الوصول إلى الإشارات المرجعية في هذه القائمة. يمكن لمالك القائمة إضافتك مرة أخرى إذا لزم الأمر.\",\n      \"action\": \"مغادرة القائمة\",\n      \"success\": \"لقد غادرت \\\"{{icon}} {{name}}\\\"\"\n    },\n    \"invitations\": {\n      \"pending\": \"الدعوات المعلَّقة\",\n      \"description\": \"راجِع دعوات المشاركة في القائمة واستجب لها\",\n      \"invited_by\": \"بدعوة من\",\n      \"accept\": \"قبول\",\n      \"decline\": \"رفض\",\n      \"accepted\": \"تم قبول الدعوة\",\n      \"declined\": \"تم رفض الدعوة\",\n      \"failed_to_accept\": \"فشل قبول الدعوة\",\n      \"failed_to_decline\": \"فشل رفض الدعوة\"\n    },\n    \"shared_lists\": \"قوائم مشتركة\"\n  },\n  \"tags\": {\n    \"all_tags\": \"جميع الوسوم\",\n    \"your_tags\": \"وسومك\",\n    \"your_tags_info\": \"الوسوم التي أضفتها مرة واحدة على الأقل\",\n    \"ai_tags\": \"وسوم الذكاء الاصطناعي\",\n    \"ai_tags_info\": \"الوسوم التي تم إضافتها تلقائياً فقط (بواسطة الذكاء الاصطناعي)\",\n    \"unused_tags\": \"وسوم غير مستخدمة\",\n    \"unused_tags_info\": \"وسوم غير مرتبطة بأي إشارات مرجعية\",\n    \"delete_all_unused_tags\": \"حذف جميع الوسوم غير المستخدمة\",\n    \"drag_and_drop_merging\": \"دمج بالسحب والإفلات\",\n    \"drag_and_drop_merging_info\": \"اسحب وأفلت الوسوم على بعضها البعض لدمجها\",\n    \"sort_by_name\": \"ترتيب حسب الاسم\",\n    \"create_tag\": \"إنشاء علامة\",\n    \"create_tag_description\": \"إنشاء علامة جديدة بدون إرفاقها بأي إشارة مرجعية\",\n    \"tag_name\": \"اسم العلامة\",\n    \"enter_tag_name\": \"أدخل اسم العلامة\",\n    \"sort_by_usage\": \"الترتيب حسب الاستخدام\",\n    \"sort_by_relevance\": \"الترتيب حسب الصلة\",\n    \"no_custom_tags\": \"لا توجد علامات مخصصة حتى الآن\",\n    \"no_ai_tags\": \"لا توجد علامات ذكاء اصطناعي حتى الآن\",\n    \"no_unused_tags\": \"ليس لديك أية علامات غير مستخدمة\",\n    \"no_unused_tags_match_your_search\": \"لا توجد علامات غير مستخدمة تطابق بحثك\",\n    \"no_tags_match_your_search\": \"لا توجد علامات تطابق بحثك\",\n    \"search_placeholder\": \"البحث عن العلامات...\",\n    \"search_or_create_placeholder\": \"البحث عن العلامات أو إنشائها...\"\n  },\n  \"search\": {\n    \"is_favorited\": \"في المفضلة\",\n    \"is_not_favorited\": \"ليس في المفضلة\",\n    \"is_archived\": \"مؤرشف\",\n    \"is_not_archived\": \"غير مؤرشف\",\n    \"has_any_tag\": \"له أي وسم\",\n    \"has_no_tags\": \"ليس له وسوم\",\n    \"is_in_any_list\": \"في أي قائمة\",\n    \"is_not_in_any_list\": \"ليس في أي قائمة\",\n    \"created_on_or_after\": \"تم إنشاؤه في أو بعد\",\n    \"not_created_on_or_after\": \"لم يتم إنشاؤه في أو بعد\",\n    \"created_on_or_before\": \"تم إنشاؤه في أو قبل\",\n    \"not_created_on_or_before\": \"لم يتم إنشاؤه في أو قبل\",\n    \"url_contains\": \"الرابط يحتوي على\",\n    \"url_does_not_contain\": \"الرابط لا يحتوي على\",\n    \"is_in_list\": \"في القائمة\",\n    \"is_not_in_list\": \"ليس في القائمة\",\n    \"has_tag\": \"له وسم\",\n    \"does_not_have_tag\": \"ليس له وسم\",\n    \"full_text_search\": \"البحث النصي الكامل\",\n    \"type_is\": \"النوع هو\",\n    \"type_is_not\": \"النوع ليس كـ\",\n    \"and\": \"و\",\n    \"or\": \"أو\",\n    \"is_from_feed\": \"من موجز RSS\",\n    \"is_not_from_feed\": \"ليس من موجز RSS\",\n    \"created_within\": \"تم إنشاؤه في غضون\",\n    \"month_s_ago\": \" منذ شهر (أشهر)\",\n    \"year_s_ago\": \" منذ سنة (سنوات)\",\n    \"created_earlier_than\": \"تم إنشاؤه في وقت أبكر من\",\n    \"day_s\": \" يوم (أيام)\",\n    \"week_s\": \" أسبوع (أسابيع)\",\n    \"month_s\": \" شهر (أشهر)\",\n    \"year_s\": \" سنة (سنوات)\",\n    \"day_s_ago\": \" منذ يوم (أيام)\",\n    \"week_s_ago\": \" منذ أسبوع (أسابيع)\",\n    \"history\": \"عمليات البحث الأخيرة\",\n    \"title_contains\": \"العنوان يحتوي على\",\n    \"title_does_not_contain\": \"العنوان لا يحتوي على\",\n    \"is_broken_link\": \"لديه رابط معطّل\",\n    \"tags\": \"العلامات\",\n    \"no_suggestions\": \"لا توجد اقتراحات\",\n    \"filters\": \"الفلاتر\",\n    \"is_not_broken_link\": \"لديه رابط صالح\",\n    \"lists\": \"القوائم\",\n    \"feeds\": \"خلاصات الأخبار\",\n    \"is_from_source\": \"المصدر هو\",\n    \"is_not_from_source\": \"المصدر ليس\"\n  },\n  \"preview\": {\n    \"view_original\": \"عرض النسخة الأصلية\",\n    \"cached_content\": \"النسخة المخزنة\",\n    \"reader_view\": \"عرض القارئ\",\n    \"tabs\": {\n      \"content\": \"المحتوى\",\n      \"details\": \"التفاصيل\"\n    },\n    \"archive_info\": \"قد لا يتم عرض الأرشيفات بشكل صحيح في السطر إذا كانت تتطلب Javascript. للحصول على أفضل النتائج، <1>قم بتنزيلها وافتحها في متصفحك</1>.\",\n    \"fetch_error_title\": \"المحتوى غير متوفر\",\n    \"fetch_error_description\": \"تعذر علينا جلب المحتوى لهذا الرابط. قد تكون الصفحة محمية أو تتطلب مصادقة أو قد تكون غير متاحة مؤقتًا.\",\n    \"crawling_in_progress\": \"جاري جلب محتوى الصفحة…\",\n    \"continue_reading\": \"استمر من حيث توقفت\",\n    \"continue_reading_percent\": \"استمر من حيث توقفت ({{percent}}%)\",\n    \"continue_button\": \"استمر\"\n  },\n  \"editor\": {\n    \"quickly_focus\": \"يمكنك التركيز سريعاً على هذا الحقل بالضغط على ⌘ + E\",\n    \"multiple_urls_dialog_title\": \"هل تريد استيراد الروابط كإشارات مرجعية منفصلة؟\",\n    \"multiple_urls_dialog_desc\": \"يحتوي المدخل على روابط متعددة في أسطر منفصلة. هل تريد استيرادها كإشارات مرجعية منفصلة؟\",\n    \"import_as_text\": \"استيراد كإشارة مرجعية نصية\",\n    \"import_as_separate_bookmarks\": \"استيراد كإشارات مرجعية منفصلة\",\n    \"placeholder\": \"الصق رابطاً، أو اكتب ملاحظة، أو اسحب صورة وأفلتها هنا...\",\n    \"new_item\": \"عنصر جديد\",\n    \"disabled_submissions\": \"الإرسال معطل\",\n    \"text_toolbar\": {\n      \"undo\": \"تراجع\",\n      \"redo\": \"إعادة\",\n      \"bold\": \"عريض\",\n      \"italic\": \"مائل\",\n      \"underline\": \"تحته خط\",\n      \"strikethrough\": \"مشطوب\",\n      \"code\": \"كود\",\n      \"highlight\": \"تمييز\",\n      \"align_left\": \"محاذاة لليسار\",\n      \"align_center\": \"محاذاة للوسط\",\n      \"align_right\": \"محاذاة لليمين\",\n      \"markdown_shortcuts\": {\n        \"label\": \"اختصارات التنسيق\",\n        \"heading\": {\n          \"label\": \"عنوان\",\n          \"example\": \"# H1, ## H2, ### H3\"\n        },\n        \"bold\": {\n          \"label\": \"عريض\",\n          \"example\": \"**نص** أو CTRL+b\"\n        },\n        \"italic\": {\n          \"label\": \"مائل\",\n          \"example\": \"*مائل* أو _مائل_ أو CTRL+i\"\n        },\n        \"blockquote\": {\n          \"label\": \"اقتباس\",\n          \"example\": \"> اقتباس\"\n        },\n        \"ordered_list\": {\n          \"label\": \"قائمة مرتبة\",\n          \"example\": \"1. عنصر القائمة\"\n        },\n        \"unordered_list\": {\n          \"label\": \"قائمة غير مرتبة\",\n          \"example\": \"- عنصر القائمة\"\n        },\n        \"inline_code\": {\n          \"label\": \"كود ضمني\",\n          \"example\": \"`كود`\"\n        },\n        \"block_code\": {\n          \"label\": \"كتلة كود\",\n          \"example\": \"``` + مسافة\"\n        }\n      }\n    },\n    \"placeholder_v2\": \"الصق رابطًا أو اكتب ملاحظة أو أسقط صورة…\"\n  },\n  \"dialogs\": {\n    \"bookmarks\": {\n      \"delete_confirmation_title\": \"حذف الإشارة المرجعية؟\",\n      \"delete_confirmation_description\": \"هل أنت متأكد من رغبتك في حذف هذه الإشارة المرجعية؟\"\n    }\n  },\n  \"toasts\": {\n    \"bookmarks\": {\n      \"updated\": \"تم تحديث الإشارة المرجعية!\",\n      \"deleted\": \"تم حذف الإشارة المرجعية!\",\n      \"refetch\": \"تم إضافة إعادة الجلب إلى قائمة الانتظار!\",\n      \"full_page_archive\": \"تم بدء إنشاء أرشيف الصفحة الكامل\",\n      \"delete_from_list\": \"تم حذف الإشارة المرجعية من القائمة\",\n      \"clipboard_copied\": \"تم نسخ الرابط إلى الحافظة!\",\n      \"preserve_pdf\": \"تم تشغيل حفظ PDF\",\n      \"update_banner\": \"تم تحديث البانر!\",\n      \"uploading_banner\": \"جاري رفع البانر...\"\n    },\n    \"lists\": {\n      \"created\": \"تم إنشاء القائمة!\",\n      \"updated\": \"تم تحديث القائمة!\",\n      \"merged\": \"تم دمج القائمة!\",\n      \"deleted\": \"تم حذف القائمة!\"\n    },\n    \"tags\": {\n      \"created\": \"تم إنشاء العلامة!\",\n      \"failed_to_create\": \"فشل إنشاء العلامة\"\n    }\n  },\n  \"cleanups\": {\n    \"cleanups\": \"التنظيفات\",\n    \"duplicate_tags\": {\n      \"title\": \"وسوم مكررة\",\n      \"merge_all_suggestions\": \"دمج جميع الاقتراحات؟\"\n    }\n  },\n  \"banners\": {\n    \"no_bookmarks\": {\n      \"title\": \"لا توجد إشارات مرجعية حتى الآن\",\n      \"description\": \"احفظ المقالات والروابط والصفحات الشيقة للوصول إليها بسرعة لاحقًا.\"\n    }\n  },\n  \"bookmark_editor\": {\n    \"title\": \"تعديل الإشارة المرجعية\",\n    \"subtitle\": \"قم بإجراء تغييرات على تفاصيل الإشارة المرجعية. انقر فوق حفظ عند الانتهاء.\",\n    \"author\": \"المؤلف\",\n    \"publisher\": \"الناشر\",\n    \"date_published\": \"تاريخ النشر\",\n    \"pick_a_date\": \"اختر تاريخًا\",\n    \"save_changes\": \"حفظ التغييرات\",\n    \"extracted_content\": \"المحتوى المستخرج\"\n  },\n  \"view_options\": {\n    \"title\": \"عرض الخيارات\",\n    \"layout\": \"تخطيط\",\n    \"columns\": \"أعمدة\",\n    \"display_options\": \"خيارات العرض\",\n    \"show_note_previews\": \"إظهار الملاحظات\",\n    \"show_tags\": \"عرض العلامات\",\n    \"show_title\": \"عرض العنوان\",\n    \"image_options\": \"خيارات الصورة\",\n    \"image_fit_cover\": \"غطاء (تعبئة)\",\n    \"image_fit_contain\": \"احتواء (ملاءمة)\"\n  },\n  \"version\": {\n    \"new_release_available\": \"ملاحظات الإصدار الجديدة متوفرة\",\n    \"whats_new_title\": \"ما الجديد في v{{version}}\",\n    \"release_notes_description\": \"إليك آخر التحديثات التي تم جلبها من ملاحظات إصدار GitHub.\",\n    \"loading_release_notes\": \"جارٍ تحميل ملاحظات الإصدار…\",\n    \"unable_to_load_release_notes\": \"تعذر تحميل ملاحظات الإصدار الآن. يرجى المحاولة مرة أخرى لاحقًا.\",\n    \"no_release_notes\": \"لم يتم نشر ملاحظات إصدار لهذا الإصدار.\",\n    \"release_notes_synced\": \"تتم مزامنة ملاحظات الإصدار من GitHub.\",\n    \"view_on_github\": \"عرض على GitHub\"\n  },\n  \"wrapped\": {\n    \"title\": \"ملخص {{year}} الخاص بك\",\n    \"subtitle\": \"عام في Karakeep\",\n    \"banner\": {\n      \"title\": \"ملخص 2025 الخاص بك جاهز!\",\n      \"description\": \"اطلع على عامك في الإشارات المرجعية\",\n      \"view_now\": \"اعرض الآن\"\n    },\n    \"button\": \"ملخص 2025\",\n    \"loading\": \"جارٍ تحميل الملخص الخاص بك...\",\n    \"failed_to_load\": \"فشل تحميل إحصائيات الملخص الخاص بك\",\n    \"sections\": {\n      \"total_saves\": {\n        \"prefix\": \"لقد حفظت\",\n        \"suffix\": \"العناصر لهذه السنة\",\n        \"suffix_singular\": \"عنصر هذه السنة\"\n      },\n      \"first_bookmark\": {\n        \"title\": \"رحلتك بدأت\",\n        \"description\": \"أول حفظ لـ {{year}}:\"\n      },\n      \"top_domains\": \"أهم مواقعك\",\n      \"top_tags\": \"أهم علاماتك\",\n      \"monthly_activity\": \"عامك في عمليات الحفظ\",\n      \"most_active_day\": \"اليوم الأكثر نشاطًا لك\",\n      \"peak_times\": {\n        \"title\": \"متى تحفظ\",\n        \"peak_hour\": \"ساعة الذروة\",\n        \"peak_day\": \"يوم الذروة\"\n      },\n      \"how_you_save\": \"كيف تحفظ\",\n      \"what_you_saved\": \"ماذا حفظت\",\n      \"summary\": {\n        \"favorites\": \"المفضلة\",\n        \"tags_created\": \"العلامات التي تم إنشاؤها\",\n        \"highlights\": \"أبرز النقاط\"\n      },\n      \"types\": {\n        \"links\": \"الروابط\",\n        \"notes\": \"الملاحظات\",\n        \"assets\": \"الأصول\"\n      }\n    },\n    \"footer\": \"صُنع بواسطة Karakeep\",\n    \"share\": \"شارك\",\n    \"download\": \"تنزيل\",\n    \"close\": \"إغلاق\",\n    \"generating\": \"جارٍ الإنشاء...\"\n  }\n}\n"
  },
  {
    "path": "apps/web/lib/i18n/locales/cs/translation.json",
    "content": "{\n  \"common\": {\n    \"password\": \"Heslo\",\n    \"url\": \"URL\",\n    \"name\": \"Název\",\n    \"email\": \"E-mail\",\n    \"action\": \"Akce\",\n    \"actions\": \"Akce\",\n    \"created_at\": \"Vytvořeno\",\n    \"key\": \"Klíč\",\n    \"type\": \"Typ\",\n    \"size\": \"Velikost\",\n    \"roles\": {\n      \"user\": \"Uživatel\",\n      \"admin\": \"Administrátor\"\n    },\n    \"something_went_wrong\": \"Něco se pokazilo\",\n    \"experimental\": \"Experimentální\",\n    \"search\": \"Hledat\",\n    \"tags\": \"Štítky\",\n    \"note\": \"Poznámka\",\n    \"attachments\": \"Přílohy\",\n    \"highlights\": \"Hlavní body\",\n    \"source\": \"Zdroj\",\n    \"screenshot\": \"Snímek obrazovky\",\n    \"video\": \"Video\",\n    \"updated_at\": \"Upraveno\",\n    \"role\": \"Role\",\n    \"archive\": \"Archivovat\",\n    \"home\": \"Domů\",\n    \"title\": \"Název\",\n    \"description\": \"Popis\",\n    \"summary\": \"Shrnutí\",\n    \"bookmark_types\": {\n      \"title\": \"Typ záložky\",\n      \"link\": \"Odkaz\",\n      \"text\": \"Text\",\n      \"media\": \"Média\"\n    },\n    \"quota\": \"Kvóta\",\n    \"bookmarks\": \"Záložky\",\n    \"storage\": \"Úložiště\",\n    \"pdf\": \"Archivovaný PDF\",\n    \"default\": \"Výchozí\",\n    \"id\": \"ID\",\n    \"last_used\": \"Použito naposledy\"\n  },\n  \"actions\": {\n    \"close\": \"Zavřít\",\n    \"create\": \"Vytvořit\",\n    \"fetch_now\": \"Načíst hned\",\n    \"summarize_with_ai\": \"Shrnout pomocí AI\",\n    \"edit_title\": \"Upravit název\",\n    \"sign_out\": \"Odhlásit se\",\n    \"merge\": \"Sloučit\",\n    \"cancel\": \"Zrušit\",\n    \"apply_all\": \"Použít vše\",\n    \"ignore\": \"Ignorovat\",\n    \"change_layout\": \"Změnit rozvržení\",\n    \"archive\": \"Archivovat\",\n    \"unarchive\": \"Zrušit archivaci\",\n    \"favorite\": \"Oblíbené\",\n    \"unfavorite\": \"Odebrat z oblíbených\",\n    \"delete\": \"Smazat\",\n    \"toggle_show_archived\": \"Zobrazit archivované\",\n    \"refresh\": \"Obnovit\",\n    \"recrawl\": \"Znovu prolézt\",\n    \"download_full_page_archive\": \"Stáhnout archiv celé stránky\",\n    \"edit_tags\": \"Upravit štítky\",\n    \"add_to_list\": \"Přidat do seznamu\",\n    \"select_all\": \"Vybrat vše\",\n    \"unselect_all\": \"Zrušit výběr všech\",\n    \"copy_link\": \"Kopírovat odkaz\",\n    \"close_bulk_edit\": \"Zavřít hromadnou úpravu\",\n    \"bulk_edit\": \"Hromadná úprava\",\n    \"manage_lists\": \"Spravovat seznamy\",\n    \"remove_from_list\": \"Odebrat ze seznamu\",\n    \"save\": \"Uložit\",\n    \"add\": \"Přidat\",\n    \"edit\": \"Upravit\",\n    \"open_editor\": \"Otevřít editor\",\n    \"sort\": {\n      \"title\": \"Seřadit\",\n      \"relevant_first\": \"Nejdůležitější jako první\",\n      \"newest_first\": \"Nejnovější jako první\",\n      \"oldest_first\": \"Od nejstaršího\"\n    },\n    \"confirm\": \"Potvrdit\",\n    \"regenerate\": \"Regenerovat\",\n    \"load_more\": \"Načíst další\",\n    \"edit_notes\": \"Upravit poznámky\",\n    \"preserve_as_pdf\": \"Uložit jako PDF\",\n    \"offline_copies\": \"Offline kopie\",\n    \"preserve_offline_archive\": \"Zachovat archiv offline\",\n    \"download_full_page_archive_file\": \"Stáhnout archivní soubor\",\n    \"download_pdf_file\": \"Stáhnout soubor PDF\",\n    \"remove\": \"Odebrat\",\n    \"more\": \"Více\",\n    \"replace_banner\": \"Nahradit banner\",\n    \"add_banner\": \"Přidat banner\",\n    \"download\": \"Stáhnout\"\n  },\n  \"settings\": {\n    \"ai\": {\n      \"tagging_rule_description\": \"Dotazy, které sem přidáte, budou během generování tagů zahrnuty jako pravidla do modelu. Finální dotazy si můžete prohlédnout v sekci náhledu výzev.\",\n      \"ai_settings\": \"Nastavení AI\",\n      \"tagging_rules\": \"Pravidla pro označování štítky\",\n      \"prompt_preview\": \"Náhled promptu\",\n      \"text_prompt\": \"Textová výzva\",\n      \"images_prompt\": \"Výzva k obrázku\",\n      \"summarization_prompt\": \"Prompt pro shrnutí\",\n      \"all_tagging\": \"Všechny štítky\",\n      \"text_tagging\": \"Označování textu\",\n      \"image_tagging\": \"Označování obrázků\",\n      \"summarization\": \"Shrnutí\",\n      \"tag_style\": \"Styl štítků\",\n      \"auto_summarization_description\": \"Automaticky generovat shrnutí pro tvoje záložky pomocí umělý inteligence.\",\n      \"auto_tagging\": \"Automatický štítkování\",\n      \"titlecase_spaces\": \"Velká písmena s mezerami\",\n      \"lowercase_underscores\": \"Malá písmena s podtržítky\",\n      \"inference_language\": \"Jazyk pro odvozování\",\n      \"titlecase_hyphens\": \"Velká písmena s pomlčkami\",\n      \"lowercase_hyphens\": \"Malá písmena s pomlčkami\",\n      \"lowercase_spaces\": \"Malá písmena s mezerami\",\n      \"inference_language_description\": \"Vyber jazyk pro štítky a souhrny generované AI.\",\n      \"tag_style_description\": \"Vyber si, jakým způsobem se mají automaticky generované štítky formátovat.\",\n      \"auto_tagging_description\": \"Automaticky generovat štítky pro tvoje záložky pomocí umělý inteligence.\",\n      \"camelCase\": \"camelCase\",\n      \"auto_summarization\": \"Automatický shrnutí\",\n      \"no_preference\": \"Bez preference\",\n      \"curated_tags\": \"Vybrané štítky\",\n      \"curated_tags_description\": \"Volitelně omezte označování pomocí AI tak, aby používala pouze štítky z tohoto seznamu. Pokud nejsou vybrány žádné štítky, umělá inteligence generuje štítky volně.\",\n      \"curated_tags_updated\": \"Vybrané štítky byly úspěšně aktualizovány!\",\n      \"curated_tags_update_failed\": \"Nepodařilo se aktualizovat vybrané štítky\"\n    },\n    \"webhooks\": {\n      \"webhooks\": \"Webhooky\",\n      \"description\": \"Webhooks můžete použít ke spouštění akcí, když jsou záložky vytvořeny, změněny nebo procházeny.\",\n      \"events\": {\n        \"title\": \"Události\",\n        \"crawled\": \"Prohledáno\",\n        \"created\": \"Vytvořeno\",\n        \"edited\": \"Upraveno\"\n      },\n      \"auth_token\": \"Ověřovací token\",\n      \"add_auth_token\": \"Přidat ověřovací token\",\n      \"edit_auth_token\": \"Upravit ověřovací token\",\n      \"create_webhook\": \"Vytvořit webhook\",\n      \"delete_webhook\": \"Smazat webhook\",\n      \"delete_webhook_confirmation\": \"Určitě chceš smazat tenhle webhook?\",\n      \"edit_webhook\": \"Upravit Webhook\",\n      \"webhook_url\": \"URL webhooku\"\n    },\n    \"api_keys\": {\n      \"new_api_key\": \"Nový API klíč\",\n      \"api_keys\": \"API klíče\",\n      \"new_api_key_desc\": \"Dej svému API klíči unikátní jméno\",\n      \"key_success\": \"Klíč byl úspěšně vytvořen\",\n      \"key_success_please_copy\": \"Zkopírujte si klíč a uložte ho na bezpečné místo. Jakmile dialog zavřete, už se k němu nedostanete.\",\n      \"regenerate_api_key\": \"Regenerovat API klíč\",\n      \"key_regenerated\": \"Klíč byl úspěšně obnoven\",\n      \"key_regenerated_please_copy\": \"Zkopírujte si nový klíč a uložte si ho na bezpečném místě. Starý klíč byl zrušen a už nebude fungovat.\",\n      \"regenerate_warning\": \"Opravdu chceš obnovit API klíč \\\"{{name}}\\\"?\",\n      \"regenerate_confirmation\": \"Tím se zruší stávající klíč a vygeneruje se nový. Všechny aplikace, které používají stávající klíč, přestanou fungovat.\"\n    },\n    \"manage_assets\": {\n      \"asset_link\": \"Odkaz na položku\",\n      \"delete_asset\": \"Smazat aktivum\",\n      \"delete_asset_confirmation\": \"Opravdu chceš tenhle majetek smazat?\",\n      \"manage_assets\": \"Spravovat aktiva\",\n      \"no_assets\": \"Zatím nemáte žádné zdroje.\",\n      \"asset_type\": \"Typ majetku\",\n      \"bookmark_link\": \"Odkaz na záložku\"\n    },\n    \"rules\": {\n      \"rules\": \"Rule Engine\",\n      \"rule_name\": \"Název pravidla\",\n      \"description\": \"Můžeš použít pravidla ke spouštění akcí, když se událost aktivuje.\",\n      \"ceate_rule\": \"Vytvořit pravidlo\",\n      \"edit_rule\": \"Upravit pravidlo\",\n      \"if\": \"Pokud ...\",\n      \"save_rule\": \"Uložit pravidlo\",\n      \"delete_rule\": \"Smazat pravidlo\",\n      \"delete_rule_confirmation\": \"Určitě chceš smazat tohle pravidlo?\",\n      \"whenever\": \"Kdykoli ...\",\n      \"enter_rule_name\": \"Zadejte název pravidla\",\n      \"describe_what_this_rule_does\": \"Popište, co toto pravidlo dělá\",\n      \"rule_has_been_created\": \"Pravidlo bylo vytvořeno!\",\n      \"rule_has_been_updated\": \"Pravidlo bylo aktualizováno!\",\n      \"rule_has_been_deleted\": \"Pravidlo bylo smazáno!\",\n      \"no_rules_created_yet\": \"Zatím nejsou vytvořena žádná pravidla\",\n      \"create_your_first_rule\": \"Vytvořte si první pravidlo pro automatizaci pracovního postupu\",\n      \"conditions_types\": {\n        \"always\": \"Vždy\",\n        \"url_contains\": \"URL obsahuje\",\n        \"imported_from_feed\": \"Importováno z kanálu\",\n        \"bookmark_type_is\": \"Typ záložky je\",\n        \"has_tag\": \"Má štítek\",\n        \"is_favourited\": \"Je oblíbené\",\n        \"is_archived\": \"Je archivováno\",\n        \"and\": \"Platí všechno z následujícího\",\n        \"or\": \"Platí alespoň jedna z následujících možností\",\n        \"url_does_not_contain\": \"Adresa URL Neobsahuje\",\n        \"title_contains\": \"Název obsahuje\",\n        \"title_does_not_contain\": \"Název neobsahuje\"\n      },\n      \"actions_types\": {\n        \"add_tag\": \"Přidat štítek\",\n        \"remove_tag\": \"Odebrat štítek\",\n        \"add_to_list\": \"Přidat do seznamu\",\n        \"remove_from_list\": \"Odebrat ze seznamu\",\n        \"download_full_page_archive\": \"Stáhnout archiv celé stránky\",\n        \"favourite_bookmark\": \"Oblíbená záložka\",\n        \"archive_bookmark\": \"Archivovat záložku\"\n      },\n      \"events_types\": {\n        \"bookmark_added\": \"Záložka byla přidána\",\n        \"tag_added\": \"Tento štítek je přidán k záložce\",\n        \"tag_removed\": \"Tento štítek je odebrán ze záložky\",\n        \"added_to_list\": \"Do tohoto seznamu je přidána záložka\",\n        \"removed_from_list\": \"Z tohoto seznamu byla odebrána záložka\",\n        \"favourited\": \"Záložka je v oblíbených\",\n        \"archived\": \"Záložka je archivována\"\n      }\n    },\n    \"back_to_app\": \"Zpět do aplikace\",\n    \"user_settings\": \"Uživatelské nastavení\",\n    \"info\": {\n      \"user_info\": \"Informace o uživateli\",\n      \"user_settings\": {\n        \"archive_display_behaviour\": {\n          \"show\": \"Zobrazit archivované záložky ve štítcích a seznamech\",\n          \"hide\": \"Skrýt archivované záložky ve štítcích a seznamech\",\n          \"title\": \"Archivované záložky\"\n        },\n        \"user_settings_updated\": \"Nastavení uživatele byla aktualizována!\",\n        \"bookmark_click_action\": {\n          \"title\": \"Akce po kliknutí na záložku\",\n          \"open_external_url\": \"Otevřít původní URL\",\n          \"open_bookmark_details\": \"Otevřít detaily záložky\"\n        }\n      },\n      \"basic_details\": \"Základní podrobnosti\",\n      \"change_password\": \"Změnit heslo\",\n      \"current_password\": \"Současné heslo\",\n      \"new_password\": \"Nový heslo\",\n      \"confirm_new_password\": \"Potvrďte nový heslo\",\n      \"options\": \"Možnosti\",\n      \"interface_lang\": \"Jazyk rozhraní\",\n      \"reader_settings\": {\n        \"local_overrides_title\": \"Aktivní nastavení specifická pro zařízení\",\n        \"using_default\": \"Používám výchozí nastavení klienta\",\n        \"clear_override_hint\": \"Vymažte přepsání zařízení, abyste použili globální nastavení ({{value}})\",\n        \"font_size\": \"Velikost písma\",\n        \"font_family\": \"Rodina písem\",\n        \"preview_inline\": \"(náhled)\",\n        \"tooltip_preview\": \"Neuložené změny náhledu\",\n        \"save_to_all_devices\": \"Všechna zařízení\",\n        \"tooltip_local\": \"Nastavení zařízení se liší od globálních\",\n        \"reset_preview\": \"Obnovit náhled\",\n        \"mono\": \"Neproporcionální\",\n        \"line_height\": \"Výška řádku\",\n        \"tooltip_default\": \"Nastavení čtení\",\n        \"title\": \"Nastavení čtečky\",\n        \"serif\": \"Patkové\",\n        \"preview\": \"Náhled\",\n        \"not_set\": \"Nenastaveno\",\n        \"clear_local_overrides\": \"Vymazat nastavení zařízení\",\n        \"preview_text\": \"Příliš žluťoučký kůň úpěl ďábelské ódy. Takto bude vypadat text v zobrazení čtečky.\",\n        \"local_overrides_cleared\": \"Nastavení specifická pro zařízení byla vymazána\",\n        \"local_overrides_description\": \"Toto zařízení má nastavení čtečky, která se liší od výchozích:\",\n        \"clear_defaults\": \"Smazat všechna výchozí nastavení\",\n        \"description\": \"Nastav výchozí nastavení textu pro zobrazení v čtečce. Tato nastavení se synchronizují na všech tvých zařízeních.\",\n        \"defaults_cleared\": \"Výchozí nastavení čtečky byla vymazána\",\n        \"save_hint\": \"Uložit nastavení jen pro toto zařízení, nebo synchronizovat na všech zařízeních\",\n        \"save_as_default\": \"Uložit jako výchozí\",\n        \"save_to_device\": \"Toto zařízení\",\n        \"sans\": \"Bezpatkové\",\n        \"tooltip_preview_and_local\": \"Neuložené změny náhledu; nastavení zařízení se liší od globálních\",\n        \"adjust_hint\": \"Upravte nastavení výše, abyste si prohlédli změny\"\n      },\n      \"avatar\": {\n        \"upload\": \"Nahrát avatara\",\n        \"change\": \"Změnit avatara\",\n        \"remove_confirm_title\": \"Odebrat avatara?\",\n        \"updated\": \"Avatar aktualizován\",\n        \"removed\": \"Avatar byl odebrán\",\n        \"description\": \"Nahrajte čtvercový obrázek, který se použije jako váš avatar.\",\n        \"remove_confirm_description\": \"Tímto vymažete vaši aktuální profilovou fotku.\",\n        \"title\": \"Profilová fotka\",\n        \"remove\": \"Odebrat avatara\"\n      }\n    },\n    \"feeds\": {\n      \"rss_subscriptions\": \"RSS odběry\",\n      \"add_a_subscription\": \"Přidat odběr\",\n      \"feed_enabled\": \"RSS kanál je povolen\",\n      \"feed_disabled\": \"RSS kanál je vypnutý\"\n    },\n    \"import\": {\n      \"import_export\": \"Import / Export\",\n      \"import_export_bookmarks\": \"Import / Export záložek\",\n      \"import_bookmarks_from_html_file\": \"Importovat záložky z HTML souboru\",\n      \"import_bookmarks_from_pocket_export\": \"Importovat záložky z exportu Pocket\",\n      \"import_bookmarks_from_matter_export\": \"Importovat záložky z exportu Matter\",\n      \"import_bookmarks_from_omnivore_export\": \"Importovat záložky z Omnivore exportu\",\n      \"import_bookmarks_from_linkwarden_export\": \"Importovat záložky z exportu Linkwarden\",\n      \"import_bookmarks_from_karakeep_export\": \"Importovat záložky z exportu Karakeep\",\n      \"import_bookmarks_from_tab_session_manager_export\": \"Importovat záložky ze správce relací panelů\",\n      \"export_links_and_notes\": \"Exportovat odkazy a poznámky\",\n      \"imported_bookmarks\": \"Importované záložky\",\n      \"import_bookmarks_from_mymind_export\": \"Importovat záložky z exportu mymind\",\n      \"import_bookmarks_from_instapaper_export\": \"Importovat záložky z exportu Instapaper\"\n    },\n    \"broken_links\": {\n      \"broken_links\": \"Nefunkční odkazy\",\n      \"last_crawled_at\": \"Poslední procházení proběhlo\",\n      \"crawling_status\": \"Stav procházení\",\n      \"crawling_failed\": \"Procházení selhalo\"\n    },\n    \"stats\": {\n      \"usage_statistics\": \"Statistiky využití\",\n      \"insights_description\": \"Statistiky o tvých zvycích a sbírce záložek\",\n      \"failed_to_load\": \"Nepodařilo se načíst statistiky\",\n      \"overview\": {\n        \"total_bookmarks\": \"Celkem záložek\",\n        \"all_saved_items\": \"Všechny uložené položky\",\n        \"favorites\": \"Oblíbené\",\n        \"starred_bookmarks\": \"Označené záložky\",\n        \"archived\": \"Archivováno\",\n        \"archived_items\": \"Archivované položky\",\n        \"tags\": \"Štítky\",\n        \"unique_tags_created\": \"Vytvořeny jedinečné štítky\",\n        \"lists\": \"Seznamy\",\n        \"bookmark_collections\": \"Sbírky záložek\",\n        \"highlights\": \"Zvýraznění\",\n        \"text_highlights\": \"Zvýraznění textu\",\n        \"storage_used\": \"Využité úložiště\",\n        \"total_asset_storage\": \"Celkové úložiště aktiv\",\n        \"this_month\": \"Tento měsíc\",\n        \"bookmarks_added\": \"Přidány záložky\"\n      },\n      \"bookmark_types\": {\n        \"title\": \"Typy záložek\",\n        \"links\": \"Odkazy\",\n        \"text_notes\": \"Textové poznámky\",\n        \"assets\": \"Aktiva\"\n      },\n      \"recent_activity\": {\n        \"title\": \"Nedávná aktivita\",\n        \"this_week\": \"Tento týden\",\n        \"this_month\": \"Tento měsíc\",\n        \"this_year\": \"Tento rok\"\n      },\n      \"top_domains\": {\n        \"title\": \"Nejlepší domény\",\n        \"no_domains_found\": \"Nenalezeny žádné domény\"\n      },\n      \"most_used_tags\": {\n        \"title\": \"Nejpoužívanější štítky\",\n        \"no_tags_found\": \"Nebyly nalezeny žádné štítky\"\n      },\n      \"activity_patterns\": {\n        \"activity_by_hour\": \"Aktivita po hodinách\",\n        \"activity_by_day\": \"Aktivita podle dne\"\n      },\n      \"storage_breakdown\": {\n        \"title\": \"Rozpis úložiště\"\n      },\n      \"bookmark_sources\": {\n        \"title\": \"Zdroje záložek\",\n        \"empty\": \"Nejsou dostupná žádná zdrojová data\"\n      }\n    },\n    \"subscription\": {\n      \"subscription\": \"Předplatné\",\n      \"manage_subscription\": \"Spravujte své předplatné a fakturační údaje\",\n      \"current_plan\": \"Aktuální tarif\",\n      \"billing_period\": \"Fakturační období\",\n      \"paid_plan\": \"Placený tarif\",\n      \"unlock_bigger_quota\": \"Odemkněte větší kvótu a podpořte projekt\",\n      \"subscribe_now\": \"Předplatit\",\n      \"manage_billing\": \"Spravovat fakturaci\",\n      \"subscription_canceled\": \"Vaše předplatné bylo zrušeno a skončí {{date}}. Můžete se kdykoli znovu přihlásit k odběru.\",\n      \"usage_quotas\": \"Využití a kvóty\",\n      \"track_usage\": \"Sledujte aktuální využití oproti limitům vašeho tarifu\",\n      \"total_bookmarks_saved\": \"Celkový počet uložených záložek\",\n      \"assets_file_storage\": \"Prostor pro aktiva a soubory\",\n      \"unlimited_usage\": \"Neomezené použití\",\n      \"quota_limit_reached\": \"Limit kvóty byl dosažen\",\n      \"approaching_quota_limit\": \"Blížíte se k limitu kvóty\",\n      \"loading_usage\": \"Načítám informace o využití…\",\n      \"free\": \"Zdarma\",\n      \"paid\": \"Placené\"\n    },\n    \"import_sessions\": {\n      \"title\": \"Importovat relace\",\n      \"description\": \"Zobrazte a spravujte relace hromadného importu. Relace se vytvoří automaticky, když importujete záložky.\",\n      \"load_error\": \"Nepodařilo se načíst relace importu\",\n      \"no_sessions\": \"Zatím žádné relace importu\",\n      \"no_sessions_detail\": \"Relace importu se zde zobrazí automaticky, když importujete záložky\",\n      \"created_at\": \"Vytvořeno: {{time}}\",\n      \"progress\": \"Průběh\",\n      \"status\": {\n        \"pending\": \"Čeká na vyřízení\",\n        \"in_progress\": \"Probíhá\",\n        \"completed\": \"Dokončeno\",\n        \"failed\": \"Selhalo\",\n        \"processing\": \"Zpracovává se\",\n        \"staging\": \"Příprava\",\n        \"running\": \"Běží\",\n        \"paused\": \"Pozastaveno\"\n      },\n      \"badges\": {\n        \"pending\": \"{{count}} čeká na vyřízení\",\n        \"processing\": \"{{count}} se zpracovává\",\n        \"completed\": \"{{count}} dokončeno\",\n        \"failed\": \"{{count}} selhalo\"\n      },\n      \"imported_to\": \"Importováno do:\",\n      \"view_list\": \"Zobrazit seznam\",\n      \"delete_dialog_title\": \"Smazat import relaci\",\n      \"delete_dialog_description\": \"Určitě chceš smazat \\\"{{name}}\\\"? Tahle akce nemůže být vrácena zpátky. Samotný záložky smazaný nebudou.\",\n      \"delete_session\": \"Smazat relaci\",\n      \"pause_session\": \"Pauza\",\n      \"resume_session\": \"Obnovit\",\n      \"view_details\": \"Zobrazit podrobnosti\",\n      \"detail\": {\n        \"page_title\": \"Podrobnosti importu relace\",\n        \"back_to_import\": \"Zpět na Import\",\n        \"filter_all\": \"Vše\",\n        \"filter_accepted\": \"Přijato\",\n        \"filter_rejected\": \"Odmítnuto\",\n        \"filter_duplicates\": \"Duplikáty\",\n        \"filter_pending\": \"Čeká na vyřízení\",\n        \"table_title\": \"Název / URL\",\n        \"table_type\": \"Typ\",\n        \"table_result\": \"Výsledek\",\n        \"table_reason\": \"Důvod\",\n        \"table_bookmark\": \"Záložka\",\n        \"result_accepted\": \"Přijato\",\n        \"result_rejected\": \"Odmítnuto\",\n        \"result_skipped_duplicate\": \"Duplikát\",\n        \"result_pending\": \"Čeká na vyřízení\",\n        \"result_processing\": \"Zpracovávání\",\n        \"no_results\": \"Pro tento filtr nebyly nalezeny žádné výsledky.\",\n        \"view_bookmark\": \"Zobrazit záložku\",\n        \"load_more\": \"Načíst další\",\n        \"no_title\": \"Bez názvu\"\n      }\n    },\n    \"backups\": {\n      \"backups\": \"Zálohy\",\n      \"page_title\": \"Zálohy\",\n      \"page_description\": \"Automaticky vytvářej a spravuj zálohy tvých záložek. Zálohy se komprimují a bezpečně ukládají.\",\n      \"configuration\": {\n        \"title\": \"Nastavení zálohování\",\n        \"enable_automatic_backups\": \"Povolit automatické zálohování\",\n        \"enable_automatic_backups_description\": \"Automaticky vytvářet zálohy mých záložek\",\n        \"backup_frequency\": \"Četnost zálohování\",\n        \"backup_frequency_description\": \"Jak často by se měly zálohy vytvářet\",\n        \"retention_period\": \"Doba uchování (dny)\",\n        \"retention_period_description\": \"Kolik dní uchovávat zálohy, než se smažou\",\n        \"frequency\": {\n          \"daily\": \"Denně\",\n          \"weekly\": \"Týdně\"\n        },\n        \"select_frequency\": \"Vyber četnost\",\n        \"save_settings\": \"Uložit nastavení\"\n      },\n      \"list\": {\n        \"title\": \"Tvoje zálohy\",\n        \"create_backup_now\": \"Vytvořit zálohu teď\",\n        \"no_backups\": \"Zatím nemáš žádné zálohy. Povol automatické zálohování nebo vytvoř zálohu ručně.\",\n        \"table\": {\n          \"created_at\": \"Vytvořeno\",\n          \"bookmarks\": \"Záložky\",\n          \"size\": \"Velikost\",\n          \"status\": \"Stav\",\n          \"actions\": \"Akce\"\n        },\n        \"status\": {\n          \"success\": \"Úspěch\",\n          \"failed\": \"Neúspěšné\",\n          \"pending\": \"Čeká se\"\n        },\n        \"actions\": {\n          \"download_backup\": \"Stáhnout zálohu\",\n          \"delete_backup\": \"Smazat zálohu\"\n        }\n      },\n      \"dialogs\": {\n        \"delete_backup_title\": \"Smazat zálohu?\",\n        \"delete_backup_description\": \"Jste si jistí, že chcete smazat tuto zálohu? Tuto akci nelze vrátit zpět.\"\n      },\n      \"toasts\": {\n        \"backup_queued\": \"Úloha zálohování byla zařazena do fronty! Bude brzy zpracována.\",\n        \"backup_deleted\": \"Záloha byla smazána!\"\n      }\n    }\n  },\n  \"lists\": {\n    \"search_query\": \"Vyhledávací dotaz\",\n    \"search_query_help\": \"Zjistěte více o jazyce vyhledávacích dotazů.\",\n    \"share_list\": \"Sdílet seznam\",\n    \"new_nested_list\": \"Nový vnořený seznam\",\n    \"no_parent\": \"Žádný rodič\",\n    \"list_type\": \"Typ seznamu\",\n    \"manual_list\": \"Ruční seznam\",\n    \"smart_list\": \"Chytrý seznam\",\n    \"description\": \"Popis (volitelné)\",\n    \"all_lists\": \"Všechny seznamy\",\n    \"favourites\": \"Oblíbené\",\n    \"new_list\": \"Nový seznam\",\n    \"edit_list\": \"Upravit seznam\",\n    \"merge_list\": \"Sloučit seznam\",\n    \"destination_list\": \"Cílový seznam\",\n    \"delete_after_merge\": \"Po sloučení smazat původní seznam\",\n    \"no_destination\": \"Žádný cíl\",\n    \"parent_list\": \"Nadřazený seznam\",\n    \"rss\": {\n      \"title\": \"RSS kanál\",\n      \"description\": \"Povolit RSS kanál pro tento seznam\",\n      \"feed_url\": \"URL RSS kanálu\"\n    },\n    \"public_list\": {\n      \"title\": \"Veřejný seznam\",\n      \"description\": \"Povolit ostatním zobrazit tento seznam\",\n      \"share_link\": \"Odkaz pro sdílení\"\n    },\n    \"delete_list\": {\n      \"title\": \"Smazat seznam\",\n      \"description\": \"Smazáním seznamu se nesmažou žádné záložky v tomto seznamu.\",\n      \"delete_children\": \"Odstranit podřízené seznamy (rekurzivně)\",\n      \"delete_children_description\": \"Pokud není zaškrtnuto, všechny přímé podřízené seznamy se stanou kořenovými seznamy.\"\n    },\n    \"shared\": \"Sdílené\",\n    \"collaborators\": {\n      \"manage\": \"Spravovat spolupracovníky\",\n      \"view\": \"Zobrazit spolupracovníky\",\n      \"collaborators\": \"Spolupracovníci\",\n      \"add\": \"Přidat spolupracovníka\",\n      \"current\": \"Současní spolupracovníci\",\n      \"enter_email\": \"Zadej e-mailovou adresu\",\n      \"please_enter_email\": \"Zadej e-mailovou adresu\",\n      \"added_successfully\": \"Spolupracovník byl úspěšně přidán\",\n      \"failed_to_add\": \"Nepodařilo se přidat spolupracovníka\",\n      \"removed\": \"Spolupracovník byl odebrán\",\n      \"failed_to_remove\": \"Nepodařilo se odebrat spolupracovníka\",\n      \"role_updated\": \"Role aktualizována\",\n      \"failed_to_update_role\": \"Nepodařilo se aktualizovat roli\",\n      \"viewer\": \"Divák\",\n      \"editor\": \"Editor\",\n      \"owner\": \"Vlastník\",\n      \"viewer_description\": \"Může zobrazit záložky v seznamu\",\n      \"editor_description\": \"Může přidávat a odebírat záložky\",\n      \"no_collaborators\": \"Zatím žádní spolupracovníci. Přidej někoho a začněte spolupracovat!\",\n      \"no_collaborators_readonly\": \"Pro tenhle seznam nejsou žádní spolupracovníci.\",\n      \"people_with_access\": \"Lidé, kteří mají přístup k tomuto seznamu\",\n      \"add_or_remove\": \"Přidej nebo odeber lidi, kteří mají přístup k tomuto seznamu\",\n      \"invitation_sent\": \"Pozvánka odeslána úspěšně\",\n      \"invitation_revoked\": \"Pozvánka zrušena\",\n      \"failed_to_revoke\": \"Nepodařilo se zrušit pozvánku\",\n      \"pending\": \"Čeká se\",\n      \"revoke\": \"Zrušit\",\n      \"declined\": \"Odmítnuto\"\n    },\n    \"leave_list\": {\n      \"title\": \"Opustit Seznam\",\n      \"confirm_message\": \"Jsi si jistý, že chceš opustit {{icon}} {{name}}?\",\n      \"warning\": \"Nebudeš už moct zobrazovat ani přistupovat k záložkam v tomto seznamu. Vlastník seznamu tě může, když bude potřeba, přidat zpátky.\",\n      \"action\": \"Opustit Seznam\",\n      \"success\": \"Opustil(a) jsi \\\"{{icon}} {{name}}\\\"\"\n    },\n    \"invitations\": {\n      \"pending\": \"Pozvánky čekající na vyřízení\",\n      \"description\": \"Zkontrolujte pozvánky ke spolupráci na seznamech a reagujte na ně\",\n      \"invited_by\": \"Pozval(a)\",\n      \"accept\": \"Přijmout\",\n      \"decline\": \"Odmítnout\",\n      \"accepted\": \"Pozvánka přijata\",\n      \"declined\": \"Pozvánka odmítnuta\",\n      \"failed_to_accept\": \"Nepodařilo se přijmout pozvánku\",\n      \"failed_to_decline\": \"Nepodařilo se odmítnout pozvánku\"\n    },\n    \"shared_lists\": \"Sdílené seznamy\"\n  },\n  \"tags\": {\n    \"unused_tags\": \"Nepoužité tagy\",\n    \"unused_tags_info\": \"Štítky, které nejsou připojeny k žádným záložkám\",\n    \"all_tags\": \"Všechny štítky\",\n    \"your_tags\": \"Tvoje štítky\",\n    \"your_tags_info\": \"Štítky, které jsi alespoň jednou připojil(a)\",\n    \"ai_tags\": \"AI Tagy\",\n    \"ai_tags_info\": \"Štítky, které byly připojeny pouze automaticky (pomocí AI)\",\n    \"delete_all_unused_tags\": \"Smazat všechny nepoužívaný štítky\",\n    \"drag_and_drop_merging\": \"Sloučení přetažením\",\n    \"drag_and_drop_merging_info\": \"Přetáhni štítky na sebe a sloučíš je\",\n    \"sort_by_name\": \"Seřadit podle jména\",\n    \"create_tag\": \"Vytvořit štítek\",\n    \"create_tag_description\": \"Vytvořte nový štítek bez připojení k žádné záložce\",\n    \"tag_name\": \"Název štítku\",\n    \"enter_tag_name\": \"Zadejte název štítku\",\n    \"sort_by_usage\": \"Seřadit podle použití\",\n    \"sort_by_relevance\": \"Seřadit podle relevance\",\n    \"no_custom_tags\": \"Zatím žádné vlastní štítky\",\n    \"no_ai_tags\": \"Zatím žádné AI štítky\",\n    \"no_unused_tags\": \"Nemáte žádné nepoužité štítky\",\n    \"no_unused_tags_match_your_search\": \"Žádné nepoužité štítky neodpovídají vašemu hledání\",\n    \"no_tags_match_your_search\": \"Žádné štítky neodpovídají vašemu hledání\",\n    \"search_placeholder\": \"Hledat štítky...\",\n    \"search_or_create_placeholder\": \"Hledat nebo vytvořit štítky...\"\n  },\n  \"search\": {\n    \"is_favorited\": \"Je oblíbené\",\n    \"created_on_or_after\": \"Vytvořeno dne nebo po\",\n    \"day_s\": \" Den(dnů)\",\n    \"has_tag\": \"Má tag\",\n    \"month_s\": \" Měsíc(e)\",\n    \"year_s\": \" Rok(y)\",\n    \"day_s_ago\": \" Dny zpátky\",\n    \"week_s_ago\": \" Před týdnem/týdny\",\n    \"month_s_ago\": \" Měsíc(e) zpět\",\n    \"year_s_ago\": \" Před lety\",\n    \"url_contains\": \"URL obsahuje\",\n    \"url_does_not_contain\": \"URL neobsahuje\",\n    \"is_in_list\": \"Je v seznamu\",\n    \"is_not_favorited\": \"Není v oblíbených\",\n    \"is_archived\": \"Je archivováno\",\n    \"is_not_archived\": \"Není archivován\",\n    \"has_any_tag\": \"Má nějaký štítek\",\n    \"has_no_tags\": \"Nemá štítek\",\n    \"is_in_any_list\": \"Je v libovolném seznamu\",\n    \"is_not_in_any_list\": \"Není v žádném seznamu\",\n    \"not_created_on_or_after\": \"Nevytvořeno dne nebo po\",\n    \"created_on_or_before\": \"Vytvořeno dne nebo před\",\n    \"not_created_on_or_before\": \"Nevytvořeno před nebo v\",\n    \"created_within\": \"Vytvořeno v\",\n    \"created_earlier_than\": \"Vytvořeno dříve než\",\n    \"week_s\": \" Týden/týdny\",\n    \"is_not_in_list\": \"Není v seznamu\",\n    \"does_not_have_tag\": \"Nemá štítek\",\n    \"full_text_search\": \"Fulltextové vyhledávání\",\n    \"type_is\": \"Typ je\",\n    \"type_is_not\": \"Typ není\",\n    \"is_from_feed\": \"Pochází z RSS kanálu\",\n    \"is_not_from_feed\": \"Není z RSS kanálu\",\n    \"and\": \"A\",\n    \"or\": \"Nebo\",\n    \"history\": \"Poslední hledání\",\n    \"title_contains\": \"Název obsahuje\",\n    \"title_does_not_contain\": \"Název neobsahuje\",\n    \"is_broken_link\": \"Má nefunkční odkaz\",\n    \"tags\": \"Štítky\",\n    \"no_suggestions\": \"Žádné návrhy\",\n    \"filters\": \"Filtry\",\n    \"is_not_broken_link\": \"Má funkční odkaz\",\n    \"lists\": \"Seznamy\",\n    \"feeds\": \"Kanály\",\n    \"is_from_source\": \"Zdroj je\",\n    \"is_not_from_source\": \"Zdroj není\"\n  },\n  \"editor\": {\n    \"disabled_submissions\": \"Odesílání příspěvků je zakázáno\",\n    \"text_toolbar\": {\n      \"bold\": \"Tučné\",\n      \"markdown_shortcuts\": {\n        \"ordered_list\": {\n          \"label\": \"Seřazený seznam\",\n          \"example\": \"1. Položka seznamu\"\n        },\n        \"italic\": {\n          \"example\": \"*Kurzíva* nebo _Kurzíva_ nebo CTRL+i\",\n          \"label\": \"Kurzíva\"\n        },\n        \"blockquote\": {\n          \"label\": \"Citát\",\n          \"example\": \"> Citace\"\n        },\n        \"unordered_list\": {\n          \"label\": \"Neuspořádaný seznam\",\n          \"example\": \"- Položka seznamu\"\n        },\n        \"label\": \"Markdown zkratky\",\n        \"heading\": {\n          \"label\": \"Nadpis\",\n          \"example\": \"# H1, ## H2, ### H3\"\n        },\n        \"bold\": {\n          \"label\": \"Tučné\",\n          \"example\": \"**text** nebo CTRL+b\"\n        },\n        \"inline_code\": {\n          \"label\": \"Vložený kód\",\n          \"example\": \"`Kód`\"\n        },\n        \"block_code\": {\n          \"label\": \"Blokovat kód\",\n          \"example\": \"``` + mezera\"\n        }\n      },\n      \"align_left\": \"Zarovnat vlevo\",\n      \"align_center\": \"Zarovnat na střed\",\n      \"align_right\": \"Zarovnat doprava\",\n      \"undo\": \"Zpět\",\n      \"redo\": \"Znovu\",\n      \"italic\": \"Kurzíva\",\n      \"underline\": \"Podtržení\",\n      \"strikethrough\": \"Přeškrtnutí\",\n      \"code\": \"Kód\",\n      \"highlight\": \"Zvýraznit\"\n    },\n    \"quickly_focus\": \"Do tohoto pole se rychle dostaneš stisknutím ⌘ + E\",\n    \"multiple_urls_dialog_title\": \"Importovat URL jako samostatné záložky?\",\n    \"multiple_urls_dialog_desc\": \"Vstup obsahuje více URL na samostatných řádcích. Chceš je importovat jako samostatné záložky?\",\n    \"import_as_text\": \"Importovat jako textovou záložku\",\n    \"import_as_separate_bookmarks\": \"Importovat jako samostatné záložky\",\n    \"placeholder\": \"Vložte odkaz nebo obrázek, napište poznámku nebo sem přetáhněte obrázek…\",\n    \"placeholder_v2\": \"Vložte odkaz, napište poznámku nebo přetáhněte obrázek…\",\n    \"new_item\": \"NOVÁ POLOŽKA\"\n  },\n  \"toasts\": {\n    \"bookmarks\": {\n      \"updated\": \"Záložka byla aktualizována!\",\n      \"deleted\": \"Záložka byla smazána!\",\n      \"refetch\": \"Opětovné načtení bylo zařazeno do fronty!\",\n      \"full_page_archive\": \"Vytváření archivu celé stránky bylo spuštěno\",\n      \"delete_from_list\": \"Záložka byla ze seznamu smazána\",\n      \"clipboard_copied\": \"Odkaz byl přidán do schránky!\",\n      \"preserve_pdf\": \"Ukládání do PDF spuštěno\",\n      \"update_banner\": \"Banner byl aktualizován!\",\n      \"uploading_banner\": \"Nahrávání banneru...\"\n    },\n    \"lists\": {\n      \"created\": \"Seznam byl vytvořen!\",\n      \"updated\": \"Seznam byl aktualizován!\",\n      \"merged\": \"Seznam byl sloučen!\",\n      \"deleted\": \"Seznam byl smazán!\"\n    },\n    \"tags\": {\n      \"created\": \"Štítek byl vytvořen!\",\n      \"failed_to_create\": \"Nepodařilo se vytvořit štítek\"\n    }\n  },\n  \"admin\": {\n    \"server_stats\": {\n      \"server_stats\": \"Statistiky serveru\",\n      \"total_users\": \"Celkem uživatelů\",\n      \"total_bookmarks\": \"Celkem záložek\",\n      \"server_version\": \"Verze serveru\"\n    },\n    \"background_jobs\": {\n      \"tidy_assets_jobs\": \"Úlohy na úklid aktiv\",\n      \"video_jobs\": \"Úlohy stahování videí\",\n      \"webhook_jobs\": \"Úlohy Webhooku\",\n      \"asset_preprocessing_jobs\": \"Úlohy předběžného zpracování aktiv\",\n      \"feed_jobs\": \"Úlohy RSS kanálu\",\n      \"job\": \"Úloha\",\n      \"background_jobs\": \"Úlohy na pozadí\",\n      \"crawler_jobs\": \"Úlohy crawleru\",\n      \"indexing_jobs\": \"Úlohy indexování\",\n      \"inference_jobs\": \"Úlohy odvozování\",\n      \"queued\": \"Ve frontě\",\n      \"pending\": \"Čeká se\",\n      \"failed\": \"Neúspěšné\",\n      \"jobs\": {\n        \"crawler\": {\n          \"title\": \"Úlohy pro crawlery\",\n          \"description\": \"Webové procházení a extrahování obsahu z URL adres\"\n        },\n        \"inference\": {\n          \"title\": \"Úlohy odvozování\",\n          \"description\": \"Tagování a shrnutí obsahu řízené umělou inteligencí\"\n        },\n        \"indexing\": {\n          \"title\": \"Úlohy indexování\",\n          \"description\": \"Aktualizace indexu vyhledávání\"\n        },\n        \"asset_preprocessing\": {\n          \"title\": \"Úlohy předběžného zpracování aktiv\",\n          \"description\": \"Předběžné zpracování obrázků a dokumentů (snímky obrazovky, extrahování textu, atd.)\"\n        },\n        \"tidy_assets\": {\n          \"title\": \"Úlohy Tidy Assets\",\n          \"description\": \"Vyčištění aktiv a optimalizace úložiště\"\n        },\n        \"video\": {\n          \"title\": \"Úlohy stahování videa\",\n          \"description\": \"Extrakce a stahování videa\"\n        },\n        \"webhook\": {\n          \"title\": \"Úlohy Webhook\",\n          \"description\": \"Externí oznámení webhook\"\n        },\n        \"feed\": {\n          \"title\": \"Úlohy kanálu RSS\",\n          \"description\": \"Zpracování kanálu RSS a aktualizace obsahu\"\n        },\n        \"admin_maintenance\": {\n          \"title\": \"Úlohy údržby správce\",\n          \"description\": \"Administrativní úklid a údržba majetku\"\n        }\n      },\n      \"monitor_and_manage\": \"Monitorujte a spravujte fronty úloh na pozadí a úlohy zpracování systému.\",\n      \"active\": \"Aktivní\",\n      \"available_actions\": \"Dostupné akce\",\n      \"status\": {\n        \"title\": \"Vysvětlení stavů úloh\",\n        \"queued\": {\n          \"title\": \"Ve frontě\",\n          \"description\": \"Úlohy čekají ve frontě na zpracování. Spustí se automaticky, až budou k dispozici zdroje.\"\n        },\n        \"unprocessed\": {\n          \"title\": \"Nezpracované\",\n          \"description\": \"Záložky, které ještě nebyly zpracovány. S největší pravděpodobností jsou již ve frontě na zpracování, pokud ne, možná je budete muset ručně znovu zařadit do fronty.\"\n        },\n        \"failed\": {\n          \"title\": \"Neúspěšné\",\n          \"description\": \"Záložky, u kterých se během zpracování vyskytly chyby. Ty mohou vyžadovat ruční zásah.\"\n        }\n      },\n      \"actions\": {\n        \"recrawl_failed_links_only\": \"Znovu projít pouze nefunkční odkazy\",\n        \"recrawl_all_links\": \"Znovu projít všechny odkazy\",\n        \"without_inference\": \"Bez odvozování\",\n        \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Znovu vygenerovat značky umělé inteligence pouze pro nefunkční záložky\",\n        \"regenerate_ai_tags_for_all_bookmarks\": \"Znovu vygenerovat značky umělé inteligence pro všechny záložky\",\n        \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Znovu vygenerovat souhrny umělé inteligence pouze pro nefunkční záložky\",\n        \"regenerate_ai_summaries_for_all_bookmarks\": \"Znovu vygenerovat souhrny umělé inteligence pro všechny záložky\",\n        \"reindex_all_bookmarks\": \"Znovu indexovat všechny záložky\",\n        \"clean_assets\": \"Vyčistit ztracené zdroje a znovu synchronizovat metadata\",\n        \"reprocess_assets_fix_mode\": \"Znovu zpracovat nezpracované zdroje\",\n        \"migrate_large_link_html_content\": \"Přesun velkého vloženého HTML obsahu do assetů\",\n        \"recrawl_pending_links_only\": \"Znovu projít pouze čekající odkazy\",\n        \"regenerate_ai_tags_for_pending_bookmarks_only\": \"Znovu vygenerovat AI tagy pouze pro čekající záložky\",\n        \"regenerate_ai_summaries_for_pending_bookmarks_only\": \"Znovu vygenerovat AI souhrny pouze pro čekající záložky\"\n      }\n    },\n    \"admin_settings\": \"Nastavení správce\",\n    \"actions\": {\n      \"recrawl_failed_links_only\": \"Znovu procházet pouze nefunkční odkazy\",\n      \"recrawl_all_links\": \"Znovu projít všechny odkazy\",\n      \"without_inference\": \"Bez inference\",\n      \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Znovu vygenerovat štítky AI pouze pro neúspěšné záložky\",\n      \"regenerate_ai_tags_for_all_bookmarks\": \"Znovu vygenerovat AI tagy pro všechny záložky\",\n      \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Znovu vygenerovat souhrny AI pouze pro neúspěšné záložky\",\n      \"regenerate_ai_summaries_for_all_bookmarks\": \"Znovu vygenerovat shrnutí AI pro všechny záložky\",\n      \"reindex_all_bookmarks\": \"Znovu indexovat všechny záložky\",\n      \"compact_assets\": \"Kompaktní aktiva\",\n      \"reprocess_assets_fix_mode\": \"Znovu zpracovat aktiva (režim opravy)\"\n    },\n    \"users_list\": {\n      \"users_list\": \"Seznam uživatelů\",\n      \"create_user\": \"Vytvořit uživatele\",\n      \"change_role\": \"Změnit roli\",\n      \"reset_password\": \"Resetovat heslo\",\n      \"delete_user\": \"Smazat uživatele\",\n      \"num_bookmarks\": \"Počet záložek\",\n      \"asset_sizes\": \"Velikosti aktiv\",\n      \"local_user\": \"Místní uživatel\",\n      \"confirm_password\": \"Potvrď heslo\",\n      \"delete_user_confirm_description\": \"Jsi si jistý, že chceš smazat uživatele „{{name}}“?\",\n      \"unlimited\": \"Neomezený\"\n    },\n    \"service_connections\": {\n      \"title\": \"Servisní připojení\",\n      \"description\": \"Monitorujte stav a připojení externích systémových závislostí\",\n      \"search_engine\": \"Vyhledávač\",\n      \"browser\": \"Prohlížeč\",\n      \"queue_system\": \"Systém front\",\n      \"status\": {\n        \"not_configured\": \"Nenakonfigurováno\",\n        \"connected\": \"Připojeno\",\n        \"disconnected\": \"Odpojeno\"\n      }\n    },\n    \"admin_tools\": {\n      \"admin_tools\": \"Nástroje správce\",\n      \"bookmark_debugger\": \"Ladička záložek\",\n      \"bookmark_id\": \"ID záložky\",\n      \"bookmark_id_placeholder\": \"Zadej ID záložky\",\n      \"lookup\": \"Vyhledat\",\n      \"debug_info\": \"Informace pro ladění\",\n      \"basic_info\": \"Základní informace\",\n      \"status\": \"Stav\",\n      \"content\": \"Obsah\",\n      \"html_preview\": \"Náhled HTML (první 1000 znaků)\",\n      \"summary\": \"Shrnutí\",\n      \"url\": \"URL\",\n      \"source_url\": \"Zdrojová URL\",\n      \"asset_type\": \"Typ zdroje\",\n      \"file_name\": \"Název souboru\",\n      \"owner_user_id\": \"ID vlastníka\",\n      \"tagging_status\": \"Stav označování štítky\",\n      \"summarization_status\": \"Stav sumarizace\",\n      \"crawl_status\": \"Stav procházení\",\n      \"crawl_status_code\": \"Stavový kód HTTP\",\n      \"crawled_at\": \"Prolezl v\",\n      \"recrawl\": \"Znovu prolézt\",\n      \"reindex\": \"Znovu indexovat\",\n      \"retag\": \"Znovu označit\",\n      \"resummarize\": \"Znovu shrnout\",\n      \"bookmark_not_found\": \"Záložka nebyla nalezena\",\n      \"action_success\": \"Akce byla úspěšně dokončena\",\n      \"action_failed\": \"Akce selhala\",\n      \"recrawl_queued\": \"Úloha pro opětovné prolezení byla zařazena do fronty\",\n      \"reindex_queued\": \"Úloha pro opětovné indexování byla zařazena do fronty\",\n      \"retag_queued\": \"Úloha pro opětovné označení byla zařazena do fronty\",\n      \"resummarize_queued\": \"Úloha pro opětovné shrnutí byla zařazena do fronty\",\n      \"view\": \"Zobrazit\",\n      \"fetch_error\": \"Chyba při načítání záložky\"\n    }\n  },\n  \"bookmark_editor\": {\n    \"author\": \"Autor\",\n    \"publisher\": \"Vydavatel\",\n    \"date_published\": \"Datum zveřejnění\",\n    \"pick_a_date\": \"Vyberte datum\",\n    \"title\": \"Upravit záložku\",\n    \"subtitle\": \"Proveď změny v podrobnostech záložky. Až budeš hotov, klikni na uložit.\",\n    \"save_changes\": \"Uložit změny\",\n    \"extracted_content\": \"Extrahovaný obsah\"\n  },\n  \"layouts\": {\n    \"masonry\": \"Zděné\",\n    \"grid\": \"Mřížka\",\n    \"list\": \"Seznam\",\n    \"compact\": \"Kompaktní\"\n  },\n  \"highlights\": {\n    \"no_highlights\": \"Zatím nemáte žádné zvýraznění.\"\n  },\n  \"options\": {\n    \"dark_mode\": \"Tmavý režim\",\n    \"light_mode\": \"Světlý režim\",\n    \"apps_extensions\": \"Aplikace a rozšíření\",\n    \"documentation\": \"Dokumentace\",\n    \"follow_us_on_x\": \"Sleduj nás na Xku\"\n  },\n  \"preview\": {\n    \"view_original\": \"Zobrazit originál\",\n    \"cached_content\": \"Obsah uložený v mezipaměti\",\n    \"reader_view\": \"Zobrazení čtečky\",\n    \"tabs\": {\n      \"content\": \"Obsah\",\n      \"details\": \"Podrobnosti\"\n    },\n    \"archive_info\": \"Archivy se nemusí vykreslovat správně inline, pokud vyžadují Javascript. Pro nejlepší výsledky si <1>stáhněte a otevřete v prohlížeči</1>.\",\n    \"fetch_error_title\": \"Obsah není k dispozici\",\n    \"fetch_error_description\": \"Nepodařilo se nám načíst obsah pro tento odkaz. Stránka může být chráněná, vyžadovat ověření nebo být dočasně nedostupná.\",\n    \"crawling_in_progress\": \"Načítám obsah stránky…\",\n    \"continue_reading\": \"Pokračuj tam, kde jsi skončil\",\n    \"continue_reading_percent\": \"Pokračuj tam, kde jsi skončil ({{percent}} %)\",\n    \"continue_button\": \"Pokračovat\"\n  },\n  \"dialogs\": {\n    \"bookmarks\": {\n      \"delete_confirmation_title\": \"Smazat záložku?\",\n      \"delete_confirmation_description\": \"Určitě chceš smazat tuhle záložku?\"\n    }\n  },\n  \"banners\": {\n    \"no_bookmarks\": {\n      \"title\": \"Zatím žádné záložky\",\n      \"description\": \"Ukládej si zajímavé články, odkazy a stránky, abys k nim měl později rychlý přístup.\"\n    }\n  },\n  \"cleanups\": {\n    \"cleanups\": \"Vyčištění\",\n    \"duplicate_tags\": {\n      \"title\": \"Duplicitní štítky\",\n      \"merge_all_suggestions\": \"Sloučit všechny návrhy?\"\n    }\n  },\n  \"view_options\": {\n    \"title\": \"Možnosti zobrazení\",\n    \"layout\": \"Rozvržení\",\n    \"columns\": \"Sloupce\",\n    \"display_options\": \"Možnosti zobrazení\",\n    \"show_note_previews\": \"Zobrazit poznámky\",\n    \"show_tags\": \"Zobrazit štítky\",\n    \"show_title\": \"Zobrazit název\",\n    \"image_options\": \"Možnosti obrázku\",\n    \"image_fit_cover\": \"Obálka (výplň)\",\n    \"image_fit_contain\": \"Obsahovat (přizpůsobit)\"\n  },\n  \"version\": {\n    \"new_release_available\": \"K dispozici jsou nové poznámky k vydání\",\n    \"whats_new_title\": \"Co je nového ve verzi v{{version}}\",\n    \"release_notes_description\": \"Tady jsou nejnovější aktualizace stažené z poznámek k vydání na GitHubu.\",\n    \"loading_release_notes\": \"Načítají se poznámky k verzi…\",\n    \"unable_to_load_release_notes\": \"Právě teď se nepodařilo načíst poznámky k verzi. Zkuste to prosím později.\",\n    \"no_release_notes\": \"Pro tuto verzi nebyly publikovány žádné poznámky k vydání.\",\n    \"release_notes_synced\": \"Poznámky k vydání se synchronizují z GitHubu.\",\n    \"view_on_github\": \"Zobrazit na GitHubu\"\n  },\n  \"wrapped\": {\n    \"title\": \"Tvůj {{year}} Wrapped\",\n    \"subtitle\": \"Rok v Karakeepu\",\n    \"banner\": {\n      \"title\": \"Tvůj 2025 Wrapped je připraven!\",\n      \"description\": \"Podívej se na svůj rok v záložkách\",\n      \"view_now\": \"Zobrazit nyní\"\n    },\n    \"button\": \"2025 Wrapped\",\n    \"loading\": \"Načítání tvého Wrapped...\",\n    \"failed_to_load\": \"Nepodařilo se načíst tvé statistiky Wrapped\",\n    \"sections\": {\n      \"total_saves\": {\n        \"prefix\": \"Uložil(a) jsi\",\n        \"suffix\": \"položek tento rok\",\n        \"suffix_singular\": \"položka tento rok\"\n      },\n      \"first_bookmark\": {\n        \"title\": \"Tvoje cesta začala\",\n        \"description\": \"První uložení v roce {{year}}:\"\n      },\n      \"top_domains\": \"Tvoje nejlepší weby\",\n      \"top_tags\": \"Tvoje nejlepší štítky\",\n      \"monthly_activity\": \"Tvůj rok v uložených položkách\",\n      \"most_active_day\": \"Tvůj nejaktivnější den\",\n      \"peak_times\": {\n        \"title\": \"Kdy ukládáš\",\n        \"peak_hour\": \"Špička\",\n        \"peak_day\": \"Nejlepší den\"\n      },\n      \"how_you_save\": \"Jak ukládáš\",\n      \"what_you_saved\": \"Co jsi uložil(a)\",\n      \"summary\": {\n        \"favorites\": \"Oblíbené\",\n        \"tags_created\": \"Vytvořené štítky\",\n        \"highlights\": \"To nejdůležitější\"\n      },\n      \"types\": {\n        \"links\": \"Odkazy\",\n        \"notes\": \"Poznámky\",\n        \"assets\": \"Aktiva\"\n      }\n    },\n    \"footer\": \"Vyrobeno s Karakeep\",\n    \"share\": \"Sdílet\",\n    \"download\": \"Stáhnout\",\n    \"close\": \"Zavřít\",\n    \"generating\": \"Generuji...\"\n  }\n}\n"
  },
  {
    "path": "apps/web/lib/i18n/locales/da/translation.json",
    "content": "{\n  \"actions\": {\n    \"select_all\": \"Vælg alle\",\n    \"sign_out\": \"Log ud\",\n    \"apply_all\": \"Anvend på alle\",\n    \"unarchive\": \"Fjern arkivering\",\n    \"favorite\": \"Stjernemarker\",\n    \"change_layout\": \"Skift visning\",\n    \"download_full_page_archive\": \"Download hele siden til arkiv\",\n    \"recrawl\": \"Gennemsøg igen\",\n    \"add_to_list\": \"Tilføj til liste\",\n    \"copy_link\": \"Kopiér link\",\n    \"close_bulk_edit\": \"Luk masseredigering\",\n    \"bulk_edit\": \"Masseredigering\",\n    \"manage_lists\": \"Administrer lister\",\n    \"remove_from_list\": \"Fjern fra liste\",\n    \"add\": \"Tilføj\",\n    \"create\": \"Opret\",\n    \"fetch_now\": \"Hent nu\",\n    \"summarize_with_ai\": \"Opsummer med AI\",\n    \"edit_title\": \"Rediger titel\",\n    \"cancel\": \"Annuller\",\n    \"ignore\": \"Ignorer\",\n    \"unselect_all\": \"Fravælg alle\",\n    \"refresh\": \"Genopfrisk\",\n    \"unfavorite\": \"Fjern stjernemarkering\",\n    \"archive\": \"Arkivér\",\n    \"delete\": \"Slet\",\n    \"edit\": \"Rediger\",\n    \"close\": \"Luk\",\n    \"edit_tags\": \"Rediger tags\",\n    \"save\": \"Gem\",\n    \"merge\": \"Sammenflet\",\n    \"sort\": {\n      \"title\": \"Sortér\",\n      \"newest_first\": \"Nyeste først\",\n      \"oldest_first\": \"Ældste først\",\n      \"relevant_first\": \"Mest relevant først\"\n    },\n    \"open_editor\": \"Åbn editor\",\n    \"toggle_show_archived\": \"Vis arkiverede\",\n    \"confirm\": \"Bekræft\",\n    \"regenerate\": \"Regenerér\",\n    \"load_more\": \"Indlæs mere\",\n    \"edit_notes\": \"Rediger noter\",\n    \"preserve_as_pdf\": \"Bevar som PDF\",\n    \"offline_copies\": \"Offline kopier\",\n    \"preserve_offline_archive\": \"Gem offline-arkiv\",\n    \"download_full_page_archive_file\": \"Download arkivfil\",\n    \"download_pdf_file\": \"Download PDF-fil\",\n    \"remove\": \"Fjern\",\n    \"more\": \"Mere\",\n    \"replace_banner\": \"Udskift banner\",\n    \"add_banner\": \"Tilføj banner\",\n    \"download\": \"Hent\"\n  },\n  \"settings\": {\n    \"import\": {\n      \"export_links_and_notes\": \"Eksporter links og noter\",\n      \"import_bookmarks_from_omnivore_export\": \"Importer bogmærker fra Omnivore-eksport\",\n      \"import_bookmarks_from_karakeep_export\": \"Importer bogmærker fra Karakeep-eksport\",\n      \"import_export\": \"Import / eksport\",\n      \"import_export_bookmarks\": \"Import / eksport bogmærker\",\n      \"import_bookmarks_from_html_file\": \"Importer bogmærker fra HTML-fil\",\n      \"import_bookmarks_from_pocket_export\": \"Importer bogmærker fra Pocket-eksport\",\n      \"import_bookmarks_from_matter_export\": \"Importer bogmærker fra Matter-eksport\",\n      \"imported_bookmarks\": \"Importerede bogmærker\",\n      \"import_bookmarks_from_linkwarden_export\": \"Importer bogmærker fra Linkwarden-eksport\",\n      \"import_bookmarks_from_tab_session_manager_export\": \"Importer bogmærker fra Tab Session Manager\",\n      \"import_bookmarks_from_mymind_export\": \"Importer bogmærker fra minmind-eksport\",\n      \"import_bookmarks_from_instapaper_export\": \"Importer bogmærker fra Instapaper-eksport\"\n    },\n    \"back_to_app\": \"Tilbage til app\",\n    \"info\": {\n      \"basic_details\": \"Grundlæggende\",\n      \"user_info\": \"Brugeroplysninger\",\n      \"interface_lang\": \"Grænseflade sprog\",\n      \"current_password\": \"Nuværende adgangskode\",\n      \"new_password\": \"Ny adgangskode\",\n      \"confirm_new_password\": \"Bekræft ny adgangskode\",\n      \"change_password\": \"Skift adgangskode\",\n      \"options\": \"Indstillinger\",\n      \"user_settings\": {\n        \"user_settings_updated\": \"Brugerindstillinger er blevet opdateret!\",\n        \"bookmark_click_action\": {\n          \"title\": \"Bogmærke klik handling\",\n          \"open_external_url\": \"Åbn original URL\",\n          \"open_bookmark_details\": \"Åbn bogmærke detaljer\"\n        },\n        \"archive_display_behaviour\": {\n          \"title\": \"Arkiverede bogmærker\",\n          \"show\": \"Vis arkiverede bogmærker i tags og lister\",\n          \"hide\": \"Skjul arkiverede bogmærker i tags og lister\"\n        }\n      },\n      \"reader_settings\": {\n        \"local_overrides_title\": \"Apparatspecifikke indstillinger er aktive\",\n        \"using_default\": \"Bruger klientstandard\",\n        \"clear_override_hint\": \"Ryd tilsidesættelsen af enheden for at bruge den globale indstilling ({{value}})\",\n        \"font_size\": \"Skriftstørrelse\",\n        \"font_family\": \"Skrifttype\",\n        \"preview_inline\": \"(forhåndsvisning)\",\n        \"tooltip_preview\": \"Ikke-gemte ændringer i forhåndsvisning\",\n        \"save_to_all_devices\": \"Alle enheder\",\n        \"tooltip_local\": \"Enhedsindstillinger adskiller sig fra globale\",\n        \"reset_preview\": \"Nulstil forhåndsvisning\",\n        \"mono\": \"Monospace\",\n        \"line_height\": \"Linjehøjde\",\n        \"tooltip_default\": \"Læseindstillinger\",\n        \"title\": \"Læserindstillinger\",\n        \"serif\": \"Serif\",\n        \"preview\": \"Forhåndsvisning\",\n        \"not_set\": \"Ikke angivet\",\n        \"clear_local_overrides\": \"Ryd enhedsindstillinger\",\n        \"preview_text\": \"\\\"The quick brown fox jumps over the lazy dog.\\\" Sådan vises din tekst i læsevisning.\",\n        \"local_overrides_cleared\": \"Apparatspecifikke indstillinger er blevet ryddet\",\n        \"local_overrides_description\": \"Denne enhed har læserindstillinger, der afviger fra dine globale standardindstillinger:\",\n        \"clear_defaults\": \"Ryd alle standarder\",\n        \"description\": \"Konfigurer standard tekstindstillinger for læsevisningen. Disse indstillinger synkroniseres på tværs af alle dine enheder.\",\n        \"defaults_cleared\": \"Læserstandarder er blevet ryddet\",\n        \"save_hint\": \"Gem indstillinger kun for denne enhed eller synkroniser på tværs af alle enheder\",\n        \"save_as_default\": \"Gem som standard\",\n        \"save_to_device\": \"Denne enhed\",\n        \"sans\": \"Sans Serif\",\n        \"tooltip_preview_and_local\": \"Ikke-gemte ændringer i forhåndsvisning; enhedsindstillinger adskiller sig fra globale\",\n        \"adjust_hint\": \"Juster indstillingerne ovenfor for at se et eksempel på ændringerne\"\n      },\n      \"avatar\": {\n        \"upload\": \"Upload avatar\",\n        \"change\": \"Skift avatar\",\n        \"remove_confirm_title\": \"Fjern avatar?\",\n        \"updated\": \"Avatar opdateret\",\n        \"removed\": \"Avatar fjernet\",\n        \"description\": \"Upload et firkantet billede, som du kan bruge som din avatar.\",\n        \"remove_confirm_description\": \"Dette vil fjerne dit nuværende profilbillede.\",\n        \"title\": \"Profilbillede\",\n        \"remove\": \"Fjern avatar\"\n      }\n    },\n    \"feeds\": {\n      \"add_a_subscription\": \"Tilføj et abonnement\",\n      \"rss_subscriptions\": \"RSS-abonnementer\",\n      \"feed_enabled\": \"RSS-feed aktiveret\",\n      \"feed_disabled\": \"RSS-feed deaktiveret\"\n    },\n    \"ai\": {\n      \"tagging_rules\": \"Regler for tagging\",\n      \"tagging_rule_description\": \"Prompts, som du tilføjer her, vil blive inkluderet som regler i modellen under taggenereringen. Du kan se de endelige prompts i afsnittet med forhåndsvisning af prompts.\",\n      \"ai_settings\": \"AI-indstillinger\",\n      \"images_prompt\": \"Billede-prompt\",\n      \"prompt_preview\": \"Forhåndsvisning af prompt\",\n      \"text_prompt\": \"Tekst-prompt\",\n      \"summarization_prompt\": \"Opsummeringsprompt\",\n      \"summarization\": \"Opsummering\",\n      \"all_tagging\": \"Tagging for alle typer\",\n      \"text_tagging\": \"Tekst-tagging\",\n      \"image_tagging\": \"Billede-tagging\",\n      \"tag_style\": \"Tag-stil\",\n      \"auto_summarization_description\": \"Generér automatisk opsummeringer til dine bogmærker ved hjælp af AI.\",\n      \"auto_tagging\": \"Automatisk taggning\",\n      \"titlecase_spaces\": \"Store forbogstaver med mellemrum\",\n      \"lowercase_underscores\": \"Små bogstaver med understregninger\",\n      \"inference_language\": \"Inferenssprog\",\n      \"titlecase_hyphens\": \"Store forbogstaver med bindestreger\",\n      \"lowercase_hyphens\": \"Små bogstaver med bindestreger\",\n      \"lowercase_spaces\": \"Små bogstaver med mellemrum\",\n      \"inference_language_description\": \"Vælg sprog for AI-genererede tags og opsummeringer.\",\n      \"tag_style_description\": \"Vælg, hvordan dine automatisk genererede tags skal formateres.\",\n      \"auto_tagging_description\": \"Generér automatisk tags til dine bogmærker ved hjælp af AI.\",\n      \"camelCase\": \"camelCase\",\n      \"auto_summarization\": \"Automatisk opsummering\",\n      \"no_preference\": \"Ingen præference\",\n      \"curated_tags\": \"Udvalgte tags\",\n      \"curated_tags_description\": \"Du kan eventuelt begrænse AI-tagging til kun at bruge tags fra denne liste. Når der ikke er valgt nogen tags, genererer AI'en frit tags.\",\n      \"curated_tags_updated\": \"Udvalgte tags er opdateret!\",\n      \"curated_tags_update_failed\": \"Kunne ikke opdatere udvalgte tags\"\n    },\n    \"broken_links\": {\n      \"crawling_status\": \"Gennemsøgningsstatus\",\n      \"broken_links\": \"Ugyldige links\",\n      \"last_crawled_at\": \"Sidst gennemsøgt\",\n      \"crawling_failed\": \"Gennemsøgning mislykkedes\"\n    },\n    \"api_keys\": {\n      \"new_api_key_desc\": \"Giv din API-nøgle et unikt navn\",\n      \"api_keys\": \"API-nøgler\",\n      \"new_api_key\": \"Ny API-nøgle\",\n      \"key_success\": \"Nøglen blev succesfuldt oprettet\",\n      \"key_success_please_copy\": \"Kopiér venligst nøglen og gem den et sikkert sted. Vær opmærksom på, at du ikke vil kunne få adgang til den igen, efter dialogboksen lukkes.\",\n      \"regenerate_api_key\": \"Regenerér API-nøgle\",\n      \"key_regenerated\": \"Nøglen blev regenereret\",\n      \"key_regenerated_please_copy\": \"Kopiér den nye nøgle, og gem den et sikkert sted. Den gamle nøgle er blevet trukket tilbage og virker ikke længere.\",\n      \"regenerate_warning\": \"Er du sikker på, at du vil regenerere API-nøglen \\\"{{name}}\\\"?\",\n      \"regenerate_confirmation\": \"Dette vil tilbagekalde den aktuelle nøgle og generere en ny. Alle programmer, der bruger den aktuelle nøgle, stopper med at virke.\"\n    },\n    \"user_settings\": \"Brugerindstillinger\",\n    \"webhooks\": {\n      \"webhooks\": \"Webhooks\",\n      \"events\": {\n        \"title\": \"Begivenheder\",\n        \"created\": \"Oprettet\",\n        \"edited\": \"Redigeret\",\n        \"crawled\": \"Gennemgået\"\n      },\n      \"description\": \"Du kan bruge webhooks til at udløse handlinger, når bogmærker oprettes, ændres eller gennemgås.\",\n      \"auth_token\": \"Adgangstoken\",\n      \"add_auth_token\": \"Tilføj adgangstoken\",\n      \"edit_auth_token\": \"Rediger adgangstoken\",\n      \"create_webhook\": \"Opret webhook\",\n      \"delete_webhook\": \"Slet webhook\",\n      \"delete_webhook_confirmation\": \"Er du sikker på, at du vil slette denne webhook?\",\n      \"edit_webhook\": \"Rediger webhook\",\n      \"webhook_url\": \"Webhook URL\"\n    },\n    \"manage_assets\": {\n      \"manage_assets\": \"Administrér ressourcer\",\n      \"no_assets\": \"Du har ingen ressourcer endnu.\",\n      \"delete_asset\": \"Slet ressource\",\n      \"asset_link\": \"Ressourcelink\",\n      \"asset_type\": \"Ressourcetype\",\n      \"delete_asset_confirmation\": \"Er du sikker på, at du vil slette denne ressource?\",\n      \"bookmark_link\": \"Bogmærkelink\"\n    },\n    \"rules\": {\n      \"whenever\": \"Når som helst ...\",\n      \"rules\": \"Regelmotor\",\n      \"rule_name\": \"Regelnavn\",\n      \"description\": \"Du kan bruge regler til at udløse handlinger, når en begivenhed udløses.\",\n      \"ceate_rule\": \"Opret regel\",\n      \"edit_rule\": \"Rediger regel\",\n      \"save_rule\": \"Gem regel\",\n      \"delete_rule\": \"Slet regel\",\n      \"delete_rule_confirmation\": \"Er du sikker på, at du vil slette denne regel?\",\n      \"if\": \"Hvis ...\",\n      \"enter_rule_name\": \"Indtast regelnavn\",\n      \"describe_what_this_rule_does\": \"Beskriv, hvad denne regel gør\",\n      \"rule_has_been_created\": \"Reglen er blevet oprettet!\",\n      \"rule_has_been_updated\": \"Reglen er blevet opdateret!\",\n      \"rule_has_been_deleted\": \"Reglen er blevet slettet!\",\n      \"no_rules_created_yet\": \"Ingen regler er oprettet endnu\",\n      \"create_your_first_rule\": \"Opret din første regel for at automatisere dit workflow\",\n      \"conditions_types\": {\n        \"always\": \"Altid\",\n        \"url_contains\": \"URL indeholder\",\n        \"imported_from_feed\": \"Importeret fra feed\",\n        \"bookmark_type_is\": \"Bogmærketype er\",\n        \"has_tag\": \"Har tag\",\n        \"is_favourited\": \"Er favoriseret\",\n        \"is_archived\": \"Er Arkiveret\",\n        \"and\": \"Alle følgende er sande\",\n        \"or\": \"Et af følgende er sandt\",\n        \"url_does_not_contain\": \"URL'en indeholder ikke\",\n        \"title_contains\": \"Titel indeholder\",\n        \"title_does_not_contain\": \"Titel indeholder ikke\"\n      },\n      \"actions_types\": {\n        \"remove_tag\": \"Fjern tag\",\n        \"add_to_list\": \"Tilføj til liste\",\n        \"add_tag\": \"Tilføj tag\",\n        \"remove_from_list\": \"Fjern fra liste\",\n        \"download_full_page_archive\": \"Download fuld sidearkiv\",\n        \"favourite_bookmark\": \"Favoritbogmærke\",\n        \"archive_bookmark\": \"Arkiver bogmærke\"\n      },\n      \"events_types\": {\n        \"bookmark_added\": \"Et bogmærke er tilføjet\",\n        \"tag_added\": \"Dette tag er føjet til et bogmærke\",\n        \"tag_removed\": \"Dette tag er fjernet fra et bogmærke\",\n        \"added_to_list\": \"Et bogmærke er tilføjet til denne liste\",\n        \"removed_from_list\": \"Et bogmærke er fjernet fra denne liste\",\n        \"favourited\": \"Et bogmærke er markeret som favorit\",\n        \"archived\": \"Et bogmærke er arkiveret\"\n      }\n    },\n    \"stats\": {\n      \"usage_statistics\": \"Anvendelsesstatistik\",\n      \"insights_description\": \"Indsigt i dine bogmærkevaner og -samling\",\n      \"failed_to_load\": \"Kunne ikke indlæse statistik\",\n      \"overview\": {\n        \"total_bookmarks\": \"Samlede bogmærker\",\n        \"all_saved_items\": \"Alle gemte elementer\",\n        \"favorites\": \"Favoritter\",\n        \"starred_bookmarks\": \"Stjerne-markerede bogmærker\",\n        \"archived\": \"Arkiveret\",\n        \"archived_items\": \"Arkiverede elementer\",\n        \"tags\": \"Tags\",\n        \"unique_tags_created\": \"Unikke tags oprettet\",\n        \"lists\": \"Lister\",\n        \"bookmark_collections\": \"Bogmærkesamlinger\",\n        \"highlights\": \"Fremhævelser\",\n        \"text_highlights\": \"Tekstfremhævelser\",\n        \"storage_used\": \"Brugt lagerplads\",\n        \"total_asset_storage\": \"Samlet lagerplads til aktiver\",\n        \"this_month\": \"Denne måned\",\n        \"bookmarks_added\": \"Bogmærker tilføjet\"\n      },\n      \"bookmark_types\": {\n        \"title\": \"Bogmærketyper\",\n        \"links\": \"Links\",\n        \"text_notes\": \"Tekstnoter\",\n        \"assets\": \"Aktiver\"\n      },\n      \"recent_activity\": {\n        \"title\": \"Seneste aktivitet\",\n        \"this_week\": \"Denne uge\",\n        \"this_month\": \"Denne måned\",\n        \"this_year\": \"I år\"\n      },\n      \"top_domains\": {\n        \"title\": \"Topdomæner\",\n        \"no_domains_found\": \"Ingen domæner fundet\"\n      },\n      \"most_used_tags\": {\n        \"title\": \"Mest brugte tags\",\n        \"no_tags_found\": \"Ingen tags fundet\"\n      },\n      \"activity_patterns\": {\n        \"activity_by_hour\": \"Aktivitet pr. time\",\n        \"activity_by_day\": \"Aktivitet efter dag\"\n      },\n      \"storage_breakdown\": {\n        \"title\": \"Opdeling af lagerplads\"\n      },\n      \"bookmark_sources\": {\n        \"title\": \"Bogmærke-kilder\",\n        \"empty\": \"Ingen kildedata tilgængelig\"\n      }\n    },\n    \"subscription\": {\n      \"subscription\": \"Abonnement\",\n      \"manage_subscription\": \"Administrer dit abonnement og dine faktureringsoplysninger\",\n      \"current_plan\": \"Nuværende abonnement\",\n      \"billing_period\": \"Faktureringsperiode\",\n      \"paid_plan\": \"Betalt abonnement\",\n      \"unlock_bigger_quota\": \"Lås op for større kvote, og støt projektet\",\n      \"subscribe_now\": \"Abonner nu\",\n      \"manage_billing\": \"Administrer fakturering\",\n      \"subscription_canceled\": \"Dit abonnement er blevet annulleret og udløber den {{date}}. Du kan genabonnere når som helst.\",\n      \"usage_quotas\": \"Forbrug og kvoter\",\n      \"track_usage\": \"Hold øje med dit nuværende forbrug i forhold til dine planbegrænsninger\",\n      \"total_bookmarks_saved\": \"Samlet antal gemte bogmærker\",\n      \"assets_file_storage\": \"Aktiver og fillagring\",\n      \"unlimited_usage\": \"Ubegrænset brug\",\n      \"quota_limit_reached\": \"Kvotegrænse nået\",\n      \"approaching_quota_limit\": \"Nærmer sig kvotegrænsen\",\n      \"loading_usage\": \"Indlæser forbrugsinformation...\",\n      \"free\": \"Gratis\",\n      \"paid\": \"Betalt\"\n    },\n    \"import_sessions\": {\n      \"title\": \"Importer sessioner\",\n      \"description\": \"Se og administrer dine masseimportsessioner. Sessioner oprettes automatisk, når du importerer bogmærker.\",\n      \"load_error\": \"Kunne ikke indlæse importsessioner\",\n      \"no_sessions\": \"Ingen importsessioner endnu\",\n      \"no_sessions_detail\": \"Importsessioner vises her automatisk, når du importerer bogmærker\",\n      \"created_at\": \"Oprettet {{time}}\",\n      \"progress\": \"Fremdrift\",\n      \"status\": {\n        \"pending\": \"Afventer\",\n        \"in_progress\": \"I gang\",\n        \"completed\": \"Fuldført\",\n        \"failed\": \"Mislykkedes\",\n        \"processing\": \"Behandler\",\n        \"staging\": \"Iscenesættelse\",\n        \"running\": \"Kører\",\n        \"paused\": \"Sat på pause\"\n      },\n      \"badges\": {\n        \"pending\": \"{{count}} afventer\",\n        \"processing\": \"{{count}} behandler\",\n        \"completed\": \"{{count}} fuldført\",\n        \"failed\": \"{{count}} mislykkedes\"\n      },\n      \"imported_to\": \"Importeret til:\",\n      \"view_list\": \"Vis liste\",\n      \"delete_dialog_title\": \"Slet importsession\",\n      \"delete_dialog_description\": \"Er du sikker på, at du vil slette \\\"{{name}}\\\"? Denne handling kan ikke fortrydes. Selve bogmærkerne vil ikke blive slettet.\",\n      \"delete_session\": \"Slet session\",\n      \"pause_session\": \"Pause\",\n      \"resume_session\": \"Genoptag\",\n      \"view_details\": \"Vis detaljer\",\n      \"detail\": {\n        \"page_title\": \"Importér sessionsdetaljer\",\n        \"back_to_import\": \"Tilbage til import\",\n        \"filter_all\": \"Alle\",\n        \"filter_accepted\": \"Accepteret\",\n        \"filter_rejected\": \"Afvist\",\n        \"filter_duplicates\": \"Duplikater\",\n        \"filter_pending\": \"Afventer\",\n        \"table_title\": \"Titel / URL\",\n        \"table_type\": \"Type\",\n        \"table_result\": \"Resultat\",\n        \"table_reason\": \"Årsag\",\n        \"table_bookmark\": \"Bogmærke\",\n        \"result_accepted\": \"Accepteret\",\n        \"result_rejected\": \"Afvist\",\n        \"result_skipped_duplicate\": \"Duplikat\",\n        \"result_pending\": \"Afventer\",\n        \"result_processing\": \"Behandler\",\n        \"no_results\": \"Ingen resultater fundet for dette filter.\",\n        \"view_bookmark\": \"Vis bogmærke\",\n        \"load_more\": \"Indlæs mere\",\n        \"no_title\": \"Ingen titel\"\n      }\n    },\n    \"backups\": {\n      \"backups\": \"Sikkerhedskopier\",\n      \"page_title\": \"Sikkerhedskopier\",\n      \"page_description\": \"Opret og administrer automatisk sikkerhedskopier af dine bogmærker. Sikkerhedskopier er komprimerede og sikkert opbevarede.\",\n      \"configuration\": {\n        \"title\": \"Konfiguration af sikkerhedskopier\",\n        \"enable_automatic_backups\": \"Aktivér automatiske sikkerhedskopier\",\n        \"enable_automatic_backups_description\": \"Opret sikkerhedskopier af dine bogmærker automatisk\",\n        \"backup_frequency\": \"Frekvens for sikkerhedskopiering\",\n        \"backup_frequency_description\": \"Hvor ofte sikkerhedskopier skal oprettes\",\n        \"retention_period\": \"Opbevaringsperiode (dage)\",\n        \"retention_period_description\": \"Hvor mange dage sikkerhedskopier skal opbevares, før de slettes\",\n        \"frequency\": {\n          \"daily\": \"Dagligt\",\n          \"weekly\": \"Ugentligt\"\n        },\n        \"select_frequency\": \"Vælg frekvens\",\n        \"save_settings\": \"Gem indstillinger\"\n      },\n      \"list\": {\n        \"title\": \"Dine sikkerhedskopier\",\n        \"create_backup_now\": \"Opret sikkerhedskopi nu\",\n        \"no_backups\": \"Du har endnu ingen sikkerhedskopier. Aktivér automatiske sikkerhedskopier, eller opret en manuelt.\",\n        \"table\": {\n          \"created_at\": \"Oprettet den\",\n          \"bookmarks\": \"Bogmærker\",\n          \"size\": \"Størrelse\",\n          \"status\": \"Status\",\n          \"actions\": \"Handlinger\"\n        },\n        \"status\": {\n          \"success\": \"Succes\",\n          \"failed\": \"Mislykket\",\n          \"pending\": \"Afventer\"\n        },\n        \"actions\": {\n          \"download_backup\": \"Hent sikkerhedskopi\",\n          \"delete_backup\": \"Slet sikkerhedskopi\"\n        }\n      },\n      \"dialogs\": {\n        \"delete_backup_title\": \"Slet sikkerhedskopi?\",\n        \"delete_backup_description\": \"Er du sikker på, at du vil slette denne sikkerhedskopi? Denne handling kan ikke fortrydes.\"\n      },\n      \"toasts\": {\n        \"backup_queued\": \"Sikkerhedskopieringsjobbet er blevet sat i kø! Det vil blive behandlet snarest.\",\n        \"backup_deleted\": \"Sikkerhedskopien er blevet slettet!\"\n      }\n    }\n  },\n  \"admin\": {\n    \"actions\": {\n      \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Genopret AI-tags kun for mislykkede bogmærker\",\n      \"recrawl_failed_links_only\": \"Gennemsøg kun mislykkede links igen\",\n      \"compact_assets\": \"Komprimér ressourcer\",\n      \"recrawl_all_links\": \"Gennemsøg alle links\",\n      \"without_inference\": \"Uden inferens\",\n      \"regenerate_ai_tags_for_all_bookmarks\": \"Genopret AI-tags for alle bogmærker\",\n      \"reindex_all_bookmarks\": \"Genindeksér alle bogmærker\",\n      \"reprocess_assets_fix_mode\": \"Genbehandling af aktiver (Fix Mode)\",\n      \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Gendan AI-opsummeringer kun for mislykkede bogmærker\",\n      \"regenerate_ai_summaries_for_all_bookmarks\": \"Gendan AI-opsummeringer for alle bogmærker\"\n    },\n    \"background_jobs\": {\n      \"inference_jobs\": \"Inferensopgaver\",\n      \"failed\": \"Mislykket\",\n      \"background_jobs\": \"Baggrundsjobs\",\n      \"crawler_jobs\": \"Gennemsøgningsopgaver\",\n      \"indexing_jobs\": \"Indekseringsopgaver\",\n      \"tidy_assets_jobs\": \"Oprydningsopgaver for ressourcer\",\n      \"job\": \"Opgave\",\n      \"queued\": \"I kø\",\n      \"pending\": \"Afventer\",\n      \"webhook_jobs\": \"Webhook-opgaver\",\n      \"video_jobs\": \"Videonedehentningsopgaver\",\n      \"feed_jobs\": \"RSS-feed-opgaver\",\n      \"asset_preprocessing_jobs\": \"Forbehandlingsopgaver for ressourcer\",\n      \"jobs\": {\n        \"crawler\": {\n          \"title\": \"Crawler Jobs\",\n          \"description\": \"Webcrawling og indholdsudtrækning fra URL'er\"\n        },\n        \"inference\": {\n          \"title\": \"Inference Jobs\",\n          \"description\": \"AI-drevet tagning og opsummering af indhold\"\n        },\n        \"indexing\": {\n          \"title\": \"Indexeringsjob\",\n          \"description\": \"Søgeindeksopdateringer\"\n        },\n        \"asset_preprocessing\": {\n          \"title\": \"Asset Præprocesseringsjob\",\n          \"description\": \"Billed- og dokumentpræprocessering (skærmbilleder, tekstudtrækning osv.)\"\n        },\n        \"tidy_assets\": {\n          \"title\": \"Ryddelige Asset Jobs\",\n          \"description\": \"Asset oprydning og lageroptimering\"\n        },\n        \"video\": {\n          \"title\": \"Video Download Jobs\",\n          \"description\": \"Videoudtrækning og download\"\n        },\n        \"webhook\": {\n          \"title\": \"Webhook Jobs\",\n          \"description\": \"Eksterne webhook notifikationer\"\n        },\n        \"feed\": {\n          \"title\": \"RSS Feed Jobs\",\n          \"description\": \"RSS-feed behandling og indholdsopdateringer\"\n        },\n        \"admin_maintenance\": {\n          \"title\": \"Admin vedligeholdelsesjob\",\n          \"description\": \"Administrativ oprydning og aktivvedligeholdelse\"\n        }\n      },\n      \"monitor_and_manage\": \"Overvåg og administrer baggrundsjobkøer og systembehandlingsopgaver\",\n      \"active\": \"Aktiv\",\n      \"available_actions\": \"Tilgængelige handlinger\",\n      \"status\": {\n        \"title\": \"Forståelse af Jobtilstande\",\n        \"queued\": {\n          \"title\": \"Sat i kø\",\n          \"description\": \"Job, der venter i køen på at blive behandlet. De starter automatisk, når der er ressourcer tilgængelige.\"\n        },\n        \"unprocessed\": {\n          \"title\": \"Ikke behandlet\",\n          \"description\": \"Bogmærker, der endnu ikke er blevet behandlet. De er sandsynligvis allerede sat i kø til behandling, hvis ikke, skal du måske manuelt sætte dem i kø igen.\"\n        },\n        \"failed\": {\n          \"title\": \"Mislykkedes\",\n          \"description\": \"Bogmærker, der stødte på fejl under behandlingen. Disse kan kræve manuel opmærksomhed.\"\n        }\n      },\n      \"actions\": {\n        \"recrawl_failed_links_only\": \"Gennemsøg kun links, der er mislykkedes, igen\",\n        \"recrawl_all_links\": \"Gennemsøg alle links igen\",\n        \"without_inference\": \"Uden inferens\",\n        \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Opret AI-tags igen kun for mislykkede bogmærker\",\n        \"regenerate_ai_tags_for_all_bookmarks\": \"Opret AI-tags for alle bogmærker igen\",\n        \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Opret AI-oversigter igen kun for mislykkede bogmærker\",\n        \"regenerate_ai_summaries_for_all_bookmarks\": \"Opret AI-oversigter for alle bogmærker igen\",\n        \"reindex_all_bookmarks\": \"Genindekser alle bogmærker\",\n        \"clean_assets\": \"Ryd op i efterladte aktiver og synkroniser metadata igen\",\n        \"reprocess_assets_fix_mode\": \"Genbehandl ubehandlede aktiver\",\n        \"migrate_large_link_html_content\": \"Flyt stort inline HTML-indhold til aktiver\",\n        \"recrawl_pending_links_only\": \"Gennemsøg kun ventende links igen\",\n        \"regenerate_ai_tags_for_pending_bookmarks_only\": \"Gendan AI-tags kun for ventende bogmærker\",\n        \"regenerate_ai_summaries_for_pending_bookmarks_only\": \"Gendan AI-resumeer kun for ventende bogmærker\"\n      }\n    },\n    \"server_stats\": {\n      \"server_version\": \"Server-version\",\n      \"server_stats\": \"Serverstatistik\",\n      \"total_users\": \"Brugere i alt\",\n      \"total_bookmarks\": \"Bogmærker i alt\"\n    },\n    \"admin_settings\": \"Admin-indstillinger\",\n    \"users_list\": {\n      \"create_user\": \"Opret bruger\",\n      \"users_list\": \"Liste over brugere\",\n      \"change_role\": \"Skift rolle\",\n      \"reset_password\": \"Nulstil adgangskode\",\n      \"delete_user\": \"Slet bruger\",\n      \"num_bookmarks\": \"Antal bogmærker\",\n      \"asset_sizes\": \"Størrelse på ressourcer\",\n      \"local_user\": \"Lokal bruger\",\n      \"confirm_password\": \"Bekræft adgangskode\",\n      \"delete_user_confirm_description\": \"Er du sikker på, at du vil slette brugeren \\\"{{name}}\\\"?\",\n      \"unlimited\": \"Ubegrænset\"\n    },\n    \"service_connections\": {\n      \"title\": \"Serviceforbindelser\",\n      \"description\": \"Overvåg tilstanden og forbindelsen af eksterne systemers afhængigheder\",\n      \"search_engine\": \"Søgemaskine\",\n      \"browser\": \"Browser\",\n      \"queue_system\": \"Køsystem\",\n      \"status\": {\n        \"not_configured\": \"Ikke konfigureret\",\n        \"connected\": \"Forbundet\",\n        \"disconnected\": \"Afbrudt\"\n      }\n    },\n    \"admin_tools\": {\n      \"admin_tools\": \"Administrationsværktøjer\",\n      \"bookmark_debugger\": \"Fejlfinding af bogmærker\",\n      \"bookmark_id\": \"Bogmærke-ID\",\n      \"bookmark_id_placeholder\": \"Indtast bogmærke-ID\",\n      \"lookup\": \"Opslag\",\n      \"debug_info\": \"Fejlfindingsoplysninger\",\n      \"basic_info\": \"Grundlæggende oplysninger\",\n      \"status\": \"Status\",\n      \"content\": \"Indhold\",\n      \"html_preview\": \"HTML-eksempel (første 1000 tegn)\",\n      \"summary\": \"Oversigt\",\n      \"url\": \"URL\",\n      \"source_url\": \"Kilde-URL\",\n      \"asset_type\": \"Asset-type\",\n      \"file_name\": \"Filnavn\",\n      \"owner_user_id\": \"Bruger-ID for ejer\",\n      \"tagging_status\": \"Tagging-status\",\n      \"summarization_status\": \"Status for opsummering\",\n      \"crawl_status\": \"Crawling-status\",\n      \"crawl_status_code\": \"HTTP-statuskode\",\n      \"crawled_at\": \"Crawlet på\",\n      \"recrawl\": \"Gennemsøg igen\",\n      \"reindex\": \"Genindekser\",\n      \"retag\": \"Tag igen\",\n      \"resummarize\": \"Opsummér igen\",\n      \"bookmark_not_found\": \"Bogmærke ikke fundet\",\n      \"action_success\": \"Handling fuldført med succes\",\n      \"action_failed\": \"Handling mislykkedes\",\n      \"recrawl_queued\": \"Gen-crawl job er blevet sat i kø\",\n      \"reindex_queued\": \"Genindekseringsjob er blevet sat i kø\",\n      \"retag_queued\": \"Gen-tag job er blevet sat i kø\",\n      \"resummarize_queued\": \"Genopsummeringsjob er blevet sat i kø\",\n      \"view\": \"Vis\",\n      \"fetch_error\": \"Fejl ved hentning af bogmærke\"\n    }\n  },\n  \"tags\": {\n    \"unused_tags\": \"Ikke-anvendte tags\",\n    \"your_tags\": \"Dine tags\",\n    \"ai_tags\": \"AI-tags\",\n    \"all_tags\": \"Alle tags\",\n    \"delete_all_unused_tags\": \"Fjern alle ikke-anvendte tags\",\n    \"drag_and_drop_merging\": \"Træk og slip-sammenfletning\",\n    \"your_tags_info\": \"Tags, du har anvendt mindst én gang\",\n    \"ai_tags_info\": \"Tags, der er tilføjet automatisk (af AI)\",\n    \"unused_tags_info\": \"Tags, der ikke er knyttet til nogen bogmærker\",\n    \"drag_and_drop_merging_info\": \"Træk og slip tags oven på hinanden for at sammenflette dem\",\n    \"sort_by_name\": \"Sorter efter navn\",\n    \"create_tag\": \"Opret tag\",\n    \"create_tag_description\": \"Opret et nyt tag uden at knytte det til noget bogmærke\",\n    \"tag_name\": \"Tag-navn\",\n    \"enter_tag_name\": \"Indtast tag-navn\",\n    \"sort_by_usage\": \"Sortér efter brug\",\n    \"sort_by_relevance\": \"Sortér efter relevans\",\n    \"no_custom_tags\": \"Ingen brugerdefinerede mærker endnu\",\n    \"no_ai_tags\": \"Ingen AI-mærker endnu\",\n    \"no_unused_tags\": \"Du har ikke nogen ubrugte mærker\",\n    \"no_unused_tags_match_your_search\": \"Ingen ubrugte mærker matcher din søgning\",\n    \"no_tags_match_your_search\": \"Ingen mærker matcher din søgning\",\n    \"search_placeholder\": \"Søg efter tags...\",\n    \"search_or_create_placeholder\": \"Søg eller opret tags...\"\n  },\n  \"options\": {\n    \"light_mode\": \"Lys tilstand\",\n    \"dark_mode\": \"Mørk tilstand\",\n    \"apps_extensions\": \"Apps & udvidelser\",\n    \"documentation\": \"Dokumentation\",\n    \"follow_us_on_x\": \"Følg os på X\"\n  },\n  \"editor\": {\n    \"quickly_focus\": \"Du kan hurtigt fokusere på dette felt ved at bruge genvejen ⌘ + E\",\n    \"multiple_urls_dialog_title\": \"Importer URL’er som separate bogmærker?\",\n    \"multiple_urls_dialog_desc\": \"Indtastningen indeholder flere URL’er på separate linjer. Ønsker du at importere dem som individuelle bogmærker?\",\n    \"import_as_text\": \"Importer som tekstbogmærke\",\n    \"import_as_separate_bookmarks\": \"Importer som separate bogmærker\",\n    \"new_item\": \"NYT ELEMENT\",\n    \"disabled_submissions\": \"Indsendelser er deaktiveret\",\n    \"text_toolbar\": {\n      \"undo\": \"Fortryd\",\n      \"redo\": \"Annuller fortryd\",\n      \"italic\": \"Kursiv\",\n      \"underline\": \"Understregning\",\n      \"strikethrough\": \"Gennemstreget\",\n      \"code\": \"Kode\",\n      \"highlight\": \"Fremhæv\",\n      \"align_left\": \"Venstrejuster\",\n      \"align_center\": \"Centreret\",\n      \"align_right\": \"Højrejuster\",\n      \"markdown_shortcuts\": {\n        \"label\": \"Markdown-genveje\",\n        \"heading\": {\n          \"label\": \"Overskrift\",\n          \"example\": \"# H1, ## H2, ### H3\"\n        },\n        \"bold\": {\n          \"label\": \"Fed\",\n          \"example\": \"**tekst** eller CTRL+b\"\n        },\n        \"blockquote\": {\n          \"label\": \"Blokcitat\",\n          \"example\": \"> Blokcitat\"\n        },\n        \"ordered_list\": {\n          \"label\": \"Opstilling med tal\",\n          \"example\": \"1. Listepunkt\"\n        },\n        \"unordered_list\": {\n          \"label\": \"Punktopstilling\",\n          \"example\": \"- Listepunkt\"\n        },\n        \"inline_code\": {\n          \"label\": \"Indlejret kode\",\n          \"example\": \"`Kode`\"\n        },\n        \"block_code\": {\n          \"label\": \"Blokkode\",\n          \"example\": \"``` + mellemrum\"\n        },\n        \"italic\": {\n          \"label\": \"Kursiv\",\n          \"example\": \"*Kursiv* eller _Kursiv_ eller CTRL+i\"\n        }\n      },\n      \"bold\": \"Fed\"\n    },\n    \"placeholder\": \"Indsæt et link eller billede, skriv en note, eller træk og slip et billede her…\",\n    \"placeholder_v2\": \"Indsæt et link, skriv en note eller slip et billede…\"\n  },\n  \"common\": {\n    \"roles\": {\n      \"user\": \"Bruger\",\n      \"admin\": \"Admin\"\n    },\n    \"tags\": \"Tags\",\n    \"experimental\": \"Eksperimentel\",\n    \"role\": \"Rolle\",\n    \"archive\": \"Arkiv\",\n    \"actions\": \"Handlinger\",\n    \"action\": \"Handling\",\n    \"created_at\": \"Oprettet\",\n    \"attachments\": \"Vedhæftninger\",\n    \"screenshot\": \"Skærmbillede\",\n    \"something_went_wrong\": \"Noget gik galt\",\n    \"search\": \"Søg\",\n    \"key\": \"Nøgle\",\n    \"name\": \"Navn\",\n    \"note\": \"Note\",\n    \"url\": \"URL\",\n    \"email\": \"E-mail\",\n    \"password\": \"Adganskode\",\n    \"video\": \"Video\",\n    \"home\": \"Hjem\",\n    \"bookmark_types\": {\n      \"link\": \"Link\",\n      \"text\": \"Tekst\",\n      \"media\": \"Medie\",\n      \"title\": \"Bogmærketype\"\n    },\n    \"highlights\": \"Fremhævninger\",\n    \"source\": \"Kilde\",\n    \"type\": \"Type\",\n    \"size\": \"Størrelse\",\n    \"updated_at\": \"Opdateret den\",\n    \"title\": \"Titel\",\n    \"description\": \"Beskrivelse\",\n    \"summary\": \"Opsummering\",\n    \"quota\": \"Kvote\",\n    \"bookmarks\": \"Bogmærker\",\n    \"storage\": \"Lagring\",\n    \"pdf\": \"Arkiveret PDF\",\n    \"default\": \"Standard\",\n    \"id\": \"ID\",\n    \"last_used\": \"Sidst brugt\"\n  },\n  \"layouts\": {\n    \"masonry\": \"Fliser\",\n    \"grid\": \"Gitter\",\n    \"list\": \"Liste\",\n    \"compact\": \"Kompakt\"\n  },\n  \"lists\": {\n    \"all_lists\": \"Alle lister\",\n    \"favourites\": \"Stjernemarkeringer\",\n    \"new_list\": \"Ny liste\",\n    \"new_nested_list\": \"Ny underliste\",\n    \"edit_list\": \"Rediger liste\",\n    \"list_type\": \"Liste Type\",\n    \"manual_list\": \"Manuel liste\",\n    \"parent_list\": \"Overordnet liste\",\n    \"no_parent\": \"Ingen overordnet\",\n    \"smart_list\": \"Smart liste\",\n    \"search_query\": \"Søgestreng\",\n    \"search_query_help\": \"Lær mere om sproget til søgeforespørgsler.\",\n    \"merge_list\": \"Flet liste\",\n    \"destination_list\": \"Destinationsliste\",\n    \"delete_after_merge\": \"Slet den oprindelige liste efter fletning\",\n    \"no_destination\": \"Ingen destination\",\n    \"description\": \"Beskrivelse (Valgfrit)\",\n    \"rss\": {\n      \"title\": \"RSS-feed\",\n      \"description\": \"Aktiver et RSS-feed for denne liste\",\n      \"feed_url\": \"RSS-feed URL\"\n    },\n    \"share_list\": \"Del liste\",\n    \"public_list\": {\n      \"title\": \"Offentlig liste\",\n      \"description\": \"Tillad andre at se denne liste\",\n      \"share_link\": \"Del link\"\n    },\n    \"delete_list\": {\n      \"title\": \"Slet liste\",\n      \"description\": \"Sletning af en liste sletter ikke nogen bogmærker på den pågældende liste.\",\n      \"delete_children\": \"Slet underordnede lister (rekursivt)\",\n      \"delete_children_description\": \"Hvis dette ikke er markeret, vil alle direkte underordnede lister blive rodlister\"\n    },\n    \"shared\": \"Delt\",\n    \"collaborators\": {\n      \"manage\": \"Administrer samarbejdspartnere\",\n      \"view\": \"Vis samarbejdspartnere\",\n      \"collaborators\": \"Samarbejdspartnere\",\n      \"add\": \"Tilføj samarbejdspartner\",\n      \"current\": \"Nuværende samarbejdspartnere\",\n      \"enter_email\": \"Indtast e-mailadresse\",\n      \"please_enter_email\": \"Angiv en e-mailadresse\",\n      \"added_successfully\": \"Samarbejdspartner tilføjet\",\n      \"failed_to_add\": \"Kunne ikke tilføje samarbejdspartner\",\n      \"removed\": \"Samarbejdspartner fjernet\",\n      \"failed_to_remove\": \"Kunne ikke fjerne samarbejdspartner\",\n      \"role_updated\": \"Rolle opdateret\",\n      \"failed_to_update_role\": \"Kunne ikke opdatere rolle\",\n      \"viewer\": \"Seer\",\n      \"editor\": \"Editor\",\n      \"owner\": \"Ejer\",\n      \"viewer_description\": \"Kan se bogmærker på listen\",\n      \"editor_description\": \"Kan tilføje og fjerne bogmærker\",\n      \"no_collaborators\": \"Ingen samarbejdspartnere endnu. Tilføj nogen for at begynde at samarbejde!\",\n      \"no_collaborators_readonly\": \"Ingen samarbejdspartnere for denne liste.\",\n      \"people_with_access\": \"Folk der har adgang til denne liste\",\n      \"add_or_remove\": \"Tilføj eller fjern folk der kan få adgang til denne liste\",\n      \"invitation_sent\": \"Invitation er sendt!\",\n      \"invitation_revoked\": \"Invitation trukket tilbage\",\n      \"failed_to_revoke\": \"Kunne ikke trække invitationen tilbage\",\n      \"pending\": \"Afventer\",\n      \"revoke\": \"Tilbagekald\",\n      \"declined\": \"Afvist\"\n    },\n    \"leave_list\": {\n      \"title\": \"Forlad liste\",\n      \"confirm_message\": \"Er du sikker på, at du vil forlade {{icon}} {{name}}?\",\n      \"warning\": \"Du vil ikke længere kunne se eller tilgå bogmærker på denne liste. Listens ejer kan tilføje dig igen, hvis det skulle blive nødvendigt.\",\n      \"action\": \"Forlad liste\",\n      \"success\": \"Du har forladt \\\"{{icon}} {{name}}\\\"\"\n    },\n    \"invitations\": {\n      \"pending\": \"Afventende invitationer\",\n      \"description\": \"Gennemgå og besvar invitationer til listesamarbejde\",\n      \"invited_by\": \"Inviteret af\",\n      \"accept\": \"Accepter\",\n      \"decline\": \"Afvis\",\n      \"accepted\": \"Invitation accepteret\",\n      \"declined\": \"Invitation afvist\",\n      \"failed_to_accept\": \"Kunne ikke acceptere invitationen\",\n      \"failed_to_decline\": \"Kunne ikke afvise invitationen\"\n    },\n    \"shared_lists\": \"Delte lister\"\n  },\n  \"preview\": {\n    \"view_original\": \"Se original\",\n    \"cached_content\": \"Cachelagret indhold\",\n    \"reader_view\": \"Læsevisning\",\n    \"tabs\": {\n      \"content\": \"Indhold\",\n      \"details\": \"Detaljer\"\n    },\n    \"archive_info\": \"Arkiver gengives muligvis ikke korrekt inline, hvis de kræver Javascript. For at opnå de bedste resultater skal du <1>downloade den og åbne den i din browser</1>.\",\n    \"fetch_error_title\": \"Indhold utilgængeligt\",\n    \"fetch_error_description\": \"Vi kunne ikke hente indholdet til dette link. Siden kan være beskyttet, kræve godkendelse eller være midlertidigt utilgængelig.\",\n    \"crawling_in_progress\": \"Henter sideindhold…\",\n    \"continue_reading\": \"Fortsæt, hvor du slap\",\n    \"continue_reading_percent\": \"Fortsæt, hvor du slap ({{percent}}%)\",\n    \"continue_button\": \"Fortsæt\"\n  },\n  \"toasts\": {\n    \"bookmarks\": {\n      \"refetch\": \"Genhentning er blevet sat i kø!\",\n      \"full_page_archive\": \"Oprettelse af hele siden til arkivering er blevet igangsat\",\n      \"delete_from_list\": \"Bogmærket er blevet slettet fra listen\",\n      \"deleted\": \"Bogmærket er blevet slettet!\",\n      \"clipboard_copied\": \"Linket er kopieret til din udklipsholder!\",\n      \"updated\": \"Bogmærket er blevet opdateret!\",\n      \"preserve_pdf\": \"PDF-bevaring er blevet udløst\",\n      \"update_banner\": \"Banneret er blevet opdateret!\",\n      \"uploading_banner\": \"Uploader banner...\"\n    },\n    \"lists\": {\n      \"created\": \"Listen er oprettet!\",\n      \"updated\": \"Listen er blevet opdateret!\",\n      \"merged\": \"Listen er blevet flettet!\",\n      \"deleted\": \"Listen er blevet slettet!\"\n    },\n    \"tags\": {\n      \"created\": \"Tag er blevet oprettet!\",\n      \"failed_to_create\": \"Kunne ikke oprette tag\"\n    }\n  },\n  \"cleanups\": {\n    \"cleanups\": \"Oprydning\",\n    \"duplicate_tags\": {\n      \"title\": \"Dublerede tags\",\n      \"merge_all_suggestions\": \"Sammenflet alle forslag?\"\n    }\n  },\n  \"highlights\": {\n    \"no_highlights\": \"Du har ikke nogen fremhævninger endnu.\"\n  },\n  \"search\": {\n    \"has_no_tags\": \"Har ingen tags\",\n    \"url_contains\": \"URL indeholder\",\n    \"is_not_in_any_list\": \"Er ikke i nogen liste\",\n    \"type_is\": \"Typen er\",\n    \"and\": \"og\",\n    \"does_not_have_tag\": \"Har ikke et tag\",\n    \"url_does_not_contain\": \"URL indeholder ikke\",\n    \"is_in_list\": \"Er i listen\",\n    \"is_archived\": \"Er arkiveret\",\n    \"is_not_favorited\": \"Er ikke stjernemarkeret\",\n    \"not_created_on_or_before\": \"Ikke oprettet den eller før\",\n    \"is_not_archived\": \"Er ikke arkiveret\",\n    \"has_tag\": \"Har tag\",\n    \"or\": \"eller\",\n    \"created_on_or_before\": \"Oprettet den eller før\",\n    \"is_favorited\": \"Er stjermarkeret\",\n    \"full_text_search\": \"Fuldtekstsøgning\",\n    \"not_created_on_or_after\": \"Ikke oprettet den eller efter\",\n    \"has_any_tag\": \"Har et tag\",\n    \"is_in_any_list\": \"Er i en liste\",\n    \"created_on_or_after\": \"Oprettet den eller efter\",\n    \"is_not_in_list\": \"Er ikke i listen\",\n    \"type_is_not\": \"Typen er ikke\",\n    \"is_from_feed\": \"Er fra RSS-feed\",\n    \"is_not_from_feed\": \"Er ikke fra RSS-feed\",\n    \"created_within\": \"Oprettet inden for\",\n    \"created_earlier_than\": \"Oprettet tidligere end\",\n    \"day_s\": \" Dag(e)\",\n    \"week_s\": \" Uge(r)\",\n    \"month_s\": \" Måned(er)\",\n    \"year_s\": \" År\",\n    \"day_s_ago\": \" Dag(e) siden\",\n    \"week_s_ago\": \" Uge(r) siden\",\n    \"month_s_ago\": \" Måned(er) siden\",\n    \"year_s_ago\": \" År siden\",\n    \"history\": \"Seneste søgninger\",\n    \"title_contains\": \"Titel indeholder\",\n    \"title_does_not_contain\": \"Titel indeholder ikke\",\n    \"is_broken_link\": \"Har Beskadet Link\",\n    \"tags\": \"Tags\",\n    \"no_suggestions\": \"Ingen forslag\",\n    \"filters\": \"Filtre\",\n    \"is_not_broken_link\": \"Har Fungerende Link\",\n    \"lists\": \"Lister\",\n    \"feeds\": \"Feeds\",\n    \"is_from_source\": \"Kilde er\",\n    \"is_not_from_source\": \"Kilden er ikke\"\n  },\n  \"dialogs\": {\n    \"bookmarks\": {\n      \"delete_confirmation_title\": \"Slet bogmærke?\",\n      \"delete_confirmation_description\": \"Er du sikker på, at du vil slette dette bogmærke?\"\n    }\n  },\n  \"banners\": {\n    \"no_bookmarks\": {\n      \"title\": \"Ingen bogmærker endnu\",\n      \"description\": \"Gem interessante artikler, links og sider for at få adgang til dem hurtigt senere.\"\n    }\n  },\n  \"bookmark_editor\": {\n    \"title\": \"Rediger bogmærke\",\n    \"subtitle\": \"Foretag ændringer i bogmærkeoplysningerne. Klik på Gem, når du er færdig.\",\n    \"author\": \"Forfatter\",\n    \"publisher\": \"Udgiver\",\n    \"date_published\": \"Udgivelsesdato\",\n    \"pick_a_date\": \"Vælg en dato\",\n    \"save_changes\": \"Gem ændringer\",\n    \"extracted_content\": \"Udtrukket indhold\"\n  },\n  \"view_options\": {\n    \"title\": \"Visningsindstillinger\",\n    \"layout\": \"Layout\",\n    \"columns\": \"Kolonner\",\n    \"display_options\": \"Visningsindstillinger\",\n    \"show_note_previews\": \"Vis noter\",\n    \"show_tags\": \"Vis tags\",\n    \"show_title\": \"Vis titel\",\n    \"image_options\": \"Billedindstillinger\",\n    \"image_fit_cover\": \"Dække (fyld)\",\n    \"image_fit_contain\": \"Indeholder (tilpas)\"\n  },\n  \"version\": {\n    \"new_release_available\": \"Nye udgivelsesnoter tilgængelige\",\n    \"whats_new_title\": \"Hvad er nyt i v{{version}}\",\n    \"release_notes_description\": \"Her er de seneste opdateringer hentet fra GitHub's udgivelsesnoter.\",\n    \"loading_release_notes\": \"Indlæser udgivelsesnoter…\",\n    \"unable_to_load_release_notes\": \"Kunne ikke indlæse udgivelsesnoter lige nu. Prøv igen senere.\",\n    \"no_release_notes\": \"Der blev ikke offentliggjort nogen udgivelsesnoter for denne version.\",\n    \"release_notes_synced\": \"Udgivelsesnoter er synkroniseret fra GitHub.\",\n    \"view_on_github\": \"Se på GitHub\"\n  },\n  \"wrapped\": {\n    \"title\": \"Din {{year}}-opsummering\",\n    \"subtitle\": \"Et år i Karakeep\",\n    \"banner\": {\n      \"title\": \"Din opsummering for 2025 er klar!\",\n      \"description\": \"Se dit år i bogmærker\",\n      \"view_now\": \"Vis nu\"\n    },\n    \"button\": \"2025-opsummering\",\n    \"loading\": \"Indlæser din opsummering...\",\n    \"failed_to_load\": \"Kunne ikke indlæse din opsummeringsstatistik\",\n    \"sections\": {\n      \"total_saves\": {\n        \"prefix\": \"Du gemte\",\n        \"suffix\": \"emner i år\",\n        \"suffix_singular\": \"emne i år\"\n      },\n      \"first_bookmark\": {\n        \"title\": \"Din rejse er begyndt\",\n        \"description\": \"Første gemte i {{year}}:\"\n      },\n      \"top_domains\": \"Dine foretrukne sider\",\n      \"top_tags\": \"Dine mest populære tags\",\n      \"monthly_activity\": \"Dit år i gemte\",\n      \"most_active_day\": \"Din mest aktive dag\",\n      \"peak_times\": {\n        \"title\": \"Hvornår du gemmer\",\n        \"peak_hour\": \"Spidsbelastningstime\",\n        \"peak_day\": \"Spidsbelastningsdag\"\n      },\n      \"how_you_save\": \"Hvordan du gemmer\",\n      \"what_you_saved\": \"Hvad du har gemt\",\n      \"summary\": {\n        \"favorites\": \"Favoritter\",\n        \"tags_created\": \"Oprettede mærker\",\n        \"highlights\": \"Højdepunkter\"\n      },\n      \"types\": {\n        \"links\": \"Links\",\n        \"notes\": \"Noter\",\n        \"assets\": \"Aktiver\"\n      }\n    },\n    \"footer\": \"Lavet med Karakeep\",\n    \"share\": \"Del\",\n    \"download\": \"Hent\",\n    \"close\": \"Luk\",\n    \"generating\": \"Genererer...\"\n  }\n}\n"
  },
  {
    "path": "apps/web/lib/i18n/locales/de/translation.json",
    "content": "{\n  \"common\": {\n    \"url\": \"URL\",\n    \"name\": \"Name\",\n    \"email\": \"E-Mail\",\n    \"password\": \"Passwort\",\n    \"action\": \"Aktion\",\n    \"actions\": \"Aktionen\",\n    \"created_at\": \"Erstellt am\",\n    \"key\": \"Schlüssel\",\n    \"role\": \"Rolle\",\n    \"roles\": {\n      \"user\": \"Benutzer\",\n      \"admin\": \"Administrator\"\n    },\n    \"something_went_wrong\": \"Etwas ist schief gelaufen\",\n    \"experimental\": \"Experimentell\",\n    \"search\": \"Suche\",\n    \"tags\": \"Tags\",\n    \"note\": \"Notiz\",\n    \"attachments\": \"Anhänge\",\n    \"screenshot\": \"Screenshot\",\n    \"video\": \"Video\",\n    \"archive\": \"Archiv\",\n    \"home\": \"Startseite\",\n    \"highlights\": \"Textmarkierungen\",\n    \"source\": \"Quelle\",\n    \"bookmark_types\": {\n      \"title\": \"Lesezeichen Kategorie\",\n      \"link\": \"Link\",\n      \"media\": \"Medien\",\n      \"text\": \"Text\"\n    },\n    \"type\": \"Typ\",\n    \"size\": \"Größe\",\n    \"updated_at\": \"Aktualisiert am\",\n    \"title\": \"Titel\",\n    \"description\": \"Beschreibung\",\n    \"summary\": \"Zusammenfassung\",\n    \"quota\": \"Kontingent\",\n    \"bookmarks\": \"Lesezeichen\",\n    \"storage\": \"Speicher\",\n    \"pdf\": \"Archivierte PDF-Datei\",\n    \"default\": \"Standard\",\n    \"id\": \"ID\",\n    \"last_used\": \"Zuletzt verwendet\"\n  },\n  \"layouts\": {\n    \"masonry\": \"Verschachtelt\",\n    \"grid\": \"Raster\",\n    \"list\": \"Liste\",\n    \"compact\": \"Kompakt\"\n  },\n  \"actions\": {\n    \"change_layout\": \"Layout ändern\",\n    \"archive\": \"Archivieren\",\n    \"unarchive\": \"Archivierung aufheben\",\n    \"favorite\": \"Favorit\",\n    \"unfavorite\": \"Favorit entfernen\",\n    \"delete\": \"Löschen\",\n    \"refresh\": \"Aktualisieren\",\n    \"download_full_page_archive\": \"Vollständiges Seitenarchiv herunterladen\",\n    \"edit_tags\": \"Tags bearbeiten\",\n    \"add_to_list\": \"Zur Liste hinzufügen\",\n    \"select_all\": \"Alle auswählen\",\n    \"unselect_all\": \"Auswahl aufheben\",\n    \"copy_link\": \"Link kopieren\",\n    \"close_bulk_edit\": \"Massenbearbeitung schließen\",\n    \"bulk_edit\": \"Massenbearbeitung\",\n    \"manage_lists\": \"Listen verwalten\",\n    \"remove_from_list\": \"Aus Liste entfernen\",\n    \"save\": \"Speichern\",\n    \"add\": \"Hinzufügen\",\n    \"edit\": \"Bearbeiten\",\n    \"create\": \"Erstellen\",\n    \"fetch_now\": \"Jetzt abrufen\",\n    \"summarize_with_ai\": \"Mit KI zusammenfassen\",\n    \"edit_title\": \"Titel bearbeiten\",\n    \"sign_out\": \"Abmelden\",\n    \"close\": \"Schließen\",\n    \"merge\": \"Zusammenführen\",\n    \"cancel\": \"Abbrechen\",\n    \"apply_all\": \"Alle anwenden\",\n    \"ignore\": \"Ignorieren\",\n    \"recrawl\": \"Erneut abrufen\",\n    \"sort\": {\n      \"title\": \"Sortieren\",\n      \"newest_first\": \"Neueste zuerst\",\n      \"oldest_first\": \"Älteste zuerst\",\n      \"relevant_first\": \"Relevanteste zuerst\"\n    },\n    \"open_editor\": \"Editor öffnen\",\n    \"toggle_show_archived\": \"Archivierte anzeigen\",\n    \"confirm\": \"Bestätigen\",\n    \"regenerate\": \"Regenerieren\",\n    \"load_more\": \"Mehr laden\",\n    \"edit_notes\": \"Notizen bearbeiten\",\n    \"preserve_as_pdf\": \"Als PDF speichern\",\n    \"offline_copies\": \"Offline-Kopien\",\n    \"preserve_offline_archive\": \"Offline-Archiv behalten\",\n    \"download_full_page_archive_file\": \"Archivdatei herunterladen\",\n    \"download_pdf_file\": \"PDF-Datei herunterladen\",\n    \"remove\": \"Entfernen\",\n    \"more\": \"Mehr\",\n    \"replace_banner\": \"Banner ersetzen\",\n    \"add_banner\": \"Banner hinzufügen\",\n    \"download\": \"Herunterladen\"\n  },\n  \"settings\": {\n    \"back_to_app\": \"Zurück zur App\",\n    \"user_settings\": \"Benutzereinstellungen\",\n    \"info\": {\n      \"user_info\": \"Benutzerinformation\",\n      \"basic_details\": \"Grundlegende Details\",\n      \"change_password\": \"Passwort ändern\",\n      \"current_password\": \"Aktuelles Passwort\",\n      \"new_password\": \"Neues Passwort\",\n      \"confirm_new_password\": \"Neues Passwort bestätigen\",\n      \"options\": \"Optionen\",\n      \"interface_lang\": \"Oberflächensprache\",\n      \"user_settings\": {\n        \"user_settings_updated\": \"Benutzereinstellungen wurden aktualisiert!\",\n        \"bookmark_click_action\": {\n          \"open_bookmark_details\": \"Lesezeichen-Details öffnen\",\n          \"title\": \"Aktion bei Lesezeichen-Klick\",\n          \"open_external_url\": \"Original-URL öffnen\"\n        },\n        \"archive_display_behaviour\": {\n          \"title\": \"Archivierte Lesezeichen\",\n          \"show\": \"Archivierte Lesezeichen in Tags und Listen anzeigen\",\n          \"hide\": \"Archivierte Lesezeichen in Tags und Listen ausblenden\"\n        }\n      },\n      \"reader_settings\": {\n        \"local_overrides_title\": \"Gerätespezifische Einstellungen aktiv\",\n        \"using_default\": \"Client-Standard verwenden\",\n        \"clear_override_hint\": \"Geräteüberschreibung löschen, um die globale Einstellung zu verwenden ({{value}})\",\n        \"font_size\": \"Schriftgröße\",\n        \"font_family\": \"Schriftfamilie\",\n        \"preview_inline\": \"(Vorschau)\",\n        \"tooltip_preview\": \"Nicht gespeicherte Vorschaueinstellungen\",\n        \"save_to_all_devices\": \"Alle Geräte\",\n        \"tooltip_local\": \"Geräteeinstellungen weichen von den globalen Einstellungen ab\",\n        \"reset_preview\": \"Vorschau zurücksetzen\",\n        \"mono\": \"Monospace\",\n        \"line_height\": \"Zeilenhöhe\",\n        \"tooltip_default\": \"Leseeinstellungen\",\n        \"title\": \"Lesereinstellungen\",\n        \"serif\": \"Serif\",\n        \"preview\": \"Vorschau\",\n        \"not_set\": \"Nicht festgelegt\",\n        \"clear_local_overrides\": \"Geräteeinstellungen löschen\",\n        \"preview_text\": \"The quick brown fox jumps over the lazy dog. So wird der Text Ihrer Leseransicht aussehen.\",\n        \"local_overrides_cleared\": \"Gerätespezifische Einstellungen wurden gelöscht\",\n        \"local_overrides_description\": \"Dieses Gerät hat Lesereinstellungen, die von Ihren globalen Standardeinstellungen abweichen:\",\n        \"clear_defaults\": \"Alle Standardeinstellungen löschen\",\n        \"description\": \"Standard-Texteinstellungen für die Leseransicht konfigurieren. Diese Einstellungen werden auf allen Ihren Geräten synchronisiert.\",\n        \"defaults_cleared\": \"Die Standardeinstellungen des Readers wurden gelöscht\",\n        \"save_hint\": \"Einstellungen nur für dieses Gerät speichern oder über alle Geräte synchronisieren\",\n        \"save_as_default\": \"Als Standard speichern\",\n        \"save_to_device\": \"Dieses Gerät\",\n        \"sans\": \"Sans Serif\",\n        \"tooltip_preview_and_local\": \"Nicht gespeicherte Vorschaueinstellungen; Geräteeinstellungen weichen von den globalen Einstellungen ab\",\n        \"adjust_hint\": \"Passe die Einstellungen oben an, um eine Vorschau der Änderungen zu sehen\"\n      },\n      \"avatar\": {\n        \"upload\": \"Avatar hochladen\",\n        \"change\": \"Avatar ändern\",\n        \"remove_confirm_title\": \"Avatar entfernen?\",\n        \"updated\": \"Avatar aktualisiert\",\n        \"removed\": \"Avatar entfernt\",\n        \"description\": \"Lade ein quadratisches Bild hoch, das du als Avatar verwenden möchtest.\",\n        \"remove_confirm_description\": \"Dadurch wird dein aktuelles Profilfoto gelöscht.\",\n        \"title\": \"Profilfoto\",\n        \"remove\": \"Avatar entfernen\"\n      }\n    },\n    \"ai\": {\n      \"ai_settings\": \"KI-Einstellungen\",\n      \"tagging_rules\": \"Tagging-Regeln\",\n      \"tagging_rule_description\": \"Eingabeaufforderungen, die Sie hier hinzufügen, werden als Regeln für das Modell bei der Tag-Generierung verwendet. Sie können die endgültigen Aufforderungen im Bereich Vorschau anzeigen.\",\n      \"prompt_preview\": \"Aufforderungsvorschau\",\n      \"text_prompt\": \"Text-Aufforderung\",\n      \"images_prompt\": \"Bild-Aufforderung\",\n      \"summarization_prompt\": \"Zusammenfassung des Prompts\",\n      \"all_tagging\": \"Gesamtes Tagging\",\n      \"text_tagging\": \"Text-Tagging\",\n      \"image_tagging\": \"Bild-Tagging\",\n      \"summarization\": \"Zusammenfassung\",\n      \"tag_style\": \"Tag-Stil\",\n      \"auto_summarization_description\": \"Automatische Zusammenfassung deiner Lesezeichen mithilfe von KI.\",\n      \"auto_tagging\": \"Automatisches Tagging\",\n      \"titlecase_spaces\": \"Titel-Schreibweise mit Leerzeichen\",\n      \"lowercase_underscores\": \"Kleinbuchstaben mit Unterstrichen\",\n      \"inference_language\": \"Schlussfolgerungs-Sprache\",\n      \"titlecase_hyphens\": \"Titel-Schreibweise mit Bindestrichen\",\n      \"lowercase_hyphens\": \"Kleinbuchstaben mit Bindestrichen\",\n      \"lowercase_spaces\": \"Kleinbuchstaben mit Leerzeichen\",\n      \"inference_language_description\": \"Sprache für von KI generierte Tags und Zusammenfassungen auswählen.\",\n      \"tag_style_description\": \"Wähle, wie deine automatisch generierten Tags formatiert werden sollen.\",\n      \"auto_tagging_description\": \"Automatische Tag-Generierung für deine Lesezeichen mithilfe von KI.\",\n      \"camelCase\": \"camelCase\",\n      \"auto_summarization\": \"Automatische Zusammenfassung\",\n      \"no_preference\": \"Keine Präferenz\",\n      \"curated_tags\": \"Ausgewählte Tags\",\n      \"curated_tags_description\": \"Beschränke die KI-Tag-Vergabe optional auf die Verwendung von Tags aus dieser Liste. Wenn keine Tags ausgewählt sind, generiert die KI Tags frei.\",\n      \"curated_tags_updated\": \"Ausgewählte Tags erfolgreich aktualisiert!\",\n      \"curated_tags_update_failed\": \"Ausgewählte Tags konnten nicht aktualisiert werden\"\n    },\n    \"feeds\": {\n      \"rss_subscriptions\": \"RSS-Abonnements\",\n      \"add_a_subscription\": \"Ein Abonnement hinzufügen\",\n      \"feed_enabled\": \"RSS-Feed aktiviert\",\n      \"feed_disabled\": \"RSS-Feed deaktiviert\"\n    },\n    \"import\": {\n      \"import_export\": \"Import / Export\",\n      \"import_export_bookmarks\": \"Lesezeichen importieren / exportieren\",\n      \"import_bookmarks_from_html_file\": \"Lesezeichen aus HTML-Datei importieren\",\n      \"import_bookmarks_from_pocket_export\": \"Lesezeichen aus Pocket-Export importieren\",\n      \"import_bookmarks_from_matter_export\": \"Lesezeichen aus Matter-Export importieren\",\n      \"import_bookmarks_from_omnivore_export\": \"Lesezeichen aus Omnivore-Export importieren\",\n      \"import_bookmarks_from_karakeep_export\": \"Lesezeichen aus Karakeep-Export importieren\",\n      \"export_links_and_notes\": \"Links und Notizen exportieren\",\n      \"imported_bookmarks\": \"Importierte Lesezeichen\",\n      \"import_bookmarks_from_linkwarden_export\": \"Importieren Sie Lesezeichen aus dem Linkwarden-Export\",\n      \"import_bookmarks_from_tab_session_manager_export\": \"Lesezeichen aus Tab Session Manager importieren\",\n      \"import_bookmarks_from_mymind_export\": \"Lesezeichen aus meinem mymind-Export importieren\",\n      \"import_bookmarks_from_instapaper_export\": \"Lesezeichen aus Instapaper-Export importieren\"\n    },\n    \"api_keys\": {\n      \"api_keys\": \"API-Schlüssel\",\n      \"new_api_key\": \"Neuer API-Schlüssel\",\n      \"new_api_key_desc\": \"Geben Sie Ihrem API-Schlüssel einen eindeutigen Namen\",\n      \"key_success\": \"Schlüssel wurde erfolgreich erstellt\",\n      \"key_success_please_copy\": \"Bitte kopieren Sie den Schlüssel und speichern Sie ihn an einem sicheren Ort. Sobald Sie das Dialogfeld schließen, können Sie nicht mehr darauf zugreifen.\",\n      \"regenerate_api_key\": \"API-Schlüssel neu generieren\",\n      \"key_regenerated\": \"Schlüssel wurde erfolgreich neu generiert\",\n      \"key_regenerated_please_copy\": \"Bitte kopiere den neuen Schlüssel und bewahre ihn an einem sicheren Ort auf. Der alte Schlüssel wurde widerrufen und funktioniert nicht mehr.\",\n      \"regenerate_warning\": \"Bist du sicher, dass du den API-Schlüssel „{{name}}“ neu generieren möchtest?\",\n      \"regenerate_confirmation\": \"Dies widerruft den aktuellen Schlüssel und generiert einen neuen. Alle Anwendungen, die den aktuellen Schlüssel verwenden, funktionieren dann nicht mehr.\"\n    },\n    \"broken_links\": {\n      \"broken_links\": \"Defekte Links\",\n      \"last_crawled_at\": \"Zuletzt abgerufen am\",\n      \"crawling_status\": \"Crawling-Status\",\n      \"crawling_failed\": \"Abrufen fehlgeschlagen\"\n    },\n    \"webhooks\": {\n      \"webhook_url\": \"Webhook URL\",\n      \"edit_auth_token\": \"Auth Token bearbeiten\",\n      \"webhooks\": \"Webhooks\",\n      \"description\": \"Du kannst Webhooks nutzen, um Aktionen auszulösen wenn Lesezeichen erstellt, geändert oder abgerufen werden sollen.\",\n      \"events\": {\n        \"title\": \"Vorgänge\",\n        \"crawled\": \"Abgerufen\",\n        \"created\": \"erstellt\",\n        \"edited\": \"bearbeitet\"\n      },\n      \"auth_token\": \"Auth Token\",\n      \"add_auth_token\": \"Auth Token hinzufügen\",\n      \"create_webhook\": \"Webhook erstellen\",\n      \"delete_webhook\": \"Webhook löschen\",\n      \"delete_webhook_confirmation\": \"Bist du sicher, dass du diesen Webhook löschen willst?\",\n      \"edit_webhook\": \"Webhook bearbeiten\"\n    },\n    \"manage_assets\": {\n      \"delete_asset\": \"Asset löschen\",\n      \"delete_asset_confirmation\": \"Bist du sicher, dass du dieses Asset löschen möchtest?\",\n      \"manage_assets\": \"Assets verwalten\",\n      \"no_assets\": \"Du hast noch keine Assets.\",\n      \"asset_type\": \"Art\",\n      \"bookmark_link\": \"Lesezeichen\",\n      \"asset_link\": \"Link\"\n    },\n    \"rules\": {\n      \"conditions_types\": {\n        \"and\": \"Alle folgenden Bedingungen sind erfüllt\",\n        \"or\": \"Eine der folgenden Bedingungen ist erfüllt\",\n        \"always\": \"Immer\",\n        \"url_contains\": \"URL enthält\",\n        \"imported_from_feed\": \"Aus Feed importiert\",\n        \"bookmark_type_is\": \"Lesezeichen-Typ ist\",\n        \"has_tag\": \"Hat Tag\",\n        \"is_favourited\": \"Ist Favorit\",\n        \"is_archived\": \"Ist archiviert\",\n        \"url_does_not_contain\": \"URL Enthält nicht\",\n        \"title_contains\": \"Titel enthält\",\n        \"title_does_not_contain\": \"Titel enthält nicht\"\n      },\n      \"rule_name\": \"Regelname\",\n      \"rules\": \"Regel-Engine\",\n      \"description\": \"Du kannst Regeln verwenden, um Aktionen auszulösen, wenn ein Ereignis eintritt.\",\n      \"ceate_rule\": \"Regel erstellen\",\n      \"edit_rule\": \"Regel bearbeiten\",\n      \"save_rule\": \"Regel speichern\",\n      \"delete_rule\": \"Regel löschen\",\n      \"delete_rule_confirmation\": \"Bist du sicher, dass du diese Regel löschen möchtest?\",\n      \"whenever\": \"Immer wenn ...\",\n      \"if\": \"Wenn ...\",\n      \"enter_rule_name\": \"Regelnamen eingeben\",\n      \"describe_what_this_rule_does\": \"Beschreibe, was diese Regel bewirkt\",\n      \"rule_has_been_created\": \"Regel wurde erstellt!\",\n      \"rule_has_been_updated\": \"Regel wurde aktualisiert!\",\n      \"rule_has_been_deleted\": \"Regel wurde gelöscht!\",\n      \"no_rules_created_yet\": \"Noch keine Regeln erstellt\",\n      \"create_your_first_rule\": \"Erstelle deine erste Regel, um deinen Workflow zu automatisieren\",\n      \"actions_types\": {\n        \"add_tag\": \"Tag hinzufügen\",\n        \"remove_tag\": \"Tag entfernen\",\n        \"add_to_list\": \"Zur Liste hinzufügen\",\n        \"remove_from_list\": \"Aus Liste entfernen\",\n        \"download_full_page_archive\": \"Vollständige Seitenarchivierung herunterladen\",\n        \"favourite_bookmark\": \"Lesezeichen favorisieren\",\n        \"archive_bookmark\": \"Lesezeichen archivieren\"\n      },\n      \"events_types\": {\n        \"bookmark_added\": \"Ein Lesezeichen wird hinzugefügt\",\n        \"tag_added\": \"Dieses Tag wird einem Lesezeichen hinzugefügt\",\n        \"tag_removed\": \"Dieses Tag wird von einem Lesezeichen entfernt\",\n        \"added_to_list\": \"Ein Lesezeichen wird zu dieser Liste hinzugefügt\",\n        \"removed_from_list\": \"Ein Lesezeichen wird aus dieser Liste entfernt\",\n        \"favourited\": \"Ein Lesezeichen wird favorisiert\",\n        \"archived\": \"Ein Lesezeichen wird archiviert\"\n      }\n    },\n    \"stats\": {\n      \"usage_statistics\": \"Nutzungsstatistiken\",\n      \"insights_description\": \"Einblicke in deine Lesezeichengewohnheiten und -sammlung\",\n      \"failed_to_load\": \"Statistiken konnten nicht geladen werden\",\n      \"overview\": {\n        \"total_bookmarks\": \"Gesamtzahl der Lesezeichen\",\n        \"all_saved_items\": \"Alle gespeicherten Elemente\",\n        \"favorites\": \"Favoriten\",\n        \"starred_bookmarks\": \"Markierte Lesezeichen\",\n        \"archived\": \"Archiviert\",\n        \"archived_items\": \"Archivierte Elemente\",\n        \"tags\": \"Tags\",\n        \"unique_tags_created\": \"Eindeutige Tags erstellt\",\n        \"lists\": \"Listen\",\n        \"bookmark_collections\": \"Lesezeichen-Sammlungen\",\n        \"highlights\": \"Markierungen\",\n        \"text_highlights\": \"Texthervorhebungen\",\n        \"storage_used\": \"Verwendeter Speicherplatz\",\n        \"total_asset_storage\": \"Gesamter Assetspeicher\",\n        \"this_month\": \"Diesen Monat\",\n        \"bookmarks_added\": \"Lesezeichen hinzugefügt\"\n      },\n      \"bookmark_types\": {\n        \"title\": \"Lesezeichen-Typen\",\n        \"links\": \"Links\",\n        \"text_notes\": \"Textnotizen\",\n        \"assets\": \"Assets\"\n      },\n      \"recent_activity\": {\n        \"title\": \"Letzte Aktivität\",\n        \"this_week\": \"Diese Woche\",\n        \"this_month\": \"Diesen Monat\",\n        \"this_year\": \"Dieses Jahr\"\n      },\n      \"top_domains\": {\n        \"title\": \"Top-Domains\",\n        \"no_domains_found\": \"Keine Domains gefunden\"\n      },\n      \"most_used_tags\": {\n        \"title\": \"Meist verwendete Tags\",\n        \"no_tags_found\": \"Keine Tags gefunden\"\n      },\n      \"activity_patterns\": {\n        \"activity_by_hour\": \"Aktivität nach Stunde\",\n        \"activity_by_day\": \"Aktivität nach Tag\"\n      },\n      \"storage_breakdown\": {\n        \"title\": \"Speicheraufschlüsselung\"\n      },\n      \"bookmark_sources\": {\n        \"title\": \"Lesezeichenquellen\",\n        \"empty\": \"Keine Quelldaten verfügbar\"\n      }\n    },\n    \"subscription\": {\n      \"subscription\": \"Abonnement\",\n      \"manage_subscription\": \"Verwalte dein Abonnement und deine Abrechnungsinformationen\",\n      \"current_plan\": \"Aktueller Plan\",\n      \"billing_period\": \"Abrechnungszeitraum\",\n      \"paid_plan\": \"Bezahlter Plan\",\n      \"unlock_bigger_quota\": \"Schalte ein größeres Kontingent frei und unterstütze das Projekt\",\n      \"subscribe_now\": \"Jetzt abonnieren\",\n      \"manage_billing\": \"Abrechnung verwalten\",\n      \"subscription_canceled\": \"Dein Abonnement wurde gekündigt und endet am {{date}}. Du kannst dich jederzeit wieder anmelden.\",\n      \"usage_quotas\": \"Nutzung & Kontingente\",\n      \"track_usage\": \"Verfolge deine aktuelle Nutzung im Vergleich zu deinen Planlimits\",\n      \"total_bookmarks_saved\": \"Anzahl der gespeicherten Lesezeichen\",\n      \"assets_file_storage\": \"Assets und Dateispeicher\",\n      \"unlimited_usage\": \"Unbegrenzte Nutzung\",\n      \"quota_limit_reached\": \"Kontingentgrenze erreicht\",\n      \"approaching_quota_limit\": \"Kontingentgrenze wird erreicht\",\n      \"loading_usage\": \"Lade Nutzungsinformationen...\",\n      \"free\": \"Kostenlos\",\n      \"paid\": \"Bezahlt\"\n    },\n    \"import_sessions\": {\n      \"title\": \"Import-Sitzungen\",\n      \"description\": \"Hier kannst du deine Massenimport-Sitzungen ansehen und verwalten. Sitzungen werden automatisch erstellt, wenn du Lesezeichen importierst.\",\n      \"load_error\": \"Import-Sitzungen konnten nicht geladen werden\",\n      \"no_sessions\": \"Noch keine Import-Sitzungen\",\n      \"no_sessions_detail\": \"Import-Sitzungen erscheinen hier automatisch, wenn du Lesezeichen importierst\",\n      \"created_at\": \"Erstellt {{time}}\",\n      \"progress\": \"Fortschritt\",\n      \"status\": {\n        \"pending\": \"Ausstehend\",\n        \"in_progress\": \"In Bearbeitung\",\n        \"completed\": \"Abgeschlossen\",\n        \"failed\": \"Fehlgeschlagen\",\n        \"processing\": \"Wird verarbeitet\",\n        \"staging\": \"Wird vorbereitet\",\n        \"running\": \"Läuft\",\n        \"paused\": \"Pausiert\"\n      },\n      \"badges\": {\n        \"pending\": \"{{count}} ausstehend\",\n        \"processing\": \"{{count}} in Bearbeitung\",\n        \"completed\": \"{{count}} abgeschlossen\",\n        \"failed\": \"{{count}} fehlgeschlagen\"\n      },\n      \"imported_to\": \"Importiert nach:\",\n      \"view_list\": \"Liste ansehen\",\n      \"delete_dialog_title\": \"Import-Sitzung löschen\",\n      \"delete_dialog_description\": \"Bist du sicher, dass du »{{name}}« löschen willst? Diese Aktion kann nicht rückgängig gemacht werden. Die Lesezeichen selbst werden nicht gelöscht.\",\n      \"delete_session\": \"Sitzung löschen\",\n      \"pause_session\": \"Pausieren\",\n      \"resume_session\": \"Fortsetzen\",\n      \"view_details\": \"Details anzeigen\",\n      \"detail\": {\n        \"page_title\": \"Details der Importsitzung\",\n        \"back_to_import\": \"Zurück zum Import\",\n        \"filter_all\": \"Alle\",\n        \"filter_accepted\": \"Akzeptiert\",\n        \"filter_rejected\": \"Abgelehnt\",\n        \"filter_duplicates\": \"Duplikate\",\n        \"filter_pending\": \"Ausstehend\",\n        \"table_title\": \"Titel / URL\",\n        \"table_type\": \"Typ\",\n        \"table_result\": \"Ergebnis\",\n        \"table_reason\": \"Grund\",\n        \"table_bookmark\": \"Lesezeichen\",\n        \"result_accepted\": \"Akzeptiert\",\n        \"result_rejected\": \"Abgelehnt\",\n        \"result_skipped_duplicate\": \"Duplikat\",\n        \"result_pending\": \"Ausstehend\",\n        \"result_processing\": \"Wird verarbeitet\",\n        \"no_results\": \"Für diesen Filter wurden keine Ergebnisse gefunden.\",\n        \"view_bookmark\": \"Lesezeichen anzeigen\",\n        \"load_more\": \"Mehr laden\",\n        \"no_title\": \"Kein Titel\"\n      }\n    },\n    \"backups\": {\n      \"backups\": \"Backups\",\n      \"page_title\": \"Backups\",\n      \"page_description\": \"Erstelle und verwalte automatisch Backups deiner Lesezeichen. Backups werden komprimiert und sicher gespeichert.\",\n      \"configuration\": {\n        \"title\": \"Backup-Konfiguration\",\n        \"enable_automatic_backups\": \"Automatische Backups aktivieren\",\n        \"enable_automatic_backups_description\": \"Automatische Backups deiner Lesezeichen erstellen\",\n        \"backup_frequency\": \"Backup-Frequenz\",\n        \"backup_frequency_description\": \"Wie oft Backups erstellt werden sollen\",\n        \"retention_period\": \"Aufbewahrungsdauer (Tage)\",\n        \"retention_period_description\": \"Wie viele Tage Backups aufbewahrt werden sollen, bevor sie gelöscht werden\",\n        \"frequency\": {\n          \"daily\": \"Täglich\",\n          \"weekly\": \"Wöchentlich\"\n        },\n        \"select_frequency\": \"Frequenz auswählen\",\n        \"save_settings\": \"Einstellungen speichern\"\n      },\n      \"list\": {\n        \"title\": \"Deine Backups\",\n        \"create_backup_now\": \"Backup jetzt erstellen\",\n        \"no_backups\": \"Du hast noch keine Backups. Aktiviere automatische Backups oder erstelle manuell eins.\",\n        \"table\": {\n          \"created_at\": \"Erstellt am\",\n          \"bookmarks\": \"Lesezeichen\",\n          \"size\": \"Größe\",\n          \"status\": \"Status\",\n          \"actions\": \"Aktionen\"\n        },\n        \"status\": {\n          \"success\": \"Erfolgreich\",\n          \"failed\": \"Fehlgeschlagen\",\n          \"pending\": \"Ausstehend\"\n        },\n        \"actions\": {\n          \"download_backup\": \"Backup herunterladen\",\n          \"delete_backup\": \"Backup löschen\"\n        }\n      },\n      \"dialogs\": {\n        \"delete_backup_title\": \"Backup löschen?\",\n        \"delete_backup_description\": \"Bist du sicher, dass du dieses Backup löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.\"\n      },\n      \"toasts\": {\n        \"backup_queued\": \"Backup-Job wurde in die Warteschlange gestellt! Er wird in Kürze bearbeitet.\",\n        \"backup_deleted\": \"Backup wurde gelöscht!\"\n      }\n    }\n  },\n  \"admin\": {\n    \"admin_settings\": \"Admin-Einstellungen\",\n    \"server_stats\": {\n      \"server_stats\": \"Serverstatistiken\",\n      \"total_users\": \"Gesamte Benutzer\",\n      \"total_bookmarks\": \"Gesamte Lesezeichen\",\n      \"server_version\": \"Serverversion\"\n    },\n    \"background_jobs\": {\n      \"background_jobs\": \"Hintergrundaufgaben\",\n      \"crawler_jobs\": \"Crawler-Aufgaben\",\n      \"indexing_jobs\": \"Indexierungsaufgaben\",\n      \"inference_jobs\": \"Inferenzaufgaben\",\n      \"tidy_assets_jobs\": \"Aufräumaufgaben\",\n      \"job\": \"Aufgabe\",\n      \"queued\": \"Warteschlange\",\n      \"pending\": \"Ausstehend\",\n      \"failed\": \"Fehlgeschlagen\",\n      \"video_jobs\": \"Video-Download-Aufträge\",\n      \"webhook_jobs\": \"Webhook-Aufträge\",\n      \"asset_preprocessing_jobs\": \"Asset-Vorverarbeitungsaufträge\",\n      \"feed_jobs\": \"RSS-Feed-Aufträge\",\n      \"jobs\": {\n        \"crawler\": {\n          \"title\": \"Crawler-Jobs\",\n          \"description\": \"Web-Crawling und Content-Extraktion von URLs\"\n        },\n        \"inference\": {\n          \"title\": \"Inferenz-Jobs\",\n          \"description\": \"KI-gestütztes Tagging und Zusammenfassung von Inhalten\"\n        },\n        \"indexing\": {\n          \"title\": \"Indexierungs-Jobs\",\n          \"description\": \"Suchindex-Aktualisierungen\"\n        },\n        \"asset_preprocessing\": {\n          \"title\": \"Asset-Vorverarbeitungs-Jobs\",\n          \"description\": \"Bild- und Dokumenten-Vorverarbeitung (Screenshots, Textextraktion, etc.)\"\n        },\n        \"tidy_assets\": {\n          \"title\": \"Tidy-Assets-Jobs\",\n          \"description\": \"Asset-Bereinigung und Speicheroptimierung\"\n        },\n        \"video\": {\n          \"title\": \"Video-Download-Jobs\",\n          \"description\": \"Videoextraktion und -download\"\n        },\n        \"webhook\": {\n          \"title\": \"Webhook-Jobs\",\n          \"description\": \"Externe Webhook-Benachrichtigungen\"\n        },\n        \"feed\": {\n          \"title\": \"RSS-Feed-Jobs\",\n          \"description\": \"RSS-Feed-Verarbeitung und Inhaltsaktualisierungen\"\n        },\n        \"admin_maintenance\": {\n          \"title\": \"Wartungsaufgaben für Admins\",\n          \"description\": \"Administrative Bereinigung und Anlagenwartung\"\n        }\n      },\n      \"monitor_and_manage\": \"Überwachung und Verwaltung von Hintergrund-Job-Warteschlangen und Systemverarbeitungsaufgaben\",\n      \"active\": \"Aktiv\",\n      \"available_actions\": \"Verfügbare Aktionen\",\n      \"status\": {\n        \"title\": \"Job-Status verstehen\",\n        \"queued\": {\n          \"title\": \"In der Warteschlange\",\n          \"description\": \"Jobs, die darauf warten, bearbeitet zu werden. Sie starten automatisch, wenn Ressourcen verfügbar sind.\"\n        },\n        \"unprocessed\": {\n          \"title\": \"Unverarbeitet\",\n          \"description\": \"Lesezeichen, die noch nicht verarbeitet wurden. Sie sind höchstwahrscheinlich bereits zur Verarbeitung in die Warteschlange gestellt, falls nicht, musst du sie möglicherweise manuell erneut in die Warteschlange stellen.\"\n        },\n        \"failed\": {\n          \"title\": \"Fehlgeschlagen\",\n          \"description\": \"Lesezeichen, bei deren Verarbeitung Fehler aufgetreten sind. Diese erfordern möglicherweise manuelle Aufmerksamkeit.\"\n        }\n      },\n      \"actions\": {\n        \"recrawl_failed_links_only\": \"Nur fehlgeschlagene Links erneut crawlen\",\n        \"recrawl_all_links\": \"Alle Links erneut crawlen\",\n        \"without_inference\": \"Ohne Inferenz\",\n        \"regenerate_ai_tags_for_failed_bookmarks_only\": \"KI-Tags nur für fehlgeschlagene Lesezeichen neu generieren\",\n        \"regenerate_ai_tags_for_all_bookmarks\": \"KI-Tags für alle Lesezeichen neu generieren\",\n        \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"KI-Zusammenfassungen nur für fehlgeschlagene Lesezeichen neu generieren\",\n        \"regenerate_ai_summaries_for_all_bookmarks\": \"KI-Zusammenfassungen für alle Lesezeichen neu generieren\",\n        \"reindex_all_bookmarks\": \"Alle Lesezeichen neu indizieren\",\n        \"clean_assets\": \"Verwaiste Assets bereinigen & Metadaten erneut synchronisieren\",\n        \"reprocess_assets_fix_mode\": \"Unverarbeitete Assets neu verarbeiten\",\n        \"migrate_large_link_html_content\": \"Große Inline-HTML-Inhalte in Assets verschieben\",\n        \"recrawl_pending_links_only\": \"Nur ausstehende Links neu crawlen\",\n        \"regenerate_ai_tags_for_pending_bookmarks_only\": \"KI-Tags nur für ausstehende Bookmarks neu generieren\",\n        \"regenerate_ai_summaries_for_pending_bookmarks_only\": \"KI-Zusammenfassungen nur für ausstehende Bookmarks neu generieren\"\n      }\n    },\n    \"actions\": {\n      \"recrawl_failed_links_only\": \"Nur fehlgeschlagene Links erneut abrufen\",\n      \"recrawl_all_links\": \"Alle Links erneut abrufen\",\n      \"without_inference\": \"Ohne Inferenz\",\n      \"regenerate_ai_tags_for_failed_bookmarks_only\": \"KI-Tags nur für fehlgeschlagene Lesezeichen neu generieren\",\n      \"regenerate_ai_tags_for_all_bookmarks\": \"KI-Tags für alle Lesezeichen neu generieren\",\n      \"reindex_all_bookmarks\": \"Alle Lesezeichen neu indizieren\",\n      \"compact_assets\": \"Assets komprimieren\",\n      \"reprocess_assets_fix_mode\": \"Assets neu verarbeiten (Fix-Modus)\",\n      \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"KI-Zusammenfassungen nur für fehlgeschlagene Lesezeichen neu generieren\",\n      \"regenerate_ai_summaries_for_all_bookmarks\": \"KI-Zusammenfassungen für alle Lesezeichen neu generieren\"\n    },\n    \"users_list\": {\n      \"users_list\": \"Benutzerliste\",\n      \"create_user\": \"Benutzer erstellen\",\n      \"change_role\": \"Rolle ändern\",\n      \"reset_password\": \"Passwort zurücksetzen\",\n      \"delete_user\": \"Benutzer löschen\",\n      \"num_bookmarks\": \"Anzahl der Lesezeichen\",\n      \"asset_sizes\": \"Asset-Größen\",\n      \"local_user\": \"Lokaler Benutzer\",\n      \"confirm_password\": \"Passwort bestätigen\",\n      \"delete_user_confirm_description\": \"Bist du sicher, dass du den Benutzer \\\"{{name}}\\\" löschen möchtest?\",\n      \"unlimited\": \"Unbegrenzt\"\n    },\n    \"service_connections\": {\n      \"title\": \"Dienstverbindungen\",\n      \"description\": \"Überwache den Zustand und die Konnektivität von externen Systemabhängigkeiten\",\n      \"search_engine\": \"Suchmaschine\",\n      \"browser\": \"Browser\",\n      \"queue_system\": \"Warteschlangensystem\",\n      \"status\": {\n        \"not_configured\": \"Nicht konfiguriert\",\n        \"connected\": \"Verbunden\",\n        \"disconnected\": \"Getrennt\"\n      }\n    },\n    \"admin_tools\": {\n      \"admin_tools\": \"Admin Tools\",\n      \"bookmark_debugger\": \"Bookmark Debugger\",\n      \"bookmark_id\": \"Bookmark-ID\",\n      \"bookmark_id_placeholder\": \"Bookmark-ID eingeben\",\n      \"lookup\": \"Nachschlagen\",\n      \"debug_info\": \"Debug-Informationen\",\n      \"basic_info\": \"Basisinformationen\",\n      \"status\": \"Status\",\n      \"content\": \"Inhalt\",\n      \"html_preview\": \"HTML-Vorschau (Erste 1000 Zeichen)\",\n      \"summary\": \"Zusammenfassung\",\n      \"url\": \"URL\",\n      \"source_url\": \"Quell-URL\",\n      \"asset_type\": \"Asset-Typ\",\n      \"file_name\": \"Dateiname\",\n      \"owner_user_id\": \"Benutzer-ID des Eigentümers\",\n      \"tagging_status\": \"Tagging-Status\",\n      \"summarization_status\": \"Zusammenfassungsstatus\",\n      \"crawl_status\": \"Crawler-Status\",\n      \"crawl_status_code\": \"HTTP-Statuscode\",\n      \"crawled_at\": \"Gecrawlt am\",\n      \"recrawl\": \"Erneutes Crawlen\",\n      \"reindex\": \"Neu indizieren\",\n      \"retag\": \"Neu verschlagworten\",\n      \"resummarize\": \"Neu zusammenfassen\",\n      \"bookmark_not_found\": \"Lesezeichen nicht gefunden\",\n      \"action_success\": \"Aktion erfolgreich abgeschlossen\",\n      \"action_failed\": \"Aktion fehlgeschlagen\",\n      \"recrawl_queued\": \"Job für erneutes Crawlen wurde in die Warteschlange gestellt\",\n      \"reindex_queued\": \"Job zum Neuindizieren wurde in die Warteschlange gestellt\",\n      \"retag_queued\": \"Job zum Neuverschlagworten wurde in die Warteschlange gestellt\",\n      \"resummarize_queued\": \"Job zum Neuzusammenfassen wurde in die Warteschlange gestellt\",\n      \"view\": \"Anzeigen\",\n      \"fetch_error\": \"Fehler beim Abrufen des Lesezeichens\"\n    }\n  },\n  \"options\": {\n    \"dark_mode\": \"Dunkelmodus\",\n    \"light_mode\": \"Hellmodus\",\n    \"apps_extensions\": \"Apps & Erweiterungen\",\n    \"documentation\": \"Dokumentation\",\n    \"follow_us_on_x\": \"Folge uns auf X\"\n  },\n  \"lists\": {\n    \"all_lists\": \"Alle Listen\",\n    \"favourites\": \"Favoriten\",\n    \"new_list\": \"Neue Liste\",\n    \"new_nested_list\": \"Neue verschachtelte Liste\",\n    \"edit_list\": \"Liste bearbeiten\",\n    \"parent_list\": \"Übergeordnete Liste\",\n    \"manual_list\": \"Manuelle Liste\",\n    \"smart_list\": \"Smart Liste\",\n    \"search_query\": \"Suchanfrage\",\n    \"list_type\": \"List Type\",\n    \"no_parent\": \"ohne übergeordnete Liste\",\n    \"search_query_help\": \"Lerne mehr über die Suchanfragen.\",\n    \"merge_list\": \"Liste zusammenführen\",\n    \"destination_list\": \"Zielliste\",\n    \"delete_after_merge\": \"Original-Liste nach dem Zusammenführen löschen\",\n    \"no_destination\": \"Kein Ziel\",\n    \"description\": \"Beschreibung (Optional)\",\n    \"share_list\": \"Liste teilen\",\n    \"rss\": {\n      \"title\": \"RSS-Feed\",\n      \"description\": \"Einen RSS-Feed für diese Liste aktivieren\",\n      \"feed_url\": \"RSS-Feed-URL\"\n    },\n    \"public_list\": {\n      \"title\": \"Öffentliche Liste\",\n      \"description\": \"Anderen erlauben, diese Liste anzusehen\",\n      \"share_link\": \"Link teilen\"\n    },\n    \"delete_list\": {\n      \"title\": \"Liste löschen\",\n      \"description\": \"Wenn du eine Liste löschst, werden keine Lesezeichen in dieser Liste gelöscht.\",\n      \"delete_children\": \"Unterlisten löschen (rekursiv)\",\n      \"delete_children_description\": \"Wenn diese Option nicht aktiviert ist, werden alle direkten Unterlisten zu Stamm-Listen\"\n    },\n    \"shared\": \"Gemeinsam genutzt\",\n    \"collaborators\": {\n      \"manage\": \"Mitarbeiter verwalten\",\n      \"view\": \"Mitarbeiter anzeigen\",\n      \"collaborators\": \"Mitarbeiter\",\n      \"add\": \"Mitarbeiter hinzufügen\",\n      \"current\": \"Aktuelle Mitarbeiter\",\n      \"enter_email\": \"E-Mail-Adresse eingeben\",\n      \"please_enter_email\": \"Bitte gib eine E-Mail-Adresse ein\",\n      \"added_successfully\": \"Mitarbeiter erfolgreich hinzugefügt\",\n      \"failed_to_add\": \"Mitarbeiter konnte nicht hinzugefügt werden\",\n      \"removed\": \"Mitarbeiter entfernt\",\n      \"failed_to_remove\": \"Mitarbeiter konnte nicht entfernt werden\",\n      \"role_updated\": \"Rolle aktualisiert\",\n      \"failed_to_update_role\": \"Rolle konnte nicht aktualisiert werden\",\n      \"viewer\": \"Betrachter\",\n      \"editor\": \"Editor\",\n      \"owner\": \"Inhaber\",\n      \"viewer_description\": \"Kann Lesezeichen in der Liste anzeigen\",\n      \"editor_description\": \"Kann Lesezeichen hinzufügen und entfernen\",\n      \"no_collaborators\": \"Noch keine Mitarbeiter. Füge jemanden hinzu, um mit der Zusammenarbeit zu beginnen!\",\n      \"no_collaborators_readonly\": \"Keine Mitarbeiter für diese Liste.\",\n      \"people_with_access\": \"Personen, die Zugriff auf diese Liste haben\",\n      \"add_or_remove\": \"Personen hinzufügen oder entfernen, die auf diese Liste zugreifen können\",\n      \"invitation_sent\": \"Einladung erfolgreich gesendet\",\n      \"invitation_revoked\": \"Einladung zurückgezogen\",\n      \"failed_to_revoke\": \"Einladung konnte nicht zurückgezogen werden\",\n      \"pending\": \"Ausstehend\",\n      \"revoke\": \"Zurückziehen\",\n      \"declined\": \"Abgelehnt\"\n    },\n    \"leave_list\": {\n      \"title\": \"Liste verlassen\",\n      \"confirm_message\": \"Bist du sicher, dass du {{icon}} {{name}} verlassen möchtest?\",\n      \"warning\": \"Du kannst keine Lesezeichen in dieser Liste mehr sehen oder darauf zugreifen. Der Listeninhaber kann dich bei Bedarf wieder hinzufügen.\",\n      \"action\": \"Liste verlassen\",\n      \"success\": \"Du hast \\\"{{icon}} {{name}}\\\" verlassen\"\n    },\n    \"invitations\": {\n      \"pending\": \"Ausstehende Einladungen\",\n      \"description\": \"Listen-Kollaborationseinladungen überprüfen und beantworten\",\n      \"invited_by\": \"Eingeladen von\",\n      \"accept\": \"Annehmen\",\n      \"decline\": \"Ablehnen\",\n      \"accepted\": \"Einladung angenommen\",\n      \"declined\": \"Einladung abgelehnt\",\n      \"failed_to_accept\": \"Einladung konnte nicht angenommen werden\",\n      \"failed_to_decline\": \"Einladung konnte nicht abgelehnt werden\"\n    },\n    \"shared_lists\": \"Geteilte Listen\"\n  },\n  \"tags\": {\n    \"all_tags\": \"Alle Tags\",\n    \"your_tags\": \"Ihre Tags\",\n    \"your_tags_info\": \"Tags, die Sie mindestens einmal angehängt haben\",\n    \"ai_tags\": \"KI-Tags\",\n    \"ai_tags_info\": \"Tags, die nur automatisch (von der KI) angehängt wurden\",\n    \"unused_tags\": \"Ungenutzte Tags\",\n    \"unused_tags_info\": \"Tags, die keinem Lesezeichen angehängt sind\",\n    \"delete_all_unused_tags\": \"Alle ungenutzen Tags löschen\",\n    \"drag_and_drop_merging\": \"Ziehen & Ablegen zum Zusammenführen\",\n    \"drag_and_drop_merging_info\": \"Ziehen und ablegen, um Tags zusammenzuführen\",\n    \"sort_by_name\": \"Nach Name sortieren\",\n    \"create_tag\": \"Tag erstellen\",\n    \"create_tag_description\": \"Erstelle einen neuen Tag, ohne ihn an ein Lesezeichen zu binden\",\n    \"tag_name\": \"Tag-Name\",\n    \"enter_tag_name\": \"Tag-Namen eingeben\",\n    \"sort_by_usage\": \"Sortieren nach Nutzung\",\n    \"sort_by_relevance\": \"Sortieren nach Relevanz\",\n    \"no_custom_tags\": \"Noch keine benutzerdefinierten Tags\",\n    \"no_ai_tags\": \"Noch keine KI-Tags\",\n    \"no_unused_tags\": \"Du hast keine unbenutzten Tags\",\n    \"no_unused_tags_match_your_search\": \"Keine unbenutzten Tags entsprechen deiner Suche\",\n    \"no_tags_match_your_search\": \"Keine Tags entsprechen deiner Suche\",\n    \"search_placeholder\": \"Tags suchen...\",\n    \"search_or_create_placeholder\": \"Tags suchen oder erstellen...\"\n  },\n  \"preview\": {\n    \"view_original\": \"Original anzeigen\",\n    \"cached_content\": \"Gecachte Inhalte\",\n    \"reader_view\": \"Leseansicht\",\n    \"tabs\": {\n      \"content\": \"Inhalt\",\n      \"details\": \"Details\"\n    },\n    \"archive_info\": \"Archive werden möglicherweise nicht korrekt inline dargestellt, wenn sie Javascript benötigen. Die besten Ergebnisse erzielst du, wenn du sie <1>herunterlädst und in deinem Browser öffnest</1>.\",\n    \"fetch_error_title\": \"Inhalte nicht verfügbar\",\n    \"fetch_error_description\": \"Wir konnten die Inhalte für diesen Link nicht abrufen. Die Seite ist möglicherweise geschützt, erfordert eine Authentifizierung oder ist vorübergehend nicht verfügbar.\",\n    \"crawling_in_progress\": \"Seiteninhalt wird abgerufen …\",\n    \"continue_reading\": \"Da weitermachen, wo du aufgehört hast\",\n    \"continue_reading_percent\": \"Da weitermachen, wo du aufgehört hast ({{percent}}%)\",\n    \"continue_button\": \"Weiter\"\n  },\n  \"editor\": {\n    \"quickly_focus\": \"Sie können schnell auf dieses Feld fokussieren, indem Sie ⌘ + E drücken\",\n    \"multiple_urls_dialog_title\": \"URLs als separate Lesezeichen importieren?\",\n    \"multiple_urls_dialog_desc\": \"Die Eingabe enthält mehrere URLs in separaten Zeilen. Möchten Sie sie als separate Lesezeichen importieren?\",\n    \"import_as_text\": \"Als Text-Lesezeichen importieren\",\n    \"import_as_separate_bookmarks\": \"Als separate Lesezeichen importieren\",\n    \"placeholder\": \"Fügen Sie einen Link oder ein Bild ein, schreiben Sie eine Notiz oder ziehen Sie ein Bild hierher …\",\n    \"new_item\": \"NEUER EINTRAG\",\n    \"disabled_submissions\": \"Einsendungen sind deaktiviert\",\n    \"text_toolbar\": {\n      \"align_right\": \"Rechtsbündig\",\n      \"italic\": \"Kursiv\",\n      \"underline\": \"Unterstrichen\",\n      \"strikethrough\": \"Durchgestrichen\",\n      \"code\": \"Quellcode\",\n      \"highlight\": \"Hervorheben\",\n      \"align_left\": \"Linksbündig\",\n      \"align_center\": \"Zentriert\",\n      \"markdown_shortcuts\": {\n        \"label\": \"Markdown-Verknüpfungen\",\n        \"inline_code\": {\n          \"example\": \"`Code`\",\n          \"label\": \"Inline-Code\"\n        },\n        \"block_code\": {\n          \"label\": \"Blockcode\",\n          \"example\": \"``` + Leertaste\"\n        },\n        \"heading\": {\n          \"label\": \"Überschrift\",\n          \"example\": \"# H1, ## H2, ### H3\"\n        },\n        \"ordered_list\": {\n          \"label\": \"Sortierte Liste\",\n          \"example\": \"1. Listenelement\"\n        },\n        \"blockquote\": {\n          \"example\": \"> Blockzitat\",\n          \"label\": \"Blockzitat\"\n        },\n        \"bold\": {\n          \"label\": \"Fett\",\n          \"example\": \"**text** oder CTRL+b\"\n        },\n        \"italic\": {\n          \"label\": \"Kursiv\",\n          \"example\": \"*Italic* oder _Italic_ or CTRL+i\"\n        },\n        \"unordered_list\": {\n          \"example\": \"- Listenelement\",\n          \"label\": \"Unsortierte Liste\"\n        }\n      },\n      \"undo\": \"Rückgängig\",\n      \"redo\": \"Wiederholen\",\n      \"bold\": \"Fett\"\n    },\n    \"placeholder_v2\": \"Füge einen Link ein, schreibe eine Notiz oder ziehe ein Bild hinein…\"\n  },\n  \"toasts\": {\n    \"bookmarks\": {\n      \"updated\": \"Das Lesezeichen wurde aktualisiert!\",\n      \"deleted\": \"Das Lesezeichen wurde gelöscht!\",\n      \"refetch\": \"Neuabruf wurde in die Warteschlange gestellt!\",\n      \"full_page_archive\": \"Erstellung des vollständigen Seitenarchivs wurde ausgelöst\",\n      \"delete_from_list\": \"Das Lesezeichen wurde aus der Liste gelöscht\",\n      \"clipboard_copied\": \"Link wurde in Ihre Zwischenablage kopiert!\",\n      \"preserve_pdf\": \"Die PDF-Speicherung wurde ausgelöst\",\n      \"update_banner\": \"Banner wurde aktualisiert!\",\n      \"uploading_banner\": \"Banner wird hochgeladen ...\"\n    },\n    \"lists\": {\n      \"created\": \"Liste wurde erstellt!\",\n      \"updated\": \"Liste wurde aktualisiert!\",\n      \"deleted\": \"Liste wurde gelöscht!\",\n      \"merged\": \"Liste wurde zusammengeführt!\"\n    },\n    \"tags\": {\n      \"created\": \"Tag wurde erstellt!\",\n      \"failed_to_create\": \"Tag konnte nicht erstellt werden\"\n    }\n  },\n  \"cleanups\": {\n    \"cleanups\": \"Bereinigungen\",\n    \"duplicate_tags\": {\n      \"title\": \"Doppelte Tags\",\n      \"merge_all_suggestions\": \"Alle Vorschläge zusammenführen?\"\n    }\n  },\n  \"highlights\": {\n    \"no_highlights\": \"Du hast noch keine Highlights.\"\n  },\n  \"dialogs\": {\n    \"bookmarks\": {\n      \"delete_confirmation_description\": \"Bist du sicher, dass du das Lesezeichen löschen willst?\",\n      \"delete_confirmation_title\": \"Lesezeichen löschen?\"\n    }\n  },\n  \"search\": {\n    \"is_favorited\": \"ist favorisiert\",\n    \"is_not_favorited\": \"ist nicht favorisiert\",\n    \"is_archived\": \"Ist archiviert\",\n    \"has_no_tags\": \"hat kein Tag\",\n    \"created_on_or_after\": \"wurde am oder danach erstellt\",\n    \"is_in_list\": \"ist in der Liste\",\n    \"is_in_any_list\": \"ist in irgendeiner Liste\",\n    \"is_not_in_any_list\": \"ist in keiner Liste\",\n    \"does_not_have_tag\": \"ist nicht getagged mit\",\n    \"full_text_search\": \"Volltextsuche\",\n    \"and\": \"und\",\n    \"or\": \"oder\",\n    \"is_not_archived\": \"Ist nicht archiviert\",\n    \"not_created_on_or_after\": \"wurde nicht am oder danach erstellt\",\n    \"url_contains\": \"URL beinhaltet\",\n    \"url_does_not_contain\": \"URL beinhaltet nicht\",\n    \"is_not_in_list\": \"ist nicht in der Liste\",\n    \"has_tag\": \"ist getagged mit\",\n    \"type_is\": \"vom Typ\",\n    \"type_is_not\": \"nicht vom Typ\",\n    \"has_any_tag\": \"hat irgendein Tag\",\n    \"created_on_or_before\": \"Erstellt am oder vor\",\n    \"not_created_on_or_before\": \"erstellt vor\",\n    \"is_from_feed\": \"Stammt aus RSS-Feed\",\n    \"is_not_from_feed\": \"Stammt nicht aus RSS-Feed\",\n    \"created_within\": \"Erstellt innerhalb\",\n    \"created_earlier_than\": \"Erstellt vor\",\n    \"day_s\": \" Tag(e)\",\n    \"week_s\": \" Woche(n)\",\n    \"month_s\": \" Monat(e)\",\n    \"year_s\": \" Jahr(e)\",\n    \"day_s_ago\": \" Vor Tag(en)\",\n    \"week_s_ago\": \" Vor Woche(n)\",\n    \"month_s_ago\": \" Vor Monat(en)\",\n    \"year_s_ago\": \" Vor Jahr(en)\",\n    \"history\": \"Letzte Suchanfragen\",\n    \"title_contains\": \"Titel enthält\",\n    \"title_does_not_contain\": \"Titel enthält nicht\",\n    \"is_broken_link\": \"Hat defekten Link\",\n    \"tags\": \"Schlagwörter\",\n    \"no_suggestions\": \"Keine Vorschläge\",\n    \"filters\": \"Filter\",\n    \"is_not_broken_link\": \"Hat funktionierenden Link\",\n    \"lists\": \"Listen\",\n    \"feeds\": \"Feeds\",\n    \"is_from_source\": \"Quelle ist\",\n    \"is_not_from_source\": \"Quelle ist nicht\"\n  },\n  \"bookmark_editor\": {\n    \"subtitle\": \"Ändere die Details des Lesezeichens. Klicke auf Speichern, wenn du fertig bist.\",\n    \"author\": \"Autor\",\n    \"publisher\": \"Herausgeber\",\n    \"date_published\": \"Veröffentlichungsdatum\",\n    \"pick_a_date\": \"Wähle ein Datum\",\n    \"save_changes\": \"Änderungen speichern\",\n    \"title\": \"Lesezeichen bearbeiten\",\n    \"extracted_content\": \"Extrahierter Inhalt\"\n  },\n  \"banners\": {\n    \"no_bookmarks\": {\n      \"title\": \"Noch keine Lesezeichen\",\n      \"description\": \"Speichere interessante Artikel, Links und Seiten, um später schnell darauf zugreifen zu können.\"\n    }\n  },\n  \"view_options\": {\n    \"title\": \"Ansichtsoptionen\",\n    \"layout\": \"Layout\",\n    \"columns\": \"Spalten\",\n    \"display_options\": \"Anzeigeoptionen\",\n    \"show_note_previews\": \"Notizen anzeigen\",\n    \"show_tags\": \"Tags anzeigen\",\n    \"show_title\": \"Titel anzeigen\",\n    \"image_options\": \"Bildoptionen\",\n    \"image_fit_cover\": \"Deckung (Füllen)\",\n    \"image_fit_contain\": \"Enthalten (Anpassen)\"\n  },\n  \"version\": {\n    \"new_release_available\": \"Neue Versionshinweise verfügbar\",\n    \"whats_new_title\": \"Was ist neu in v{{version}}\",\n    \"release_notes_description\": \"Hier sind die neuesten Updates, die aus den GitHub-Versionshinweisen stammen.\",\n    \"loading_release_notes\": \"Lade Versionshinweise…\",\n    \"unable_to_load_release_notes\": \"Versionshinweise konnten zurzeit nicht geladen werden. Bitte versuche es später noch einmal.\",\n    \"no_release_notes\": \"Für diese Version wurden keine Versionshinweise veröffentlicht.\",\n    \"release_notes_synced\": \"Versionshinweise werden von GitHub synchronisiert.\",\n    \"view_on_github\": \"Auf GitHub ansehen\"\n  },\n  \"wrapped\": {\n    \"title\": \"Dein {{year}} Jahresrückblick\",\n    \"subtitle\": \"Ein Jahr in Karakeep\",\n    \"banner\": {\n      \"title\": \"Dein Jahresrückblick 2025 ist fertig!\",\n      \"description\": \"Dein Jahr in Lesezeichen ansehen\",\n      \"view_now\": \"Jetzt ansehen\"\n    },\n    \"button\": \"Jahresrückblick 2025\",\n    \"loading\": \"Dein Jahresrückblick wird geladen ...\",\n    \"failed_to_load\": \"Das Laden deiner Jahresrückblick-Statistiken ist fehlgeschlagen\",\n    \"sections\": {\n      \"total_saves\": {\n        \"prefix\": \"Du hast gespeichert\",\n        \"suffix\": \"Dinge dieses Jahr\",\n        \"suffix_singular\": \"Ding dieses Jahr\"\n      },\n      \"first_bookmark\": {\n        \"title\": \"Deine Reise hat begonnen\",\n        \"description\": \"Erste Speicherung von {{year}}:\"\n      },\n      \"top_domains\": \"Deine Top-Websites\",\n      \"top_tags\": \"Deine Top-Tags\",\n      \"monthly_activity\": \"Dein Sparjahr\",\n      \"most_active_day\": \"Dein aktivster Tag\",\n      \"peak_times\": {\n        \"title\": \"Wann du speicherst\",\n        \"peak_hour\": \"Stoßzeit\",\n        \"peak_day\": \"Stärkster Tag\"\n      },\n      \"how_you_save\": \"Wie du speicherst\",\n      \"what_you_saved\": \"Was du gespeichert hast\",\n      \"summary\": {\n        \"favorites\": \"Favoriten\",\n        \"tags_created\": \"Erstellte Tags\",\n        \"highlights\": \"Highlights\"\n      },\n      \"types\": {\n        \"links\": \"Links\",\n        \"notes\": \"Notizen\",\n        \"assets\": \"Assets\"\n      }\n    },\n    \"footer\": \"Gemacht mit Karakeep\",\n    \"share\": \"Teilen\",\n    \"download\": \"Herunterladen\",\n    \"close\": \"Schließen\",\n    \"generating\": \"Wird erstellt …\"\n  }\n}\n"
  },
  {
    "path": "apps/web/lib/i18n/locales/el/translation.json",
    "content": "{\n  \"common\": {\n    \"url\": \"URL\",\n    \"name\": \"Όνομα\",\n    \"email\": \"Email\",\n    \"password\": \"Κωδικός Πρόσβασης\",\n    \"action\": \"Ενέργεια\",\n    \"actions\": \"Ενέργειες\",\n    \"created_at\": \"Δημιουργήθηκε στις\",\n    \"updated_at\": \"Ανανεώθηκε στις\",\n    \"key\": \"Κλειδί\",\n    \"role\": \"Ρόλος\",\n    \"type\": \"Τύπος\",\n    \"size\": \"Μέγεθος\",\n    \"roles\": {\n      \"user\": \"Χρήστης\",\n      \"admin\": \"Διαχειριστής\"\n    },\n    \"something_went_wrong\": \"Κάτι πήγε στραβά\",\n    \"experimental\": \"Πειραματικό\",\n    \"search\": \"Αναζήτηση\",\n    \"tags\": \"Ετικέτες\",\n    \"note\": \"Σημείωση\",\n    \"attachments\": \"Συνημμένα\",\n    \"highlights\": \"Επιλογές\",\n    \"source\": \"Πηγή\",\n    \"screenshot\": \"Στιγμιότυπο οθόνης\",\n    \"video\": \"Βίντεο\",\n    \"archive\": \"Αρχείο\",\n    \"home\": \"Αρχική\",\n    \"title\": \"Τίτλος\",\n    \"description\": \"Περιγραφή\",\n    \"summary\": \"Περίληψη\",\n    \"bookmark_types\": {\n      \"title\": \"Τύπος Σελιδοδείκτη\",\n      \"link\": \"Σύνδεσμος\",\n      \"text\": \"Κείμενο\",\n      \"media\": \"Πολυμέσα\"\n    },\n    \"quota\": \"Ποσόστωση\",\n    \"bookmarks\": \"Σελιδοδείκτες\",\n    \"storage\": \"Αποθήκευση\",\n    \"pdf\": \"Αρχειοθετημένο PDF\",\n    \"default\": \"Προεπιλογή\",\n    \"id\": \"ID\",\n    \"last_used\": \"Τελευταία Χρήση\"\n  },\n  \"layouts\": {\n    \"masonry\": \"Πλινθοδομή\",\n    \"grid\": \"Πλέγμα\",\n    \"list\": \"Λίστα\",\n    \"compact\": \"Συμπαγής\"\n  },\n  \"actions\": {\n    \"change_layout\": \"Αλλαγή Διάταξης\",\n    \"archive\": \"Αρχειοθέτηση\",\n    \"unarchive\": \"Αναίρεση Αρχειοθέτησης\",\n    \"favorite\": \"Αγαπημένο\",\n    \"unfavorite\": \"Αφαίρεση από Αγαπημένα\",\n    \"delete\": \"Διαγραφή\",\n    \"toggle_show_archived\": \"Εμφάνιση Αρχειοθετημένων\",\n    \"refresh\": \"Ανανέωση\",\n    \"recrawl\": \"Επαναφόρτωση\",\n    \"download_full_page_archive\": \"Λήψη Πλήρους Αρχείου Σελίδας\",\n    \"edit_tags\": \"Επεξεργασία Ετικετών\",\n    \"add_to_list\": \"Προσθήκη στη Λίστα\",\n    \"select_all\": \"Επιλογή Όλων\",\n    \"unselect_all\": \"Αποεπιλογή Όλων\",\n    \"copy_link\": \"Αντιγραφή Συνδέσμου\",\n    \"close_bulk_edit\": \"Κλείσιμο Μαζικής Επεξεργασίας\",\n    \"bulk_edit\": \"Μαζική Επεξεργασία\",\n    \"manage_lists\": \"Διαχείριση Λιστών\",\n    \"remove_from_list\": \"Αφαίρεση από Λίστα\",\n    \"save\": \"Αποθήκευση\",\n    \"add\": \"Προσθήκη\",\n    \"edit\": \"Επεξεργασία\",\n    \"open_editor\": \"Άνοιγμα Επεξεργαστή\",\n    \"create\": \"Δημιουργία\",\n    \"fetch_now\": \"Φόρτωση Τώρα\",\n    \"summarize_with_ai\": \"Περίληψη με AI\",\n    \"edit_title\": \"Επεξεργασία Τίτλου\",\n    \"sign_out\": \"Αποσύνδεση\",\n    \"close\": \"Κλείσιμο\",\n    \"merge\": \"Συγχώνευση\",\n    \"cancel\": \"Ακύρωση\",\n    \"apply_all\": \"Εφαρμογή σε Όλα\",\n    \"ignore\": \"Αγνόηση\",\n    \"sort\": {\n      \"title\": \"Ταξινόμηση\",\n      \"relevant_first\": \"Πιο Σχετικά Πρώτα\",\n      \"newest_first\": \"Νεότερα Πρώτα\",\n      \"oldest_first\": \"Παλαιότερα Πρώτα\"\n    },\n    \"confirm\": \"Επιβεβαίωση\",\n    \"regenerate\": \"Ανανέωση\",\n    \"load_more\": \"Φόρτωσε περισσότερα\",\n    \"edit_notes\": \"Επεξεργασία σημειώσεων\",\n    \"preserve_as_pdf\": \"Διατήρηση ως PDF\",\n    \"offline_copies\": \"Αντίγραφα εκτός σύνδεσης\",\n    \"preserve_offline_archive\": \"Διατήρηση αρχείου εκτός σύνδεσης\",\n    \"download_full_page_archive_file\": \"Λήψη αρχείου\",\n    \"download_pdf_file\": \"Λήψη αρχείου PDF\",\n    \"remove\": \"Κατάργηση\",\n    \"more\": \"Περισσότερα\",\n    \"replace_banner\": \"Αντικατάσταση Banner\",\n    \"add_banner\": \"Προσθήκη Banner\",\n    \"download\": \"Λήψη\"\n  },\n  \"highlights\": {\n    \"no_highlights\": \"Δεν έχετε ακόμα επιλογές.\"\n  },\n  \"settings\": {\n    \"back_to_app\": \"Επιστροφή στην Εφαρμογή\",\n    \"user_settings\": \"Ρυθμίσεις Χρήστη\",\n    \"info\": {\n      \"user_info\": \"Πληροφορίες Χρήστη\",\n      \"basic_details\": \"Βασικές Λεπτομέρειες\",\n      \"change_password\": \"Αλλαγή Κωδικού Πρόσβασης\",\n      \"current_password\": \"Τρέχων Κωδικός Πρόσβασης\",\n      \"new_password\": \"Νέος Κωδικός Πρόσβασης\",\n      \"confirm_new_password\": \"Επιβεβαίωση Νέου Κωδικού Πρόσβασης\",\n      \"options\": \"Επιλογές\",\n      \"interface_lang\": \"Γλώσσα Διεπαφής\",\n      \"user_settings\": {\n        \"user_settings_updated\": \"Οι ρυθμίσεις χρήστη ενημερώθηκαν!\",\n        \"bookmark_click_action\": {\n          \"title\": \"Ενέργεια Κλικ Σελιδοδείκτη\",\n          \"open_external_url\": \"Άνοιγμα Αρχικού URL\",\n          \"open_bookmark_details\": \"Άνοιγμα Λεπτομερειών Σελιδοδείκτη\"\n        },\n        \"archive_display_behaviour\": {\n          \"title\": \"Αρχειοθετημένοι Σελιδοδείκτες\",\n          \"show\": \"Εμφάνιση αρχειοθετημένων σελιδοδεικτών σε ετικέτες και λίστες\",\n          \"hide\": \"Απόκρυψη αρχειοθετημένων σελιδοδεικτών από ετικέτες και λίστες\"\n        }\n      },\n      \"reader_settings\": {\n        \"local_overrides_title\": \"Ενεργές ρυθμίσεις για συγκεκριμένη συσκευή\",\n        \"using_default\": \"Χρήση της προεπιλογής του πελάτη\",\n        \"clear_override_hint\": \"Εκκαθαρίστε την παράκαμψη συσκευής για να χρησιμοποιήσετε την καθολική ρύθμιση ({{value}})\",\n        \"font_size\": \"Μέγεθος γραμματοσειράς\",\n        \"font_family\": \"Οικογένεια γραμματοσειράς\",\n        \"preview_inline\": \"(προεπισκόπηση)\",\n        \"tooltip_preview\": \"Μη αποθηκευμένες αλλαγές προεπισκόπησης\",\n        \"save_to_all_devices\": \"Όλες οι συσκευές\",\n        \"tooltip_local\": \"Οι ρυθμίσεις της συσκευής διαφέρουν από τις καθολικές ρυθμίσεις\",\n        \"reset_preview\": \"Επαναφορά προεπισκόπησης\",\n        \"mono\": \"Monospace\",\n        \"line_height\": \"Ύψος γραμμής\",\n        \"tooltip_default\": \"Ρυθμίσεις ανάγνωσης\",\n        \"title\": \"Ρυθμίσεις ανάγνωσης\",\n        \"serif\": \"Serif\",\n        \"preview\": \"Προεπισκόπηση\",\n        \"not_set\": \"Δεν έχει οριστεί\",\n        \"clear_local_overrides\": \"Εκκαθάριση ρυθμίσεων συσκευής\",\n        \"preview_text\": \"Η γρήγορη καφέ αλεπού πηδάει πάνω από τον τεμπέλη σκύλο. Έτσι θα φαίνεται το κείμενό σου στην προβολή ανάγνωσης.\",\n        \"local_overrides_cleared\": \"Οι ρυθμίσεις για συγκεκριμένη συσκευή έχουν εκκαθαριστεί\",\n        \"local_overrides_description\": \"Αυτή η συσκευή έχει ρυθμίσεις ανάγνωσης που διαφέρουν από τις καθολικές προεπιλογές σου:\",\n        \"clear_defaults\": \"Εκκαθάριση όλων των προεπιλογών\",\n        \"description\": \"Ρύθμισε τις προεπιλεγμένες ρυθμίσεις κειμένου για την προβολή ανάγνωσης. Αυτές οι ρυθμίσεις συγχρονίζονται απ' όλες τις συσκευές σου.\",\n        \"defaults_cleared\": \"Οι προεπιλογές ανάγνωσης έχουν εκκαθαριστεί\",\n        \"save_hint\": \"Αποθηκεύστε τις ρυθμίσεις μόνο για αυτή τη συσκευή ή συγχρονίστε σε όλες τις συσκευές\",\n        \"save_as_default\": \"Αποθήκευση ως προεπιλογή\",\n        \"save_to_device\": \"Αυτή η συσκευή\",\n        \"sans\": \"Sans Serif\",\n        \"tooltip_preview_and_local\": \"Μη αποθηκευμένες αλλαγές προεπισκόπησης; οι ρυθμίσεις της συσκευής διαφέρουν από τις καθολικές\",\n        \"adjust_hint\": \"Προσαρμόστε τις παραπάνω ρυθμίσεις για να κάνετε προεπισκόπηση των αλλαγών\"\n      },\n      \"avatar\": {\n        \"upload\": \"Ανέβασε avatar\",\n        \"change\": \"Άλλαξε avatar\",\n        \"remove_confirm_title\": \"Να αφαιρεθεί το avatar;\",\n        \"updated\": \"Το avatar ανανεώθηκε\",\n        \"removed\": \"Το avatar αφαιρέθηκε\",\n        \"description\": \"Ανέβασε μια τετράγωνη εικόνα για να τη χρησιμοποιήσεις ως avatar.\",\n        \"remove_confirm_description\": \"Αυτό θα διαγράψει την τρέχουσα φωτογραφία προφίλ σου.\",\n        \"title\": \"Φωτογραφία Προφίλ\",\n        \"remove\": \"Αφαίρεσε avatar\"\n      }\n    },\n    \"ai\": {\n      \"ai_settings\": \"Ρυθμίσεις AI\",\n      \"tagging_rules\": \"Κανόνες Ετικετοποίησης\",\n      \"tagging_rule_description\": \"Οι οδηγίες που θα προσθέσετε εδώ θα συμπεριληφθούν ως κανόνες στο μοντέλο κατά τη δημιουργία ετικετών. Μπορείτε να δείτε τις τελικές οδηγίες στην ενότητα προεπισκόπησης οδηγιών.\",\n      \"prompt_preview\": \"Προεπισκόπηση Οδηγιών\",\n      \"text_prompt\": \"Οδηγία Κειμένου\",\n      \"images_prompt\": \"Οδηγία Εικόνων\",\n      \"summarization_prompt\": \"Οδηγία Περίληψης\",\n      \"all_tagging\": \"Όλη η Ετικετοποίηση\",\n      \"text_tagging\": \"Ετικετοποίηση Κειμένου\",\n      \"image_tagging\": \"Ετικετοποίηση Εικόνων\",\n      \"summarization\": \"Περίληψη\",\n      \"tag_style\": \"Στυλ ετικέτας\",\n      \"auto_summarization_description\": \"Δημιουργήστε αυτόματα περιλήψεις για τους σελιδοδείκτες σας χρησιμοποιώντας AI.\",\n      \"auto_tagging\": \"Αυτόματη προσθήκη ετικετών\",\n      \"titlecase_spaces\": \"Κεφαλαία ανά λέξη με κενά\",\n      \"lowercase_underscores\": \"Μικρά με κάτω παύλες\",\n      \"inference_language\": \"Γλώσσα εξαγωγής συμπερασμάτων\",\n      \"titlecase_hyphens\": \"Κεφαλαία ανά λέξη με παύλες\",\n      \"lowercase_hyphens\": \"Μικρά με παύλες\",\n      \"lowercase_spaces\": \"Μικρά με κενά\",\n      \"inference_language_description\": \"Διάλεξε γλώσσα για τις ετικέτες και τις περιλήψεις που δημιουργούνται από την AI.\",\n      \"tag_style_description\": \"Διάλεξε πώς να μορφοποιηθούν οι αυτόματα δημιουργημένες ετικέτες σου.\",\n      \"auto_tagging_description\": \"Δημιουργήστε αυτόματα ετικέτες για τους σελιδοδείκτες σας χρησιμοποιώντας AI.\",\n      \"camelCase\": \"camelCase\",\n      \"auto_summarization\": \"Αυτόματη δημιουργία περιλήψεων\",\n      \"no_preference\": \"Καμία προτίμηση\",\n      \"curated_tags\": \"Επιλεγμένες ετικέτες\",\n      \"curated_tags_description\": \"Προαιρετικά, περιορίστε την προσθήκη ετικετών AI ώστε να χρησιμοποιεί μόνο ετικέτες από αυτήν τη λίστα. Όταν δεν επιλέγονται ετικέτες, η AI δημιουργεί ελεύθερα ετικέτες.\",\n      \"curated_tags_updated\": \"Οι επιλεγμένες ετικέτες ενημερώθηκαν με επιτυχία!\",\n      \"curated_tags_update_failed\": \"Αποτυχία ενημέρωσης επιλεγμένων ετικετών\"\n    },\n    \"feeds\": {\n      \"rss_subscriptions\": \"Συνδρομές RSS\",\n      \"add_a_subscription\": \"Προσθήκη Συνδρομής\",\n      \"feed_enabled\": \"RSS Feed ενεργοποιημένο\",\n      \"feed_disabled\": \"RSS Feed απενεργοποιημένο\"\n    },\n    \"webhooks\": {\n      \"webhooks\": \"Webhooks\",\n      \"description\": \"Μπορείτε να χρησιμοποιήσετε webhooks για να ενεργοποιήσετε ενέργειες όταν δημιουργούνται, αλλάζουν ή σαρώνονται σελιδοδείκτες.\",\n      \"events\": {\n        \"title\": \"Γεγονότα\",\n        \"crawled\": \"Σαρώθηκε\",\n        \"created\": \"Δημιουργήθηκε\",\n        \"edited\": \"Επεξεργάστηκε\"\n      },\n      \"auth_token\": \"Διακριτικό Πιστοποίησης\",\n      \"add_auth_token\": \"Προσθήκη Διακριτικού Πιστοποίησης\",\n      \"edit_auth_token\": \"Επεξεργασία Διακριτικού Πιστοποίησης\",\n      \"create_webhook\": \"Δημιουργία Webhook\",\n      \"delete_webhook\": \"Διαγραφή Webhook\",\n      \"delete_webhook_confirmation\": \"Είστε σίγουροι ότι θέλετε να διαγράψετε αυτό το webhook;\",\n      \"edit_webhook\": \"Επεξεργασία Webhook\",\n      \"webhook_url\": \"URL Webhook\"\n    },\n    \"import\": {\n      \"import_export\": \"Εισαγωγή / Εξαγωγή\",\n      \"import_export_bookmarks\": \"Εισαγωγή / Εξαγωγή Σελιδοδεικτών\",\n      \"import_bookmarks_from_html_file\": \"Εισαγωγή Σελιδοδεικτών από αρχείο HTML\",\n      \"import_bookmarks_from_pocket_export\": \"Εισαγωγή Σελιδοδεικτών από εξαγωγή Pocket\",\n      \"import_bookmarks_from_matter_export\": \"Εισαγωγή Σελιδοδεικτών από εξαγωγή Matter\",\n      \"import_bookmarks_from_omnivore_export\": \"Εισαγωγή Σελιδοδεικτών από εξαγωγή Omnivore\",\n      \"import_bookmarks_from_linkwarden_export\": \"Εισαγωγή Σελιδοδεικτών από εξαγωγή Linkwarden\",\n      \"import_bookmarks_from_karakeep_export\": \"Εισαγωγή Σελιδοδεικτών από εξαγωγή Karakeep\",\n      \"import_bookmarks_from_tab_session_manager_export\": \"Εισαγωγή Σελιδοδεικτών από Tab Session Manager\",\n      \"export_links_and_notes\": \"Εξαγωγή Συνδέσμων και Σημειώσεων\",\n      \"imported_bookmarks\": \"Εισαγόμενοι Σελιδοδείκτες\",\n      \"import_bookmarks_from_mymind_export\": \"Εισαγωγή σελιδοδεικτών από την εξαγωγή mymind\",\n      \"import_bookmarks_from_instapaper_export\": \"Εισαγωγή σελιδοδεικτών από την εξαγωγή του Instapaper\"\n    },\n    \"api_keys\": {\n      \"api_keys\": \"Κλειδιά API\",\n      \"new_api_key\": \"Νέο Κλειδί API\",\n      \"new_api_key_desc\": \"Δώστε στο κλειδί API σας ένα μοναδικό όνομα\",\n      \"key_success\": \"Το κλειδί δημιουργήθηκε επιτυχώς\",\n      \"key_success_please_copy\": \"Παρακαλώ αντιγράψτε το κλειδί και αποθηκεύστε το σε ασφαλές μέρος. Μόλις κλείσετε το παράθυρο διαλόγου, δεν θα μπορείτε να το αποκτήσετε ξανά.\",\n      \"regenerate_api_key\": \"Ανανέωση κλειδιού API\",\n      \"key_regenerated\": \"Το κλειδί ανανεώθηκε με επιτυχία\",\n      \"key_regenerated_please_copy\": \"Αντιγράψτε το νέο κλειδί και αποθηκεύστε το κάπου με ασφάλεια. Το παλιό κλειδί έχει ανακληθεί και δεν θα λειτουργεί πλέον.\",\n      \"regenerate_warning\": \"Είστε σίγουροι ότι θέλετε να ανανεώσετε το κλειδί API \\\"{{name}}\\\";\",\n      \"regenerate_confirmation\": \"Αυτό θα ανακαλέσει το τρέχον κλειδί και θα δημιουργήσει ένα νέο. Οποιεσδήποτε εφαρμογές χρησιμοποιούν το τρέχον κλειδί θα σταματήσουν να λειτουργούν.\"\n    },\n    \"broken_links\": {\n      \"broken_links\": \"Χαλασμένοι Σύνδεσμοι\",\n      \"last_crawled_at\": \"Τελευταία Σάρωση στις\",\n      \"crawling_status\": \"Κατάσταση Σάρωσης\",\n      \"crawling_failed\": \"Η Σάρωση Απέτυχε\"\n    },\n    \"manage_assets\": {\n      \"manage_assets\": \"Διαχείριση Στοιχείων\",\n      \"no_assets\": \"Δεν έχετε ακόμα στοιχεία.\",\n      \"asset_type\": \"Τύπος Στοιχείου\",\n      \"bookmark_link\": \"Σύνδεσμος Σελιδοδείκτη\",\n      \"asset_link\": \"Σύνδεσμος Στοιχείου\",\n      \"delete_asset\": \"Διαγραφή Στοιχείου\",\n      \"delete_asset_confirmation\": \"Είστε σίγουροι ότι θέλετε να διαγράψετε αυτό το στοιχείο;\"\n    },\n    \"rules\": {\n      \"rules\": \"Μηχανή Κανόνων\",\n      \"rule_name\": \"Όνομα Κανόνα\",\n      \"description\": \"Μπορείτε να χρησιμοποιήσετε κανόνες για να ενεργοποιήσετε ενέργειες όταν συμβαίνει ένα γεγονός.\",\n      \"ceate_rule\": \"Δημιουργία Κανόνα\",\n      \"edit_rule\": \"Επεξεργασία Κανόνα\",\n      \"save_rule\": \"Αποθήκευση Κανόνα\",\n      \"delete_rule\": \"Διαγραφή Κανόνα\",\n      \"delete_rule_confirmation\": \"Είστε σίγουροι ότι θέλετε να διαγράψετε αυτόν τον κανόνα;\",\n      \"whenever\": \"Όποτε ...\",\n      \"if\": \"Αν ...\",\n      \"enter_rule_name\": \"Εισάγετε όνομα κανόνα\",\n      \"describe_what_this_rule_does\": \"Περιγράψτε τι κάνει αυτός ο κανόνας\",\n      \"rule_has_been_created\": \"Ο κανόνας δημιουργήθηκε!\",\n      \"rule_has_been_updated\": \"Ο κανόνας ενημερώθηκε!\",\n      \"rule_has_been_deleted\": \"Ο κανόνας διαγράφηκε!\",\n      \"no_rules_created_yet\": \"Δεν έχουν δημιουργηθεί κανόνες ακόμα\",\n      \"create_your_first_rule\": \"Δημιουργήστε τον πρώτο σας κανόνα για να αυτοματοποιήσετε τη ροή εργασίας σας\",\n      \"conditions_types\": {\n        \"always\": \"Πάντα\",\n        \"url_contains\": \"Το URL Περιέχει\",\n        \"imported_from_feed\": \"Εισάγεται από Feed\",\n        \"bookmark_type_is\": \"Ο Τύπος Σελιδοδείκτη Είναι\",\n        \"has_tag\": \"Έχει Ετικέτα\",\n        \"is_favourited\": \"Είναι Αγαπημένο\",\n        \"is_archived\": \"Είναι Αρχειοθετημένο\",\n        \"and\": \"Όλα τα παρακάτω είναι αληθή\",\n        \"or\": \"Οποιοδήποτε από τα παρακάτω είναι αληθές\",\n        \"url_does_not_contain\": \"Το URL δεν περιέχει\",\n        \"title_contains\": \"Ο τίτλος περιέχει\",\n        \"title_does_not_contain\": \"Ο τίτλος δεν περιέχει\"\n      },\n      \"actions_types\": {\n        \"add_tag\": \"Προσθήκη Ετικέτας\",\n        \"remove_tag\": \"Αφαίρεση Ετικέτας\",\n        \"add_to_list\": \"Προσθήκη στη Λίστα\",\n        \"remove_from_list\": \"Αφαίρεση από Λίστα\",\n        \"download_full_page_archive\": \"Λήψη Πλήρους Αρχείου Σελίδας\",\n        \"favourite_bookmark\": \"Σημείωση ως Αγαπημένο\",\n        \"archive_bookmark\": \"Αρχειοθέτηση Σελιδοδείκτη\"\n      },\n      \"events_types\": {\n        \"bookmark_added\": \"Προστίθεται ένας σελιδοδείκτης\",\n        \"tag_added\": \"Αυτή η ετικέτα προστίθεται σε έναν σελιδοδείκτη\",\n        \"tag_removed\": \"Αυτή η ετικέτα αφαιρείται από έναν σελιδοδείκτη\",\n        \"added_to_list\": \"Ένας σελιδοδείκτης προστίθεται σε αυτή τη λίστα\",\n        \"removed_from_list\": \"Ένας σελιδοδείκτης αφαιρείται από αυτή τη λίστα\",\n        \"favourited\": \"Ένας σελιδοδείκτης σημειώνεται ως αγαπημένος\",\n        \"archived\": \"Ένας σελιδοδείκτης αρχειοθετείται\"\n      }\n    },\n    \"stats\": {\n      \"usage_statistics\": \"Στατιστικά Χρήσης\",\n      \"insights_description\": \"Πληροφορίες για τις συνήθειες και τη συλλογή σελιδοδεικτών σου\",\n      \"failed_to_load\": \"Αποτυχία φόρτωσης στατιστικών\",\n      \"overview\": {\n        \"total_bookmarks\": \"Σύνολο σελιδοδεικτών\",\n        \"all_saved_items\": \"Όλα τα αποθηκευμένα στοιχεία\",\n        \"favorites\": \"Αγαπημένα\",\n        \"starred_bookmarks\": \"Σελιδοδείκτες με αστέρι\",\n        \"archived\": \"Αρχειοθετημένα\",\n        \"archived_items\": \"Αρχειοθετημένα στοιχεία\",\n        \"tags\": \"Ετικέτες\",\n        \"unique_tags_created\": \"Δημιουργήθηκαν μοναδικά tags\",\n        \"lists\": \"Λίστες\",\n        \"bookmark_collections\": \"Συλλογές σελιδοδεικτών\",\n        \"highlights\": \"Επισημάνσεις\",\n        \"text_highlights\": \"Επισημάνσεις κειμένου\",\n        \"storage_used\": \"Χώρος αποθήκευσης που χρησιμοποιείται\",\n        \"total_asset_storage\": \"Συνολικός αποθηκευτικός χώρος στοιχείων\",\n        \"this_month\": \"Αυτόν τον μήνα\",\n        \"bookmarks_added\": \"Προστέθηκαν σελιδοδείκτες\"\n      },\n      \"bookmark_types\": {\n        \"title\": \"Τύποι σελιδοδεικτών\",\n        \"links\": \"Σύνδεσμοι\",\n        \"text_notes\": \"Σημειώσεις κειμένου\",\n        \"assets\": \"Στοιχεία\"\n      },\n      \"recent_activity\": {\n        \"title\": \"Πρόσφατη δραστηριότητα\",\n        \"this_week\": \"Αυτή την εβδομάδα\",\n        \"this_month\": \"Αυτόν τον μήνα\",\n        \"this_year\": \"Φέτος\"\n      },\n      \"top_domains\": {\n        \"title\": \"Κορυφαίοι τομείς\",\n        \"no_domains_found\": \"Δεν βρέθηκαν τομείς\"\n      },\n      \"most_used_tags\": {\n        \"title\": \"Tags που χρησιμοποιούνται περισσότερο\",\n        \"no_tags_found\": \"Δεν βρέθηκαν ετικέτες\"\n      },\n      \"activity_patterns\": {\n        \"activity_by_hour\": \"Δραστηριότητα ανά ώρα\",\n        \"activity_by_day\": \"Δραστηριότητα ανά ημέρα\"\n      },\n      \"storage_breakdown\": {\n        \"title\": \"Ανάλυση αποθήκευσης\"\n      },\n      \"bookmark_sources\": {\n        \"title\": \"Πηγές σελιδοδεικτών\",\n        \"empty\": \"Δεν υπάρχουν διαθέσιμα δεδομένα προέλευσης\"\n      }\n    },\n    \"subscription\": {\n      \"subscription\": \"Συνδρομή\",\n      \"manage_subscription\": \"Διαχειρίσου τη συνδρομή σου και τις πληροφορίες χρέωσης\",\n      \"current_plan\": \"Τρέχον πρόγραμμα\",\n      \"billing_period\": \"Περίοδος χρέωσης\",\n      \"paid_plan\": \"Πληρωμένο πρόγραμμα\",\n      \"unlock_bigger_quota\": \"Ξεκλείδωσε μεγαλύτερη ποσόστωση και υποστήριξε το έργο\",\n      \"subscribe_now\": \"Εγγραφή τώρα\",\n      \"manage_billing\": \"Διαχείριση χρεώσεων\",\n      \"subscription_canceled\": \"Η συνδρομή σου έχει ακυρωθεί και θα λήξει στις {{date}}. Μπορείς να εγγραφείς ξανά ανά πάσα στιγμή.\",\n      \"usage_quotas\": \"Χρήση & Ποσοστώσεις\",\n      \"track_usage\": \"Παρακολούθησε την τρέχουσα χρήση σου σε σχέση με τα όρια του προγράμματός σου\",\n      \"total_bookmarks_saved\": \"Σύνολο αποθηκευμένων σελιδοδεικτών\",\n      \"assets_file_storage\": \"Αποθήκευση στοιχείων και αρχείων\",\n      \"unlimited_usage\": \"Απεριόριστη χρήση\",\n      \"quota_limit_reached\": \"Έφτασες το όριο ποσόστωσης\",\n      \"approaching_quota_limit\": \"Πλησιάζει το όριο ποσόστωσης\",\n      \"loading_usage\": \"Φόρτωση πληροφοριών χρήσης...\",\n      \"free\": \"Δωρεάν\",\n      \"paid\": \"Πληρωμένο\"\n    },\n    \"import_sessions\": {\n      \"title\": \"Εισαγωγή Συνεδριών\",\n      \"description\": \"Δείτε και διαχειριστείτε τις μαζικές σας συνεδρίες εισαγωγής. Οι συνεδρίες δημιουργούνται αυτόματα όταν εισάγετε σελιδοδείκτες.\",\n      \"load_error\": \"Αποτυχία φόρτωσης συνεδριών εισαγωγής\",\n      \"no_sessions\": \"Δεν υπάρχουν ακόμη συνεδρίες εισαγωγής\",\n      \"no_sessions_detail\": \"Οι συνεδρίες εισαγωγής θα εμφανίζονται εδώ αυτόματα όταν εισάγετε σελιδοδείκτες\",\n      \"created_at\": \"Δημιουργήθηκε {{time}}\",\n      \"progress\": \"Πρόοδος\",\n      \"status\": {\n        \"pending\": \"Σε εκκρεμότητα\",\n        \"in_progress\": \"Σε εξέλιξη\",\n        \"completed\": \"Ολοκληρώθηκε\",\n        \"failed\": \"Αποτυχία\",\n        \"processing\": \"Επεξεργασία\",\n        \"staging\": \"Στάδιο\",\n        \"running\": \"Εκτέλεση\",\n        \"paused\": \"Σε παύση\"\n      },\n      \"badges\": {\n        \"pending\": \"{{count}} σε εκκρεμότητα\",\n        \"processing\": \"{{count}} σε επεξεργασία\",\n        \"completed\": \"{{count}} ολοκληρώθηκε\",\n        \"failed\": \"{{count}} απέτυχε\"\n      },\n      \"imported_to\": \"Εισήχθη σε:\",\n      \"view_list\": \"Προβολή Λίστας\",\n      \"delete_dialog_title\": \"Διαγραφή Συνεδρίας Εισαγωγής\",\n      \"delete_dialog_description\": \"Είστε σίγουρος ότι θέλετε να διαγράψετε το \\\"{{name}}\\\"; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί. Οι ίδιοι οι σελιδοδείκτες δεν θα διαγραφούν.\",\n      \"delete_session\": \"Διαγραφή συνεδρίας\",\n      \"pause_session\": \"Παύση\",\n      \"resume_session\": \"Επαναφορά\",\n      \"view_details\": \"Προβολή λεπτομερειών\",\n      \"detail\": {\n        \"page_title\": \"Εισαγωγή λεπτομερειών συνεδρίας\",\n        \"back_to_import\": \"Επιστροφή στην εισαγωγή\",\n        \"filter_all\": \"Όλα\",\n        \"filter_accepted\": \"Αποδεκτά\",\n        \"filter_rejected\": \"Απορριφθέντα\",\n        \"filter_duplicates\": \"Διπλότυπα\",\n        \"filter_pending\": \"Σε εκκρεμότητα\",\n        \"table_title\": \"Τίτλος / URL\",\n        \"table_type\": \"Τύπος\",\n        \"table_result\": \"Αποτέλεσμα\",\n        \"table_reason\": \"Λόγος\",\n        \"table_bookmark\": \"Σελιδοδείκτης\",\n        \"result_accepted\": \"Αποδεκτά\",\n        \"result_rejected\": \"Απορριφθέντα\",\n        \"result_skipped_duplicate\": \"Διπλότυπο\",\n        \"result_pending\": \"Σε εκκρεμότητα\",\n        \"result_processing\": \"Επεξεργασία\",\n        \"no_results\": \"Δεν βρέθηκαν αποτελέσματα για αυτό το φίλτρο.\",\n        \"view_bookmark\": \"Προβολή σελιδοδείκτη\",\n        \"load_more\": \"Φόρτωσε κι άλλα\",\n        \"no_title\": \"Κανένας τίτλος\"\n      }\n    },\n    \"backups\": {\n      \"backups\": \"Αντίγραφα ασφαλείας\",\n      \"page_title\": \"Αντίγραφα ασφαλείας\",\n      \"page_description\": \"Αυτόματη δημιουργία και διαχείριση αντιγράφων ασφαλείας των σελιδοδεικτών σου. Τα αντίγραφα ασφαλείας συμπιέζονται και αποθηκεύονται με ασφάλεια.\",\n      \"configuration\": {\n        \"title\": \"Διαμόρφωση αντιγράφων ασφαλείας\",\n        \"enable_automatic_backups\": \"Ενεργοποίηση αυτόματων αντιγράφων ασφαλείας\",\n        \"enable_automatic_backups_description\": \"Αυτόματη δημιουργία αντιγράφων ασφαλείας των σελιδοδεικτών σου\",\n        \"backup_frequency\": \"Συχνότητα δημιουργίας αντιγράφων ασφαλείας\",\n        \"backup_frequency_description\": \"Πόσο συχνά πρέπει να δημιουργούνται αντίγραφα ασφαλείας\",\n        \"retention_period\": \"Περίοδος διατήρησης (ημέρες)\",\n        \"retention_period_description\": \"Πόσες ημέρες να διατηρούνται τα αντίγραφα ασφαλείας πριν διαγραφούν\",\n        \"frequency\": {\n          \"daily\": \"Καθημερινά\",\n          \"weekly\": \"Εβδομαδιαία\"\n        },\n        \"select_frequency\": \"Επιλογή συχνότητας\",\n        \"save_settings\": \"Αποθήκευση ρυθμίσεων\"\n      },\n      \"list\": {\n        \"title\": \"Τα αντίγραφα ασφαλείας σου\",\n        \"create_backup_now\": \"Δημιουργία αντιγράφου ασφαλείας τώρα\",\n        \"no_backups\": \"Δεν έχεις ακόμα αντίγραφα ασφαλείας. Ενεργοποίησε τα αυτόματα αντίγραφα ασφαλείας ή δημιούργησε ένα χειροκίνητα.\",\n        \"table\": {\n          \"created_at\": \"Δημιουργήθηκε στις\",\n          \"bookmarks\": \"Σελιδοδείκτες\",\n          \"size\": \"Μέγεθος\",\n          \"status\": \"Κατάσταση\",\n          \"actions\": \"Ενέργειες\"\n        },\n        \"status\": {\n          \"success\": \"Επιτυχία\",\n          \"failed\": \"Αποτυχία\",\n          \"pending\": \"Σε εκκρεμότητα\"\n        },\n        \"actions\": {\n          \"download_backup\": \"Λήψη Αντιγράφου Ασφαλείας\",\n          \"delete_backup\": \"Διαγραφή Αντιγράφου Ασφαλείας\"\n        }\n      },\n      \"dialogs\": {\n        \"delete_backup_title\": \"Διαγραφή αντιγράφου ασφαλείας;\",\n        \"delete_backup_description\": \"Είστε σίγουρος ότι θέλετε να διαγράψετε αυτό το αντίγραφο ασφαλείας; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.\"\n      },\n      \"toasts\": {\n        \"backup_queued\": \"Η εργασία δημιουργίας αντιγράφου ασφαλείας έχει μπει στην ουρά! Θα διεκπεραιωθεί σε λίγο.\",\n        \"backup_deleted\": \"Το αντίγραφο ασφαλείας διαγράφηκε!\"\n      }\n    }\n  },\n  \"admin\": {\n    \"admin_settings\": \"Ρυθμίσεις Διαχειριστή\",\n    \"server_stats\": {\n      \"server_stats\": \"Στατιστικά Διακομιστή\",\n      \"total_users\": \"Συνολικοί Χρήστες\",\n      \"total_bookmarks\": \"Συνολικοί Σελιδοδείκτες\",\n      \"server_version\": \"Έκδοση Διακομιστή\"\n    },\n    \"background_jobs\": {\n      \"background_jobs\": \"Εργασίες Παρασκηνίου\",\n      \"crawler_jobs\": \"Εργασίες Σάρωσης\",\n      \"indexing_jobs\": \"Εργασίες Ευρετηρίασης\",\n      \"inference_jobs\": \"Εργασίες Εξαγωγής Συμπερασμάτων\",\n      \"tidy_assets_jobs\": \"Εργασίες Οργάνωσης Στοιχείων\",\n      \"video_jobs\": \"Εργασίες Λήψης Βίντεο\",\n      \"webhook_jobs\": \"Εργασίες Webhook\",\n      \"asset_preprocessing_jobs\": \"Εργασίες Προεπεξεργασίας Στοιχείων\",\n      \"feed_jobs\": \"Εργασίες RSS Feed\",\n      \"job\": \"Εργασία\",\n      \"queued\": \"Στην Ουρά\",\n      \"pending\": \"Εκκρεμής\",\n      \"failed\": \"Απέτυχε\",\n      \"jobs\": {\n        \"crawler\": {\n          \"title\": \"Εργασίες ανίχνευσης\",\n          \"description\": \"Ανίχνευση ιστού και εξαγωγή περιεχομένου από URL\"\n        },\n        \"inference\": {\n          \"title\": \"Εργασίες συμπερασμού\",\n          \"description\": \"Προσθήκη ετικετών και περιληπτική παρουσίαση περιεχομένου μέσω τεχνητής νοημοσύνης\"\n        },\n        \"indexing\": {\n          \"title\": \"Εργασίες ευρετηρίασης\",\n          \"description\": \"Ενημερώσεις ευρετηρίου αναζήτησης\"\n        },\n        \"asset_preprocessing\": {\n          \"title\": \"Εργασίες προεπεξεργασίας στοιχείων\",\n          \"description\": \"Επεξεργασία εικόνων και εγγράφων (στιγμιότυπα οθόνης, εξαγωγή κειμένου, κ.λπ.)\"\n        },\n        \"tidy_assets\": {\n          \"title\": \"Εργασίες τακτοποίησης στοιχείων\",\n          \"description\": \"Εκκαθάριση στοιχείων και βελτιστοποίηση χώρου αποθήκευσης\"\n        },\n        \"video\": {\n          \"title\": \"Εργασίες λήψης βίντεο\",\n          \"description\": \"Εξαγωγή και λήψη βίντεο\"\n        },\n        \"webhook\": {\n          \"title\": \"Εργασίες Webhook\",\n          \"description\": \"Εξωτερικές ειδοποιήσεις webhook\"\n        },\n        \"feed\": {\n          \"title\": \"Εργασίες RSS Feed\",\n          \"description\": \"Επεξεργασία RSS feed και ενημερώσεις περιεχομένου\"\n        },\n        \"admin_maintenance\": {\n          \"title\": \"Εργασίες συντήρησης διαχειριστή\",\n          \"description\": \"Διοικητικός καθαρισμός και συντήρηση στοιχείων\"\n        }\n      },\n      \"monitor_and_manage\": \"Παρακολούθηση και διαχείριση ουρών εργασιών παρασκηνίου και εργασιών επεξεργασίας συστήματος\",\n      \"active\": \"Ενεργό\",\n      \"available_actions\": \"Διαθέσιμες ενέργειες\",\n      \"status\": {\n        \"title\": \"Κατανόηση των καταστάσεων εργασίας\",\n        \"queued\": {\n          \"title\": \"Σε αναμονή\",\n          \"description\": \"Εργασίες που περιμένουν στην ουρά για να υποβληθούν σε επεξεργασία. Θα ξεκινήσουν αυτόματα όταν υπάρχουν διαθέσιμοι πόροι.\"\n        },\n        \"unprocessed\": {\n          \"title\": \"Ανέπεξεργαστο\",\n          \"description\": \"Σελιδοδείκτες που δεν έχουν υποστεί ακόμη επεξεργασία. Πιθανότατα έχουν ήδη μπει στην ουρά για επεξεργασία, αν όχι, ίσως χρειαστεί να τους προσθέσετε ξανά στην ουρά χειροκίνητα.\"\n        },\n        \"failed\": {\n          \"title\": \"Αποτυχία\",\n          \"description\": \"Σελιδοδείκτες που αντιμετώπισαν σφάλματα κατά την επεξεργασία. Αυτά μπορεί να χρειάζονται χειροκίνητη προσοχή.\"\n        }\n      },\n      \"actions\": {\n        \"recrawl_failed_links_only\": \"Επανέλεγχος μόνο των συνδέσμων που απέτυχαν\",\n        \"recrawl_all_links\": \"Επανέλεγχος όλων των συνδέσμων\",\n        \"without_inference\": \"Χωρίς συμπεράσματα\",\n        \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Αναδημιουργία ετικετών AI μόνο για σελιδοδείκτες που απέτυχαν\",\n        \"regenerate_ai_tags_for_all_bookmarks\": \"Αναδημιουργία ετικετών AI για όλους τους σελιδοδείκτες\",\n        \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Αναδημιουργία περιλήψεων AI μόνο για σελιδοδείκτες που απέτυχαν\",\n        \"regenerate_ai_summaries_for_all_bookmarks\": \"Αναδημιουργία περιλήψεων AI για όλους τους σελιδοδείκτες\",\n        \"reindex_all_bookmarks\": \"Επανευρετηρίαση όλων των σελιδοδεικτών\",\n        \"clean_assets\": \"Εκκαθάριση εκκρεμών στοιχείων και επανασυγχρονισμός μεταδεδομένων\",\n        \"reprocess_assets_fix_mode\": \"Επανεξεργασία μη επεξεργασμένων στοιχείων\",\n        \"migrate_large_link_html_content\": \"Μετακινήστε Μεγάλο Ενσωματωμένο Περιεχόμενο HTML σε Στοιχεία\",\n        \"recrawl_pending_links_only\": \"Επανέλεγξε Μόνο τις Εκκρεμείς Συνδέσεις\",\n        \"regenerate_ai_tags_for_pending_bookmarks_only\": \"Αναδημιούργησε τις Ετικέτες AI Μόνο για τους Εκκρεμείς Σελιδοδείκτες\",\n        \"regenerate_ai_summaries_for_pending_bookmarks_only\": \"Αναδημιούργησε τις Περιλήψεις AI Μόνο για τους Εκκρεμείς Σελιδοδείκτες\"\n      }\n    },\n    \"actions\": {\n      \"recrawl_failed_links_only\": \"Επανασάρωση Μόνο Αποτυχημένων Συνδέσμων\",\n      \"recrawl_all_links\": \"Επανασάρωση Όλων των Συνδέσμων\",\n      \"without_inference\": \"Χωρίς Εξαγωγή Συμπερασμάτων\",\n      \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Αναδημιουργία Ετικετών AI Μόνο για Αποτυχημένους Σελιδοδείκτες\",\n      \"regenerate_ai_tags_for_all_bookmarks\": \"Αναδημιουργία Ετικετών AI για Όλους τους Σελιδοδείκτες\",\n      \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Αναδημιουργία Περιλήψεων AI Μόνο για Αποτυχημένους Σελιδοδείκτες\",\n      \"regenerate_ai_summaries_for_all_bookmarks\": \"Αναδημιουργία Περιλήψεων AI για Όλους τους Σελιδοδείκτες\",\n      \"reindex_all_bookmarks\": \"Επαναευρετηρίαση Όλων των Σελιδοδεικτών\",\n      \"compact_assets\": \"Συμπίεση Στοιχείων\",\n      \"reprocess_assets_fix_mode\": \"Επαναεπεξεργασία Στοιχείων (Λειτουργία Διόρθωσης)\"\n    },\n    \"users_list\": {\n      \"users_list\": \"Λίστα Χρηστών\",\n      \"create_user\": \"Δημιουργία Χρήστη\",\n      \"change_role\": \"Αλλαγή Ρόλου\",\n      \"reset_password\": \"Επαναφορά Κωδικού Πρόσβασης\",\n      \"delete_user\": \"Διαγραφή Χρήστη\",\n      \"num_bookmarks\": \"Αριθμός Σελιδοδεικτών\",\n      \"asset_sizes\": \"Μεγέθη Στοιχείων\",\n      \"local_user\": \"Τοπικός Χρήστης\",\n      \"confirm_password\": \"Επιβεβαίωση Κωδικού Πρόσβασης\",\n      \"delete_user_confirm_description\": \"Είσαι σίγουρος ότι θέλεις να διαγράψεις τον χρήστη \\\"{{name}}\\\";\",\n      \"unlimited\": \"Απεριόριστο\"\n    },\n    \"service_connections\": {\n      \"title\": \"Συνδέσεις εξυπηρέτησης\",\n      \"description\": \"Παρακολούθησε την εύρυθμη λειτουργία και τη συνδεσιμότητα των εξωτερικών συστημικών εξαρτήσεων\",\n      \"search_engine\": \"Μηχανή αναζήτησης\",\n      \"browser\": \"Πρόγραμμα περιήγησης\",\n      \"queue_system\": \"Σύστημα ουράς\",\n      \"status\": {\n        \"not_configured\": \"Δεν έχει ρυθμιστεί\",\n        \"connected\": \"Συνδέθηκε\",\n        \"disconnected\": \"Αποσυνδέθηκε\"\n      }\n    },\n    \"admin_tools\": {\n      \"admin_tools\": \"Εργαλεία διαχειριστή\",\n      \"bookmark_debugger\": \"Εργαλείο εντοπισμού σφαλμάτων σελιδοδεικτών\",\n      \"bookmark_id\": \"Αναγνωριστικό σελιδοδείκτη\",\n      \"bookmark_id_placeholder\": \"Εισαγάγετε το αναγνωριστικό σελιδοδείκτη\",\n      \"lookup\": \"Αναζήτηση\",\n      \"debug_info\": \"Πληροφορίες εντοπισμού σφαλμάτων\",\n      \"basic_info\": \"Βασικές πληροφορίες\",\n      \"status\": \"Κατάσταση\",\n      \"content\": \"Περιεχόμενο\",\n      \"html_preview\": \"Προεπισκόπηση HTML (Πρώτοι 1000 χαρακτήρες)\",\n      \"summary\": \"Περίληψη\",\n      \"url\": \"URL\",\n      \"source_url\": \"URL προέλευσης\",\n      \"asset_type\": \"Τύπος στοιχείου\",\n      \"file_name\": \"Όνομα αρχείου\",\n      \"owner_user_id\": \"Αναγνωριστικό χρήστη κατόχου\",\n      \"tagging_status\": \"Κατάσταση προσθήκης ετικετών\",\n      \"summarization_status\": \"Κατάσταση συνοπτικής παρουσίασης\",\n      \"crawl_status\": \"Κατάσταση ανίχνευσης\",\n      \"crawl_status_code\": \"Κωδικός κατάστασης HTTP\",\n      \"crawled_at\": \"Έγινε ανίχνευση στις\",\n      \"recrawl\": \"Επανέλεγχος\",\n      \"reindex\": \"Επανευρετηρίαση\",\n      \"retag\": \"Επανασήμανση\",\n      \"resummarize\": \"Επανάληψη σύνοψης\",\n      \"resummarize_queued\": \"Η εργασία επανάληψης σύνοψης έχει μπει στην ουρά\",\n      \"view\": \"Προβολή\",\n      \"bookmark_not_found\": \"Δεν βρέθηκε ο σελιδοδείκτης\",\n      \"action_success\": \"Η ενέργεια ολοκληρώθηκε με επιτυχία\",\n      \"action_failed\": \"Η ενέργεια απέτυχε\",\n      \"recrawl_queued\": \"Η εργασία επανελέγχου έχει μπει στην ουρά\",\n      \"reindex_queued\": \"Η εργασία επανευρετηρίασης έχει μπει στην ουρά\",\n      \"retag_queued\": \"Η εργασία επανασήμανσης έχει μπει στην ουρά\",\n      \"fetch_error\": \"Σφάλμα κατά τη λήψη του σελιδοδείκτη\"\n    }\n  },\n  \"options\": {\n    \"dark_mode\": \"Σκοτεινή Λειτουργία\",\n    \"light_mode\": \"Ανοιχτή Λειτουργία\",\n    \"apps_extensions\": \"Εφαρμογές & Επεκτάσεις\",\n    \"documentation\": \"Τεκμηρίωση\",\n    \"follow_us_on_x\": \"Ακολουθήστε μας στο X\"\n  },\n  \"lists\": {\n    \"all_lists\": \"Όλες οι Λίστες\",\n    \"favourites\": \"Αγαπημένα\",\n    \"new_list\": \"Νέα Λίστα\",\n    \"edit_list\": \"Επεξεργασία Λίστας\",\n    \"share_list\": \"Κοινοποίηση Λίστας\",\n    \"new_nested_list\": \"Νέα Ένθετη Λίστα\",\n    \"merge_list\": \"Συγχώνευση Λίστας\",\n    \"destination_list\": \"Λίστα Προορισμού\",\n    \"delete_after_merge\": \"Διαγραφή αρχικής λίστας μετά τη συγχώνευση\",\n    \"no_destination\": \"Χωρίς Προορισμό\",\n    \"parent_list\": \"Γονική Λίστα\",\n    \"no_parent\": \"Χωρίς Γονέα\",\n    \"list_type\": \"Τύπος Λίστας\",\n    \"manual_list\": \"Χειροκίνητη Λίστα\",\n    \"smart_list\": \"Έξυπνη Λίστα\",\n    \"search_query\": \"Ερώτημα Αναζήτησης\",\n    \"search_query_help\": \"Μάθετε περισσότερα για τη γλώσσα ερωτημάτων αναζήτησης.\",\n    \"description\": \"Περιγραφή (Προαιρετική)\",\n    \"rss\": {\n      \"title\": \"Ροή RSS\",\n      \"description\": \"Ενεργοποίηση RSS feed για αυτή τη λίστα\",\n      \"feed_url\": \"URL RSS Feed\"\n    },\n    \"public_list\": {\n      \"title\": \"Δημόσια Λίστα\",\n      \"description\": \"Επιτρέψτε σε άλλους να βλέπουν αυτή τη λίστα\",\n      \"share_link\": \"Σύνδεσμος Κοινοποίησης\"\n    },\n    \"delete_list\": {\n      \"title\": \"Διαγραφή λίστας\",\n      \"description\": \"Η διαγραφή λίστας δεν διαγράφει οποιουσδήποτε σελιδοδείκτες σε αυτήν.\",\n      \"delete_children\": \"Διαγραφή θυγατρικών λιστών (αναδρομικά)\",\n      \"delete_children_description\": \"Αν δεν είναι επιλεγμένο, όλες οι άμεσες θυγατρικές λίστες θα γίνουν βασικές λίστες\"\n    },\n    \"shared\": \"Κοινόχρηστο\",\n    \"collaborators\": {\n      \"manage\": \"Διαχείριση συνεργατών\",\n      \"view\": \"Προβολή συνεργατών\",\n      \"collaborators\": \"Συνεργάτες\",\n      \"add\": \"Προσθήκη συνεργάτη\",\n      \"current\": \"Τρέχοντες συνεργάτες\",\n      \"enter_email\": \"Εισαγάγετε διεύθυνση email\",\n      \"please_enter_email\": \"Εισαγάγετε μια διεύθυνση email\",\n      \"added_successfully\": \"Ο συνεργάτης προστέθηκε με επιτυχία\",\n      \"failed_to_add\": \"Αποτυχία προσθήκης συνεργάτη\",\n      \"removed\": \"Ο συνεργάτης αφαιρέθηκε\",\n      \"failed_to_remove\": \"Αποτυχία αφαίρεσης συνεργάτη\",\n      \"role_updated\": \"Ο ρόλος ενημερώθηκε\",\n      \"failed_to_update_role\": \"Αποτυχία ενημέρωσης ρόλου\",\n      \"viewer\": \"Θεατής\",\n      \"editor\": \"Επεξεργαστής\",\n      \"owner\": \"Κάτοχος\",\n      \"viewer_description\": \"Μπορεί να δει σελιδοδείκτες στη λίστα\",\n      \"editor_description\": \"Μπορεί να προσθέσει και να αφαιρέσει σελιδοδείκτες\",\n      \"no_collaborators\": \"Δεν υπάρχουν ακόμη συνεργάτες. Προσθέστε κάποιον για να ξεκινήσετε τη συνεργασία!\",\n      \"no_collaborators_readonly\": \"Δεν υπάρχουν συνεργάτες για αυτή τη λίστα.\",\n      \"people_with_access\": \"Άτομα που έχουν πρόσβαση σε αυτή τη λίστα\",\n      \"add_or_remove\": \"Προσθήκη ή κατάργηση ατόμων που μπορούν να έχουν πρόσβαση σε αυτήν τη λίστα\",\n      \"invitation_sent\": \"Η πρόσκληση εστάλη κανονικά\",\n      \"invitation_revoked\": \"Η πρόσκληση ανακλήθηκε\",\n      \"failed_to_revoke\": \"Αποτυχία ανάκλησης πρόσκλησης\",\n      \"pending\": \"Σε εκκρεμότητα\",\n      \"revoke\": \"Ανάκληση\",\n      \"declined\": \"Απορρίφθηκε\"\n    },\n    \"leave_list\": {\n      \"title\": \"Έξοδος από τη λίστα\",\n      \"confirm_message\": \"Είστε βέβαιοι ότι θέλετε να βγείτε από τη λίστα {{icon}} {{name}};\",\n      \"warning\": \"Δεν θα μπορείτε πλέον να προβάλλετε ή να αποκτήσετε πρόσβαση σε σελιδοδείκτες σε αυτήν τη λίστα. Ο κάτοχος της λίστας μπορεί να σας προσθέσει ξανά, εάν χρειαστεί.\",\n      \"action\": \"Έξοδος από τη λίστα\",\n      \"success\": \"Αποχωρήσατε από τη λίστα \\\"{{icon}} {{name}}\\\"\"\n    },\n    \"invitations\": {\n      \"pending\": \"Εκκρεμείς προσκλήσεις\",\n      \"description\": \"Ελέγξτε και απαντήστε σε προσκλήσεις συνεργασίας λίστας\",\n      \"invited_by\": \"Πρόσκληση από\",\n      \"accept\": \"Αποδοχή\",\n      \"decline\": \"Απόρριψη\",\n      \"accepted\": \"Η πρόσκληση έγινε αποδεκτή\",\n      \"declined\": \"Η πρόσκληση απορρίφθηκε\",\n      \"failed_to_accept\": \"Αποτυχία αποδοχής πρόσκλησης\",\n      \"failed_to_decline\": \"Αποτυχία απόρριψης πρόσκλησης\"\n    },\n    \"shared_lists\": \"Κοινόχρηστες λίστες\"\n  },\n  \"tags\": {\n    \"all_tags\": \"Όλες οι Ετικέτες\",\n    \"your_tags\": \"Οι Ετικέτες σας\",\n    \"your_tags_info\": \"Ετικέτες που έχουν προσαρτηθεί τουλάχιστον μία φορά από εσάς\",\n    \"ai_tags\": \"Ετικέτες AI\",\n    \"ai_tags_info\": \"Ετικέτες που προσαρτήθηκαν μόνο αυτόματα (από AI)\",\n    \"unused_tags\": \"Αχρησιμοποίητες Ετικέτες\",\n    \"unused_tags_info\": \"Ετικέτες που δεν είναι προσαρτημένες σε κανέναν σελιδοδείκτη\",\n    \"delete_all_unused_tags\": \"Διαγραφή Όλων των Αχρησιμοποίητων Ετικετών\",\n    \"drag_and_drop_merging\": \"Συγχώνευση με Σύρσιμο & Απόθεση\",\n    \"drag_and_drop_merging_info\": \"Σύρετε και αποθέστε ετικέτες η μία πάνω στην άλλη για να τις συγχωνεύσετε\",\n    \"sort_by_name\": \"Ταξινόμηση κατά Όνομα\",\n    \"create_tag\": \"Δημιουργία ετικέτας\",\n    \"create_tag_description\": \"Δημιουργία νέας ετικέτας χωρίς να την επισυνάψετε σε οποιονδήποτε σελιδοδείκτη\",\n    \"tag_name\": \"Όνομα ετικέτας\",\n    \"enter_tag_name\": \"Εισαγάγετε όνομα ετικέτας\",\n    \"sort_by_usage\": \"Ταξινόμηση ανά Χρήση\",\n    \"sort_by_relevance\": \"Ταξινόμηση ανά Σχετικότητα\",\n    \"no_custom_tags\": \"Δεν υπάρχουν ακόμα προσαρμοσμένες ετικέτες\",\n    \"no_ai_tags\": \"Δεν υπάρχουν ακόμα ετικέτες τεχνητής νοημοσύνης\",\n    \"no_unused_tags\": \"Δεν έχεις καμία αχρησιμοποίητη ετικέτα\",\n    \"no_unused_tags_match_your_search\": \"Δεν υπάρχουν αχρησιμοποίητες ετικέτες που να ταιριάζουν με την αναζήτησή σου\",\n    \"no_tags_match_your_search\": \"Καμία ετικέτα δεν ταιριάζει με την αναζήτησή σου\",\n    \"search_placeholder\": \"Αναζήτηση ετικετών...\",\n    \"search_or_create_placeholder\": \"Αναζητήστε ή δημιουργήστε ετικέτες...\"\n  },\n  \"search\": {\n    \"is_favorited\": \"Είναι Αγαπημένο\",\n    \"is_not_favorited\": \"Δεν Είναι Αγαπημένο\",\n    \"is_archived\": \"Είναι Αρχειοθετημένο\",\n    \"is_not_archived\": \"Δεν Είναι Αρχειοθετημένο\",\n    \"has_any_tag\": \"Έχει Οποιαδήποτε Ετικέτα\",\n    \"has_no_tags\": \"Δεν Έχει Ετικέτες\",\n    \"is_in_any_list\": \"Είναι σε Οποιαδήποτε Λίστα\",\n    \"is_not_in_any_list\": \"Δεν Είναι σε Καμία Λίστα\",\n    \"created_on_or_after\": \"Δημιουργήθηκε την ή μετά την\",\n    \"not_created_on_or_after\": \"Δεν Δημιουργήθηκε την ή μετά την\",\n    \"created_on_or_before\": \"Δημιουργήθηκε την ή πριν την\",\n    \"not_created_on_or_before\": \"Δεν Δημιουργήθηκε την ή πριν την\",\n    \"created_within\": \"Δημιουργήθηκε Εντός\",\n    \"created_earlier_than\": \"Δημιουργήθηκε Νωρίτερα από\",\n    \"day_s\": \" Ημέρα(ες)\",\n    \"week_s\": \" Εβδομάδα(ες)\",\n    \"month_s\": \" Μήνας(ες)\",\n    \"year_s\": \" Έτος(η)\",\n    \"day_s_ago\": \" Ημέρα(ες) Πριν\",\n    \"week_s_ago\": \" Εβδομάδα(ες) Πριν\",\n    \"month_s_ago\": \" Μήνας(ες) Πριν\",\n    \"year_s_ago\": \" Έτος(η) Πριν\",\n    \"url_contains\": \"Το URL Περιέχει\",\n    \"url_does_not_contain\": \"Το URL Δεν Περιέχει\",\n    \"is_in_list\": \"Είναι στη Λίστα\",\n    \"is_not_in_list\": \"Δεν Είναι στη Λίστα\",\n    \"has_tag\": \"Έχει Ετικέτα\",\n    \"does_not_have_tag\": \"Δεν Έχει Ετικέτα\",\n    \"full_text_search\": \"Αναζήτηση Πλήρους Κειμένου\",\n    \"type_is\": \"Ο Τύπος είναι\",\n    \"type_is_not\": \"Ο Τύπος δεν είναι\",\n    \"is_from_feed\": \"Είναι από RSS Feed\",\n    \"is_not_from_feed\": \"Δεν είναι από RSS Feed\",\n    \"and\": \"Και\",\n    \"or\": \"Ή\",\n    \"history\": \"Πρόσφατες αναζητήσεις\",\n    \"title_contains\": \"Ο τίτλος περιέχει\",\n    \"title_does_not_contain\": \"Ο τίτλος δεν περιέχει\",\n    \"is_broken_link\": \"Έχει κατεστραμμένο σύνδεσμο\",\n    \"tags\": \"Ετικέτες\",\n    \"no_suggestions\": \"Χωρίς προτάσεις\",\n    \"filters\": \"Φίλτρα\",\n    \"is_not_broken_link\": \"Έχει σύνδεσμο που λειτουργεί\",\n    \"lists\": \"Λίστες\",\n    \"feeds\": \"Ροές\",\n    \"is_from_source\": \"Η πηγή είναι\",\n    \"is_not_from_source\": \"Η πηγή δεν είναι\"\n  },\n  \"preview\": {\n    \"view_original\": \"Προβολή Πρωτότυπου\",\n    \"cached_content\": \"Περιεχόμενο σε Cache\",\n    \"reader_view\": \"Προβολή Ανάγνωσης\",\n    \"tabs\": {\n      \"content\": \"Περιεχόμενο\",\n      \"details\": \"Λεπτομέρειες\"\n    },\n    \"archive_info\": \"Τα αρχεία ενδέχεται να μην αποδίδονται σωστά ενσωματωμένα, εάν απαιτούν Javascript. Για καλύτερα αποτελέσματα, <1>κατεβάστε το και ανοίξτε το στο πρόγραμμα περιήγησής σας</1>.\",\n    \"fetch_error_title\": \"Δεν είναι δυνατή η πρόσβαση στο περιεχόμενο\",\n    \"fetch_error_description\": \"Δεν μπορέσαμε να λάβουμε το περιεχόμενο για αυτόν τον σύνδεσμο. Η σελίδα ενδέχεται να είναι προστατευμένη, να απαιτεί έλεγχο ταυτότητας ή να είναι προσωρινά μη διαθέσιμη.\",\n    \"crawling_in_progress\": \"Λήψη περιεχομένου σελίδας…\",\n    \"continue_reading\": \"Συνέχισε από εκεί που σταμάτησες\",\n    \"continue_reading_percent\": \"Συνέχισε από εκεί που σταμάτησες ({{percent}}%)\",\n    \"continue_button\": \"Συνέχισε\"\n  },\n  \"editor\": {\n    \"quickly_focus\": \"Μπορείτε να εστιάσετε γρήγορα σε αυτό το πεδίο πατώντας ⌘ + E\",\n    \"multiple_urls_dialog_title\": \"Εισαγωγή URLs ως Ξεχωριστών Σελιδοδεικτών;\",\n    \"multiple_urls_dialog_desc\": \"Η είσοδος περιέχει πολλαπλά URLs σε ξεχωριστές γραμμές. Θέλετε να τα εισάγετε ως ξεχωριστούς σελιδοδείκτες;\",\n    \"import_as_text\": \"Εισαγωγή ως Σελιδοδείκτης Κειμένου\",\n    \"import_as_separate_bookmarks\": \"Εισαγωγή ως Ξεχωριστούς Σελιδοδείκτες\",\n    \"placeholder\": \"Επικολλήστε έναν σύνδεσμο ή μια εικόνα, γράψτε μια σημείωση ή σύρετε και αποθέστε μια εικόνα εδώ…\",\n    \"placeholder_v2\": \"Επικολλήστε έναν σύνδεσμο, γράψτε μια σημείωση ή αποθέστε μια εικόνα…\",\n    \"new_item\": \"ΝΈΟ ΣΤΟΙΧΕΊΟ\",\n    \"disabled_submissions\": \"Οι υποβολές είναι απενεργοποιημένες\",\n    \"text_toolbar\": {\n      \"undo\": \"Αναίρεση\",\n      \"redo\": \"Επανάληψη\",\n      \"bold\": \"Έντονα\",\n      \"italic\": \"Πλάγια\",\n      \"underline\": \"Υπογράμμιση\",\n      \"strikethrough\": \"Διαγράμμιση\",\n      \"code\": \"Κώδικας\",\n      \"highlight\": \"Επισήμανση\",\n      \"align_left\": \"Αριστερή Στοίχιση\",\n      \"align_center\": \"Κεντρική Στοίχιση\",\n      \"align_right\": \"Δεξιά Στοίχιση\",\n      \"markdown_shortcuts\": {\n        \"label\": \"Συντομεύσεις Markdown\",\n        \"heading\": {\n          \"label\": \"Επικεφαλίδα\",\n          \"example\": \"# H1, ## H2, ### H3\"\n        },\n        \"bold\": {\n          \"label\": \"Έντονα\",\n          \"example\": \"**κείμενο** ή CTRL+b\"\n        },\n        \"italic\": {\n          \"label\": \"Πλάγια\",\n          \"example\": \"*Πλάγια* ή _Πλάγια_ ή CTRL+i\"\n        },\n        \"blockquote\": {\n          \"label\": \"Παράθεμα\",\n          \"example\": \"> Παράθεμα\"\n        },\n        \"ordered_list\": {\n          \"label\": \"Αριθμημένη Λίστα\",\n          \"example\": \"1. Στοιχείο λίστας\"\n        },\n        \"unordered_list\": {\n          \"label\": \"Μη Αριθμημένη Λίστα\",\n          \"example\": \"- Στοιχείο λίστας\"\n        },\n        \"inline_code\": {\n          \"label\": \"Κώδικας στη Γραμμή\",\n          \"example\": \"`Κώδικας`\"\n        },\n        \"block_code\": {\n          \"label\": \"Μπλοκ Κώδικα\",\n          \"example\": \"``` + διάστημα\"\n        }\n      }\n    }\n  },\n  \"dialogs\": {\n    \"bookmarks\": {\n      \"delete_confirmation_title\": \"Διαγραφή Σελιδοδείκτη;\",\n      \"delete_confirmation_description\": \"Είστε σίγουροι ότι θέλετε να διαγράψετε αυτόν τον σελιδοδείκτη;\"\n    }\n  },\n  \"toasts\": {\n    \"bookmarks\": {\n      \"updated\": \"Ο σελιδοδείκτης ενημερώθηκε!\",\n      \"deleted\": \"Ο σελιδοδείκτης διαγράφηκε!\",\n      \"refetch\": \"Η επαναφόρτωση μπήκε στην ουρά!\",\n      \"full_page_archive\": \"Η δημιουργία Πλήρους Αρχείου Σελίδας ενεργοποιήθηκε\",\n      \"delete_from_list\": \"Ο σελιδοδείκτης διαγράφηκε από τη λίστα\",\n      \"clipboard_copied\": \"Ο σύνδεσμος προστέθηκε στο πρόχειρό σας!\",\n      \"preserve_pdf\": \"Η διατήρηση PDF έχει ενεργοποιηθεί\",\n      \"update_banner\": \"Το banner ενημερώθηκε!\",\n      \"uploading_banner\": \"Μεταφόρτωση banner...\"\n    },\n    \"lists\": {\n      \"created\": \"Η λίστα δημιουργήθηκε!\",\n      \"updated\": \"Η λίστα ενημερώθηκε!\",\n      \"merged\": \"Η λίστα συγχωνεύτηκε!\",\n      \"deleted\": \"Η λίστα διαγράφηκε!\"\n    },\n    \"tags\": {\n      \"created\": \"Η ετικέτα δημιουργήθηκε!\",\n      \"failed_to_create\": \"Αποτυχία δημιουργίας ετικέτας\"\n    }\n  },\n  \"banners\": {\n    \"no_bookmarks\": {\n      \"title\": \"Δεν υπάρχουν σελιδοδείκτες ακόμα\",\n      \"description\": \"Αποθηκεύστε ενδιαφέροντα άρθρα, συνδέσμους και σελίδες για να τα προσπελάσετε γρήγορα αργότερα.\"\n    }\n  },\n  \"cleanups\": {\n    \"cleanups\": \"Καθαρισμοί\",\n    \"duplicate_tags\": {\n      \"title\": \"Διπλότυπες Ετικέτες\",\n      \"merge_all_suggestions\": \"Συγχώνευση όλων των προτάσεων;\"\n    }\n  },\n  \"bookmark_editor\": {\n    \"title\": \"Επεξεργασία Σελιδοδείκτη\",\n    \"subtitle\": \"Κάντε αλλαγές στις λεπτομέρειες του σελιδοδείκτη. Κάντε κλικ στο αποθήκευση όταν τελειώσετε.\",\n    \"author\": \"Συγγραφέας\",\n    \"publisher\": \"Εκδότης\",\n    \"date_published\": \"Ημερομηνία Δημοσίευσης\",\n    \"pick_a_date\": \"Επιλέξτε μια ημερομηνία\",\n    \"save_changes\": \"Αποθήκευση αλλαγών\",\n    \"extracted_content\": \"Εξαγόμενο Περιεχόμενο\"\n  },\n  \"view_options\": {\n    \"title\": \"Επιλογές προβολής\",\n    \"layout\": \"Διάταξη\",\n    \"columns\": \"Στήλες\",\n    \"display_options\": \"Επιλογές εμφάνισης\",\n    \"show_note_previews\": \"Εμφάνιση σημειώσεων\",\n    \"show_tags\": \"Εμφάνιση ετικετών\",\n    \"show_title\": \"Εμφάνιση τίτλου\",\n    \"image_options\": \"Επιλογές Εικόνας\",\n    \"image_fit_cover\": \"Κάλυψη (Γέμισμα)\",\n    \"image_fit_contain\": \"Περιέχει (Εφαρμογή)\"\n  },\n  \"version\": {\n    \"new_release_available\": \"Διατίθενται νέες σημειώσεις έκδοσης\",\n    \"whats_new_title\": \"Τι νέο υπάρχει στην έκδοση {{version}}\",\n    \"release_notes_description\": \"Εδώ θα βρείτε τις πιο πρόσφατες ενημερώσεις που ανακτήθηκαν από τις σημειώσεις έκδοσης του GitHub.\",\n    \"loading_release_notes\": \"Φόρτωση σημειώσεων έκδοσης…\",\n    \"unable_to_load_release_notes\": \"Δεν είναι δυνατή η φόρτωση των σημειώσεων έκδοσης αυτήν τη στιγμή. Προσπαθήστε ξανά αργότερα.\",\n    \"no_release_notes\": \"Δεν δημοσιεύτηκαν σημειώσεις έκδοσης για αυτήν την έκδοση.\",\n    \"release_notes_synced\": \"Οι σημειώσεις έκδοσης συγχρονίζονται από το GitHub.\",\n    \"view_on_github\": \"Προβολή στο GitHub\"\n  },\n  \"wrapped\": {\n    \"title\": \"Το {{year}} σου σε περιτύλιγμα\",\n    \"subtitle\": \"Μια χρονιά στο Karakeep\",\n    \"banner\": {\n      \"title\": \"Το δικό σου 2025 Wrapped είναι έτοιμο!\",\n      \"description\": \"Δείτε τη χρονιά σας σε σελιδοδείκτες\",\n      \"view_now\": \"Προβολή τώρα\"\n    },\n    \"button\": \"2025 Wrapped\",\n    \"loading\": \"Φόρτωση του Wrapped σου...\",\n    \"failed_to_load\": \"Αποτυχία φόρτωσης των στατιστικών σου Wrapped\",\n    \"sections\": {\n      \"total_saves\": {\n        \"prefix\": \"Έχεις αποθηκεύσει\",\n        \"suffix\": \"αντικείμενα φέτος\",\n        \"suffix_singular\": \"αντικείμενο φέτος\"\n      },\n      \"first_bookmark\": {\n        \"title\": \"Το Ταξίδι σου Ξεκίνησε\",\n        \"description\": \"Η πρώτη σου αποθήκευση του {{year}}:\"\n      },\n      \"top_domains\": \"Οι Κορυφαίες σελίδες σου\",\n      \"top_tags\": \"Οι Κορυφαίες ετικέτες σου\",\n      \"monthly_activity\": \"Η Χρονιά σου σε Αποθηκεύσεις\",\n      \"most_active_day\": \"Η πιο δραστήρια σου μέρα\",\n      \"peak_times\": {\n        \"title\": \"Πότε Αποθηκεύεις\",\n        \"peak_hour\": \"Ώρα Αιχμής\",\n        \"peak_day\": \"Μέρα Αιχμής\"\n      },\n      \"how_you_save\": \"Πώς Αποθηκεύεις\",\n      \"what_you_saved\": \"Τι Αποθήκευσες\",\n      \"summary\": {\n        \"favorites\": \"Αγαπημένα\",\n        \"tags_created\": \"Ετικέτες που Δημιουργήθηκαν\",\n        \"highlights\": \"Στιγμιότυπα\"\n      },\n      \"types\": {\n        \"links\": \"Σύνδεσμοι\",\n        \"notes\": \"Σημειώσεις\",\n        \"assets\": \"Περιουσιακά στοιχεία\"\n      }\n    },\n    \"footer\": \"Δημιουργήθηκε με το Karakeep\",\n    \"share\": \"Κοινή χρήση\",\n    \"download\": \"Λήψη\",\n    \"close\": \"Κλείσιμο\",\n    \"generating\": \"Δημιουργία...\"\n  }\n}\n"
  },
  {
    "path": "apps/web/lib/i18n/locales/en/translation.json",
    "content": "{\n  \"common\": {\n    \"default\": \"Default\",\n    \"id\": \"ID\",\n    \"url\": \"URL\",\n    \"name\": \"Name\",\n    \"email\": \"Email\",\n    \"password\": \"Password\",\n    \"action\": \"Action\",\n    \"actions\": \"Actions\",\n    \"created_at\": \"Created At\",\n    \"updated_at\": \"Updated At\",\n    \"last_used\": \"Last Used\",\n    \"key\": \"Key\",\n    \"role\": \"Role\",\n    \"type\": \"Type\",\n    \"size\": \"Size\",\n    \"roles\": {\n      \"user\": \"User\",\n      \"admin\": \"Admin\"\n    },\n    \"something_went_wrong\": \"Something went wrong\",\n    \"experimental\": \"Experimental\",\n    \"search\": \"Search\",\n    \"tags\": \"Tags\",\n    \"note\": \"Note\",\n    \"attachments\": \"Attachments\",\n    \"highlights\": \"Highlights\",\n    \"source\": \"Source\",\n    \"screenshot\": \"Screenshot\",\n    \"pdf\": \"Archived PDF\",\n    \"video\": \"Video\",\n    \"archive\": \"Archive\",\n    \"home\": \"Home\",\n    \"title\": \"Title\",\n    \"description\": \"Description\",\n    \"summary\": \"Summary\",\n    \"bookmark_types\": {\n      \"title\": \"Bookmark Type\",\n      \"link\": \"Link\",\n      \"text\": \"Text\",\n      \"media\": \"Media\"\n    },\n    \"quota\": \"Quota\",\n    \"bookmarks\": \"Bookmarks\",\n    \"storage\": \"Storage\"\n  },\n  \"layouts\": {\n    \"masonry\": \"Masonry\",\n    \"grid\": \"Grid\",\n    \"list\": \"List\",\n    \"compact\": \"Compact\"\n  },\n  \"view_options\": {\n    \"title\": \"View Options\",\n    \"layout\": \"Layout\",\n    \"columns\": \"Columns\",\n    \"display_options\": \"Display Options\",\n    \"show_note_previews\": \"Show Notes\",\n    \"show_tags\": \"Show Tags\",\n    \"show_title\": \"Show Title\",\n    \"image_options\": \"Image Options\",\n    \"image_fit_cover\": \"Cover (Fill)\",\n    \"image_fit_contain\": \"Contain (Fit)\"\n  },\n  \"actions\": {\n    \"change_layout\": \"Change Layout\",\n    \"archive\": \"Archive\",\n    \"unarchive\": \"Un-archive\",\n    \"favorite\": \"Favorite\",\n    \"unfavorite\": \"Unfavorite\",\n    \"delete\": \"Delete\",\n    \"toggle_show_archived\": \"Show Archived\",\n    \"refresh\": \"Refresh\",\n    \"recrawl\": \"Recrawl\",\n    \"offline_copies\": \"Offline Copies\",\n    \"preserve_offline_archive\": \"Preserve Offline Archive\",\n    \"download_full_page_archive_file\": \"Download Archive File\",\n    \"preserve_as_pdf\": \"Preserve as PDF\",\n    \"download_pdf_file\": \"Download PDF File\",\n    \"edit_tags\": \"Edit Tags\",\n    \"edit_notes\": \"Edit Notes\",\n    \"add_to_list\": \"Add to List\",\n    \"select_all\": \"Select All\",\n    \"unselect_all\": \"Unselect All\",\n    \"copy_link\": \"Copy Link\",\n    \"close_bulk_edit\": \"Close Bulk Edit\",\n    \"bulk_edit\": \"Bulk Edit\",\n    \"manage_lists\": \"Manage Lists\",\n    \"remove_from_list\": \"Remove from List\",\n    \"save\": \"Save\",\n    \"add\": \"Add\",\n    \"remove\": \"Remove\",\n    \"edit\": \"Edit\",\n    \"confirm\": \"Confirm\",\n    \"open_editor\": \"Open Editor\",\n    \"create\": \"Create\",\n    \"fetch_now\": \"Fetch Now\",\n    \"summarize_with_ai\": \"Summarize with AI\",\n    \"edit_title\": \"Edit Title\",\n    \"sign_out\": \"Sign Out\",\n    \"close\": \"Close\",\n    \"merge\": \"Merge\",\n    \"cancel\": \"Cancel\",\n    \"regenerate\": \"Regenerate\",\n    \"apply_all\": \"Apply All\",\n    \"ignore\": \"Ignore\",\n    \"more\": \"More\",\n    \"replace_banner\": \"Replace Banner\",\n    \"add_banner\": \"Add Banner\",\n    \"download\": \"Download\",\n    \"sort\": {\n      \"title\": \"Sort\",\n      \"relevant_first\": \"Most Relevant First\",\n      \"newest_first\": \"Newest First\",\n      \"oldest_first\": \"Oldest First\"\n    },\n    \"load_more\": \"Load More\"\n  },\n  \"highlights\": {\n    \"no_highlights\": \"You don't have any highlights yet.\"\n  },\n  \"settings\": {\n    \"back_to_app\": \"Back To App\",\n    \"user_settings\": \"User Settings\",\n    \"info\": {\n      \"user_info\": \"User Info\",\n      \"basic_details\": \"Basic Details\",\n      \"change_password\": \"Change Password\",\n      \"current_password\": \"Current Password\",\n      \"new_password\": \"New Password\",\n      \"confirm_new_password\": \"Confirm New Password\",\n      \"options\": \"Options\",\n      \"interface_lang\": \"Interface Language\",\n      \"avatar\": {\n        \"title\": \"Profile Photo\",\n        \"description\": \"Upload a square image to use as your avatar.\",\n        \"upload\": \"Upload avatar\",\n        \"change\": \"Change avatar\",\n        \"remove\": \"Remove avatar\",\n        \"remove_confirm_title\": \"Remove avatar?\",\n        \"remove_confirm_description\": \"This will clear your current profile photo.\",\n        \"updated\": \"Avatar updated\",\n        \"removed\": \"Avatar removed\"\n      },\n      \"user_settings\": {\n        \"user_settings_updated\": \"User settings have been updated!\",\n        \"bookmark_click_action\": {\n          \"title\": \"Bookmark Click Action\",\n          \"open_external_url\": \"Open Original URL\",\n          \"open_bookmark_details\": \"Open Bookmark Details\"\n        },\n        \"archive_display_behaviour\": {\n          \"title\": \"Archived Bookmarks\",\n          \"show\": \"Show archived bookmarks in tags and lists\",\n          \"hide\": \"Hide archived bookmarks in tags and lists\"\n        }\n      },\n      \"reader_settings\": {\n        \"title\": \"Reader Settings\",\n        \"description\": \"Configure default text settings for the reader view. These settings sync across all your devices.\",\n        \"font_family\": \"Font Family\",\n        \"font_size\": \"Font Size\",\n        \"line_height\": \"Line Height\",\n        \"save_as_default\": \"Save as default\",\n        \"clear_defaults\": \"Clear all defaults\",\n        \"not_set\": \"Not set\",\n        \"using_default\": \"Using client default\",\n        \"preview\": \"Preview\",\n        \"preview_text\": \"The quick brown fox jumps over the lazy dog. This is how your reader view text will appear.\",\n        \"defaults_cleared\": \"Reader defaults have been cleared\",\n        \"local_overrides_title\": \"Device-specific settings active\",\n        \"local_overrides_description\": \"This device has reader settings that differ from your global defaults:\",\n        \"local_overrides_cleared\": \"Device-specific settings have been cleared\",\n        \"clear_local_overrides\": \"Clear device settings\",\n        \"serif\": \"Serif\",\n        \"sans\": \"Sans Serif\",\n        \"mono\": \"Monospace\",\n        \"tooltip_default\": \"Reading settings\",\n        \"tooltip_preview\": \"Unsaved preview changes\",\n        \"tooltip_local\": \"Device settings differ from global\",\n        \"tooltip_preview_and_local\": \"Unsaved preview changes; device settings differ from global\",\n        \"reset_preview\": \"Reset preview\",\n        \"save_to_device\": \"This device\",\n        \"save_to_all_devices\": \"All devices\",\n        \"save_hint\": \"Save settings for this device only or sync across all devices\",\n        \"adjust_hint\": \"Adjust settings above to preview changes\",\n        \"clear_override_hint\": \"Clear device override to use global setting ({{value}})\",\n        \"preview_inline\": \"(preview)\"\n      }\n    },\n    \"stats\": {\n      \"usage_statistics\": \"Usage Statistics\",\n      \"insights_description\": \"Insights into your bookmarking habits and collection\",\n      \"failed_to_load\": \"Failed to load statistics\",\n      \"overview\": {\n        \"total_bookmarks\": \"Total Bookmarks\",\n        \"all_saved_items\": \"All saved items\",\n        \"favorites\": \"Favorites\",\n        \"starred_bookmarks\": \"Starred bookmarks\",\n        \"archived\": \"Archived\",\n        \"archived_items\": \"Archived items\",\n        \"tags\": \"Tags\",\n        \"unique_tags_created\": \"Unique tags created\",\n        \"lists\": \"Lists\",\n        \"bookmark_collections\": \"Bookmark collections\",\n        \"highlights\": \"Highlights\",\n        \"text_highlights\": \"Text highlights\",\n        \"storage_used\": \"Storage Used\",\n        \"total_asset_storage\": \"Total asset storage\",\n        \"this_month\": \"This Month\",\n        \"bookmarks_added\": \"Bookmarks added\"\n      },\n      \"bookmark_types\": {\n        \"title\": \"Bookmark Types\",\n        \"links\": \"Links\",\n        \"text_notes\": \"Text Notes\",\n        \"assets\": \"Assets\"\n      },\n      \"recent_activity\": {\n        \"title\": \"Recent Activity\",\n        \"this_week\": \"This Week\",\n        \"this_month\": \"This Month\",\n        \"this_year\": \"This Year\"\n      },\n      \"top_domains\": {\n        \"title\": \"Top Domains\",\n        \"no_domains_found\": \"No domains found\"\n      },\n      \"most_used_tags\": {\n        \"title\": \"Most Used Tags\",\n        \"no_tags_found\": \"No tags found\"\n      },\n      \"activity_patterns\": {\n        \"activity_by_hour\": \"Activity by Hour\",\n        \"activity_by_day\": \"Activity by Day\"\n      },\n      \"storage_breakdown\": {\n        \"title\": \"Storage Breakdown\"\n      },\n      \"bookmark_sources\": {\n        \"title\": \"Bookmark Sources\",\n        \"empty\": \"No source data available\"\n      }\n    },\n    \"ai\": {\n      \"ai_settings\": \"AI Settings\",\n      \"auto_tagging\": \"Auto-tagging\",\n      \"auto_tagging_description\": \"Automatically generate tags for your bookmarks using AI.\",\n      \"auto_summarization\": \"Auto-summarization\",\n      \"auto_summarization_description\": \"Automatically generate summaries for your bookmarks using AI.\",\n      \"tagging_rules\": \"Tagging Rules\",\n      \"tagging_rule_description\": \"Prompts that you add here will be included as rules to the model during tag generation. You can view the final prompts in the prompt preview section.\",\n      \"prompt_preview\": \"Prompt Preview\",\n      \"text_prompt\": \"Text Prompt\",\n      \"images_prompt\": \"Image Prompt\",\n      \"summarization_prompt\": \"Summarization Prompt\",\n      \"all_tagging\": \"All Tagging\",\n      \"text_tagging\": \"Text Tagging\",\n      \"image_tagging\": \"Image Tagging\",\n      \"summarization\": \"Summarization\",\n      \"tag_style\": \"Tag Style\",\n      \"tag_style_description\": \"Choose how your auto-generated tags should be formatted.\",\n      \"lowercase_hyphens\": \"Lowercase with hyphens\",\n      \"lowercase_spaces\": \"Lowercase with spaces\",\n      \"lowercase_underscores\": \"Lowercase with underscores\",\n      \"titlecase_spaces\": \"Title case with spaces\",\n      \"titlecase_hyphens\": \"Title case with hyphens\",\n      \"camelCase\": \"camelCase\",\n      \"no_preference\": \"No preference\",\n      \"inference_language\": \"Inference Language\",\n      \"inference_language_description\": \"Choose language for AI-generated tags and summaries.\",\n      \"curated_tags\": \"Curated Tags\",\n      \"curated_tags_description\": \"Optionally restrict AI tagging to only use tags from this list. When no tags are selected, the AI generates tags freely.\",\n      \"curated_tags_updated\": \"Curated tags updated successfully!\",\n      \"curated_tags_update_failed\": \"Failed to update curated tags\"\n    },\n    \"feeds\": {\n      \"rss_subscriptions\": \"RSS Subscriptions\",\n      \"add_a_subscription\": \"Add a Subscription\",\n      \"feed_enabled\": \"RSS Feed enabled\",\n      \"feed_disabled\": \"RSS Feed disabled\"\n    },\n    \"webhooks\": {\n      \"webhooks\": \"Webhooks\",\n      \"description\": \"You can use webhooks to trigger actions when bookmarks are created, changed or crawled.\",\n      \"events\": {\n        \"title\": \"Events\",\n        \"crawled\": \"Crawled\",\n        \"created\": \"Created\",\n        \"edited\": \"Edited\"\n      },\n      \"auth_token\": \"Auth Token\",\n      \"add_auth_token\": \"Add Auth Token\",\n      \"edit_auth_token\": \"Edit Auth Token\",\n      \"create_webhook\": \"Create Webhook\",\n      \"delete_webhook\": \"Delete Webhook\",\n      \"delete_webhook_confirmation\": \"Are you sure you want to delete this webhook?\",\n      \"edit_webhook\": \"Edit Webhook\",\n      \"webhook_url\": \"Webhook URL\"\n    },\n    \"import\": {\n      \"import_export\": \"Import / Export\",\n      \"import_export_bookmarks\": \"Import / Export Bookmarks\",\n      \"import_bookmarks_from_html_file\": \"Import Bookmarks from HTML file\",\n      \"import_bookmarks_from_pocket_export\": \"Import Bookmarks from Pocket export\",\n      \"import_bookmarks_from_matter_export\": \"Import Bookmarks from Matter export\",\n      \"import_bookmarks_from_omnivore_export\": \"Import Bookmarks from Omnivore export\",\n      \"import_bookmarks_from_linkwarden_export\": \"Import Bookmarks from Linkwarden export\",\n      \"import_bookmarks_from_karakeep_export\": \"Import Bookmarks from Karakeep export\",\n      \"import_bookmarks_from_tab_session_manager_export\": \"Import Bookmarks from Tab Session Manager\",\n      \"import_bookmarks_from_mymind_export\": \"Import Bookmarks from mymind export\",\n      \"import_bookmarks_from_instapaper_export\": \"Import Bookmarks from Instapaper export\",\n      \"export_links_and_notes\": \"Export Links and Notes\",\n      \"imported_bookmarks\": \"Imported Bookmarks\"\n    },\n    \"api_keys\": {\n      \"api_keys\": \"API Keys\",\n      \"new_api_key\": \"New API Key\",\n      \"new_api_key_desc\": \"Give your API key a unique name\",\n      \"key_success\": \"Key was successfully created\",\n      \"key_success_please_copy\": \"Please copy the key and store it somewhere safe. Once you close the dialog, you won't be able to access it again.\",\n      \"regenerate_api_key\": \"Regenerate API Key\",\n      \"key_regenerated\": \"Key was successfully regenerated\",\n      \"key_regenerated_please_copy\": \"Please copy the new key and store it somewhere safe. The old key has been revoked and will no longer work.\",\n      \"regenerate_warning\": \"Are you sure you want to regenerate the API key \\\"{{name}}\\\"?\",\n      \"regenerate_confirmation\": \"This will revoke the current key and generate a new one. Any applications using the current key will stop working.\"\n    },\n    \"broken_links\": {\n      \"broken_links\": \"Broken Links\",\n      \"last_crawled_at\": \"Last Crawled At\",\n      \"crawling_status\": \"Crawling Status\",\n      \"crawling_failed\": \"Crawling Failed\"\n    },\n    \"manage_assets\": {\n      \"manage_assets\": \"Manage Assets\",\n      \"no_assets\": \"You don't have any assets yet.\",\n      \"asset_type\": \"Asset Type\",\n      \"bookmark_link\": \"Bookmark Link\",\n      \"asset_link\": \"Asset Link\",\n      \"delete_asset\": \"Delete Asset\",\n      \"delete_asset_confirmation\": \"Are you sure you want to delete this asset?\"\n    },\n    \"rules\": {\n      \"rules\": \"Rule Engine\",\n      \"rule_name\": \"Rule Name\",\n      \"description\": \"You can use rules to trigger actions when an event fires.\",\n      \"ceate_rule\": \"Create Rule\",\n      \"edit_rule\": \"Edit Rule\",\n      \"save_rule\": \"Save Rule\",\n      \"delete_rule\": \"Delete Rule\",\n      \"delete_rule_confirmation\": \"Are you sure you want to delete this rule?\",\n      \"whenever\": \"Whenever ...\",\n      \"if\": \"If ...\",\n      \"enter_rule_name\": \"Enter rule name\",\n      \"describe_what_this_rule_does\": \"Describe what this rule does\",\n      \"rule_has_been_created\": \"Rule has been created!\",\n      \"rule_has_been_updated\": \"Rule has been updated!\",\n      \"rule_has_been_deleted\": \"Rule has been deleted!\",\n      \"no_rules_created_yet\": \"No rules created yet\",\n      \"create_your_first_rule\": \"Create your first rule to automate your workflow\",\n      \"conditions_types\": {\n        \"always\": \"Always\",\n        \"url_contains\": \"URL Contains\",\n        \"url_does_not_contain\": \"URL Does Not Contain\",\n        \"title_contains\": \"Title Contains\",\n        \"title_does_not_contain\": \"Title Does Not Contain\",\n        \"imported_from_feed\": \"Imported From Feed\",\n        \"bookmark_type_is\": \"Bookmark Type Is\",\n        \"bookmark_source_is\": \"Bookmark Source Is\",\n        \"has_tag\": \"Has Tag\",\n        \"is_favourited\": \"Is Favourited\",\n        \"is_archived\": \"Is Archived\",\n        \"and\": \"All of the following are true\",\n        \"or\": \"Any of the following are true\"\n      },\n      \"actions_types\": {\n        \"add_tag\": \"Add Tag\",\n        \"remove_tag\": \"Remove Tag\",\n        \"add_to_list\": \"Add to List\",\n        \"remove_from_list\": \"Remove from List\",\n        \"download_full_page_archive\": \"Download Full Page Archive\",\n        \"favourite_bookmark\": \"Favourite Bookmark\",\n        \"archive_bookmark\": \"Archive Bookmark\"\n      },\n      \"events_types\": {\n        \"bookmark_added\": \"A bookmark is added\",\n        \"tag_added\": \"This tag is added to a bookmark\",\n        \"tag_removed\": \"This tag is removed from a bookmark\",\n        \"added_to_list\": \"A bookmark is added to this list\",\n        \"removed_from_list\": \"A bookmark is removed from this list\",\n        \"favourited\": \"A bookmark is favourited\",\n        \"archived\": \"A bookmark is archived\"\n      }\n    },\n    \"subscription\": {\n      \"subscription\": \"Subscription\",\n      \"manage_subscription\": \"Manage your subscription and billing information\",\n      \"current_plan\": \"Current Plan\",\n      \"billing_period\": \"Billing Period\",\n      \"paid_plan\": \"Paid Plan\",\n      \"unlock_bigger_quota\": \"Unlock bigger quota and support the project\",\n      \"subscribe_now\": \"Subscribe Now\",\n      \"manage_billing\": \"Manage Billing\",\n      \"subscription_canceled\": \"Your subscription has been canceled and will end on {{date}}. You can resubscribe at any time.\",\n      \"usage_quotas\": \"Usage & Quotas\",\n      \"track_usage\": \"Track your current usage against your plan limits\",\n      \"total_bookmarks_saved\": \"Total bookmarks saved\",\n      \"assets_file_storage\": \"Assets and file storage\",\n      \"unlimited_usage\": \"Unlimited usage\",\n      \"quota_limit_reached\": \"Quota limit reached\",\n      \"approaching_quota_limit\": \"Approaching quota limit\",\n      \"loading_usage\": \"Loading usage information...\",\n      \"free\": \"Free\",\n      \"paid\": \"Paid\"\n    },\n    \"import_sessions\": {\n      \"title\": \"Import Sessions\",\n      \"description\": \"View and manage your bulk import sessions. Sessions are automatically created when you import bookmarks.\",\n      \"load_error\": \"Failed to load import sessions\",\n      \"no_sessions\": \"No import sessions yet\",\n      \"no_sessions_detail\": \"Import sessions will appear here automatically when you import bookmarks\",\n      \"created_at\": \"Created {{time}}\",\n      \"progress\": \"Progress\",\n      \"status\": {\n        \"staging\": \"Staging\",\n        \"pending\": \"Pending\",\n        \"running\": \"Running\",\n        \"paused\": \"Paused\",\n        \"completed\": \"Completed\",\n        \"failed\": \"Failed\"\n      },\n      \"badges\": {\n        \"pending\": \"{{count}} pending\",\n        \"processing\": \"{{count}} processing\",\n        \"completed\": \"{{count}} completed\",\n        \"failed\": \"{{count}} failed\"\n      },\n      \"imported_to\": \"Imported to:\",\n      \"view_list\": \"View List\",\n      \"delete_dialog_title\": \"Delete Import Session\",\n      \"delete_dialog_description\": \"Are you sure you want to delete \\\"{{name}}\\\"? This action cannot be undone. The bookmarks themselves will not be deleted.\",\n      \"delete_session\": \"Delete Session\",\n      \"pause_session\": \"Pause\",\n      \"resume_session\": \"Resume\",\n      \"view_details\": \"View Details\",\n      \"detail\": {\n        \"page_title\": \"Import Session Details\",\n        \"back_to_import\": \"Back to Import\",\n        \"filter_all\": \"All\",\n        \"filter_accepted\": \"Accepted\",\n        \"filter_rejected\": \"Rejected\",\n        \"filter_duplicates\": \"Duplicates\",\n        \"filter_pending\": \"Pending\",\n        \"table_title\": \"Title / URL\",\n        \"table_type\": \"Type\",\n        \"table_result\": \"Result\",\n        \"table_reason\": \"Reason\",\n        \"table_bookmark\": \"Bookmark\",\n        \"result_accepted\": \"Accepted\",\n        \"result_rejected\": \"Rejected\",\n        \"result_skipped_duplicate\": \"Duplicate\",\n        \"result_pending\": \"Pending\",\n        \"result_processing\": \"Processing\",\n        \"no_results\": \"No results found for this filter.\",\n        \"view_bookmark\": \"View Bookmark\",\n        \"load_more\": \"Load More\",\n        \"no_title\": \"No title\"\n      }\n    },\n    \"backups\": {\n      \"backups\": \"Backups\",\n      \"page_title\": \"Backups\",\n      \"page_description\": \"Automatically create and manage backups of your bookmarks. Backups are compressed and stored securely.\",\n      \"configuration\": {\n        \"title\": \"Backup Configuration\",\n        \"enable_automatic_backups\": \"Enable Automatic Backups\",\n        \"enable_automatic_backups_description\": \"Automatically create backups of your bookmarks\",\n        \"backup_frequency\": \"Backup Frequency\",\n        \"backup_frequency_description\": \"How often backups should be created\",\n        \"retention_period\": \"Retention Period (days)\",\n        \"retention_period_description\": \"How many days to keep backups before deleting them\",\n        \"frequency\": {\n          \"daily\": \"Daily\",\n          \"weekly\": \"Weekly\"\n        },\n        \"select_frequency\": \"Select frequency\",\n        \"save_settings\": \"Save Settings\"\n      },\n      \"list\": {\n        \"title\": \"Your Backups\",\n        \"create_backup_now\": \"Create Backup Now\",\n        \"no_backups\": \"You don't have any backups yet. Enable automatic backups or create one manually.\",\n        \"table\": {\n          \"created_at\": \"Created At\",\n          \"bookmarks\": \"Bookmarks\",\n          \"size\": \"Size\",\n          \"status\": \"Status\",\n          \"actions\": \"Actions\"\n        },\n        \"status\": {\n          \"success\": \"Success\",\n          \"failed\": \"Failed\",\n          \"pending\": \"Pending\"\n        },\n        \"actions\": {\n          \"download_backup\": \"Download Backup\",\n          \"delete_backup\": \"Delete Backup\"\n        }\n      },\n      \"dialogs\": {\n        \"delete_backup_title\": \"Delete Backup?\",\n        \"delete_backup_description\": \"Are you sure you want to delete this backup? This action cannot be undone.\"\n      },\n      \"toasts\": {\n        \"backup_queued\": \"Backup job has been queued! It will be processed shortly.\",\n        \"backup_deleted\": \"Backup has been deleted!\"\n      }\n    }\n  },\n  \"admin\": {\n    \"admin_settings\": \"Admin Settings\",\n    \"server_stats\": {\n      \"server_stats\": \"Server Stats\",\n      \"total_users\": \"Total Users\",\n      \"total_bookmarks\": \"Total Bookmarks\",\n      \"server_version\": \"Server Version\"\n    },\n    \"service_connections\": {\n      \"title\": \"Service Connections\",\n      \"description\": \"Monitor the health and connectivity of external system dependencies\",\n      \"search_engine\": \"Search Engine\",\n      \"browser\": \"Browser\",\n      \"queue_system\": \"Queue System\",\n      \"status\": {\n        \"not_configured\": \"Not Configured\",\n        \"connected\": \"Connected\",\n        \"disconnected\": \"Disconnected\"\n      }\n    },\n    \"background_jobs\": {\n      \"jobs\": {\n        \"crawler\": {\n          \"title\": \"Crawler Jobs\",\n          \"description\": \"Web crawling and content extraction from URLs\"\n        },\n        \"inference\": {\n          \"title\": \"Inference Jobs\",\n          \"description\": \"AI-powered tagging and summarization of content\"\n        },\n        \"indexing\": {\n          \"title\": \"Indexing Jobs\",\n          \"description\": \"Search index updates\"\n        },\n        \"asset_preprocessing\": {\n          \"title\": \"Asset Preprocessing Jobs\",\n          \"description\": \"Image and document preprocessing (screenshots, text extraction, etc.)\"\n        },\n        \"admin_maintenance\": {\n          \"title\": \"Admin Maintenance Jobs\",\n          \"description\": \"Administrative cleanup and asset maintenance\"\n        },\n        \"video\": {\n          \"title\": \"Video Download Jobs\",\n          \"description\": \"Video extraction and download\"\n        },\n        \"webhook\": {\n          \"title\": \"Webhook Jobs\",\n          \"description\": \"External webhook notifications\"\n        },\n        \"feed\": {\n          \"title\": \"RSS Feed Jobs\",\n          \"description\": \"RSS feed processing and content updates\"\n        }\n      },\n      \"background_jobs\": \"Background Jobs\",\n      \"monitor_and_manage\": \"Monitor and manage background job queues and system processing tasks\",\n      \"active\": \"Active\",\n      \"available_actions\": \"Available Actions\",\n      \"status\": {\n        \"title\": \"Understanding Job States\",\n        \"queued\": {\n          \"title\": \"Queued\",\n          \"description\": \"Jobs waiting in line to be processed. They will start automatically when resources are available.\"\n        },\n        \"unprocessed\": {\n          \"title\": \"Unprocessed\",\n          \"description\": \"Bookmarks that have not yet been processed. They are most likely already queued for processing, if not, you might need to manually re-enqueue them.\"\n        },\n        \"failed\": {\n          \"title\": \"Failed\",\n          \"description\": \"Bookmarks that encountered errors during processing. These may need manual attention.\"\n        }\n      },\n      \"actions\": {\n        \"recrawl_pending_links_only\": \"Recrawl Pending Links Only\",\n        \"recrawl_failed_links_only\": \"Recrawl Failed Links Only\",\n        \"recrawl_all_links\": \"Recrawl All Links\",\n        \"without_inference\": \"Without Inference\",\n        \"regenerate_ai_tags_for_pending_bookmarks_only\": \"Regenerate AI Tags for Pending Bookmarks Only\",\n        \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Regenerate AI Tags for Failed Bookmarks Only\",\n        \"regenerate_ai_tags_for_all_bookmarks\": \"Regenerate AI Tags for All Bookmarks\",\n        \"regenerate_ai_summaries_for_pending_bookmarks_only\": \"Regenerate AI Summaries for Pending Bookmarks Only\",\n        \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Regenerate AI Summaries for Failed Bookmarks Only\",\n        \"regenerate_ai_summaries_for_all_bookmarks\": \"Regenerate AI Summaries for All Bookmarks\",\n        \"reindex_all_bookmarks\": \"Reindex All Bookmarks\",\n        \"clean_assets\": \"Clean Dangling Assets & Re-sync Metadata\",\n        \"migrate_large_link_html_content\": \"Move Large Inline HTML Content to Assets\",\n        \"reprocess_assets_fix_mode\": \"Reprocess Unprocessed Assets\"\n      }\n    },\n    \"users_list\": {\n      \"users_list\": \"Users List\",\n      \"create_user\": \"Create User\",\n      \"change_role\": \"Change Role\",\n      \"reset_password\": \"Reset Password\",\n      \"delete_user\": \"Delete User\",\n      \"delete_user_confirm_description\": \"Are you sure you want to delete user \\\"{{name}}\\\"?\",\n      \"num_bookmarks\": \"Num Bookmarks\",\n      \"asset_sizes\": \"Asset Sizes\",\n      \"local_user\": \"Local User\",\n      \"confirm_password\": \"Confirm Password\",\n      \"unlimited\": \"Unlimited\"\n    },\n    \"admin_tools\": {\n      \"admin_tools\": \"Admin Tools\",\n      \"bookmark_debugger\": \"Bookmark Debugger\",\n      \"bookmark_id\": \"Bookmark ID\",\n      \"bookmark_id_placeholder\": \"Enter bookmark ID\",\n      \"lookup\": \"Lookup\",\n      \"debug_info\": \"Debug Information\",\n      \"basic_info\": \"Basic Information\",\n      \"status\": \"Status\",\n      \"content\": \"Content\",\n      \"html_preview\": \"HTML Preview (First 1000 chars)\",\n      \"summary\": \"Summary\",\n      \"url\": \"URL\",\n      \"source_url\": \"Source URL\",\n      \"asset_type\": \"Asset Type\",\n      \"file_name\": \"File Name\",\n      \"owner_user_id\": \"Owner User ID\",\n      \"tagging_status\": \"Tagging Status\",\n      \"summarization_status\": \"Summarization Status\",\n      \"crawl_status\": \"Crawl Status\",\n      \"crawl_status_code\": \"HTTP Status Code\",\n      \"crawled_at\": \"Crawled At\",\n      \"recrawl\": \"Re-crawl\",\n      \"reindex\": \"Re-index\",\n      \"retag\": \"Re-tag\",\n      \"resummarize\": \"Re-summarize\",\n      \"bookmark_not_found\": \"Bookmark not found\",\n      \"action_success\": \"Action completed successfully\",\n      \"action_failed\": \"Action failed\",\n      \"recrawl_queued\": \"Re-crawl job has been queued\",\n      \"reindex_queued\": \"Re-index job has been queued\",\n      \"retag_queued\": \"Re-tag job has been queued\",\n      \"resummarize_queued\": \"Re-summarize job has been queued\",\n      \"view\": \"View\",\n      \"fetch_error\": \"Error fetching bookmark\"\n    }\n  },\n  \"options\": {\n    \"dark_mode\": \"Dark Mode\",\n    \"light_mode\": \"Light Mode\",\n    \"apps_extensions\": \"Apps & Extensions\",\n    \"documentation\": \"Documentation\",\n    \"follow_us_on_x\": \"Follow us on X\"\n  },\n  \"lists\": {\n    \"all_lists\": \"All Lists\",\n    \"favourites\": \"Favourites\",\n    \"shared\": \"Shared\",\n    \"shared_lists\": \"Shared Lists\",\n    \"new_list\": \"New List\",\n    \"edit_list\": \"Edit List\",\n    \"share_list\": \"Share List\",\n    \"new_nested_list\": \"New Nested List\",\n    \"merge_list\": \"Merge List\",\n    \"destination_list\": \"Destination List\",\n    \"delete_after_merge\": \"Delete original list after merge\",\n    \"no_destination\": \"No Destination\",\n    \"parent_list\": \"Parent List\",\n    \"no_parent\": \"No Parent\",\n    \"list_type\": \"List Type\",\n    \"manual_list\": \"Manual List\",\n    \"smart_list\": \"Smart List\",\n    \"search_query\": \"Search Query\",\n    \"search_query_help\": \"Learn more about the search query language.\",\n    \"description\": \"Description (Optional)\",\n    \"delete_list\": {\n      \"title\": \"Delete List\",\n      \"description\": \"Deleting a list doesn't delete any bookmarks in that list.\",\n      \"delete_children\": \"Delete children lists (recursively)\",\n      \"delete_children_description\": \"If not checked, all direct children lists will become root lists\"\n    },\n    \"rss\": {\n      \"title\": \"RSS Feed\",\n      \"description\": \"Enable an RSS feed for this list\",\n      \"feed_url\": \"RSS Feed URL\"\n    },\n    \"public_list\": {\n      \"title\": \"Public List\",\n      \"description\": \"Allow others to view this list\",\n      \"share_link\": \"Share Link\"\n    },\n    \"collaborators\": {\n      \"manage\": \"Manage Collaborators\",\n      \"view\": \"View Collaborators\",\n      \"collaborators\": \"Collaborators\",\n      \"add\": \"Add Collaborator\",\n      \"current\": \"Current Collaborators\",\n      \"enter_email\": \"Enter email address\",\n      \"please_enter_email\": \"Please enter an email address\",\n      \"added_successfully\": \"Collaborator added successfully\",\n      \"invitation_sent\": \"Invitation sent successfully\",\n      \"failed_to_add\": \"Failed to add collaborator\",\n      \"removed\": \"Collaborator removed\",\n      \"failed_to_remove\": \"Failed to remove collaborator\",\n      \"role_updated\": \"Role updated\",\n      \"failed_to_update_role\": \"Failed to update role\",\n      \"invitation_revoked\": \"Invitation revoked\",\n      \"failed_to_revoke\": \"Failed to revoke invitation\",\n      \"viewer\": \"Viewer\",\n      \"editor\": \"Editor\",\n      \"owner\": \"Owner\",\n      \"pending\": \"Pending\",\n      \"revoke\": \"Revoke\",\n      \"declined\": \"Declined\",\n      \"viewer_description\": \"Can view bookmarks in the list\",\n      \"editor_description\": \"Can add and remove bookmarks\",\n      \"no_collaborators\": \"No collaborators yet. Add someone to start collaborating!\",\n      \"no_collaborators_readonly\": \"No collaborators for this list.\",\n      \"people_with_access\": \"People who have access to this list\",\n      \"add_or_remove\": \"Add or remove people who can access this list\"\n    },\n    \"invitations\": {\n      \"pending\": \"Pending Invitations\",\n      \"description\": \"Review and respond to list collaboration invitations\",\n      \"invited_by\": \"Invited by\",\n      \"accept\": \"Accept\",\n      \"decline\": \"Decline\",\n      \"accepted\": \"Invitation accepted\",\n      \"declined\": \"Invitation declined\",\n      \"failed_to_accept\": \"Failed to accept invitation\",\n      \"failed_to_decline\": \"Failed to decline invitation\"\n    },\n    \"leave_list\": {\n      \"title\": \"Leave List\",\n      \"confirm_message\": \"Are you sure you want to leave {{icon}} {{name}}?\",\n      \"warning\": \"You will no longer be able to view or access bookmarks in this list. The list owner can add you back if needed.\",\n      \"action\": \"Leave List\",\n      \"success\": \"You have left \\\"{{icon}} {{name}}\\\"\"\n    }\n  },\n  \"tags\": {\n    \"all_tags\": \"All Tags\",\n    \"your_tags\": \"Your Tags\",\n    \"your_tags_info\": \"Tags that were attached at least once by you\",\n    \"ai_tags\": \"AI Tags\",\n    \"ai_tags_info\": \"Tags that were only attached automatically (by AI)\",\n    \"unused_tags\": \"Unused Tags\",\n    \"unused_tags_info\": \"Tags that are not attached to any bookmarks\",\n    \"delete_all_unused_tags\": \"Delete All Unused Tags\",\n    \"drag_and_drop_merging\": \"Drag & Drop Merging\",\n    \"drag_and_drop_merging_info\": \"Drag and drop tags on each other to merge them\",\n    \"sort_by_name\": \"Sort by Name\",\n    \"sort_by_usage\": \"Sort by Usage\",\n    \"sort_by_relevance\": \"Sort by Relevance\",\n    \"create_tag\": \"Create Tag\",\n    \"create_tag_description\": \"Create a new tag without attaching it to any bookmark\",\n    \"tag_name\": \"Tag Name\",\n    \"enter_tag_name\": \"Enter tag name\",\n    \"search_placeholder\": \"Search tags...\",\n    \"search_or_create_placeholder\": \"Search or create tags...\",\n    \"no_custom_tags\": \"No custom tags yet\",\n    \"no_ai_tags\": \"No AI tags yet\",\n    \"no_unused_tags\": \"You don't have any unused tags\",\n    \"no_unused_tags_match_your_search\": \"No unused tags match your search\",\n    \"no_tags_match_your_search\": \"No tags match your search\"\n  },\n  \"search\": {\n    \"is_favorited\": \"Is Favorited\",\n    \"is_not_favorited\": \"Is Not Favorited\",\n    \"is_archived\": \"Is Archived\",\n    \"is_not_archived\": \"Is Not Archived\",\n    \"has_any_tag\": \"Has Any Tag\",\n    \"has_no_tags\": \"Has No Tag\",\n    \"is_in_any_list\": \"Is In Any List\",\n    \"is_not_in_any_list\": \"Is not In Any List\",\n    \"created_on_or_after\": \"Created on or After\",\n    \"not_created_on_or_after\": \"Not Created on or After\",\n    \"created_on_or_before\": \"Created on or Before\",\n    \"not_created_on_or_before\": \"Not Created on or Before\",\n    \"created_within\": \"Created Within\",\n    \"created_earlier_than\": \"Created Earlier Than\",\n    \"day_s\": \" Day(s)\",\n    \"week_s\": \" Week(s)\",\n    \"month_s\": \" Month(s)\",\n    \"year_s\": \" Year(s)\",\n    \"day_s_ago\": \" Day(s) Ago\",\n    \"week_s_ago\": \" Week(s) Ago\",\n    \"month_s_ago\": \" Month(s) Ago\",\n    \"year_s_ago\": \" Year(s) Ago\",\n    \"url_contains\": \"URL Contains\",\n    \"url_does_not_contain\": \"URL Does Not Contain\",\n    \"title_contains\": \"Title Contains\",\n    \"title_does_not_contain\": \"Title Does Not Contain\",\n    \"is_in_list\": \"Is In List\",\n    \"is_not_in_list\": \"Is not In List\",\n    \"has_tag\": \"Has Tag\",\n    \"does_not_have_tag\": \"Does Not Have Tag\",\n    \"full_text_search\": \"Full Text Search\",\n    \"type_is\": \"Type is\",\n    \"type_is_not\": \"Type is not\",\n    \"is_from_feed\": \"Is from RSS Feed\",\n    \"is_not_from_feed\": \"Is not from RSS Feed\",\n    \"is_from_source\": \"Source is\",\n    \"is_not_from_source\": \"Source is not\",\n    \"is_broken_link\": \"Has Broken Link\",\n    \"is_not_broken_link\": \"Has Working Link\",\n    \"and\": \"And\",\n    \"or\": \"Or\",\n    \"history\": \"Recent Searches\",\n    \"filters\": \"Filters\",\n    \"tags\": \"Tags\",\n    \"lists\": \"Lists\",\n    \"feeds\": \"Feeds\",\n    \"no_suggestions\": \"No suggestions\"\n  },\n  \"preview\": {\n    \"view_original\": \"View Original\",\n    \"cached_content\": \"Cached Content\",\n    \"reader_view\": \"Reader View\",\n    \"archive_info\": \"Archives may not render correctly inline if they require Javascript. For best results, <1>download it and open in your browser</1>.\",\n    \"fetch_error_title\": \"Content Unavailable\",\n    \"fetch_error_description\": \"We couldn't fetch the content for this link. The page may be protected, require authentication, or be temporarily unavailable.\",\n    \"crawling_in_progress\": \"Fetching page content…\",\n    \"continue_reading\": \"Continue where you left off\",\n    \"continue_reading_percent\": \"Continue where you left off ({{percent}}%)\",\n    \"continue_button\": \"Continue\",\n    \"tabs\": {\n      \"content\": \"Content\",\n      \"details\": \"Details\"\n    }\n  },\n  \"editor\": {\n    \"multiple_urls_dialog_title\": \"Importing URLs as separate Bookmarks?\",\n    \"multiple_urls_dialog_desc\": \"The input contains multiple URLs on separate lines. Do you want to import them as separate bookmarks?\",\n    \"import_as_text\": \"Import as Text Bookmark\",\n    \"import_as_separate_bookmarks\": \"Import as separate Bookmarks\",\n    \"placeholder\": \"Paste a link or an image, write a note or drag and drop an image in here…\",\n    \"placeholder_v2\": \"Paste a link, write a note or drop an image…\",\n    \"new_item\": \"NEW ITEM\",\n    \"disabled_submissions\": \"Submissions are disabled\",\n    \"text_toolbar\": {\n      \"undo\": \"Undo\",\n      \"redo\": \"Redo\",\n      \"bold\": \"Bold\",\n      \"italic\": \"Italic\",\n      \"underline\": \"Underline\",\n      \"strikethrough\": \"Strikethrough\",\n      \"code\": \"Code\",\n      \"highlight\": \"Highlight\",\n      \"align_left\": \"Left Align\",\n      \"align_center\": \"Center Align\",\n      \"align_right\": \"Right Align\",\n      \"markdown_shortcuts\": {\n        \"label\": \"Markdown shortcuts\",\n        \"heading\": {\n          \"label\": \"Heading\",\n          \"example\": \"# H1, ## H2, ### H3\"\n        },\n        \"bold\": {\n          \"label\": \"Bold\",\n          \"example\": \"**text** or CTRL+b\"\n        },\n        \"italic\": {\n          \"label\": \"Italic\",\n          \"example\": \"*Italic* or _Italic_ or CTRL+i\"\n        },\n        \"blockquote\": {\n          \"label\": \"Blockquote\",\n          \"example\": \"> Blockquote\"\n        },\n        \"ordered_list\": {\n          \"label\": \"Ordered List\",\n          \"example\": \"1. List item\"\n        },\n        \"unordered_list\": {\n          \"label\": \"Unordered List\",\n          \"example\": \"- List item\"\n        },\n        \"inline_code\": {\n          \"label\": \"Inline Code\",\n          \"example\": \"`Code`\"\n        },\n        \"block_code\": {\n          \"label\": \"Block Code\",\n          \"example\": \"``` + space\"\n        }\n      }\n    }\n  },\n  \"dialogs\": {\n    \"bookmarks\": {\n      \"delete_confirmation_title\": \"Delete Bookmark?\",\n      \"delete_confirmation_description\": \"Are you sure you want to delete this bookmark?\"\n    }\n  },\n  \"toasts\": {\n    \"bookmarks\": {\n      \"updated\": \"The bookmark has been updated!\",\n      \"deleted\": \"The bookmark has been deleted!\",\n      \"refetch\": \"Re-fetch has been enqueued!\",\n      \"full_page_archive\": \"Full Page Archive creation has been triggered\",\n      \"preserve_pdf\": \"PDF preservation has been triggered\",\n      \"delete_from_list\": \"The bookmark has been deleted from the list\",\n      \"clipboard_copied\": \"Link has been added to your clipboard!\",\n      \"update_banner\": \"Banner has been updated!\",\n      \"uploading_banner\": \"Uploading banner...\"\n    },\n    \"lists\": {\n      \"created\": \"List has been created!\",\n      \"updated\": \"List has been updated!\",\n      \"merged\": \"List has been merged!\",\n      \"deleted\": \"List has been deleted!\"\n    },\n    \"tags\": {\n      \"created\": \"Tag has been created!\",\n      \"failed_to_create\": \"Failed to create tag\"\n    }\n  },\n  \"banners\": {\n    \"no_bookmarks\": {\n      \"title\": \"No bookmarks yet\",\n      \"description\": \"Save interesting articles, links, and pages to access them quickly later.\"\n    }\n  },\n  \"cleanups\": {\n    \"cleanups\": \"Cleanups\",\n    \"duplicate_tags\": {\n      \"title\": \"Duplicate Tags\",\n      \"merge_all_suggestions\": \"Merge all suggestions?\"\n    }\n  },\n  \"bookmark_editor\": {\n    \"title\": \"Edit Bookmark\",\n    \"subtitle\": \"Make changes to the bookmark details. Click save when you're done.\",\n    \"author\": \"Author\",\n    \"publisher\": \"Publisher\",\n    \"date_published\": \"Date Published\",\n    \"pick_a_date\": \"Pick a date\",\n    \"save_changes\": \"Save changes\",\n    \"extracted_content\": \"Extracted Content\"\n  },\n  \"version\": {\n    \"new_release_available\": \"New release notes available\",\n    \"whats_new_title\": \"What's new in v{{version}}\",\n    \"release_notes_description\": \"Here are the latest updates fetched from the GitHub release notes.\",\n    \"loading_release_notes\": \"Loading release notes…\",\n    \"unable_to_load_release_notes\": \"Unable to load release notes right now. Please try again later.\",\n    \"no_release_notes\": \"No release notes were published for this version.\",\n    \"release_notes_synced\": \"Release notes are synced from GitHub.\",\n    \"view_on_github\": \"View on GitHub\"\n  },\n  \"wrapped\": {\n    \"title\": \"Your {{year}} Wrapped\",\n    \"subtitle\": \"A Year in Karakeep\",\n    \"banner\": {\n      \"title\": \"Your 2025 Wrapped is ready!\",\n      \"description\": \"See your year in bookmarks\",\n      \"view_now\": \"View Now\"\n    },\n    \"button\": \"2025 Wrapped\",\n    \"loading\": \"Loading your Wrapped...\",\n    \"failed_to_load\": \"Failed to load your Wrapped stats\",\n    \"sections\": {\n      \"total_saves\": {\n        \"prefix\": \"You saved\",\n        \"suffix\": \"items this year\",\n        \"suffix_singular\": \"item this year\"\n      },\n      \"first_bookmark\": {\n        \"title\": \"Your Journey Started\",\n        \"description\": \"First save of {{year}}:\"\n      },\n      \"top_domains\": \"Your Top Sites\",\n      \"top_tags\": \"Your Top Tags\",\n      \"monthly_activity\": \"Your Year in Saves\",\n      \"most_active_day\": \"Your Most Active Day\",\n      \"peak_times\": {\n        \"title\": \"When You Save\",\n        \"peak_hour\": \"Peak Hour\",\n        \"peak_day\": \"Peak Day\"\n      },\n      \"how_you_save\": \"How You Save\",\n      \"what_you_saved\": \"What You Saved\",\n      \"summary\": {\n        \"favorites\": \"Favorites\",\n        \"tags_created\": \"Tags Created\",\n        \"highlights\": \"Highlights\"\n      },\n      \"types\": {\n        \"links\": \"Links\",\n        \"notes\": \"Notes\",\n        \"assets\": \"Assets\"\n      }\n    },\n    \"footer\": \"Made with Karakeep\",\n    \"share\": \"Share\",\n    \"download\": \"Download\",\n    \"close\": \"Close\",\n    \"generating\": \"Generating...\"\n  }\n}\n"
  },
  {
    "path": "apps/web/lib/i18n/locales/en_US/translation.json",
    "content": "{\n  \"common\": {\n    \"something_went_wrong\": \"Something went wrong\",\n    \"experimental\": \"Experimental\",\n    \"search\": \"Search\",\n    \"tags\": \"Tags\",\n    \"note\": \"Note\",\n    \"attachments\": \"Attachments\",\n    \"highlights\": \"Highlights\",\n    \"source\": \"Source\",\n    \"url\": \"URL\",\n    \"name\": \"Name\",\n    \"email\": \"Email\",\n    \"password\": \"Password\",\n    \"action\": \"Action\",\n    \"actions\": \"Actions\",\n    \"created_at\": \"Created At\",\n    \"updated_at\": \"Updated At\",\n    \"key\": \"Key\",\n    \"role\": \"Role\",\n    \"type\": \"Type\",\n    \"size\": \"Size\",\n    \"roles\": {\n      \"user\": \"User\",\n      \"admin\": \"Admin\"\n    },\n    \"screenshot\": \"Screenshot\",\n    \"pdf\": \"Archived PDF\",\n    \"video\": \"Video\",\n    \"archive\": \"Archive\",\n    \"home\": \"Home\",\n    \"title\": \"Title\",\n    \"description\": \"Description\",\n    \"summary\": \"Summary\",\n    \"bookmark_types\": {\n      \"title\": \"Bookmark Type\",\n      \"link\": \"Link\",\n      \"text\": \"Text\",\n      \"media\": \"Media\"\n    },\n    \"quota\": \"Quota\",\n    \"bookmarks\": \"Bookmarks\",\n    \"storage\": \"Storage\",\n    \"default\": \"Default\",\n    \"id\": \"ID\",\n    \"last_used\": \"Last Used\"\n  },\n  \"layouts\": {\n    \"masonry\": \"Masonry\",\n    \"grid\": \"Grid\",\n    \"list\": \"List\",\n    \"compact\": \"Compact\"\n  },\n  \"actions\": {\n    \"sort\": {\n      \"newest_first\": \"Newest First\",\n      \"oldest_first\": \"Oldest First\",\n      \"title\": \"Sort\",\n      \"relevant_first\": \"Most Relevant First\"\n    },\n    \"change_layout\": \"Change Layout\",\n    \"archive\": \"Archive\",\n    \"unarchive\": \"Un-archive\",\n    \"favorite\": \"Favorite\",\n    \"unfavorite\": \"Unfavorite\",\n    \"delete\": \"Delete\",\n    \"refresh\": \"Refresh\",\n    \"recrawl\": \"Recrawl\",\n    \"offline_copies\": \"Offline Copies\",\n    \"download_full_page_archive\": \"Download Full Page Archive\",\n    \"preserve_as_pdf\": \"Preserve as PDF\",\n    \"edit_tags\": \"Edit Tags\",\n    \"add_to_list\": \"Add to List\",\n    \"select_all\": \"Select All\",\n    \"unselect_all\": \"Unselect All\",\n    \"copy_link\": \"Copy Link\",\n    \"close_bulk_edit\": \"Close Bulk Edit\",\n    \"bulk_edit\": \"Bulk Edit\",\n    \"manage_lists\": \"Manage Lists\",\n    \"remove_from_list\": \"Remove from List\",\n    \"save\": \"Save\",\n    \"add\": \"Add\",\n    \"edit\": \"Edit\",\n    \"open_editor\": \"Open Editor\",\n    \"create\": \"Create\",\n    \"fetch_now\": \"Fetch Now\",\n    \"summarize_with_ai\": \"Summarize with AI\",\n    \"edit_title\": \"Edit Title\",\n    \"sign_out\": \"Sign Out\",\n    \"close\": \"Close\",\n    \"merge\": \"Merge\",\n    \"cancel\": \"Cancel\",\n    \"apply_all\": \"Apply All\",\n    \"ignore\": \"Ignore\",\n    \"toggle_show_archived\": \"Show Archived\",\n    \"confirm\": \"Confirm\",\n    \"regenerate\": \"Regenerate\",\n    \"load_more\": \"Load More\",\n    \"edit_notes\": \"Edit Notes\",\n    \"preserve_offline_archive\": \"Preserve Offline Archive\",\n    \"download_full_page_archive_file\": \"Download Archive File\",\n    \"download_pdf_file\": \"Download PDF File\",\n    \"remove\": \"Remove\",\n    \"more\": \"More\",\n    \"replace_banner\": \"Replace Banner\",\n    \"add_banner\": \"Add Banner\",\n    \"download\": \"Download\"\n  },\n  \"highlights\": {\n    \"no_highlights\": \"You don't have any highlights yet.\"\n  },\n  \"tags\": {\n    \"your_tags\": \"Your Tags\",\n    \"ai_tags\": \"AI Tags\",\n    \"all_tags\": \"All Tags\",\n    \"your_tags_info\": \"Tags that were attached at least once by you\",\n    \"ai_tags_info\": \"Tags that were only attached automatically (by AI)\",\n    \"unused_tags\": \"Unused Tags\",\n    \"unused_tags_info\": \"Tags that are not attached to any bookmarks\",\n    \"delete_all_unused_tags\": \"Delete All Unused Tags\",\n    \"drag_and_drop_merging\": \"Drag & Drop Merging\",\n    \"drag_and_drop_merging_info\": \"Drag and drop tags on each other to merge them\",\n    \"sort_by_name\": \"Sort by Name\",\n    \"create_tag\": \"Create Tag\",\n    \"create_tag_description\": \"Create a new tag without attaching it to any bookmark\",\n    \"tag_name\": \"Tag Name\",\n    \"enter_tag_name\": \"Enter tag name\",\n    \"sort_by_usage\": \"Sort by Usage\",\n    \"sort_by_relevance\": \"Sort by Relevance\",\n    \"no_custom_tags\": \"No custom tags yet\",\n    \"no_ai_tags\": \"No AI tags yet\",\n    \"no_unused_tags\": \"You don't have any unused tags\",\n    \"no_unused_tags_match_your_search\": \"No unused tags match your search\",\n    \"no_tags_match_your_search\": \"No tags match your search\",\n    \"search_placeholder\": \"Search tags...\",\n    \"search_or_create_placeholder\": \"Search or create tags...\"\n  },\n  \"editor\": {\n    \"text_toolbar\": {\n      \"markdown_shortcuts\": {\n        \"ordered_list\": {\n          \"label\": \"Ordered List\",\n          \"example\": \"1. List item\"\n        },\n        \"label\": \"Markdown shortcuts\",\n        \"heading\": {\n          \"label\": \"Heading\",\n          \"example\": \"# H1, ## H2, ### H3\"\n        },\n        \"bold\": {\n          \"label\": \"Bold\",\n          \"example\": \"**text** or CTRL+b\"\n        },\n        \"italic\": {\n          \"label\": \"Italic\",\n          \"example\": \"*Italic* or _Italic_ or CTRL+i\"\n        },\n        \"blockquote\": {\n          \"label\": \"Blockquote\",\n          \"example\": \"> Blockquote\"\n        },\n        \"unordered_list\": {\n          \"label\": \"Unordered List\",\n          \"example\": \"- List item\"\n        },\n        \"inline_code\": {\n          \"label\": \"Inline Code\",\n          \"example\": \"`Code`\"\n        },\n        \"block_code\": {\n          \"label\": \"Block Code\",\n          \"example\": \"``` + space\"\n        }\n      },\n      \"undo\": \"Undo\",\n      \"redo\": \"Redo\",\n      \"bold\": \"Bold\",\n      \"italic\": \"Italic\",\n      \"underline\": \"Underline\",\n      \"strikethrough\": \"Strikethrough\",\n      \"code\": \"Code\",\n      \"highlight\": \"Highlight\",\n      \"align_left\": \"Left Align\",\n      \"align_center\": \"Center Align\",\n      \"align_right\": \"Right Align\"\n    },\n    \"quickly_focus\": \"You can quickly focus on this field by pressing ⌘ + E\",\n    \"multiple_urls_dialog_title\": \"Importing URLs as separate Bookmarks?\",\n    \"multiple_urls_dialog_desc\": \"The input contains multiple URLs on separate lines. Do you want to import them as separate bookmarks?\",\n    \"import_as_text\": \"Import as Text Bookmark\",\n    \"import_as_separate_bookmarks\": \"Import as separate Bookmarks\",\n    \"placeholder\": \"Paste a link or an image, write a note or drag and drop an image in here…\",\n    \"placeholder_v2\": \"Paste a link, write a note or drop an image…\",\n    \"new_item\": \"NEW ITEM\",\n    \"disabled_submissions\": \"Submissions are disabled\"\n  },\n  \"bookmark_editor\": {\n    \"title\": \"Edit Bookmark\",\n    \"subtitle\": \"Make changes to the bookmark details. Click save when you're done.\",\n    \"author\": \"Author\",\n    \"publisher\": \"Publisher\",\n    \"date_published\": \"Date Published\",\n    \"pick_a_date\": \"Pick a date\",\n    \"save_changes\": \"Save changes\",\n    \"extracted_content\": \"Extracted Content\"\n  },\n  \"settings\": {\n    \"back_to_app\": \"Back To App\",\n    \"user_settings\": \"User Settings\",\n    \"info\": {\n      \"user_info\": \"User Info\",\n      \"basic_details\": \"Basic Details\",\n      \"change_password\": \"Change Password\",\n      \"current_password\": \"Current Password\",\n      \"new_password\": \"New Password\",\n      \"confirm_new_password\": \"Confirm New Password\",\n      \"options\": \"Options\",\n      \"interface_lang\": \"Interface Language\",\n      \"avatar\": {\n        \"title\": \"Profile Photo\",\n        \"description\": \"Upload a square image to use as your avatar.\",\n        \"upload\": \"Upload avatar\",\n        \"change\": \"Change avatar\",\n        \"remove\": \"Remove avatar\",\n        \"remove_confirm_title\": \"Remove avatar?\",\n        \"remove_confirm_description\": \"This will clear your current profile photo.\",\n        \"updated\": \"Avatar updated\",\n        \"removed\": \"Avatar removed\"\n      },\n      \"user_settings\": {\n        \"user_settings_updated\": \"User settings have been updated!\",\n        \"bookmark_click_action\": {\n          \"title\": \"Bookmark Click Action\",\n          \"open_external_url\": \"Open Original URL\",\n          \"open_bookmark_details\": \"Open Bookmark Details\"\n        },\n        \"archive_display_behaviour\": {\n          \"title\": \"Archived Bookmarks\",\n          \"show\": \"Show archived bookmarks in tags and lists\",\n          \"hide\": \"Hide archived bookmarks in tags and lists\"\n        }\n      },\n      \"reader_settings\": {\n        \"local_overrides_title\": \"Device-specific settings active\",\n        \"using_default\": \"Using client default\",\n        \"clear_override_hint\": \"Clear device override to use global setting ({{value}})\",\n        \"font_size\": \"Font Size\",\n        \"font_family\": \"Font Family\",\n        \"preview_inline\": \"(preview)\",\n        \"tooltip_preview\": \"Unsaved preview changes\",\n        \"save_to_all_devices\": \"All devices\",\n        \"tooltip_local\": \"Device settings differ from global\",\n        \"reset_preview\": \"Reset preview\",\n        \"mono\": \"Monospace\",\n        \"line_height\": \"Line Height\",\n        \"tooltip_default\": \"Reading settings\",\n        \"title\": \"Reader Settings\",\n        \"serif\": \"Serif\",\n        \"preview\": \"Preview\",\n        \"not_set\": \"Not set\",\n        \"clear_local_overrides\": \"Clear device settings\",\n        \"preview_text\": \"The quick brown fox jumps over the lazy dog. This is how your reader view text will appear.\",\n        \"local_overrides_cleared\": \"Device-specific settings have been cleared\",\n        \"local_overrides_description\": \"This device has reader settings that differ from your global defaults:\",\n        \"clear_defaults\": \"Clear all defaults\",\n        \"description\": \"Configure default text settings for the reader view. These settings sync across all your devices.\",\n        \"defaults_cleared\": \"Reader defaults have been cleared\",\n        \"save_hint\": \"Save settings for this device only or sync across all devices\",\n        \"save_as_default\": \"Save as default\",\n        \"save_to_device\": \"This device\",\n        \"sans\": \"Sans Serif\",\n        \"tooltip_preview_and_local\": \"Unsaved preview changes; device settings differ from global\",\n        \"adjust_hint\": \"Adjust settings above to preview changes\"\n      }\n    },\n    \"ai\": {\n      \"ai_settings\": \"AI Settings\",\n      \"tagging_rules\": \"Tagging Rules\",\n      \"tagging_rule_description\": \"Prompts that you add here will be included as rules to the model during tag generation. You can view the final prompts in the prompt preview section.\",\n      \"prompt_preview\": \"Prompt Preview\",\n      \"text_prompt\": \"Text Prompt\",\n      \"images_prompt\": \"Image Prompt\",\n      \"summarization_prompt\": \"Summarization Prompt\",\n      \"all_tagging\": \"All Tagging\",\n      \"text_tagging\": \"Text Tagging\",\n      \"image_tagging\": \"Image Tagging\",\n      \"summarization\": \"Summarization\",\n      \"tag_style\": \"Tag Style\",\n      \"auto_summarization_description\": \"Automatically generate summaries for your bookmarks using AI.\",\n      \"auto_tagging\": \"Auto-tagging\",\n      \"titlecase_spaces\": \"Title case with spaces\",\n      \"lowercase_underscores\": \"Lowercase with underscores\",\n      \"inference_language\": \"Inference Language\",\n      \"titlecase_hyphens\": \"Title case with hyphens\",\n      \"lowercase_hyphens\": \"Lowercase with hyphens\",\n      \"lowercase_spaces\": \"Lowercase with spaces\",\n      \"inference_language_description\": \"Choose language for AI-generated tags and summaries.\",\n      \"tag_style_description\": \"Choose how your auto-generated tags should be formatted.\",\n      \"auto_tagging_description\": \"Automatically generate tags for your bookmarks using AI.\",\n      \"camelCase\": \"camelCase\",\n      \"auto_summarization\": \"Auto-summarization\",\n      \"no_preference\": \"No preference\",\n      \"curated_tags\": \"Curated Tags\",\n      \"curated_tags_description\": \"Optionally restrict AI tagging to only use tags from this list. When no tags are selected, the AI generates tags freely.\",\n      \"curated_tags_updated\": \"Curated tags updated successfully!\",\n      \"curated_tags_update_failed\": \"Failed to update curated tags\"\n    },\n    \"feeds\": {\n      \"rss_subscriptions\": \"RSS Subscriptions\",\n      \"add_a_subscription\": \"Add a Subscription\",\n      \"feed_enabled\": \"RSS Feed enabled\",\n      \"feed_disabled\": \"RSS Feed disabled\"\n    },\n    \"webhooks\": {\n      \"webhooks\": \"Webhooks\",\n      \"description\": \"You can use webhooks to trigger actions when bookmarks are created, changed or crawled.\",\n      \"events\": {\n        \"title\": \"Events\",\n        \"crawled\": \"Crawled\",\n        \"created\": \"Created\",\n        \"edited\": \"Edited\",\n        \"deleted\": \"Deleted\"\n      },\n      \"auth_token\": \"Auth Token\",\n      \"delete_webhook\": \"Delete Webhook\",\n      \"add_auth_token\": \"Add Auth Token\",\n      \"edit_auth_token\": \"Edit Auth Token\",\n      \"create_webhook\": \"Create Webhook\",\n      \"delete_webhook_confirmation\": \"Are you sure you want to delete this webhook?\",\n      \"edit_webhook\": \"Edit Webhook\",\n      \"webhook_url\": \"Webhook URL\"\n    },\n    \"import\": {\n      \"import_export\": \"Import / Export\",\n      \"import_export_bookmarks\": \"Import / Export Bookmarks\",\n      \"import_bookmarks_from_html_file\": \"Import Bookmarks from HTML file\",\n      \"import_bookmarks_from_pocket_export\": \"Import Bookmarks from Pocket export\",\n      \"import_bookmarks_from_matter_export\": \"Import Bookmarks from Matter export\",\n      \"import_bookmarks_from_omnivore_export\": \"Import Bookmarks from Omnivore export\",\n      \"import_bookmarks_from_linkwarden_export\": \"Import Bookmarks from Linkwarden export\",\n      \"import_bookmarks_from_karakeep_export\": \"Import Bookmarks from Karakeep export\",\n      \"import_bookmarks_from_tab_session_manager_export\": \"Import Bookmarks from Tab Session Manager\",\n      \"export_links_and_notes\": \"Export Links and Notes\",\n      \"imported_bookmarks\": \"Imported Bookmarks\",\n      \"import_bookmarks_from_mymind_export\": \"Import Bookmarks from mymind export\",\n      \"import_bookmarks_from_instapaper_export\": \"Import Bookmarks from Instapaper export\"\n    },\n    \"api_keys\": {\n      \"api_keys\": \"API Keys\",\n      \"new_api_key\": \"New API Key\",\n      \"new_api_key_desc\": \"Give your API key a unique name\",\n      \"key_success\": \"Key was successfully created\",\n      \"key_success_please_copy\": \"Please copy the key and store it somewhere safe. Once you close the dialog, you won't be able to access it again.\",\n      \"regenerate_api_key\": \"Regenerate API Key\",\n      \"key_regenerated\": \"Key was successfully regenerated\",\n      \"key_regenerated_please_copy\": \"Please copy the new key and store it somewhere safe. The old key has been revoked and will no longer work.\",\n      \"regenerate_warning\": \"Are you sure you want to regenerate the API key \\\"{{name}}\\\"?\",\n      \"regenerate_confirmation\": \"This will revoke the current key and generate a new one. Any applications using the current key will stop working.\"\n    },\n    \"broken_links\": {\n      \"broken_links\": \"Broken Links\",\n      \"last_crawled_at\": \"Last Crawled At\",\n      \"crawling_status\": \"Crawling Status\",\n      \"crawling_failed\": \"Crawling Failed\"\n    },\n    \"manage_assets\": {\n      \"manage_assets\": \"Manage Assets\",\n      \"no_assets\": \"You don't have any assets yet.\",\n      \"asset_type\": \"Asset Type\",\n      \"bookmark_link\": \"Bookmark Link\",\n      \"asset_link\": \"Asset Link\",\n      \"delete_asset\": \"Delete Asset\",\n      \"delete_asset_confirmation\": \"Are you sure you want to delete this asset?\"\n    },\n    \"rules\": {\n      \"rules\": \"Rule Engine\",\n      \"rule_name\": \"Rule Name\",\n      \"description\": \"You can use rules to trigger actions when an event fires.\",\n      \"ceate_rule\": \"Create Rule\",\n      \"edit_rule\": \"Edit Rule\",\n      \"save_rule\": \"Save Rule\",\n      \"delete_rule\": \"Delete Rule\",\n      \"delete_rule_confirmation\": \"Are you sure you want to delete this rule?\",\n      \"whenever\": \"Whenever ...\",\n      \"if\": \"If ...\",\n      \"enter_rule_name\": \"Enter rule name\",\n      \"describe_what_this_rule_does\": \"Describe what this rule does\",\n      \"rule_has_been_created\": \"Rule has been created!\",\n      \"rule_has_been_updated\": \"Rule has been updated!\",\n      \"rule_has_been_deleted\": \"Rule has been deleted!\",\n      \"no_rules_created_yet\": \"No rules created yet\",\n      \"create_your_first_rule\": \"Create your first rule to automate your workflow\",\n      \"conditions_types\": {\n        \"always\": \"Always\",\n        \"url_contains\": \"URL Contains\",\n        \"imported_from_feed\": \"Imported From Feed\",\n        \"bookmark_type_is\": \"Bookmark Type Is\",\n        \"has_tag\": \"Has Tag\",\n        \"is_favourited\": \"Is Favorited\",\n        \"is_archived\": \"Is Archived\",\n        \"and\": \"All of the following are true\",\n        \"or\": \"Any of the following are true\",\n        \"url_does_not_contain\": \"URL Does Not Contain\",\n        \"title_contains\": \"Title Contains\",\n        \"title_does_not_contain\": \"Title Does Not Contain\"\n      },\n      \"actions_types\": {\n        \"add_tag\": \"Add Tag\",\n        \"remove_tag\": \"Remove Tag\",\n        \"add_to_list\": \"Add to List\",\n        \"remove_from_list\": \"Remove from List\",\n        \"download_full_page_archive\": \"Download Full Page Archive\",\n        \"favourite_bookmark\": \"Favorite Bookmark\",\n        \"archive_bookmark\": \"Archive Bookmark\"\n      },\n      \"events_types\": {\n        \"bookmark_added\": \"A bookmark is added\",\n        \"tag_added\": \"This tag is added to a bookmark\",\n        \"tag_removed\": \"This tag is removed from a bookmark\",\n        \"added_to_list\": \"A bookmark is added to this list\",\n        \"removed_from_list\": \"A bookmark is removed from this list\",\n        \"favourited\": \"A bookmark is favorited\",\n        \"archived\": \"A bookmark is archived\"\n      }\n    },\n    \"stats\": {\n      \"usage_statistics\": \"Usage Statistics\",\n      \"insights_description\": \"Insights into your bookmarking habits and collection\",\n      \"failed_to_load\": \"Failed to load statistics\",\n      \"overview\": {\n        \"total_bookmarks\": \"Total Bookmarks\",\n        \"all_saved_items\": \"All saved items\",\n        \"favorites\": \"Favorites\",\n        \"starred_bookmarks\": \"Starred bookmarks\",\n        \"archived\": \"Archived\",\n        \"archived_items\": \"Archived items\",\n        \"tags\": \"Tags\",\n        \"unique_tags_created\": \"Unique tags created\",\n        \"lists\": \"Lists\",\n        \"bookmark_collections\": \"Bookmark collections\",\n        \"highlights\": \"Highlights\",\n        \"text_highlights\": \"Text highlights\",\n        \"storage_used\": \"Storage Used\",\n        \"total_asset_storage\": \"Total asset storage\",\n        \"this_month\": \"This Month\",\n        \"bookmarks_added\": \"Bookmarks added\"\n      },\n      \"bookmark_types\": {\n        \"title\": \"Bookmark Types\",\n        \"links\": \"Links\",\n        \"text_notes\": \"Text Notes\",\n        \"assets\": \"Assets\"\n      },\n      \"recent_activity\": {\n        \"title\": \"Recent Activity\",\n        \"this_week\": \"This Week\",\n        \"this_month\": \"This Month\",\n        \"this_year\": \"This Year\"\n      },\n      \"top_domains\": {\n        \"title\": \"Top Domains\",\n        \"no_domains_found\": \"No domains found\"\n      },\n      \"most_used_tags\": {\n        \"title\": \"Most Used Tags\",\n        \"no_tags_found\": \"No tags found\"\n      },\n      \"activity_patterns\": {\n        \"activity_by_hour\": \"Activity by Hour\",\n        \"activity_by_day\": \"Activity by Day\"\n      },\n      \"storage_breakdown\": {\n        \"title\": \"Storage Breakdown\"\n      },\n      \"bookmark_sources\": {\n        \"title\": \"Bookmark Sources\",\n        \"empty\": \"No source data available\"\n      }\n    },\n    \"subscription\": {\n      \"subscription\": \"Subscription\",\n      \"manage_subscription\": \"Manage your subscription and billing information\",\n      \"current_plan\": \"Current Plan\",\n      \"billing_period\": \"Billing Period\",\n      \"paid_plan\": \"Paid Plan\",\n      \"unlock_bigger_quota\": \"Unlock bigger quota and support the project\",\n      \"subscribe_now\": \"Subscribe Now\",\n      \"manage_billing\": \"Manage Billing\",\n      \"subscription_canceled\": \"Your subscription has been canceled and will end on {{date}}. You can resubscribe at any time.\",\n      \"usage_quotas\": \"Usage & Quotas\",\n      \"track_usage\": \"Track your current usage against your plan limits\",\n      \"total_bookmarks_saved\": \"Total bookmarks saved\",\n      \"assets_file_storage\": \"Assets and file storage\",\n      \"unlimited_usage\": \"Unlimited usage\",\n      \"quota_limit_reached\": \"Quota limit reached\",\n      \"approaching_quota_limit\": \"Approaching quota limit\",\n      \"loading_usage\": \"Loading usage information...\",\n      \"free\": \"Free\",\n      \"paid\": \"Paid\"\n    },\n    \"import_sessions\": {\n      \"title\": \"Import Sessions\",\n      \"description\": \"View and manage your bulk import sessions. Sessions are automatically created when you import bookmarks.\",\n      \"load_error\": \"Failed to load import sessions\",\n      \"no_sessions\": \"No import sessions yet\",\n      \"no_sessions_detail\": \"Import sessions will appear here automatically when you import bookmarks\",\n      \"created_at\": \"Created {{time}}\",\n      \"progress\": \"Progress\",\n      \"status\": {\n        \"pending\": \"Pending\",\n        \"in_progress\": \"In progress\",\n        \"completed\": \"Completed\",\n        \"failed\": \"Failed\",\n        \"processing\": \"Processing\",\n        \"staging\": \"Staging\",\n        \"running\": \"Running\",\n        \"paused\": \"Paused\"\n      },\n      \"badges\": {\n        \"pending\": \"{{count}} pending\",\n        \"processing\": \"{{count}} processing\",\n        \"completed\": \"{{count}} completed\",\n        \"failed\": \"{{count}} failed\"\n      },\n      \"imported_to\": \"Imported to:\",\n      \"view_list\": \"View List\",\n      \"delete_dialog_title\": \"Delete Import Session\",\n      \"delete_dialog_description\": \"Are you sure you want to delete \\\"{{name}}\\\"? This action cannot be undone. The bookmarks themselves will not be deleted.\",\n      \"delete_session\": \"Delete Session\",\n      \"pause_session\": \"Pause\",\n      \"resume_session\": \"Resume\",\n      \"view_details\": \"View Details\",\n      \"detail\": {\n        \"page_title\": \"Import Session Details\",\n        \"back_to_import\": \"Back to Import\",\n        \"filter_all\": \"All\",\n        \"filter_accepted\": \"Accepted\",\n        \"filter_rejected\": \"Rejected\",\n        \"filter_duplicates\": \"Duplicates\",\n        \"filter_pending\": \"Pending\",\n        \"table_title\": \"Title / URL\",\n        \"table_type\": \"Type\",\n        \"table_result\": \"Result\",\n        \"table_reason\": \"Reason\",\n        \"table_bookmark\": \"Bookmark\",\n        \"result_accepted\": \"Accepted\",\n        \"result_rejected\": \"Rejected\",\n        \"result_skipped_duplicate\": \"Duplicate\",\n        \"result_pending\": \"Pending\",\n        \"result_processing\": \"Processing\",\n        \"no_results\": \"No results found for this filter.\",\n        \"view_bookmark\": \"View Bookmark\",\n        \"load_more\": \"Load More\",\n        \"no_title\": \"No title\"\n      }\n    },\n    \"backups\": {\n      \"backups\": \"Backups\",\n      \"page_title\": \"Backups\",\n      \"page_description\": \"Automatically create and manage backups of your bookmarks. Backups are compressed and stored securely.\",\n      \"configuration\": {\n        \"title\": \"Backup Configuration\",\n        \"enable_automatic_backups\": \"Enable Automatic Backups\",\n        \"enable_automatic_backups_description\": \"Automatically create backups of your bookmarks\",\n        \"backup_frequency\": \"Backup Frequency\",\n        \"backup_frequency_description\": \"How often backups should be created\",\n        \"retention_period\": \"Retention Period (days)\",\n        \"retention_period_description\": \"How many days to keep backups before deleting them\",\n        \"frequency\": {\n          \"daily\": \"Daily\",\n          \"weekly\": \"Weekly\"\n        },\n        \"select_frequency\": \"Select frequency\",\n        \"save_settings\": \"Save Settings\"\n      },\n      \"list\": {\n        \"title\": \"Your Backups\",\n        \"create_backup_now\": \"Create Backup Now\",\n        \"no_backups\": \"You don't have any backups yet. Enable automatic backups or create one manually.\",\n        \"table\": {\n          \"created_at\": \"Created At\",\n          \"bookmarks\": \"Bookmarks\",\n          \"size\": \"Size\",\n          \"status\": \"Status\",\n          \"actions\": \"Actions\"\n        },\n        \"status\": {\n          \"success\": \"Success\",\n          \"failed\": \"Failed\",\n          \"pending\": \"Pending\"\n        },\n        \"actions\": {\n          \"download_backup\": \"Download Backup\",\n          \"delete_backup\": \"Delete Backup\"\n        }\n      },\n      \"dialogs\": {\n        \"delete_backup_title\": \"Delete Backup?\",\n        \"delete_backup_description\": \"Are you sure you want to delete this backup? This action cannot be undone.\"\n      },\n      \"toasts\": {\n        \"backup_queued\": \"Backup job has been queued! It will be processed shortly.\",\n        \"backup_deleted\": \"Backup has been deleted!\"\n      }\n    }\n  },\n  \"admin\": {\n    \"admin_settings\": \"Admin Settings\",\n    \"server_stats\": {\n      \"server_stats\": \"Server Stats\",\n      \"total_users\": \"Total Users\",\n      \"total_bookmarks\": \"Total Bookmarks\",\n      \"server_version\": \"Server Version\"\n    },\n    \"background_jobs\": {\n      \"background_jobs\": \"Background Jobs\",\n      \"crawler_jobs\": \"Crawler Jobs\",\n      \"indexing_jobs\": \"Indexing Jobs\",\n      \"inference_jobs\": \"Inference Jobs\",\n      \"tidy_assets_jobs\": \"Tidy Assets Jobs\",\n      \"video_jobs\": \"Video Download Jobs\",\n      \"webhook_jobs\": \"Webhook Jobs\",\n      \"asset_preprocessing_jobs\": \"Asset Preprocessing Jobs\",\n      \"feed_jobs\": \"RSS Feed Jobs\",\n      \"job\": \"Job\",\n      \"queued\": \"Queued\",\n      \"pending\": \"Pending\",\n      \"failed\": \"Failed\",\n      \"jobs\": {\n        \"crawler\": {\n          \"title\": \"Crawler Jobs\",\n          \"description\": \"Web crawling and content extraction from URLs\"\n        },\n        \"inference\": {\n          \"title\": \"Inference Jobs\",\n          \"description\": \"AI-powered tagging and summarization of content\"\n        },\n        \"indexing\": {\n          \"title\": \"Indexing Jobs\",\n          \"description\": \"Search index updates\"\n        },\n        \"asset_preprocessing\": {\n          \"title\": \"Asset Preprocessing Jobs\",\n          \"description\": \"Image and document preprocessing (screenshots, text extraction, etc.)\"\n        },\n        \"tidy_assets\": {\n          \"title\": \"Tidy Assets Jobs\",\n          \"description\": \"Asset cleanup and storage optimization\"\n        },\n        \"video\": {\n          \"title\": \"Video Download Jobs\",\n          \"description\": \"Video extraction and download\"\n        },\n        \"webhook\": {\n          \"title\": \"Webhook Jobs\",\n          \"description\": \"External webhook notifications\"\n        },\n        \"feed\": {\n          \"title\": \"RSS Feed Jobs\",\n          \"description\": \"RSS feed processing and content updates\"\n        },\n        \"admin_maintenance\": {\n          \"title\": \"Admin Maintenance Jobs\",\n          \"description\": \"Administrative cleanup and asset maintenance\"\n        }\n      },\n      \"monitor_and_manage\": \"Monitor and manage background job queues and system processing tasks\",\n      \"active\": \"Active\",\n      \"available_actions\": \"Available Actions\",\n      \"status\": {\n        \"title\": \"Understanding Job States\",\n        \"queued\": {\n          \"title\": \"Queued\",\n          \"description\": \"Jobs waiting in line to be processed. They will start automatically when resources are available.\"\n        },\n        \"unprocessed\": {\n          \"title\": \"Unprocessed\",\n          \"description\": \"Bookmarks that have not yet been processed. They are most likely already queued for processing, if not, you might need to manually re-enqueue them.\"\n        },\n        \"failed\": {\n          \"title\": \"Failed\",\n          \"description\": \"Bookmarks that encountered errors during processing. These may need manual attention.\"\n        }\n      },\n      \"actions\": {\n        \"recrawl_failed_links_only\": \"Recrawl Failed Links Only\",\n        \"recrawl_all_links\": \"Recrawl All Links\",\n        \"without_inference\": \"Without Inference\",\n        \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Regenerate AI Tags for Failed Bookmarks Only\",\n        \"regenerate_ai_tags_for_all_bookmarks\": \"Regenerate AI Tags for All Bookmarks\",\n        \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Regenerate AI Summaries for Failed Bookmarks Only\",\n        \"regenerate_ai_summaries_for_all_bookmarks\": \"Regenerate AI Summaries for All Bookmarks\",\n        \"reindex_all_bookmarks\": \"Reindex All Bookmarks\",\n        \"clean_assets\": \"Clean Dangling Assets & Re-sync Metadata\",\n        \"migrate_large_link_html_content\": \"Move Large Inline HTML Content to Assets\",\n        \"reprocess_assets_fix_mode\": \"Reprocess Unprocessed Assets\",\n        \"recrawl_pending_links_only\": \"Recrawl Pending Links Only\",\n        \"regenerate_ai_tags_for_pending_bookmarks_only\": \"Regenerate AI Tags for Pending Bookmarks Only\",\n        \"regenerate_ai_summaries_for_pending_bookmarks_only\": \"Regenerate AI Summaries for Pending Bookmarks Only\"\n      }\n    },\n    \"actions\": {\n      \"recrawl_failed_links_only\": \"Recrawl Failed Links Only\",\n      \"recrawl_all_links\": \"Recrawl All Links\",\n      \"without_inference\": \"Without Inference\",\n      \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Regenerate AI Tags for Failed Bookmarks Only\",\n      \"regenerate_ai_tags_for_all_bookmarks\": \"Regenerate AI Tags for All Bookmarks\",\n      \"reindex_all_bookmarks\": \"Reindex All Bookmarks\",\n      \"compact_assets\": \"Compact Assets\",\n      \"reprocess_assets_fix_mode\": \"Reprocess Assets (Fix Mode)\",\n      \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Regenerate AI Summaries for Failed Bookmarks Only\",\n      \"regenerate_ai_summaries_for_all_bookmarks\": \"Regenerate AI Summaries for All Bookmarks\"\n    },\n    \"users_list\": {\n      \"users_list\": \"Users List\",\n      \"create_user\": \"Create User\",\n      \"change_role\": \"Change Role\",\n      \"reset_password\": \"Reset Password\",\n      \"delete_user\": \"Delete User\",\n      \"num_bookmarks\": \"Num Bookmarks\",\n      \"asset_sizes\": \"Asset Sizes\",\n      \"local_user\": \"Local User\",\n      \"confirm_password\": \"Confirm Password\",\n      \"delete_user_confirm_description\": \"Are you sure you want to delete user \\\"{{name}}\\\"?\",\n      \"unlimited\": \"Unlimited\"\n    },\n    \"service_connections\": {\n      \"title\": \"Service Connections\",\n      \"description\": \"Monitor the health and connectivity of external system dependencies\",\n      \"search_engine\": \"Search Engine\",\n      \"browser\": \"Browser\",\n      \"queue_system\": \"Queue System\",\n      \"status\": {\n        \"not_configured\": \"Not Configured\",\n        \"connected\": \"Connected\",\n        \"disconnected\": \"Disconnected\"\n      }\n    },\n    \"admin_tools\": {\n      \"admin_tools\": \"Admin Tools\",\n      \"bookmark_debugger\": \"Bookmark Debugger\",\n      \"bookmark_id\": \"Bookmark ID\",\n      \"bookmark_id_placeholder\": \"Enter bookmark ID\",\n      \"lookup\": \"Lookup\",\n      \"debug_info\": \"Debug Information\",\n      \"basic_info\": \"Basic Information\",\n      \"status\": \"Status\",\n      \"content\": \"Content\",\n      \"html_preview\": \"HTML Preview (First 1000 chars)\",\n      \"summary\": \"Summary\",\n      \"url\": \"URL\",\n      \"source_url\": \"Source URL\",\n      \"asset_type\": \"Asset Type\",\n      \"file_name\": \"File Name\",\n      \"owner_user_id\": \"Owner User ID\",\n      \"tagging_status\": \"Tagging Status\",\n      \"summarization_status\": \"Summarization Status\",\n      \"crawl_status\": \"Crawl Status\",\n      \"crawl_status_code\": \"HTTP Status Code\",\n      \"crawled_at\": \"Crawled At\",\n      \"recrawl\": \"Re-crawl\",\n      \"reindex\": \"Re-index\",\n      \"retag\": \"Re-tag\",\n      \"resummarize\": \"Re-summarize\",\n      \"bookmark_not_found\": \"Bookmark not found\",\n      \"action_success\": \"Action completed successfully\",\n      \"action_failed\": \"Action failed\",\n      \"recrawl_queued\": \"Re-crawl job has been queued\",\n      \"reindex_queued\": \"Re-index job has been queued\",\n      \"retag_queued\": \"Re-tag job has been queued\",\n      \"resummarize_queued\": \"Re-summarize job has been queued\",\n      \"view\": \"View\",\n      \"fetch_error\": \"Error fetching bookmark\"\n    }\n  },\n  \"options\": {\n    \"dark_mode\": \"Dark Mode\",\n    \"light_mode\": \"Light Mode\",\n    \"apps_extensions\": \"Apps & Extensions\",\n    \"documentation\": \"Documentation\",\n    \"follow_us_on_x\": \"Follow us on X\"\n  },\n  \"lists\": {\n    \"all_lists\": \"All Lists\",\n    \"favourites\": \"Favorites\",\n    \"new_list\": \"New List\",\n    \"edit_list\": \"Edit List\",\n    \"new_nested_list\": \"New Nested List\",\n    \"merge_list\": \"Merge List\",\n    \"destination_list\": \"Destination List\",\n    \"delete_after_merge\": \"Delete original list after merge\",\n    \"no_destination\": \"No Destination\",\n    \"parent_list\": \"Parent List\",\n    \"no_parent\": \"No Parent\",\n    \"list_type\": \"List Type\",\n    \"manual_list\": \"Manual List\",\n    \"smart_list\": \"Smart List\",\n    \"search_query\": \"Search Query\",\n    \"search_query_help\": \"Learn more about the search query language.\",\n    \"description\": \"Description (Optional)\",\n    \"share_list\": \"Share List\",\n    \"rss\": {\n      \"title\": \"RSS Feed\",\n      \"description\": \"Enable an RSS feed for this list\",\n      \"feed_url\": \"RSS Feed URL\"\n    },\n    \"public_list\": {\n      \"title\": \"Public List\",\n      \"description\": \"Allow others to view this list\",\n      \"share_link\": \"Share Link\"\n    },\n    \"delete_list\": {\n      \"title\": \"Delete List\",\n      \"description\": \"Deleting a list doesn't delete any bookmarks in that list.\",\n      \"delete_children\": \"Delete children lists (recursively)\",\n      \"delete_children_description\": \"If not checked, all direct children lists will become root lists\"\n    },\n    \"shared\": \"Shared\",\n    \"collaborators\": {\n      \"manage\": \"Manage Collaborators\",\n      \"view\": \"View Collaborators\",\n      \"collaborators\": \"Collaborators\",\n      \"add\": \"Add Collaborator\",\n      \"current\": \"Current Collaborators\",\n      \"enter_email\": \"Enter email address\",\n      \"please_enter_email\": \"Please enter an email address\",\n      \"added_successfully\": \"Collaborator added successfully\",\n      \"failed_to_add\": \"Failed to add collaborator\",\n      \"removed\": \"Collaborator removed\",\n      \"failed_to_remove\": \"Failed to remove collaborator\",\n      \"role_updated\": \"Role updated\",\n      \"failed_to_update_role\": \"Failed to update role\",\n      \"viewer\": \"Viewer\",\n      \"editor\": \"Editor\",\n      \"owner\": \"Owner\",\n      \"viewer_description\": \"Can view bookmarks in the list\",\n      \"editor_description\": \"Can add and remove bookmarks\",\n      \"no_collaborators\": \"No collaborators yet. Add someone to start collaborating!\",\n      \"no_collaborators_readonly\": \"No collaborators for this list.\",\n      \"people_with_access\": \"People who have access to this list\",\n      \"add_or_remove\": \"Add or remove people who can access this list\",\n      \"invitation_sent\": \"Invitation sent successfully\",\n      \"invitation_revoked\": \"Invitation revoked\",\n      \"failed_to_revoke\": \"Failed to revoke invitation\",\n      \"pending\": \"Pending\",\n      \"revoke\": \"Revoke\",\n      \"declined\": \"Declined\"\n    },\n    \"leave_list\": {\n      \"title\": \"Leave List\",\n      \"confirm_message\": \"Are you sure you want to leave {{icon}} {{name}}?\",\n      \"warning\": \"You will no longer be able to view or access bookmarks in this list. The list owner can add you back if needed.\",\n      \"action\": \"Leave List\",\n      \"success\": \"You have left \\\"{{icon}} {{name}}\\\"\"\n    },\n    \"invitations\": {\n      \"pending\": \"Pending Invitations\",\n      \"description\": \"Review and respond to list collaboration invitations\",\n      \"invited_by\": \"Invited by\",\n      \"accept\": \"Accept\",\n      \"decline\": \"Decline\",\n      \"accepted\": \"Invitation accepted\",\n      \"declined\": \"Invitation declined\",\n      \"failed_to_accept\": \"Failed to accept invitation\",\n      \"failed_to_decline\": \"Failed to decline invitation\"\n    },\n    \"shared_lists\": \"Shared Lists\"\n  },\n  \"search\": {\n    \"is_favorited\": \"Is Favorited\",\n    \"is_not_favorited\": \"Is Not Favorited\",\n    \"is_archived\": \"Is Archived\",\n    \"is_not_archived\": \"Is Not Archived\",\n    \"has_any_tag\": \"Has Any Tag\",\n    \"has_no_tags\": \"Has No Tag\",\n    \"is_in_any_list\": \"Is In Any List\",\n    \"is_not_in_any_list\": \"Is not In Any List\",\n    \"created_on_or_after\": \"Created on or After\",\n    \"not_created_on_or_after\": \"Not Created on or After\",\n    \"created_on_or_before\": \"Created on or Before\",\n    \"not_created_on_or_before\": \"Not Created on or Before\",\n    \"created_within\": \"Created Within\",\n    \"created_earlier_than\": \"Created Earlier Than\",\n    \"day_s\": \" Day(s)\",\n    \"week_s\": \" Week(s)\",\n    \"month_s\": \" Month(s)\",\n    \"year_s\": \" Year(s)\",\n    \"day_s_ago\": \" Day(s) Ago\",\n    \"week_s_ago\": \" Week(s) Ago\",\n    \"month_s_ago\": \" Month(s) Ago\",\n    \"year_s_ago\": \" Year(s) Ago\",\n    \"url_contains\": \"URL Contains\",\n    \"is_in_list\": \"Is In List\",\n    \"is_not_in_list\": \"Is not In List\",\n    \"has_tag\": \"Has Tag\",\n    \"does_not_have_tag\": \"Does Not Have Tag\",\n    \"full_text_search\": \"Full Text Search\",\n    \"type_is\": \"Type is\",\n    \"type_is_not\": \"Type is not\",\n    \"url_does_not_contain\": \"URL Does Not Contain\",\n    \"title_contains\": \"Title Contains\",\n    \"title_does_not_contain\": \"Title Does Not Contain\",\n    \"is_from_feed\": \"Is from RSS Feed\",\n    \"is_not_from_feed\": \"Is not from RSS Feed\",\n    \"and\": \"And\",\n    \"or\": \"Or\",\n    \"history\": \"Recent Searches\",\n    \"filters\": \"Filters\",\n    \"tags\": \"Tags\",\n    \"lists\": \"Lists\",\n    \"no_suggestions\": \"No suggestions\",\n    \"is_broken_link\": \"Has Broken Link\",\n    \"is_not_broken_link\": \"Has Working Link\",\n    \"feeds\": \"Feeds\",\n    \"is_from_source\": \"Source is\",\n    \"is_not_from_source\": \"Source is not\"\n  },\n  \"preview\": {\n    \"view_original\": \"View Original\",\n    \"cached_content\": \"Cached Content\",\n    \"reader_view\": \"Reader View\",\n    \"tabs\": {\n      \"content\": \"Content\",\n      \"details\": \"Details\"\n    },\n    \"archive_info\": \"Archives may not render correctly inline if they require Javascript. For best results, <1>download it and open in your browser</1>.\",\n    \"fetch_error_title\": \"Content Unavailable\",\n    \"fetch_error_description\": \"We couldn't fetch the content for this link. The page may be protected, require authentication, or be temporarily unavailable.\",\n    \"crawling_in_progress\": \"Fetching page content…\",\n    \"continue_reading\": \"Continue where you left off\",\n    \"continue_reading_percent\": \"Continue where you left off ({{percent}}%)\",\n    \"continue_button\": \"Continue\"\n  },\n  \"toasts\": {\n    \"bookmarks\": {\n      \"deleted\": \"The bookmark has been deleted!\",\n      \"refetch\": \"Re-fetch has been enqueued!\",\n      \"full_page_archive\": \"Full Page Archive creation has been triggered\",\n      \"preserve_pdf\": \"PDF preservation has been triggered\",\n      \"delete_from_list\": \"The bookmark has been deleted from the list\",\n      \"clipboard_copied\": \"Link has been added to your clipboard!\",\n      \"updated\": \"The bookmark has been updated!\",\n      \"update_banner\": \"Banner has been updated!\",\n      \"uploading_banner\": \"Uploading banner...\"\n    },\n    \"lists\": {\n      \"created\": \"List has been created!\",\n      \"updated\": \"List has been updated!\",\n      \"merged\": \"List has been merged!\",\n      \"deleted\": \"List has been deleted!\"\n    },\n    \"tags\": {\n      \"created\": \"Tag has been created!\",\n      \"failed_to_create\": \"Failed to create tag\"\n    }\n  },\n  \"dialogs\": {\n    \"bookmarks\": {\n      \"delete_confirmation_title\": \"Delete Bookmark?\",\n      \"delete_confirmation_description\": \"Are you sure you want to delete this bookmark?\"\n    }\n  },\n  \"banners\": {\n    \"no_bookmarks\": {\n      \"title\": \"No bookmarks yet\",\n      \"description\": \"Save interesting articles, links, and pages to access them quickly later.\"\n    }\n  },\n  \"cleanups\": {\n    \"cleanups\": \"Cleanups\",\n    \"duplicate_tags\": {\n      \"title\": \"Duplicate Tags\",\n      \"merge_all_suggestions\": \"Merge all suggestions?\"\n    }\n  },\n  \"view_options\": {\n    \"title\": \"View Options\",\n    \"layout\": \"Layout\",\n    \"columns\": \"Columns\",\n    \"display_options\": \"Display Options\",\n    \"show_note_previews\": \"Show Notes\",\n    \"show_tags\": \"Show Tags\",\n    \"show_title\": \"Show Title\",\n    \"image_options\": \"Image Options\",\n    \"image_fit_cover\": \"Cover (Fill)\",\n    \"image_fit_contain\": \"Contain (Fit)\"\n  },\n  \"version\": {\n    \"new_release_available\": \"New release notes available\",\n    \"whats_new_title\": \"What's new in v{{version}}\",\n    \"release_notes_description\": \"Here are the latest updates fetched from the GitHub release notes.\",\n    \"loading_release_notes\": \"Loading release notes…\",\n    \"unable_to_load_release_notes\": \"Unable to load release notes right now. Please try again later.\",\n    \"no_release_notes\": \"No release notes were published for this version.\",\n    \"release_notes_synced\": \"Release notes are synced from GitHub.\",\n    \"view_on_github\": \"View on GitHub\"\n  },\n  \"wrapped\": {\n    \"title\": \"Your {{year}} Wrapped\",\n    \"subtitle\": \"A Year in Karakeep\",\n    \"banner\": {\n      \"title\": \"Your 2025 Wrapped is ready!\",\n      \"description\": \"See your year in bookmarks\",\n      \"view_now\": \"View Now\"\n    },\n    \"button\": \"2025 Wrapped\",\n    \"loading\": \"Loading your Wrapped...\",\n    \"failed_to_load\": \"Failed to load your Wrapped stats\",\n    \"sections\": {\n      \"total_saves\": {\n        \"prefix\": \"You saved\",\n        \"suffix\": \"items this year\",\n        \"suffix_singular\": \"item this year\"\n      },\n      \"first_bookmark\": {\n        \"title\": \"Your Journey Started\",\n        \"description\": \"First save of {{year}}:\"\n      },\n      \"top_domains\": \"Your Top Sites\",\n      \"top_tags\": \"Your Top Tags\",\n      \"monthly_activity\": \"Your Year in Saves\",\n      \"most_active_day\": \"Your Most Active Day\",\n      \"peak_times\": {\n        \"title\": \"When You Save\",\n        \"peak_hour\": \"Peak Hour\",\n        \"peak_day\": \"Peak Day\"\n      },\n      \"how_you_save\": \"How You Save\",\n      \"types\": {\n        \"notes\": \"Notes\",\n        \"assets\": \"Assets\",\n        \"links\": \"Links\"\n      },\n      \"what_you_saved\": \"What You Saved\",\n      \"summary\": {\n        \"favorites\": \"Favorites\",\n        \"tags_created\": \"Tags Created\",\n        \"highlights\": \"Highlights\"\n      }\n    },\n    \"footer\": \"Made with Karakeep\",\n    \"share\": \"Share\",\n    \"download\": \"Download\",\n    \"close\": \"Close\",\n    \"generating\": \"Generating...\"\n  }\n}\n"
  },
  {
    "path": "apps/web/lib/i18n/locales/es/translation.json",
    "content": "{\n  \"common\": {\n    \"tags\": \"Etiquetas\",\n    \"roles\": {\n      \"user\": \"Usuario\",\n      \"admin\": \"Admin\"\n    },\n    \"attachments\": \"Adjuntos\",\n    \"name\": \"Nombre\",\n    \"email\": \"Email\",\n    \"role\": \"Rol\",\n    \"url\": \"URL\",\n    \"action\": \"Acción\",\n    \"actions\": \"Acciones\",\n    \"created_at\": \"Creado:\",\n    \"something_went_wrong\": \"Algo ha salido mal\",\n    \"search\": \"Buscar\",\n    \"screenshot\": \"Captura de pantalla\",\n    \"video\": \"Vídeo\",\n    \"archive\": \"Archivo\",\n    \"home\": \"Inicio\",\n    \"password\": \"Contraseña\",\n    \"key\": \"Clave\",\n    \"experimental\": \"Experimental\",\n    \"note\": \"Nota\",\n    \"highlights\": \"Destacados\",\n    \"source\": \"Fuente\",\n    \"type\": \"Escribe\",\n    \"size\": \"Tamaño\",\n    \"bookmark_types\": {\n      \"title\": \"Tipo de marcador\",\n      \"link\": \"Enlace\",\n      \"text\": \"Texto\",\n      \"media\": \"Medios\"\n    },\n    \"summary\": \"Resumen\",\n    \"updated_at\": \"Actualizado el\",\n    \"title\": \"Título\",\n    \"description\": \"Descripción\",\n    \"quota\": \"Cuota\",\n    \"bookmarks\": \"Marcadores\",\n    \"storage\": \"Almacenamiento\",\n    \"pdf\": \"PDF archivado\",\n    \"default\": \"Predeterminado\",\n    \"id\": \"ID\",\n    \"last_used\": \"Último usado\"\n  },\n  \"settings\": {\n    \"info\": {\n      \"change_password\": \"Cambiar contraseña\",\n      \"new_password\": \"Nueva contraseña\",\n      \"interface_lang\": \"Idioma de la interfaz\",\n      \"user_info\": \"Información del usuario\",\n      \"basic_details\": \"Detalles básicos\",\n      \"current_password\": \"Contraseña actual\",\n      \"confirm_new_password\": \"Confirmar nueva contraseña\",\n      \"options\": \"Ajustes\",\n      \"user_settings\": {\n        \"user_settings_updated\": \"¡La configuración del usuario se ha actualizado!\",\n        \"bookmark_click_action\": {\n          \"title\": \"Acción al hacer clic en el marcador\",\n          \"open_external_url\": \"Abrir URL original\",\n          \"open_bookmark_details\": \"Abrir detalles del marcador\"\n        },\n        \"archive_display_behaviour\": {\n          \"title\": \"Marcadores archivados\",\n          \"show\": \"Mostrar marcadores archivados en etiquetas y listas\",\n          \"hide\": \"Ocultar marcadores archivados en etiquetas y listas\"\n        }\n      },\n      \"reader_settings\": {\n        \"local_overrides_title\": \"Ajustes específicos del dispositivo activos\",\n        \"using_default\": \"Usando el valor predeterminado del cliente\",\n        \"clear_override_hint\": \"Borra la configuración específica del dispositivo para usar la configuración global ({{value}})\",\n        \"font_size\": \"Tamaño de fuente\",\n        \"font_family\": \"Familia de fuentes\",\n        \"preview_inline\": \"(vista previa)\",\n        \"tooltip_preview\": \"Cambios de la vista previa sin guardar\",\n        \"save_to_all_devices\": \"Todos los dispositivos\",\n        \"tooltip_local\": \"Los ajustes del dispositivo difieren de los globales\",\n        \"reset_preview\": \"Restablecer vista previa\",\n        \"mono\": \"Monoespacio\",\n        \"line_height\": \"Altura de la línea\",\n        \"tooltip_default\": \"Ajustes de lectura\",\n        \"title\": \"Ajustes del lector\",\n        \"serif\": \"Con gracias\",\n        \"preview\": \"Vista previa\",\n        \"not_set\": \"No configurado\",\n        \"clear_local_overrides\": \"Borrar la configuración del dispositivo\",\n        \"preview_text\": \"El veloz murciélago hindú comía feliz cardillo y kiwi. Así es como aparecerá el texto en tu vista de lectura.\",\n        \"local_overrides_cleared\": \"Se han borrado los ajustes específicos del dispositivo\",\n        \"local_overrides_description\": \"Este dispositivo tiene ajustes de lector que difieren de los valores predeterminados globales:\",\n        \"clear_defaults\": \"Borrar todos los valores predeterminados\",\n        \"description\": \"Configura los ajustes de texto predeterminados para la vista de lectura. Estos ajustes se sincronizan en todos tus dispositivos.\",\n        \"defaults_cleared\": \"Se han borrado los valores predeterminados del lector\",\n        \"save_hint\": \"Guarda la configuración sólo para este dispositivo o sincronízala en todos los dispositivos\",\n        \"save_as_default\": \"Guardar como predeterminado\",\n        \"save_to_device\": \"Este dispositivo\",\n        \"sans\": \"Sin gracias\",\n        \"tooltip_preview_and_local\": \"Cambios de la vista previa sin guardar; los ajustes del dispositivo difieren de los globales\",\n        \"adjust_hint\": \"Just the tip: ajusta la configuración de arriba para previsualizar los cambios\"\n      },\n      \"avatar\": {\n        \"upload\": \"Subir avatar\",\n        \"change\": \"Cambiar avatar\",\n        \"remove_confirm_title\": \"¿Eliminar avatar?\",\n        \"updated\": \"Avatar actualizado\",\n        \"removed\": \"Avatar eliminado\",\n        \"description\": \"Sube una imagen cuadrada para usarla como tu avatar.\",\n        \"remove_confirm_description\": \"Esto borrará tu foto de perfil actual.\",\n        \"title\": \"Foto de perfil\",\n        \"remove\": \"Eliminar avatar\"\n      }\n    },\n    \"back_to_app\": \"Volver a la aplicación\",\n    \"ai\": {\n      \"ai_settings\": \"Ajustes de la IA\",\n      \"tagging_rules\": \"Reglas de etiquetado\",\n      \"tagging_rule_description\": \"Los prompts que añadas aquí se incluirán como reglas durante la generación de etiquetas. Puedes comprobar el prompt final en la sección de previsualización.\",\n      \"prompt_preview\": \"Previsualización\",\n      \"text_prompt\": \"Prompt para texto\",\n      \"images_prompt\": \"Prompt para Imagen\",\n      \"summarization\": \"Resumen\",\n      \"summarization_prompt\": \"Indicación de resumen\",\n      \"all_tagging\": \"Todo el etiquetado\",\n      \"text_tagging\": \"Etiquetado de texto\",\n      \"image_tagging\": \"Etiquetado de imágenes\",\n      \"tag_style\": \"Estilo de etiqueta\",\n      \"auto_summarization_description\": \"Genera resúmenes automáticamente para tus marcadores usando IA.\",\n      \"auto_tagging\": \"Etiquetado automático\",\n      \"titlecase_spaces\": \"Mayúsculas y minúsculas con espacios\",\n      \"lowercase_underscores\": \"Minúsculas con guiones bajos\",\n      \"inference_language\": \"Idioma de Inferencia\",\n      \"titlecase_hyphens\": \"Mayúsculas y minúsculas con guiones\",\n      \"lowercase_hyphens\": \"Minúsculas con guiones\",\n      \"lowercase_spaces\": \"Minúsculas con espacios\",\n      \"inference_language_description\": \"Elige el idioma para las etiquetas y los resúmenes generados por la IA.\",\n      \"tag_style_description\": \"Elige cómo quieres que se formateen las etiquetas que se generan automáticamente.\",\n      \"auto_tagging_description\": \"Genera etiquetas automáticamente para tus marcadores usando IA.\",\n      \"camelCase\": \"camelCase\",\n      \"auto_summarization\": \"Resumen automático\",\n      \"no_preference\": \"Sin preferencia\",\n      \"curated_tags\": \"Etiquetas seleccionadas\",\n      \"curated_tags_description\": \"Opcionalmente, restringe el etiquetado de la IA para que solo use etiquetas de esta lista. Cuando no se seleccionan etiquetas, la IA genera etiquetas libremente.\",\n      \"curated_tags_updated\": \"¡Etiquetas seleccionadas actualizadas correctamente!\",\n      \"curated_tags_update_failed\": \"Error al actualizar las etiquetas seleccionadas\"\n    },\n    \"user_settings\": \"Ajustes de usuario\",\n    \"feeds\": {\n      \"add_a_subscription\": \"Añadir subscripción\",\n      \"rss_subscriptions\": \"Subscripciones RSS\",\n      \"feed_enabled\": \"Fuente RSS activada\",\n      \"feed_disabled\": \"Fuente RSS desactivada\"\n    },\n    \"import\": {\n      \"import_export\": \"Importar / Exportar\",\n      \"import_export_bookmarks\": \"Importar / Exportar marcadores\",\n      \"import_bookmarks_from_pocket_export\": \"Importar marcadores desde exportación de Pocket\",\n      \"import_bookmarks_from_matter_export\": \"Importar marcadores desde exportación de Matter\",\n      \"export_links_and_notes\": \"Exportar links y notas\",\n      \"imported_bookmarks\": \"Marcadores importados\",\n      \"import_bookmarks_from_karakeep_export\": \"Importar marcadores desde exportación de Karakeep\",\n      \"import_bookmarks_from_html_file\": \"Importar marcadores desde archivo HTML\",\n      \"import_bookmarks_from_omnivore_export\": \"Importar marcadores desde exportación de Omnivore\",\n      \"import_bookmarks_from_linkwarden_export\": \"Importar marcadores desde Linkwarden\",\n      \"import_bookmarks_from_tab_session_manager_export\": \"Importar marcadores desde Tab Session Manager\",\n      \"import_bookmarks_from_mymind_export\": \"Importar marcadores desde la exportación de mymind\",\n      \"import_bookmarks_from_instapaper_export\": \"Importar marcadores desde la exportación de Instapaper\"\n    },\n    \"api_keys\": {\n      \"api_keys\": \"Claves APIs\",\n      \"new_api_key\": \"Crear clave API\",\n      \"new_api_key_desc\": \"Dale a la clave API un nombre único\",\n      \"key_success\": \"Clave creada correctamente\",\n      \"key_success_please_copy\": \"Copia la clave y guárdala en un lugar seguro. Una vez cierres este mensaje, no podrás volver a verla.\",\n      \"regenerate_api_key\": \"Regenerar clave API\",\n      \"key_regenerated\": \"La clave se ha regenerado correctamente\",\n      \"key_regenerated_please_copy\": \"Copia la nueva clave y guárdala en un lugar seguro. La clave antigua ha sido revocada y ya no funcionará.\",\n      \"regenerate_warning\": \"¿Estás seguro de que quieres regenerar la clave API \\\"{{name}}\\\"?\",\n      \"regenerate_confirmation\": \"Esto revocará la clave actual y generará una nueva. Cualquier aplicación que utilice la clave actual dejará de funcionar.\"\n    },\n    \"broken_links\": {\n      \"broken_links\": \"Enlaces rotos\",\n      \"last_crawled_at\": \"Última vez indexado:\",\n      \"crawling_status\": \"Estado de la indexación\",\n      \"crawling_failed\": \"Indexado fallido\"\n    },\n    \"manage_assets\": {\n      \"asset_type\": \"Tipo de activo\",\n      \"manage_assets\": \"Administrar activos\",\n      \"no_assets\": \"Aún no tienes ningún activo.\",\n      \"bookmark_link\": \"Enlace del marcador\",\n      \"asset_link\": \"Enlace del activo\",\n      \"delete_asset\": \"Borrar activo\",\n      \"delete_asset_confirmation\": \"¿Estás seguro de que quieres eliminar este activo?\"\n    },\n    \"webhooks\": {\n      \"events\": {\n        \"crawled\": \"Rastreado\",\n        \"created\": \"Creado\",\n        \"edited\": \"Editado\",\n        \"title\": \"Eventos\"\n      },\n      \"auth_token\": \"Token de autenticación\",\n      \"add_auth_token\": \"Añadir token de autenticación\",\n      \"edit_auth_token\": \"Editar token de autenticación\",\n      \"create_webhook\": \"Crear webhook\",\n      \"delete_webhook\": \"Borrar Webhook\",\n      \"delete_webhook_confirmation\": \"¿Estás seguro de que quieres eliminar este webhook?\",\n      \"edit_webhook\": \"Editar webhook\",\n      \"webhook_url\": \"URL del webhook\",\n      \"webhooks\": \"Webhooks\",\n      \"description\": \"Puedes usar webhooks para activar acciones cuando se crean, cambian o rastrean marcadores.\"\n    },\n    \"rules\": {\n      \"rules\": \"Motor de reglas\",\n      \"description\": \"Puedes usar reglas para activar acciones cuando se active un evento.\",\n      \"rule_name\": \"Nombre de la regla\",\n      \"ceate_rule\": \"Crear regla\",\n      \"edit_rule\": \"Editar regla\",\n      \"save_rule\": \"Guardar regla\",\n      \"delete_rule\": \"Borrar regla\",\n      \"delete_rule_confirmation\": \"¿Seguro que quieres borrar esta regla?\",\n      \"whenever\": \"Cuando...\",\n      \"if\": \"Si...\",\n      \"enter_rule_name\": \"Introduce el nombre de la regla\",\n      \"describe_what_this_rule_does\": \"Describe lo que hace esta regla\",\n      \"rule_has_been_created\": \"¡Se ha creado la regla!\",\n      \"rule_has_been_updated\": \"¡La regla se ha actualizado!\",\n      \"rule_has_been_deleted\": \"¡Se ha eliminado la regla!\",\n      \"no_rules_created_yet\": \"Aún no se han creado reglas\",\n      \"create_your_first_rule\": \"Crea tu primera regla para automatizar tu flujo de trabajo\",\n      \"conditions_types\": {\n        \"always\": \"Siempre\",\n        \"url_contains\": \"La URL contiene\",\n        \"imported_from_feed\": \"Importado desde el feed\",\n        \"bookmark_type_is\": \"El tipo de marcador es\",\n        \"has_tag\": \"Tiene etiqueta\",\n        \"is_favourited\": \"Está marcado como favorito\",\n        \"is_archived\": \"Está archivado\",\n        \"and\": \"Todo lo siguiente es verdad\",\n        \"or\": \"Cualquiera de las siguientes opciones es verdadera\",\n        \"url_does_not_contain\": \"La URL no contiene\",\n        \"title_contains\": \"El título contiene\",\n        \"title_does_not_contain\": \"El título no contiene\"\n      },\n      \"actions_types\": {\n        \"add_tag\": \"Añadir etiqueta\",\n        \"remove_tag\": \"Eliminar etiqueta\",\n        \"add_to_list\": \"Añadir a la lista\",\n        \"remove_from_list\": \"Eliminar de la lista\",\n        \"download_full_page_archive\": \"Descargar archivo de página completa\",\n        \"favourite_bookmark\": \"Marcar marcador como favorito\",\n        \"archive_bookmark\": \"Archivar marcador\"\n      },\n      \"events_types\": {\n        \"bookmark_added\": \"Se ha añadido un marcador\",\n        \"tag_added\": \"Esta etiqueta se añade a un marcador\",\n        \"tag_removed\": \"Esta etiqueta se elimina de un marcador\",\n        \"added_to_list\": \"Se añade un marcador a esta lista\",\n        \"removed_from_list\": \"Se elimina un marcador de esta lista\",\n        \"favourited\": \"Un marcador se ha marcado como favorito\",\n        \"archived\": \"Se archiva un marcador\"\n      }\n    },\n    \"stats\": {\n      \"usage_statistics\": \"Estadísticas de uso\",\n      \"insights_description\": \"Información sobre tus hábitos de marcación y colección\",\n      \"failed_to_load\": \"No se pudieron cargar las estadísticas\",\n      \"overview\": {\n        \"total_bookmarks\": \"Total de marcadores\",\n        \"all_saved_items\": \"Todos los elementos guardados\",\n        \"favorites\": \"Favoritos\",\n        \"starred_bookmarks\": \"Marcadores destacados\",\n        \"archived\": \"Archivado\",\n        \"archived_items\": \"Elementos archivados\",\n        \"tags\": \"Etiquetas\",\n        \"unique_tags_created\": \"Etiquetas únicas creadas\",\n        \"lists\": \"Listas\",\n        \"bookmark_collections\": \"Colecciones de marcadores\",\n        \"highlights\": \"Resaltados\",\n        \"text_highlights\": \"Resaltados de texto\",\n        \"storage_used\": \"Almacenamiento utilizado\",\n        \"total_asset_storage\": \"Almacenamiento total de activos\",\n        \"this_month\": \"Este mes\",\n        \"bookmarks_added\": \"Marcadores añadidos\"\n      },\n      \"bookmark_types\": {\n        \"title\": \"Tipos de marcadores\",\n        \"links\": \"Enlaces\",\n        \"text_notes\": \"Notas de texto\",\n        \"assets\": \"Activos\"\n      },\n      \"recent_activity\": {\n        \"title\": \"Actividad reciente\",\n        \"this_week\": \"Esta semana\",\n        \"this_month\": \"Este mes\",\n        \"this_year\": \"Este año\"\n      },\n      \"top_domains\": {\n        \"title\": \"Dominios principales\",\n        \"no_domains_found\": \"No se encontraron dominios\"\n      },\n      \"most_used_tags\": {\n        \"title\": \"Etiquetas más usadas\",\n        \"no_tags_found\": \"No se encontraron etiquetas\"\n      },\n      \"activity_patterns\": {\n        \"activity_by_hour\": \"Actividad por hora\",\n        \"activity_by_day\": \"Actividad por día\"\n      },\n      \"storage_breakdown\": {\n        \"title\": \"Desglose del almacenamiento\"\n      },\n      \"bookmark_sources\": {\n        \"title\": \"Fuentes de marcadores\",\n        \"empty\": \"Sin datos de origen disponibles\"\n      }\n    },\n    \"subscription\": {\n      \"subscription\": \"Suscripción\",\n      \"manage_subscription\": \"Gestiona tu suscripción e información de facturación\",\n      \"current_plan\": \"Plan actual\",\n      \"billing_period\": \"Periodo de facturación\",\n      \"paid_plan\": \"Plan de pago\",\n      \"unlock_bigger_quota\": \"Desbloquea una cuota mayor y apoya el proyecto\",\n      \"subscribe_now\": \"Suscríbete ahora\",\n      \"manage_billing\": \"Gestionar la facturación\",\n      \"subscription_canceled\": \"Tu suscripción ha sido cancelada y finalizará el {{date}}. Puedes volver a suscribirte en cualquier momento.\",\n      \"usage_quotas\": \"Uso y cuotas\",\n      \"track_usage\": \"Haz un seguimiento de tu uso actual en comparación con los límites de tu plan\",\n      \"total_bookmarks_saved\": \"Total de marcadores guardados\",\n      \"assets_file_storage\": \"Almacenamiento de archivos y recursos\",\n      \"unlimited_usage\": \"Uso ilimitado\",\n      \"quota_limit_reached\": \"Límite de cuota alcanzado\",\n      \"approaching_quota_limit\": \"Te estás acercando al límite de la cuota\",\n      \"loading_usage\": \"Cargando información de uso...\",\n      \"free\": \"Gratis\",\n      \"paid\": \"De pago\"\n    },\n    \"import_sessions\": {\n      \"title\": \"Importar sesiones\",\n      \"description\": \"Consulta y gestiona tus sesiones de importación masiva. Las sesiones se crean automáticamente al importar marcadores.\",\n      \"load_error\": \"No se pudieron cargar las sesiones de importación\",\n      \"no_sessions\": \"Aún no hay sesiones de importación\",\n      \"no_sessions_detail\": \"Las sesiones de importación aparecerán aquí automáticamente cuando importes marcadores\",\n      \"created_at\": \"Creado {{time}}\",\n      \"progress\": \"Progreso\",\n      \"status\": {\n        \"pending\": \"Pendiente\",\n        \"in_progress\": \"En curso\",\n        \"completed\": \"Completado\",\n        \"failed\": \"Falló\",\n        \"processing\": \"Procesando\",\n        \"staging\": \"En preparación\",\n        \"running\": \"En curso\",\n        \"paused\": \"En pausa\"\n      },\n      \"badges\": {\n        \"pending\": \"{{count}} pendientes\",\n        \"processing\": \"{{count}} procesando\",\n        \"completed\": \"{{count}} completados\",\n        \"failed\": \"{{count}} fallaron\"\n      },\n      \"imported_to\": \"Importado a:\",\n      \"view_list\": \"Ver lista\",\n      \"delete_dialog_title\": \"Borrar sesión de importación\",\n      \"delete_dialog_description\": \"¿Seguro que quieres borrar \\\"{{name}}\\\"? Esta acción no se puede deshacer. Los marcadores en sí no se borrarán.\",\n      \"delete_session\": \"Eliminar sesión\",\n      \"pause_session\": \"Pausar\",\n      \"resume_session\": \"Reanudar\",\n      \"view_details\": \"Ver detalles\",\n      \"detail\": {\n        \"page_title\": \"Ver detalles de la sesión de importación\",\n        \"back_to_import\": \"Volver a la importación\",\n        \"filter_all\": \"Todo\",\n        \"filter_accepted\": \"Aceptado\",\n        \"filter_rejected\": \"Rechazado\",\n        \"filter_duplicates\": \"Duplicados\",\n        \"filter_pending\": \"Pendientes\",\n        \"table_title\": \"Título/URL\",\n        \"table_type\": \"Tipo\",\n        \"table_result\": \"Resultado\",\n        \"table_reason\": \"Razón\",\n        \"table_bookmark\": \"Marcador\",\n        \"result_accepted\": \"Aceptado\",\n        \"result_rejected\": \"Rechazado\",\n        \"result_skipped_duplicate\": \"Duplicar\",\n        \"result_pending\": \"Pendientes\",\n        \"result_processing\": \"Procesando\",\n        \"no_results\": \"No se encontraron resultados para este filtro.\",\n        \"view_bookmark\": \"Ver marcador\",\n        \"load_more\": \"Cargar más\",\n        \"no_title\": \"Sin título\"\n      }\n    },\n    \"backups\": {\n      \"backups\": \"Copias de seguridad\",\n      \"page_title\": \"Copias de seguridad\",\n      \"page_description\": \"Crea y gestiona automáticamente copias de seguridad de tus marcadores. Las copias de seguridad se comprimen y se almacenan de forma segura.\",\n      \"configuration\": {\n        \"title\": \"Configuración de la copia de seguridad\",\n        \"enable_automatic_backups\": \"Activar copias de seguridad automáticas\",\n        \"enable_automatic_backups_description\": \"Crear automáticamente copias de seguridad de tus marcadores\",\n        \"backup_frequency\": \"Frecuencia de copia de seguridad\",\n        \"backup_frequency_description\": \"Con qué frecuencia se deben crear copias de seguridad\",\n        \"retention_period\": \"Período de retención (días)\",\n        \"retention_period_description\": \"Cuántos días conservar las copias de seguridad antes de eliminarlas\",\n        \"frequency\": {\n          \"daily\": \"Diariamente\",\n          \"weekly\": \"Semanalmente\"\n        },\n        \"select_frequency\": \"Seleccionar frecuencia\",\n        \"save_settings\": \"Guardar ajustes\"\n      },\n      \"list\": {\n        \"title\": \"Tus copias de seguridad\",\n        \"create_backup_now\": \"Crear copia de seguridad ahora\",\n        \"no_backups\": \"Aún no tienes ninguna copia de seguridad. Activa las copias de seguridad automáticas o crea una manualmente.\",\n        \"table\": {\n          \"created_at\": \"Creado en\",\n          \"bookmarks\": \"Marcadores\",\n          \"size\": \"Tamaño\",\n          \"status\": \"Estado\",\n          \"actions\": \"Acciones\"\n        },\n        \"status\": {\n          \"success\": \"Éxito\",\n          \"failed\": \"Fallido\",\n          \"pending\": \"Pendiente\"\n        },\n        \"actions\": {\n          \"download_backup\": \"Descargar copia de seguridad\",\n          \"delete_backup\": \"Borrar copia de seguridad\"\n        }\n      },\n      \"dialogs\": {\n        \"delete_backup_title\": \"¿Borrar copia de seguridad?\",\n        \"delete_backup_description\": \"¿Seguro que quieres borrar esta copia de seguridad? Esta acción no se puede deshacer.\"\n      },\n      \"toasts\": {\n        \"backup_queued\": \"¡Se ha puesto en cola el trabajo de copia de seguridad! Se procesará en breve.\",\n        \"backup_deleted\": \"¡Se ha borrado la copia de seguridad!\"\n      }\n    }\n  },\n  \"actions\": {\n    \"unselect_all\": \"Deseleccionar todo\",\n    \"select_all\": \"Seleccionar todo\",\n    \"change_layout\": \"Cambiar diseño\",\n    \"unfavorite\": \"Quitar de favoritos\",\n    \"delete\": \"Eliminar\",\n    \"refresh\": \"Actualizar\",\n    \"download_full_page_archive\": \"Descargar Archivo de Página Completa\",\n    \"edit_tags\": \"Editar etiquetas\",\n    \"remove_from_list\": \"Eliminar de la lista\",\n    \"save\": \"Guardar\",\n    \"add\": \"Añadir\",\n    \"edit\": \"Editar\",\n    \"create\": \"Crear\",\n    \"fetch_now\": \"Actualizar ahora\",\n    \"summarize_with_ai\": \"Resumir con IA\",\n    \"edit_title\": \"Cambiar título\",\n    \"sign_out\": \"Cerrar sesión\",\n    \"close\": \"Cerrar\",\n    \"merge\": \"Unir\",\n    \"ignore\": \"Ignorar\",\n    \"archive\": \"Archivar\",\n    \"unarchive\": \"Desarchivar\",\n    \"favorite\": \"Marcar como favorito\",\n    \"add_to_list\": \"Añadir a la lista\",\n    \"copy_link\": \"Copiar enlace\",\n    \"cancel\": \"Cancelar\",\n    \"apply_all\": \"Aplicar todo\",\n    \"recrawl\": \"Volver a crawlear\",\n    \"close_bulk_edit\": \"Cerrar editor en masa\",\n    \"bulk_edit\": \"Editar en masa\",\n    \"manage_lists\": \"Administrar listas\",\n    \"sort\": {\n      \"title\": \"Ordenar\",\n      \"newest_first\": \"Más recientes primero\",\n      \"oldest_first\": \"Más antiguos primero\",\n      \"relevant_first\": \"Más relevantes primero\"\n    },\n    \"open_editor\": \"Abrir editor\",\n    \"toggle_show_archived\": \"Mostrar archivados\",\n    \"confirm\": \"Confirmar\",\n    \"regenerate\": \"Regenerar\",\n    \"load_more\": \"Cargar más\",\n    \"edit_notes\": \"Editar notas\",\n    \"preserve_as_pdf\": \"Conservar como PDF\",\n    \"offline_copies\": \"Copias sin conexión\",\n    \"preserve_offline_archive\": \"Preservar archivo sin conexión\",\n    \"download_full_page_archive_file\": \"Descargar archivo\",\n    \"download_pdf_file\": \"Descargar archivo PDF\",\n    \"remove\": \"Eliminar\",\n    \"more\": \"Más\",\n    \"replace_banner\": \"Reemplazar el banner\",\n    \"add_banner\": \"Añadir un banner\",\n    \"download\": \"Descargar\"\n  },\n  \"layouts\": {\n    \"compact\": \"Compacto\",\n    \"masonry\": \"Mampostería\",\n    \"grid\": \"Rejilla\",\n    \"list\": \"Lista\"\n  },\n  \"admin\": {\n    \"admin_settings\": \"Ajustes de Administrador\",\n    \"server_stats\": {\n      \"server_stats\": \"Estadísticas del servidor\",\n      \"total_users\": \"Número de usuarios\",\n      \"total_bookmarks\": \"Número de marcadores\",\n      \"server_version\": \"Versión del servidor\"\n    },\n    \"background_jobs\": {\n      \"background_jobs\": \"Operaciones en segundo plano\",\n      \"indexing_jobs\": \"Trabajos de indexación\",\n      \"pending\": \"Pendiente\",\n      \"failed\": \"Error\",\n      \"queued\": \"En cola\",\n      \"crawler_jobs\": \"Trabajos de crawling\",\n      \"inference_jobs\": \"Trabajos de inferencia\",\n      \"tidy_assets_jobs\": \"Trabajos de refinación\",\n      \"job\": \"Trabajo\",\n      \"webhook_jobs\": \"Trabajos de Webhook\",\n      \"asset_preprocessing_jobs\": \"Trabajos de preprocesamiento de activos\",\n      \"feed_jobs\": \"Trabajos de fuente RSS\",\n      \"video_jobs\": \"Trabajos de descarga de vídeo\",\n      \"jobs\": {\n        \"crawler\": {\n          \"title\": \"Trabajos de rastreador\",\n          \"description\": \"Rastreo web y extracción de contenido de URLs\"\n        },\n        \"inference\": {\n          \"title\": \"Trabajos de inferencia\",\n          \"description\": \"Etiquetado y resumen de contenido impulsado por IA\"\n        },\n        \"indexing\": {\n          \"title\": \"Trabajos de indexación\",\n          \"description\": \"Actualizaciones del índice de búsqueda\"\n        },\n        \"asset_preprocessing\": {\n          \"title\": \"Trabajos de preprocesamiento de activos\",\n          \"description\": \"Preprocesamiento de imágenes y documentos (capturas de pantalla, extracción de texto, etc.)\"\n        },\n        \"tidy_assets\": {\n          \"title\": \"Trabajos de activos ordenados\",\n          \"description\": \"Limpieza de activos y optimización de almacenamiento\"\n        },\n        \"video\": {\n          \"title\": \"Trabajos de descarga de video\",\n          \"description\": \"Extracción y descarga de video\"\n        },\n        \"webhook\": {\n          \"title\": \"Trabajos de webhook\",\n          \"description\": \"Notificaciones de webhook externas\"\n        },\n        \"feed\": {\n          \"title\": \"Trabajos de fuentes RSS\",\n          \"description\": \"Procesamiento de feeds RSS y actualizaciones de contenido\"\n        },\n        \"admin_maintenance\": {\n          \"title\": \"Tareas de mantenimiento del administrador\",\n          \"description\": \"Limpieza administrativa y mantenimiento de activos\"\n        }\n      },\n      \"monitor_and_manage\": \"Monitoriza y gestiona las colas de trabajos en segundo plano y las tareas de procesamiento del sistema\",\n      \"active\": \"Activo\",\n      \"available_actions\": \"Acciones disponibles\",\n      \"status\": {\n        \"title\": \"Entendiendo los estados del trabajo\",\n        \"queued\": {\n          \"title\": \"En cola\",\n          \"description\": \"Tareas esperando en la cola para ser procesadas. Comenzarán automáticamente cuando haya recursos disponibles.\"\n        },\n        \"unprocessed\": {\n          \"title\": \"Sin procesar\",\n          \"description\": \"Marcadores que aún no han sido procesados. Lo más probable es que ya estén en cola para ser procesados, si no, es posible que tengas que volver a ponerlos en cola manualmente.\"\n        },\n        \"failed\": {\n          \"title\": \"Fallidos\",\n          \"description\": \"Marcadores que encontraron errores durante el procesamiento. Estos pueden necesitar atención manual.\"\n        }\n      },\n      \"actions\": {\n        \"recrawl_failed_links_only\": \"Volver a rastrear solo los enlaces fallidos\",\n        \"recrawl_all_links\": \"Volver a rastrear todos los enlaces\",\n        \"without_inference\": \"Sin inferencia\",\n        \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Regenerar etiquetas de IA solo para los marcadores fallidos\",\n        \"regenerate_ai_tags_for_all_bookmarks\": \"Regenerar etiquetas de IA para todos los marcadores\",\n        \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Regenerar resúmenes de IA solo para los marcadores fallidos\",\n        \"regenerate_ai_summaries_for_all_bookmarks\": \"Regenerar resúmenes de IA para todos los marcadores\",\n        \"reindex_all_bookmarks\": \"Volver a indexar todos los marcadores\",\n        \"clean_assets\": \"Limpiar los activos colgantes y volver a sincronizar los metadatos\",\n        \"reprocess_assets_fix_mode\": \"Volver a procesar los activos no procesados\",\n        \"migrate_large_link_html_content\": \"Mueve contenido HTML grande en línea a los recursos\",\n        \"recrawl_pending_links_only\": \"Volver a rastrear solo los enlaces pendientes\",\n        \"regenerate_ai_tags_for_pending_bookmarks_only\": \"Volver a generar etiquetas de IA solo para los marcadores pendientes\",\n        \"regenerate_ai_summaries_for_pending_bookmarks_only\": \"Volver a generar resúmenes de IA solo para los marcadores pendientes\"\n      }\n    },\n    \"actions\": {\n      \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Regenerar etiquetas IA solo en marcadores fallidos\",\n      \"regenerate_ai_tags_for_all_bookmarks\": \"Regenerar etiquetas IA para todos los marcadores\",\n      \"reindex_all_bookmarks\": \"Reindexar marcadores\",\n      \"compact_assets\": \"Optimizar multimedia\",\n      \"without_inference\": \"Sin inferencia\",\n      \"recrawl_failed_links_only\": \"Recrawlear solo los enlaces fallidos\",\n      \"recrawl_all_links\": \"Recrawlear todos los enlaces\",\n      \"reprocess_assets_fix_mode\": \"Reprocesar assets (modo fijo)\",\n      \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Regenerar resúmenes de IA solo para marcadores fallidos\",\n      \"regenerate_ai_summaries_for_all_bookmarks\": \"Regenerar resúmenes de IA para todos los marcadores\"\n    },\n    \"users_list\": {\n      \"users_list\": \"Lista de usuarios\",\n      \"create_user\": \"Crear usuario\",\n      \"change_role\": \"Cambiar rol\",\n      \"reset_password\": \"Reiniciar contraseña\",\n      \"delete_user\": \"Borrar usuario\",\n      \"num_bookmarks\": \"Núm. Marcadores\",\n      \"asset_sizes\": \"Espacio utilizado\",\n      \"local_user\": \"Usuario local\",\n      \"confirm_password\": \"Confirmar contraseña\",\n      \"delete_user_confirm_description\": \"¿Seguro que quieres eliminar al usuario \\\"{{name}}\\\"?\",\n      \"unlimited\": \"Ilimitado\"\n    },\n    \"service_connections\": {\n      \"title\": \"Conexiones de servicio\",\n      \"description\": \"Supervisar el estado y la conectividad de las dependencias del sistema externo\",\n      \"search_engine\": \"Motor de búsqueda\",\n      \"browser\": \"Navegador\",\n      \"queue_system\": \"Sistema de colas\",\n      \"status\": {\n        \"not_configured\": \"No configurado\",\n        \"connected\": \"Conectado\",\n        \"disconnected\": \"Desconectado\"\n      }\n    },\n    \"admin_tools\": {\n      \"admin_tools\": \"Herramientas de administración\",\n      \"bookmark_debugger\": \"Depurador de marcadores\",\n      \"bookmark_id\": \"ID del marcador\",\n      \"bookmark_id_placeholder\": \"Introducir ID del marcador\",\n      \"lookup\": \"Búsqueda\",\n      \"debug_info\": \"Información de depuración\",\n      \"basic_info\": \"Información básica\",\n      \"status\": \"Estado\",\n      \"content\": \"Contenido\",\n      \"html_preview\": \"Vista previa HTML (primeros 1000 caracteres)\",\n      \"summary\": \"Resumen\",\n      \"url\": \"URL\",\n      \"source_url\": \"URL de origen\",\n      \"asset_type\": \"Tipo de activo\",\n      \"file_name\": \"Nombre del archivo\",\n      \"owner_user_id\": \"ID de usuario propietario\",\n      \"tagging_status\": \"Estado de etiquetado\",\n      \"summarization_status\": \"Estado de resumen\",\n      \"crawl_status\": \"Estado de rastreo\",\n      \"crawl_status_code\": \"Código de estado HTTP\",\n      \"crawled_at\": \"Rastreado en\",\n      \"recrawl\": \"Volver a rastrear\",\n      \"reindex\": \"Volver a indexar\",\n      \"retag\": \"Volver a etiquetar\",\n      \"resummarize\": \"Volver a resumir\",\n      \"bookmark_not_found\": \"Marcador no encontrado\",\n      \"action_success\": \"Acción completada con éxito\",\n      \"action_failed\": \"Error en la acción\",\n      \"recrawl_queued\": \"Se ha puesto en cola el trabajo de volver a rastrear\",\n      \"reindex_queued\": \"Se ha puesto en cola el trabajo de volver a indexar\",\n      \"retag_queued\": \"Se ha puesto en cola el trabajo de volver a etiquetar\",\n      \"resummarize_queued\": \"Se ha puesto en cola el trabajo de volver a resumir\",\n      \"view\": \"Ver\",\n      \"fetch_error\": \"Error al obtener el marcador\"\n    }\n  },\n  \"options\": {\n    \"dark_mode\": \"Modo oscuro\",\n    \"light_mode\": \"Modo claro\",\n    \"apps_extensions\": \"Apps y extensiones\",\n    \"documentation\": \"Documentación\",\n    \"follow_us_on_x\": \"Síguenos en X\"\n  },\n  \"lists\": {\n    \"all_lists\": \"Todas las listas\",\n    \"favourites\": \"Favoritos\",\n    \"new_list\": \"Nueva lista\",\n    \"new_nested_list\": \"Nueva lista anidada\",\n    \"list_type\": \"Tipo de lista\",\n    \"manual_list\": \"Lista manual\",\n    \"edit_list\": \"Editar lista\",\n    \"parent_list\": \"Lista de padres\",\n    \"no_parent\": \"Sin padre\",\n    \"smart_list\": \"Lista inteligente\",\n    \"search_query\": \"Consulta de búsqueda\",\n    \"search_query_help\": \"Más información sobre el lenguaje de consulta de búsqueda.\",\n    \"merge_list\": \"Fusionar lista\",\n    \"destination_list\": \"Lista de destino\",\n    \"no_destination\": \"Sin destino\",\n    \"description\": \"Descripción (opcional)\",\n    \"delete_after_merge\": \"Eliminar la lista original después de la fusión\",\n    \"public_list\": {\n      \"share_link\": \"Compartir enlace\",\n      \"title\": \"Lista pública\",\n      \"description\": \"Permitir que otros vean esta lista\"\n    },\n    \"share_list\": \"Compartir lista\",\n    \"rss\": {\n      \"title\": \"Fuente RSS\",\n      \"description\": \"Activar una fuente RSS para esta lista\",\n      \"feed_url\": \"URL de la fuente RSS\"\n    },\n    \"delete_list\": {\n      \"title\": \"Eliminar lista\",\n      \"description\": \"Eliminar una lista no elimina ningún marcador de esa lista.\",\n      \"delete_children\": \"Eliminar listas secundarias (de forma recursiva)\",\n      \"delete_children_description\": \"Si no está marcado, todas las listas secundarias directas se convertirán en listas raíz\"\n    },\n    \"shared\": \"Compartido\",\n    \"collaborators\": {\n      \"manage\": \"Gestionar colaboradores\",\n      \"view\": \"Ver colaboradores\",\n      \"collaborators\": \"Colaboradores\",\n      \"add\": \"Añadir colaborador\",\n      \"current\": \"Colaboradores actuales\",\n      \"enter_email\": \"Introduce la dirección de correo electrónico\",\n      \"please_enter_email\": \"Por favor, introduce una dirección de correo electrónico\",\n      \"added_successfully\": \"Colaborador añadido correctamente\",\n      \"failed_to_add\": \"No se pudo añadir el colaborador\",\n      \"removed\": \"Colaborador eliminado\",\n      \"failed_to_remove\": \"No se pudo eliminar el colaborador\",\n      \"role_updated\": \"Rol actualizado\",\n      \"failed_to_update_role\": \"No se pudo actualizar el rol\",\n      \"viewer\": \"Visor\",\n      \"editor\": \"Editor\",\n      \"owner\": \"Propietario\",\n      \"viewer_description\": \"Puede ver los marcadores en la lista\",\n      \"editor_description\": \"Puede añadir y eliminar marcadores\",\n      \"no_collaborators\": \"Aún no hay colaboradores. ¡Añade a alguien para empezar a colaborar!\",\n      \"no_collaborators_readonly\": \"No hay colaboradores en esta lista.\",\n      \"people_with_access\": \"Personas que tienen acceso a esta lista\",\n      \"add_or_remove\": \"Añade o elimina personas que puedan acceder a esta lista\",\n      \"invitation_sent\": \"Invitación enviada correctamente\",\n      \"invitation_revoked\": \"Invitación revocada\",\n      \"failed_to_revoke\": \"No se pudo revocar la invitación\",\n      \"pending\": \"Pendiente\",\n      \"revoke\": \"Revocar\",\n      \"declined\": \"Rechazada\"\n    },\n    \"leave_list\": {\n      \"title\": \"Salir de la lista\",\n      \"confirm_message\": \"¿Estás seguro de que quieres salir de {{icon}} {{name}}?\",\n      \"warning\": \"Ya no podrás ver ni acceder a los marcadores de esta lista. El propietario de la lista puede volverte a añadir si es necesario.\",\n      \"action\": \"Salir de la lista\",\n      \"success\": \"Has abandonado \\\"{{icon}} {{name}}\\\"\"\n    },\n    \"invitations\": {\n      \"pending\": \"Invitaciones pendientes\",\n      \"description\": \"Revisa y responde a las invitaciones de colaboración en la lista\",\n      \"invited_by\": \"Invitado por\",\n      \"accept\": \"Aceptar\",\n      \"decline\": \"Rechazar\",\n      \"accepted\": \"Invitación aceptada\",\n      \"declined\": \"Invitación rechazada\",\n      \"failed_to_accept\": \"No se pudo aceptar la invitación\",\n      \"failed_to_decline\": \"No se pudo rechazar la invitación\"\n    },\n    \"shared_lists\": \"Listas compartidas\"\n  },\n  \"tags\": {\n    \"all_tags\": \"Todas las etiquetas\",\n    \"your_tags\": \"Tus etiquetas\",\n    \"your_tags_info\": \"Etiquetas que has añadido al menos una vez\",\n    \"ai_tags\": \"Etiquetas IA\",\n    \"ai_tags_info\": \"Etiquetas solo usadas automáticamente (por IA)\",\n    \"drag_and_drop_merging\": \"Fusionar arrastrando y soltando\",\n    \"sort_by_name\": \"Ordenar por nombre\",\n    \"delete_all_unused_tags\": \"Eliminar etiquetas no usadas\",\n    \"unused_tags\": \"Etiquetas sin usar\",\n    \"unused_tags_info\": \"Etiquetas sin marcadores\",\n    \"drag_and_drop_merging_info\": \"Arrastra y suelta etiquetas sobre otras para unirlas\",\n    \"create_tag\": \"Crear etiqueta\",\n    \"create_tag_description\": \"Crea una nueva etiqueta sin adjuntarla a ningún marcador\",\n    \"tag_name\": \"Nombre de la etiqueta\",\n    \"enter_tag_name\": \"Introduce el nombre de la etiqueta\",\n    \"sort_by_usage\": \"Ordenar por uso\",\n    \"sort_by_relevance\": \"Ordenar por relevancia\",\n    \"no_custom_tags\": \"Aún no hay etiquetas personalizadas\",\n    \"no_ai_tags\": \"Aún no hay etiquetas de IA\",\n    \"no_unused_tags\": \"No tienes ninguna etiqueta sin usar\",\n    \"no_unused_tags_match_your_search\": \"Ninguna etiqueta sin usar coincide con tu búsqueda\",\n    \"no_tags_match_your_search\": \"Ninguna etiqueta coincide con tu búsqueda\",\n    \"search_placeholder\": \"Buscar etiquetas...\",\n    \"search_or_create_placeholder\": \"Buscar o crear etiquetas...\"\n  },\n  \"preview\": {\n    \"view_original\": \"Ver original\",\n    \"cached_content\": \"Contenido cacheado\",\n    \"reader_view\": \"Vista de lectura\",\n    \"tabs\": {\n      \"content\": \"Contenido\",\n      \"details\": \"Detalles\"\n    },\n    \"archive_info\": \"Es posible que los archivos no se rendericen correctamente en línea si requieren Javascript. Para obtener mejores resultados, <1>descárgalo y ábrelo en tu navegador</1>.\",\n    \"fetch_error_title\": \"Contenido no disponible\",\n    \"fetch_error_description\": \"No pudimos obtener el contenido de este enlace. Es posible que la página esté protegida, requiera autenticación o no esté disponible temporalmente.\",\n    \"crawling_in_progress\": \"Obteniendo contenido de la página…\",\n    \"continue_reading\": \"Continúa donde lo dejaste\",\n    \"continue_reading_percent\": \"Continúa donde lo dejaste ({{percent}}%)\",\n    \"continue_button\": \"Continuar\"\n  },\n  \"editor\": {\n    \"multiple_urls_dialog_title\": \"¿Importar URLs como marcadores independientes?\",\n    \"multiple_urls_dialog_desc\": \"La entrada contiene varias URLs en líneas independientes. ¿Quieres importarlas como distintos marcadores?\",\n    \"import_as_text\": \"Importar como marcador de texto\",\n    \"import_as_separate_bookmarks\": \"Importar como distintos marcadores\",\n    \"placeholder\": \"Pega un enlace o una imagen, escribe una nota o arrastra una imagen aquí…\",\n    \"new_item\": \"NUEVA ENTRADA\",\n    \"text_toolbar\": {\n      \"undo\": \"Deshacer\",\n      \"redo\": \"Rehacer\",\n      \"bold\": \"Negrita\",\n      \"underline\": \"Subrayado\",\n      \"strikethrough\": \"Tachado\",\n      \"code\": \"Código\",\n      \"align_left\": \"Alinear a la izquierda\",\n      \"align_center\": \"Alinear al centro\",\n      \"align_right\": \"Alinear a la derecha\",\n      \"markdown_shortcuts\": {\n        \"label\": \"Atajos de Markdown\",\n        \"heading\": {\n          \"label\": \"Cabecera\",\n          \"example\": \"# H1, ## H2, ### H3\"\n        },\n        \"bold\": {\n          \"label\": \"Negrita\",\n          \"example\": \"**texto** o CTRL+B\"\n        },\n        \"italic\": {\n          \"label\": \"Cursiva\",\n          \"example\": \"*texto*, _texto_ o CTRL+I\"\n        },\n        \"ordered_list\": {\n          \"example\": \"1. Texto\",\n          \"label\": \"Lista ordenada\"\n        },\n        \"unordered_list\": {\n          \"example\": \"- Texto\",\n          \"label\": \"Lista sin orden\"\n        },\n        \"inline_code\": {\n          \"example\": \"`Texto`\",\n          \"label\": \"Código en línea\"\n        },\n        \"block_code\": {\n          \"label\": \"Código en bloque\",\n          \"example\": \"``` + espacio\"\n        },\n        \"blockquote\": {\n          \"label\": \"Cita en Bloque\",\n          \"example\": \"> Texto\"\n        }\n      },\n      \"italic\": \"Cursiva\",\n      \"highlight\": \"Destacar\"\n    },\n    \"quickly_focus\": \"Puedes enfocar este campo pulsando ⌘ + E\",\n    \"disabled_submissions\": \"Entradas deshabilitadas\",\n    \"placeholder_v2\": \"Pega un enlace, escribe una nota o suelta una imagen…\"\n  },\n  \"toasts\": {\n    \"bookmarks\": {\n      \"updated\": \"¡El marcador se ha actualizado!\",\n      \"refetch\": \"¡Se ha solicitado la actualización!\",\n      \"deleted\": \"¡El marcador se ha eliminado!\",\n      \"full_page_archive\": \"Se ha pedido un Archivo de Página Completa\",\n      \"delete_from_list\": \"El marcador se ha borrado de la lista\",\n      \"clipboard_copied\": \"¡El enlace se ha copiado en tu portapapeles!\",\n      \"preserve_pdf\": \"Se ha activado la preservación en PDF\",\n      \"update_banner\": \"¡El banner ha sido actualizado!\",\n      \"uploading_banner\": \"Subiendo banner...\"\n    },\n    \"lists\": {\n      \"created\": \"¡Enlace creado correctamente!\",\n      \"updated\": \"¡La lista ha sido actualizada!\",\n      \"merged\": \"¡La lista se ha fusionado!\",\n      \"deleted\": \"¡La lista ha sido eliminada!\"\n    },\n    \"tags\": {\n      \"created\": \"¡Se ha creado la etiqueta!\",\n      \"failed_to_create\": \"No se ha podido crear la etiqueta\"\n    }\n  },\n  \"cleanups\": {\n    \"cleanups\": \"Limpiezas\",\n    \"duplicate_tags\": {\n      \"title\": \"Etiquetas duplicadas\",\n      \"merge_all_suggestions\": \"¿Fusionar todas las sugerencias?\"\n    }\n  },\n  \"highlights\": {\n    \"no_highlights\": \"Aún no tienes nada destacado.\"\n  },\n  \"search\": {\n    \"full_text_search\": \"Búsqueda de texto completo\",\n    \"created_on_or_after\": \"Creado en o después de\",\n    \"not_created_on_or_after\": \"No creado en o después de\",\n    \"created_on_or_before\": \"Creado en o antes de\",\n    \"url_does_not_contain\": \"La URL no contiene\",\n    \"is_in_list\": \"Está en la lista\",\n    \"or\": \"O\",\n    \"is_in_any_list\": \"Está en cualquier lista\",\n    \"is_not_in_any_list\": \"No está en ninguna lista\",\n    \"is_not_in_list\": \"No está en la lista\",\n    \"has_tag\": \"Tiene etiqueta\",\n    \"does_not_have_tag\": \"No tiene etiqueta\",\n    \"type_is_not\": \"El tipo no es\",\n    \"is_favorited\": \"Está en favoritos\",\n    \"is_not_favorited\": \"No está en favoritos\",\n    \"is_archived\": \"Está archivado\",\n    \"has_no_tags\": \"No tiene etiqueta\",\n    \"not_created_on_or_before\": \"No creado en o antes de\",\n    \"url_contains\": \"La URL contiene\",\n    \"and\": \"Y\",\n    \"is_not_archived\": \"No está archivado\",\n    \"has_any_tag\": \"Tiene alguna etiqueta\",\n    \"type_is\": \"El tipo es\",\n    \"is_from_feed\": \"Es de un feed RSS\",\n    \"is_not_from_feed\": \"No es de un feed RSS\",\n    \"week_s\": \" Semana(s)\",\n    \"created_within\": \"Creado dentro de\",\n    \"created_earlier_than\": \"Creado antes de\",\n    \"day_s\": \" Día(s)\",\n    \"month_s\": \" Mes(es)\",\n    \"year_s\": \" Año(s)\",\n    \"day_s_ago\": \" Hace día(s)\",\n    \"week_s_ago\": \" Hace semana(s)\",\n    \"month_s_ago\": \" Hace mes(es)\",\n    \"year_s_ago\": \" Hace año(s)\",\n    \"history\": \"Búsquedas recientes\",\n    \"title_contains\": \"El título contiene\",\n    \"title_does_not_contain\": \"El título no contiene\",\n    \"is_broken_link\": \"Tiene enlace roto\",\n    \"tags\": \"Etiquetas\",\n    \"no_suggestions\": \"Sin sugerencias\",\n    \"filters\": \"Filtros\",\n    \"is_not_broken_link\": \"Tiene enlace que funciona\",\n    \"lists\": \"Listas\",\n    \"feeds\": \"Feeds\",\n    \"is_from_source\": \"El origen es\",\n    \"is_not_from_source\": \"El origen no es\"\n  },\n  \"dialogs\": {\n    \"bookmarks\": {\n      \"delete_confirmation_description\": \"¿Seguro que quieres borrar este marcador?\",\n      \"delete_confirmation_title\": \"¿Borrar marcador?\"\n    }\n  },\n  \"bookmark_editor\": {\n    \"title\": \"Editar marcador\",\n    \"subtitle\": \"Realiza cambios en los detalles del marcador. Haz clic en guardar cuando hayas terminado.\",\n    \"author\": \"Autor\",\n    \"publisher\": \"Editor\",\n    \"date_published\": \"Fecha de publicación\",\n    \"pick_a_date\": \"Elige una fecha\",\n    \"save_changes\": \"Guardar cambios\",\n    \"extracted_content\": \"Contenido extraído\"\n  },\n  \"banners\": {\n    \"no_bookmarks\": {\n      \"title\": \"Aún no hay marcadores\",\n      \"description\": \"Guarda artículos, enlaces y páginas interesantes para acceder a ellos rápidamente más tarde.\"\n    }\n  },\n  \"view_options\": {\n    \"title\": \"Ver opciones\",\n    \"layout\": \"Disposición\",\n    \"columns\": \"Columnas\",\n    \"display_options\": \"Opciones de visualización\",\n    \"show_note_previews\": \"Mostrar notas\",\n    \"show_tags\": \"Mostrar etiquetas\",\n    \"show_title\": \"Mostrar título\",\n    \"image_options\": \"Opciones de imagen\",\n    \"image_fit_cover\": \"Portada (rellenar)\",\n    \"image_fit_contain\": \"Contener (ajustar)\"\n  },\n  \"version\": {\n    \"new_release_available\": \"Nuevas notas de la versión disponibles\",\n    \"whats_new_title\": \"¿Qué hay de nuevo en la v{{version}}?\",\n    \"release_notes_description\": \"Aquí están las últimas actualizaciones obtenidas de las notas de la versión de GitHub.\",\n    \"loading_release_notes\": \"Cargando las notas de la versión…\",\n    \"unable_to_load_release_notes\": \"No se pueden cargar las notas de la versión ahora mismo. Por favor, inténtelo de nuevo más tarde.\",\n    \"no_release_notes\": \"No se publicaron notas de la versión para esta versión.\",\n    \"release_notes_synced\": \"Las notas de la versión están sincronizadas desde GitHub.\",\n    \"view_on_github\": \"Ver en GitHub\"\n  },\n  \"wrapped\": {\n    \"title\": \"Tu resumen de {{year}}\",\n    \"subtitle\": \"Un año en Karakeep\",\n    \"banner\": {\n      \"title\": \"¡Tu resumen de 2025 está listo!\",\n      \"description\": \"Mira tu año en marcadores\",\n      \"view_now\": \"Ver ahora\"\n    },\n    \"button\": \"Resumen de 2025\",\n    \"loading\": \"Cargando tu resumen...\",\n    \"failed_to_load\": \"No se pudieron cargar tus estadísticas del resumen\",\n    \"sections\": {\n      \"total_saves\": {\n        \"prefix\": \"Guardaste\",\n        \"suffix\": \"artículos este año\",\n        \"suffix_singular\": \"artículo este año\"\n      },\n      \"first_bookmark\": {\n        \"title\": \"Tu aventura ha comenzado\",\n        \"description\": \"Primer guardado de {{year}}:\"\n      },\n      \"top_domains\": \"Tus sitios principales\",\n      \"top_tags\": \"Tus etiquetas destacadas\",\n      \"monthly_activity\": \"Tu año en guardados\",\n      \"most_active_day\": \"Tu día más activo\",\n      \"peak_times\": {\n        \"title\": \"Cuándo guardas\",\n        \"peak_hour\": \"Hora de mayor actividad\",\n        \"peak_day\": \"Día de mayor actividad\"\n      },\n      \"how_you_save\": \"Cómo guardas\",\n      \"what_you_saved\": \"Qué has guardado\",\n      \"summary\": {\n        \"favorites\": \"Favoritos\",\n        \"tags_created\": \"Etiquetas creadas\",\n        \"highlights\": \"Puntos destacados\"\n      },\n      \"types\": {\n        \"links\": \"Enlaces\",\n        \"notes\": \"Notas\",\n        \"assets\": \"Activos\"\n      }\n    },\n    \"footer\": \"Hecho con Karakeep\",\n    \"share\": \"Compartir\",\n    \"download\": \"Descargar\",\n    \"close\": \"Cerrar\",\n    \"generating\": \"Generando...\"\n  }\n}\n"
  },
  {
    "path": "apps/web/lib/i18n/locales/fa/translation.json",
    "content": "{\n  \"common\": {\n    \"url\": \"آدرس اینترنتی\",\n    \"name\": \"نام\",\n    \"email\": \"ایمیل\",\n    \"password\": \"رمزعبور\",\n    \"key\": \"کلید\",\n    \"role\": \"نقش\",\n    \"size\": \"حجم\",\n    \"roles\": {\n      \"user\": \"کاربر\",\n      \"admin\": \"مدیر\"\n    },\n    \"action\": \"عملیات\",\n    \"actions\": \"عملیات‌ها\",\n    \"created_at\": \"تاریخ ایجاد\",\n    \"updated_at\": \"تاریخ به‌روزرسانی\",\n    \"type\": \"نوع\",\n    \"something_went_wrong\": \"خطایی رخ داد\",\n    \"experimental\": \"آزمایشی\",\n    \"search\": \"جستجو\",\n    \"tags\": \"برچسب‌ها\",\n    \"note\": \"یادداشت\",\n    \"attachments\": \"پیوست‌ها\",\n    \"highlights\": \"هایلایت‌ها\",\n    \"source\": \"منبع\",\n    \"screenshot\": \"اسکرین‌شات\",\n    \"video\": \"ویدئو\",\n    \"home\": \"خانه\",\n    \"archive\": \"بایگانی\",\n    \"title\": \"عنوان\",\n    \"bookmarks\": \"نشانک‌ها\",\n    \"storage\": \"فضای ذخیره‌سازی\",\n    \"summary\": \"خلاصه\",\n    \"description\": \"توضیحات\",\n    \"bookmark_types\": {\n      \"title\": \"نوع نشانک\",\n      \"link\": \"لینک\",\n      \"text\": \"متن\",\n      \"media\": \"رسانه\"\n    },\n    \"quota\": \"سهمیه\",\n    \"pdf\": \"پی‌دی‌اف بایگانی‌شده\",\n    \"default\": \"پیش‌فرض\",\n    \"id\": \"شناسه\",\n    \"last_used\": \"آخرین استفاده\"\n  },\n  \"layouts\": {\n    \"grid\": \"شبکه‌ای\",\n    \"masonry\": \"کاشی‌چینی\",\n    \"list\": \"لیستی\",\n    \"compact\": \"فشرده\"\n  },\n  \"actions\": {\n    \"change_layout\": \"تغییر چیدمان\",\n    \"add_to_list\": \"افزودن به فهرست\",\n    \"delete\": \"حذف\",\n    \"edit_tags\": \"ویرایش برچسب‌ها\",\n    \"archive\": \"بایگانی\",\n    \"unselect_all\": \"لغو انتخاب همه\",\n    \"unarchive\": \"خروج از بایگانی\",\n    \"favorite\": \"افزودن به علاقه‌مندی\",\n    \"select_all\": \"انتخاب همه\",\n    \"remove_from_list\": \"حذف از فهرست\",\n    \"bulk_edit\": \"ویرایش گروهی\",\n    \"manage_lists\": \"مدیریت فهرست‌ها\",\n    \"unfavorite\": \"حذف از علاقه‌مندی‌ها\",\n    \"copy_link\": \"کپی لینک\",\n    \"save\": \"ذخیره\",\n    \"toggle_show_archived\": \"نمایش موارد بایگانی‌شده\",\n    \"refresh\": \"به‌روزرسانی\",\n    \"confirm\": \"تأیید\",\n    \"download_full_page_archive\": \"دانلود نسخهٔ بایگانی کامل صفحه\",\n    \"recrawl\": \"بازخزش\",\n    \"close_bulk_edit\": \"بستن ویرایش گروهی\",\n    \"create\": \"ایجاد\",\n    \"summarize_with_ai\": \"خلاصه‌سازی با هوش مصنوعی\",\n    \"edit\": \"ویرایش\",\n    \"fetch_now\": \"بازیابی اکنون\",\n    \"add\": \"افزودن\",\n    \"open_editor\": \"باز کردن ویرایشگر\",\n    \"edit_title\": \"ویرایش عنوان\",\n    \"sign_out\": \"خروج\",\n    \"regenerate\": \"تولید مجدد\",\n    \"merge\": \"ادغام\",\n    \"cancel\": \"انصراف\",\n    \"close\": \"بستن\",\n    \"ignore\": \"نادیده‌گرفتن\",\n    \"apply_all\": \"اعمال برای همه\",\n    \"sort\": {\n      \"relevant_first\": \"مرتبط‌ترین‌ها ابتدا\",\n      \"title\": \"مرتب‌سازی\",\n      \"newest_first\": \"جدیدترین‌ها ابتدا\",\n      \"oldest_first\": \"قدیمی‌ترین‌ها ابتدا\"\n    },\n    \"load_more\": \"بارگذاری بیشتر\",\n    \"edit_notes\": \"ویرایش یادداشت‌ها\",\n    \"preserve_as_pdf\": \"به عنوان پی‌دی‌اف نگهداری‌اش کن\",\n    \"offline_copies\": \"نسخه‌های آفلاین\",\n    \"preserve_offline_archive\": \"بایگانی آفلاین را حفظ کن\",\n    \"download_full_page_archive_file\": \"دانلود فایل بایگانی\",\n    \"download_pdf_file\": \"دانلود فایل PDF\",\n    \"remove\": \"حذف\",\n    \"more\": \"بیشتر\",\n    \"replace_banner\": \"جایگزینی بنر\",\n    \"add_banner\": \"اضافه کردن بنر\",\n    \"download\": \"دانلود\"\n  },\n  \"settings\": {\n    \"stats\": {\n      \"overview\": {\n        \"tags\": \"برچسب‌ها\",\n        \"archived_items\": \"آیتم‌های بایگانی‌شده\",\n        \"unique_tags_created\": \"تعداد برچسب‌های منحصربه‌فرد ایجادشده\",\n        \"lists\": \"فهرست‌ها\",\n        \"bookmark_collections\": \"مجموعه‌های نشانک\",\n        \"highlights\": \"هایلایت‌ها\",\n        \"text_highlights\": \"هایلایت‌های متنی\",\n        \"storage_used\": \"فضای مصرف‌شده\",\n        \"total_asset_storage\": \"کل فضای ذخیره‌سازی دارایی‌ها\",\n        \"this_month\": \"این ماه\",\n        \"bookmarks_added\": \"نشانک‌های افزوده‌شده\",\n        \"total_bookmarks\": \"کل نشانک‌ها\",\n        \"all_saved_items\": \"تمام آیتم‌های ذخیره‌شده\",\n        \"starred_bookmarks\": \"نشانک‌های ستاره‌دار\",\n        \"favorites\": \"علاقه‌مندی‌ها\",\n        \"archived\": \"بایگانی‌شده\"\n      },\n      \"bookmark_types\": {\n        \"title\": \"انواع نشانک\",\n        \"text_notes\": \"یادداشت‌های متنی\",\n        \"links\": \"لینک‌ها\",\n        \"assets\": \"دارایی‌ها\"\n      },\n      \"recent_activity\": {\n        \"title\": \"فعالیت‌های اخیر\",\n        \"this_week\": \"این هفته\",\n        \"this_month\": \"این ماه\",\n        \"this_year\": \"امسال\"\n      },\n      \"top_domains\": {\n        \"title\": \"دامنه‌های برتر\",\n        \"no_domains_found\": \"دامنه‌ای یافت نشد\"\n      },\n      \"most_used_tags\": {\n        \"title\": \"پرکاربردترین برچسب‌ها\",\n        \"no_tags_found\": \"برچسبی یافت نشد\"\n      },\n      \"usage_statistics\": \"آمار مصرف\",\n      \"insights_description\": \"بینش‌هایی دربارهٔ عادات ذخیره‌سازی و مجموعهٔ شما\",\n      \"failed_to_load\": \"بارگذاری آمار ناموفق بود\",\n      \"activity_patterns\": {\n        \"activity_by_hour\": \"فعالیت بر اساس ساعت\",\n        \"activity_by_day\": \"فعالیت بر اساس روز\"\n      },\n      \"storage_breakdown\": {\n        \"title\": \"جزئیات مصرف فضا\"\n      },\n      \"bookmark_sources\": {\n        \"title\": \"منابع نشانک\",\n        \"empty\": \"اطلاعات منبعی در دسترس نیست.\"\n      }\n    },\n    \"back_to_app\": \"بازگشت به برنامه\",\n    \"info\": {\n      \"user_info\": \"اطلاعات کاربر\",\n      \"basic_details\": \"جزئیات پایه\",\n      \"current_password\": \"رمزعبور فعلی\",\n      \"change_password\": \"تغییر رمزعبور\",\n      \"options\": \"گزینه‌ها\",\n      \"new_password\": \"رمزعبور جدید\",\n      \"confirm_new_password\": \"تأیید رمزعبور جدید\",\n      \"interface_lang\": \"زبان رابط کاربری\",\n      \"user_settings\": {\n        \"user_settings_updated\": \"تنظیمات کاربر به‌روزرسانی شد!\",\n        \"bookmark_click_action\": {\n          \"title\": \"اقدام هنگام کلیک روی نشانک\",\n          \"open_external_url\": \"باز کردن آدرس اصلی\",\n          \"open_bookmark_details\": \"باز کردن جزئیات نشانک\"\n        },\n        \"archive_display_behaviour\": {\n          \"title\": \"نشانک‌های بایگانی‌شده\",\n          \"show\": \"نمایش نشانک‌های بایگانی‌شده در برچسب‌ها و فهرست‌ها\",\n          \"hide\": \"مخفی‌کردن نشانک‌های بایگانی‌شده در برچسب‌ها و فهرست‌ها\"\n        }\n      },\n      \"reader_settings\": {\n        \"local_overrides_title\": \"تنظیمات مختص دستگاه فعال هستن\",\n        \"using_default\": \"در حال استفاده از پیش‌فرض مشتری\",\n        \"clear_override_hint\": \"پاک کردن لغو دستگاه برای استفاده از تنظیمات سراسری ({{value}})\",\n        \"font_size\": \"اندازه فونت\",\n        \"font_family\": \"خانواده فونت\",\n        \"preview_inline\": \"(پیش‌نمایش)\",\n        \"tooltip_preview\": \"تغییرات پیش نمایش ذخیره نشده\",\n        \"save_to_all_devices\": \"همه دستگاه‌ها\",\n        \"tooltip_local\": \"تنظیمات دستگاه با تنظیمات سراسری فرق داره\",\n        \"reset_preview\": \"بازنشانی پیش‌نمایش\",\n        \"mono\": \"تک‌فاصله\",\n        \"line_height\": \"ارتفاع خط\",\n        \"tooltip_default\": \"تنظیمات خواندن\",\n        \"title\": \"تنظیمات خواننده\",\n        \"serif\": \"سری‌دار\",\n        \"preview\": \"پیش‌نمایش\",\n        \"not_set\": \"تنظیم نشده\",\n        \"clear_local_overrides\": \"تنظیمات دستگاه رو پاک کن\",\n        \"preview_text\": \"روباه قهوه‌ای زرنگ از روی سگ تنبل می‌پره. متن قسمت خواننده اینجوری نمایش داده میشه.\",\n        \"local_overrides_cleared\": \"تنظیمات دستگاه پاک شدن\",\n        \"local_overrides_description\": \"این دستگاه تنظیمات خواننده‌ای داره که با تنظیمات پیش‌فرض کلی‌ت فرق دارن:\",\n        \"clear_defaults\": \"همه پیش‌فرض‌ها رو پاک کن\",\n        \"description\": \"تنظیم متن پیش‌فرض برای بخش خواننده رو پیکربندی کن. این تنظیمات با بقیه دستگاه‌هات هماهنگ میشه.\",\n        \"defaults_cleared\": \"پیش‌فرض‌های قسمت خواننده پاک شدن\",\n        \"save_hint\": \"ذخیره تنظیمات فقط برای این دستگاه یا همگام سازی بین همه دستگاه ها\",\n        \"save_as_default\": \"به عنوان پیش‌فرض ذخیره کن\",\n        \"save_to_device\": \"این دستگاه\",\n        \"sans\": \"بدون سری\",\n        \"tooltip_preview_and_local\": \"تغییرات پیش‌نمایش ذخیره نشده‌؛ تنظیمات دستگاه با تنظیمات کلی فرق داره\",\n        \"adjust_hint\": \"برای پیش نمایش تغییرات، تنظیمات بالا را تنظیم کنید\"\n      },\n      \"avatar\": {\n        \"upload\": \"بارگذاری آواتار\",\n        \"change\": \"تغییر آواتار\",\n        \"remove_confirm_title\": \"آواتار حذف بشه؟\",\n        \"updated\": \"آواتار به‌روز شد\",\n        \"removed\": \"آواتار حذف شد\",\n        \"description\": \"یه عکس مربع بارگذاری کن تا به عنوان آواتارت استفاده بشه.\",\n        \"remove_confirm_description\": \"این کار عکس پروفایل فعلیتو پاک می‌کنه.\",\n        \"title\": \"عکس پروفایل\",\n        \"remove\": \"حذف آواتار\"\n      }\n    },\n    \"user_settings\": \"تنظیمات کاربر\",\n    \"ai\": {\n      \"tagging_rules\": \"قواعد برچسب‌گذاری\",\n      \"prompt_preview\": \"پیش‌نمایش پرامپت\",\n      \"tagging_rule_description\": \"پرامپت‌هایی که اینجا اضافه می‌کنید هنگام تولید برچسب به‌عنوان قانون به مدل اعمال می‌شوند. پیش‌نمایش نهایی پرامپت‌ها در بخش مربوط نمایش داده می‌شود.\",\n      \"images_prompt\": \"پرامپت تصویری\",\n      \"all_tagging\": \"همهٔ برچسب‌گذاری‌ها\",\n      \"ai_settings\": \"تنظیمات هوش مصنوعی\",\n      \"image_tagging\": \"برچسب‌گذاری تصویر\",\n      \"text_prompt\": \"پرامپت متنی\",\n      \"text_tagging\": \"برچسب‌گذاری متن\",\n      \"summarization_prompt\": \"پرامپت خلاصه‌سازی\",\n      \"summarization\": \"خلاصه‌سازی\",\n      \"tag_style\": \"استایل برچسب\",\n      \"auto_summarization_description\": \"به‌طور خودکار با استفاده از هوش مصنوعی برای نشانک‌هایت خلاصه تولید کن.\",\n      \"auto_tagging\": \"برچسب‌گذاری خودکار\",\n      \"titlecase_spaces\": \"حالت عنوان با فاصله‌ها\",\n      \"lowercase_underscores\": \"حروف کوچک با زیرخط‌ها\",\n      \"inference_language\": \"زبان استنباطی\",\n      \"titlecase_hyphens\": \"حالت عنوان با خط تیره\",\n      \"lowercase_hyphens\": \"حروف کوچک با خط تیره\",\n      \"lowercase_spaces\": \"حروف کوچک با فاصله‌ها\",\n      \"inference_language_description\": \"زبانی را برای برچسب‌ها و خلاصه‌های تولید شده توسط هوش مصنوعی انتخاب کنید.\",\n      \"tag_style_description\": \"انتخاب کنید که برچسب‌های تولیدشده خودکار شما چگونه قالب‌بندی شوند.\",\n      \"auto_tagging_description\": \"به‌طور خودکار با استفاده از هوش مصنوعی برای نشانک‌هایت برچسب تولید کن.\",\n      \"camelCase\": \"camelCase\",\n      \"auto_summarization\": \"خلاصه‌سازی خودکار\",\n      \"no_preference\": \"بدون ترجیح\",\n      \"curated_tags\": \"برچسب‌های انتخاب‌شده\",\n      \"curated_tags_description\": \"به‌صورت اختیاری، برچسب‌گذاری هوش مصنوعی را محدود کنید تا فقط از برچسب‌های این فهرست استفاده کند. هنگامی که هیچ برچسبی انتخاب نشود، هوش مصنوعی برچسب‌ها را آزادانه تولید می‌کند.\",\n      \"curated_tags_updated\": \"برچسب‌های انتخاب‌شده با موفقیت به‌روز شدند!\",\n      \"curated_tags_update_failed\": \"به‌روزرسانی برچسب‌های انتخاب‌شده با خطا مواجه شد\"\n    },\n    \"feeds\": {\n      \"feed_enabled\": \"خوراک RSS فعال شد\",\n      \"rss_subscriptions\": \"اشتراک‌های RSS\",\n      \"feed_disabled\": \"خوراک RSS غیرفعال شد\",\n      \"add_a_subscription\": \"افزودن اشتراک\"\n    },\n    \"webhooks\": {\n      \"events\": {\n        \"title\": \"رویدادها\",\n        \"crawled\": \"خزیده‌شده\",\n        \"edited\": \"ویرایش‌شده\",\n        \"created\": \"ایجادشده\"\n      },\n      \"delete_webhook_confirmation\": \"آیا از حذف این وب‌هوک مطمئن هستید؟\",\n      \"webhooks\": \"وب‌هوک‌ها\",\n      \"description\": \"می‌توانید با وب‌هوک‌ها هنگام ایجاد، تغییر یا خزیدن نشانک‌ها اقداماتی را اجرا کنید.\",\n      \"edit_auth_token\": \"ویرایش توکن احراز هویت\",\n      \"add_auth_token\": \"افزودن توکن احراز هویت\",\n      \"auth_token\": \"توکن احراز هویت\",\n      \"create_webhook\": \"ایجاد وب‌هوک\",\n      \"delete_webhook\": \"حذف وب‌هوک\",\n      \"edit_webhook\": \"ویرایش وب‌هوک\",\n      \"webhook_url\": \"آدرس وب‌هوک\"\n    },\n    \"import\": {\n      \"import_export\": \"درون‌ریزی / برون‌بری\",\n      \"import_bookmarks_from_html_file\": \"درون‌ریزی نشانک‌ها از فایل HTML\",\n      \"import_export_bookmarks\": \"درون‌ریزی / برون‌بری نشانک‌ها\",\n      \"import_bookmarks_from_pocket_export\": \"درون‌ریزی نشانک‌ها از خروجی Pocket\",\n      \"import_bookmarks_from_matter_export\": \"درون‌ریزی نشانک‌ها از خروجی Matter\",\n      \"import_bookmarks_from_omnivore_export\": \"درون‌ریزی نشانک‌ها از خروجی Omnivore\",\n      \"import_bookmarks_from_linkwarden_export\": \"درون‌ریزی نشانک‌ها از خروجی Linkwarden\",\n      \"import_bookmarks_from_tab_session_manager_export\": \"درون‌ریزی نشانک‌ها از Tab Session Manager\",\n      \"export_links_and_notes\": \"برون‌بری لینک‌ها و یادداشت‌ها\",\n      \"import_bookmarks_from_karakeep_export\": \"درون‌ریزی نشانک‌ها از خروجی Karakeep\",\n      \"imported_bookmarks\": \"نشانک‌های درون‌ریزی‌شده\",\n      \"import_bookmarks_from_mymind_export\": \"وارد کردن نشانک ها از خروجی mymind\",\n      \"import_bookmarks_from_instapaper_export\": \"درون‌ریزی نشانک‌ها از خروجی Instapaper\"\n    },\n    \"api_keys\": {\n      \"new_api_key\": \"کلید API جدید\",\n      \"regenerate_api_key\": \"تولید مجدد کلید API\",\n      \"key_regenerated\": \"کلید با موفقیت بازتولید شد\",\n      \"key_regenerated_please_copy\": \"لطفاً کلید جدید را کپی و در محلی امن ذخیره کنید. کلید قبلی لغو شده و دیگر کار نخواهد کرد.\",\n      \"api_keys\": \"کلیدهای API\",\n      \"new_api_key_desc\": \"برای کلید API یک نام یکتا بگذارید\",\n      \"key_success\": \"کلید با موفقیت ایجاد شد\",\n      \"key_success_please_copy\": \"لطفاً کلید را کپی کرده و در جایی امن نگه‌داری کنید. پس از بستن این پنجره دیگر به آن دسترسی نخواهید داشت.\",\n      \"regenerate_warning\": \"آیا برای تولید مجدد کلید API «{{name}}» مطمئن هستید؟\",\n      \"regenerate_confirmation\": \"با این کار کلید فعلی لغو و کلید جدیدی تولید می‌شود. تمام برنامه‌هایی که از کلید فعلی استفاده می‌کنند از کار می‌افتند.\"\n    },\n    \"broken_links\": {\n      \"crawling_status\": \"وضعیت خزیدن\",\n      \"last_crawled_at\": \"آخرین زمان خزیدن\",\n      \"broken_links\": \"لینک‌های خراب\",\n      \"crawling_failed\": \"خزیدن ناموفق بود\"\n    },\n    \"manage_assets\": {\n      \"no_assets\": \"هنوز دارایی‌ای ندارید.\",\n      \"asset_type\": \"نوع دارایی\",\n      \"delete_asset\": \"حذف دارایی\",\n      \"delete_asset_confirmation\": \"آیا از حذف این دارایی مطمئن هستید؟\",\n      \"manage_assets\": \"مدیریت دارایی‌ها\",\n      \"bookmark_link\": \"لینک نشانک\",\n      \"asset_link\": \"لینک دارایی\"\n    },\n    \"rules\": {\n      \"ceate_rule\": \"ایجاد قاعده\",\n      \"edit_rule\": \"ویرایش قاعده\",\n      \"rule_name\": \"نام قاعده\",\n      \"rules\": \"موتور قواعد\",\n      \"save_rule\": \"ذخیرهٔ قاعده\",\n      \"whenever\": \"هر زمان که …\",\n      \"if\": \"اگر …\",\n      \"enter_rule_name\": \"نام قاعده را وارد کنید\",\n      \"delete_rule_confirmation\": \"آیا از حذف این قاعده مطمئن هستید؟\",\n      \"description\": \"می‌توانید با قواعد، هنگام وقوع رویداد اقداماتی را اجرا کنید.\",\n      \"delete_rule\": \"حذف قاعده\",\n      \"conditions_types\": {\n        \"and\": \"همهٔ موارد زیر برقرار باشند\",\n        \"always\": \"همیشه\",\n        \"has_tag\": \"دارای برچسب باشد\",\n        \"imported_from_feed\": \"از خوراک وارد شده\",\n        \"url_contains\": \"آدرس شامل باشد\",\n        \"bookmark_type_is\": \"نوع نشانک باشد\",\n        \"is_favourited\": \"به علاقه‌مندی‌ها افزوده شده باشد\",\n        \"is_archived\": \"بایگانی شده باشد\",\n        \"or\": \"هر یک از موارد زیر برقرار باشد\",\n        \"url_does_not_contain\": \"آدرس اینترنتی شامل نمی‌شود\",\n        \"title_contains\": \"عنوان شامل شود\",\n        \"title_does_not_contain\": \"عنوان شامل نشود\"\n      },\n      \"describe_what_this_rule_does\": \"توضیح دهید این قاعده چه می‌کند\",\n      \"rule_has_been_deleted\": \"قاعده حذف شد!\",\n      \"no_rules_created_yet\": \"هنوز قاعده‌ای ایجاد نشده است\",\n      \"rule_has_been_created\": \"قاعده ایجاد شد!\",\n      \"rule_has_been_updated\": \"قاعده به‌روزرسانی شد!\",\n      \"create_your_first_rule\": \"برای خودکارسازی فرایند، اولین قاعده را بسازید\",\n      \"events_types\": {\n        \"tag_removed\": \"این برچسب از نشانکی حذف شود\",\n        \"bookmark_added\": \"نشانکی افزوده شود\",\n        \"archived\": \"نشانکی بایگانی شود\",\n        \"tag_added\": \"این برچسب به نشانکی افزوده شود\",\n        \"added_to_list\": \"نشانکی به این فهرست افزوده شود\",\n        \"removed_from_list\": \"نشانکی از این فهرست حذف شود\",\n        \"favourited\": \"نشانکی به علاقه‌مندی‌ها افزوده شود\"\n      },\n      \"actions_types\": {\n        \"add_tag\": \"افزودن برچسب\",\n        \"remove_tag\": \"حذف برچسب\",\n        \"remove_from_list\": \"حذف از فهرست\",\n        \"add_to_list\": \"افزودن به فهرست\",\n        \"download_full_page_archive\": \"دانلود نسخهٔ بایگانی کامل صفحه\",\n        \"favourite_bookmark\": \"افزودن به علاقه‌مندی‌ها\",\n        \"archive_bookmark\": \"بایگانی نشانک\"\n      }\n    },\n    \"subscription\": {\n      \"manage_subscription\": \"مدیریت اشتراک و اطلاعات صورتحساب\",\n      \"billing_period\": \"دورهٔ صورتحساب\",\n      \"current_plan\": \"طرح فعلی\",\n      \"subscription_canceled\": \"اشتراک شما لغو شده و در تاریخ {{date}} پایان می‌یابد. هر زمان می‌توانید دوباره مشترک شوید.\",\n      \"paid_plan\": \"طرح پولی\",\n      \"unlock_bigger_quota\": \"سهمیهٔ بیشتر را فعال و از پروژه حمایت کنید\",\n      \"usage_quotas\": \"مصرف و سهمیه‌ها\",\n      \"subscribe_now\": \"اکنون اشتراک بگیرید\",\n      \"subscription\": \"اشتراک\",\n      \"manage_billing\": \"مدیریت صورتحساب\",\n      \"track_usage\": \"مصرف فعلی را نسبت به محدودیت‌های طرح خود پیگیری کنید\",\n      \"total_bookmarks_saved\": \"مجموع نشانک‌های ذخیره‌شده\",\n      \"assets_file_storage\": \"دارایی‌ها و فضای فایل\",\n      \"unlimited_usage\": \"مصرف نامحدود\",\n      \"quota_limit_reached\": \"سقف سهمیه پر شد\",\n      \"approaching_quota_limit\": \"نزدیک به سقف سهمیه\",\n      \"loading_usage\": \"در حال بارگذاری اطلاعات مصرف…\",\n      \"free\": \"رایگان\",\n      \"paid\": \"پرداخت شده\"\n    },\n    \"import_sessions\": {\n      \"title\": \"وارد کردن جلسات\",\n      \"description\": \"مشاهده و مدیریت جلسات واردات دسته‌ای خود را انجام دهید. جلسات هنگام وارد کردن نشانک‌ها به‌طور خودکار ایجاد می‌شوند.\",\n      \"load_error\": \"بارگیری جلسات با مشکل مواجه شد\",\n      \"no_sessions\": \"هنوز جلسه وارداتی وجود ندارد\",\n      \"no_sessions_detail\": \"هنگامی که نشانک‌ها را وارد کنید، به‌طور خودکار جلسات وارد شده در اینجا ظاهر می‌شوند\",\n      \"created_at\": \"ایجادشده در {{time}}\",\n      \"progress\": \"پیشرفت\",\n      \"status\": {\n        \"pending\": \"در انتظار\",\n        \"in_progress\": \"در حال انجام\",\n        \"completed\": \"تکمیل‌شده\",\n        \"failed\": \"ناموفق\",\n        \"processing\": \"در حال پردازش\",\n        \"staging\": \"در حال آماده سازی\",\n        \"running\": \"در حال اجرا\",\n        \"paused\": \"متوقف شده\"\n      },\n      \"badges\": {\n        \"pending\": \"{{count}} مورد در انتظار\",\n        \"processing\": \"{{count}} مورد در حال پردازش\",\n        \"completed\": \"{{count}} مورد تکمیل‌شده\",\n        \"failed\": \"{{count}} مورد ناموفق\"\n      },\n      \"imported_to\": \"درون‌ریزی‌شده به:\",\n      \"view_list\": \"مشاهدهٔ فهرست\",\n      \"delete_dialog_title\": \"حذف نشست درون‌ریزی\",\n      \"delete_dialog_description\": \"آیا مطمئنید می‌خواهید «{{name}}» را حذف کنید؟ این عمل قابل بازگشت نیست. خود نشانک‌ها حذف نخواهند شد.\",\n      \"delete_session\": \"حذف جلسه\",\n      \"pause_session\": \"توقف\",\n      \"resume_session\": \"ادامه\",\n      \"view_details\": \"مشاهده جزئیات\",\n      \"detail\": {\n        \"page_title\": \"جزئیات جلسه وارد کردن\",\n        \"back_to_import\": \"بازگشت به وارد کردن\",\n        \"filter_all\": \"همه\",\n        \"filter_accepted\": \"پذیرفته شده\",\n        \"filter_rejected\": \"رد شده\",\n        \"filter_duplicates\": \"تکراری‌ها\",\n        \"filter_pending\": \"در انتظار\",\n        \"table_title\": \"عنوان / آدرس اینترنتی\",\n        \"table_type\": \"نوع\",\n        \"table_result\": \"نتیجه\",\n        \"table_reason\": \"علت\",\n        \"table_bookmark\": \"نشانک\",\n        \"result_accepted\": \"پذیرفته شده\",\n        \"result_rejected\": \"رد شده\",\n        \"result_skipped_duplicate\": \"تکراری\",\n        \"result_pending\": \"در انتظار\",\n        \"result_processing\": \"در حال پردازش\",\n        \"no_results\": \"هیچ نتیجه‌ای برای این فیلتر یافت نشد.\",\n        \"view_bookmark\": \"مشاهده‌ی نشانک\",\n        \"load_more\": \"بیشتر بار کن\",\n        \"no_title\": \"بدون عنوان\"\n      }\n    },\n    \"backups\": {\n      \"backups\": \"پشتیبان‌گیری‌ها\",\n      \"page_title\": \"پشتیبان‌گیری‌ها\",\n      \"page_description\": \"به صورت خودکار از بوکمارک‌هات پشتیبان‌گیری ایجاد و مدیریت کن. پشتیبان‌گیری‌ها فشرده شده و به طور امن ذخیره می‌شن.\",\n      \"configuration\": {\n        \"title\": \"پیکربندی پشتیبان‌گیری\",\n        \"enable_automatic_backups\": \"فعال کردن پشتیبان‌گیری خودکار\",\n        \"enable_automatic_backups_description\": \"به صورت خودکار از بوکمارک‌هات پشتیبان‌گیری ایجاد کن\",\n        \"backup_frequency\": \"دورهٔ تناوب پشتیبان‌گیری\",\n        \"backup_frequency_description\": \"پشتیبان‌گیری‌ها باید هر چند وقت یک بار ایجاد شن\",\n        \"retention_period\": \"دورهٔ نگهداری (روزها)\",\n        \"retention_period_description\": \"پشتیبان‌گیری‌ها رو قبل از پاک کردنشون چند روز نگه دار\",\n        \"frequency\": {\n          \"daily\": \"روزانه\",\n          \"weekly\": \"هفتگی\"\n        },\n        \"select_frequency\": \"انتخاب دوزهٔ تناوب\",\n        \"save_settings\": \"ذخیرهٔ تنظیمات\"\n      },\n      \"list\": {\n        \"title\": \"پشتیبان‌گیری‌های شما\",\n        \"create_backup_now\": \"الان پشتیبان‌گیری ایجاد کن\",\n        \"no_backups\": \"هنوز هیچ پشتیبان‌گیری‌ای نداری. پشتیبان‌گیری خودکار رو فعال کن یا به صورت دستی یه دونه ایجاد کن.\",\n        \"table\": {\n          \"created_at\": \"ایجاد شده در\",\n          \"bookmarks\": \"نشانک‌ها\",\n          \"size\": \"اندازه\",\n          \"status\": \"وضعیت\",\n          \"actions\": \"اقدامات\"\n        },\n        \"status\": {\n          \"success\": \"موفقیت‌آمیز\",\n          \"failed\": \"ناموفق\",\n          \"pending\": \"در انتظار\"\n        },\n        \"actions\": {\n          \"download_backup\": \"بارگیری پشتیبان\",\n          \"delete_backup\": \"حذف پشتیبان\"\n        }\n      },\n      \"dialogs\": {\n        \"delete_backup_title\": \"حذف پشتیبان؟\",\n        \"delete_backup_description\": \"آیا مطمئنید که می‌خواهید این پشتیبان را حذف کنید؟ این عمل قابل برگشت نیست.\"\n      },\n      \"toasts\": {\n        \"backup_queued\": \"وظیفه پشتیبان‌گیری در صف قرار گرفت! به زودی پردازش خواهد شد.\",\n        \"backup_deleted\": \"پشتیبان حذف شد!\"\n      }\n    }\n  },\n  \"highlights\": {\n    \"no_highlights\": \"هنوز هیچ هایلایتی ندارید.\"\n  },\n  \"admin\": {\n    \"admin_settings\": \"تنظیمات مدیر\",\n    \"server_stats\": {\n      \"server_stats\": \"آمار سرور\",\n      \"total_users\": \"تعداد کل کاربران\",\n      \"total_bookmarks\": \"کل نشانک‌ها\",\n      \"server_version\": \"نسخه سرور\"\n    },\n    \"service_connections\": {\n      \"title\": \"اتصالات سرویس\",\n      \"description\": \"سلامت و اتصال وابستگی‌های سیستم خارجی را زیر نظر بگیرید\",\n      \"search_engine\": \"موتور جستجو\",\n      \"browser\": \"مرورگر\",\n      \"queue_system\": \"سیستم صف\",\n      \"status\": {\n        \"not_configured\": \"پیکربندی نشده\",\n        \"connected\": \"متصل شد\",\n        \"disconnected\": \"قطع شده\"\n      }\n    },\n    \"background_jobs\": {\n      \"jobs\": {\n        \"crawler\": {\n          \"title\": \"وظایف خزنده\",\n          \"description\": \"خزش وب و استخراج محتوا از URLها\"\n        },\n        \"inference\": {\n          \"title\": \"وظایف استنتاج\",\n          \"description\": \"برچسب زنی مبتنی بر هوش مصنوعی و خلاصه سازی محتوا\"\n        },\n        \"indexing\": {\n          \"title\": \"وظایف فهرست گذاری\",\n          \"description\": \"به روز رسانی های فهرست جستجو\"\n        },\n        \"asset_preprocessing\": {\n          \"title\": \"وظایف پیش پردازش دارایی\",\n          \"description\": \"پیش‌پردازش تصویر و سند (اسکرین‌شات، استخراج متن و …)\"\n        },\n        \"tidy_assets\": {\n          \"title\": \"وظایف دارایی های مرتب\",\n          \"description\": \"پاکسازی دارایی و بهینه سازی ذخیره سازی\"\n        },\n        \"video\": {\n          \"title\": \"وظایف دانلود ویدیو\",\n          \"description\": \"استخراج و دانلود ویدیو\"\n        },\n        \"webhook\": {\n          \"title\": \"وظایف Webhook\",\n          \"description\": \"اعلان های Webhook خارجی\"\n        },\n        \"feed\": {\n          \"title\": \"وظایف فید RSS\",\n          \"description\": \"پردازش فید RSS و به روز رسانی های محتوا\"\n        },\n        \"admin_maintenance\": {\n          \"title\": \"کارهای تعمیر و نگهداری ادمین\",\n          \"description\": \"پاکسازی مدیریتی و نگهداری دارایی\"\n        }\n      },\n      \"background_jobs\": \"وظایف پس زمینه\",\n      \"monitor_and_manage\": \"نظارت و مدیریت صف های وظیفه پس زمینه و وظایف پردازش سیستم\",\n      \"active\": \"فعال\",\n      \"status\": {\n        \"queued\": {\n          \"description\": \"کارها در صف انتظار برای پردازش هستند. وقتی منابع در دسترس باشند، به طور خودکار شروع می شوند.\",\n          \"title\": \"در صف\"\n        },\n        \"failed\": {\n          \"title\": \"ناموفق\",\n          \"description\": \"نشانک‌هایی که در طول پردازش با خطا مواجه شده‌اند. این موارد ممکن است نیاز به بررسی دستی داشته باشند.\"\n        },\n        \"title\": \"درک وضعیت‌های شغلی\",\n        \"unprocessed\": {\n          \"title\": \"پردازش نشده\",\n          \"description\": \"نشانک‌هایی که هنوز پردازش نشده‌اند. به احتمال زیاد آن‌ها از قبل در صف پردازش قرار دارند، اگر نه، ممکن است لازم باشد آن‌ها را به صورت دستی دوباره در صف قرار دهید.\"\n        }\n      },\n      \"actions\": {\n        \"recrawl_failed_links_only\": \"فقط پیوندهای ناموفق را دوباره بخز\",\n        \"regenerate_ai_tags_for_failed_bookmarks_only\": \"تولید مجدد برچسب‌های هوش مصنوعی فقط برای نشانک‌های ناموفق\",\n        \"regenerate_ai_tags_for_all_bookmarks\": \"تولید مجدد برچسب‌های هوش مصنوعی برای همهٔ نشانک‌ها\",\n        \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"تولید مجدد خلاصه‌های هوش مصنوعی فقط برای نشانک‌های ناموفق\",\n        \"regenerate_ai_summaries_for_all_bookmarks\": \"بازسازی خلاصه های هوش مصنوعی برای همه نشانک ها\",\n        \"reindex_all_bookmarks\": \"بازسازی فهرست تمام نشانک‌ها\",\n        \"without_inference\": \"بدون استنتاج\",\n        \"recrawl_all_links\": \"دوباره خزیدن همه لینک‌ها\",\n        \"clean_assets\": \"پاکسازی دارایی‌های معلق و همگام‌سازی دوباره فراداده‌ها\",\n        \"reprocess_assets_fix_mode\": \"پردازش مجدد دارایی‌های پردازش نشده\",\n        \"migrate_large_link_html_content\": \"انتقال محتوای بزرگ HTML درون‌خطی به دارایی‌ها\",\n        \"recrawl_pending_links_only\": \"فقط لینک‌های در انتظار بازخزش شوند\",\n        \"regenerate_ai_tags_for_pending_bookmarks_only\": \"فقط تگ‌های هوش مصنوعی برای نشانک‌های در انتظار دوباره ساخته شوند\",\n        \"regenerate_ai_summaries_for_pending_bookmarks_only\": \"فقط خلاصه‌های هوش مصنوعی برای نشانک‌های در انتظار دوباره ساخته شوند\"\n      },\n      \"available_actions\": \"اقدامات موجود\"\n    },\n    \"users_list\": {\n      \"change_role\": \"تغییر نقش\",\n      \"reset_password\": \"بازنشانی گذرواژه\",\n      \"delete_user\": \"حذف کاربر\",\n      \"delete_user_confirm_description\": \"آیا مطمئن هستید می‌خواهید کاربر «{{name}}» را حذف کنید؟\",\n      \"num_bookmarks\": \"تعداد نشانک‌ها\",\n      \"asset_sizes\": \"اندازه‌های دارایی\",\n      \"local_user\": \"کاربر محلی\",\n      \"confirm_password\": \"تأیید گذرواژه\",\n      \"unlimited\": \"نامحدود\",\n      \"users_list\": \"لیست کاربران\",\n      \"create_user\": \"ایجاد کاربر\"\n    },\n    \"admin_tools\": {\n      \"admin_tools\": \"ابزارهای ادمین\",\n      \"bookmark_debugger\": \"اشکال‌زدای نشانک\",\n      \"bookmark_id\": \"شناسه نشانک\",\n      \"bookmark_id_placeholder\": \"شناسه نشانک را وارد کنید\",\n      \"lookup\": \"جستجو\",\n      \"debug_info\": \"اطلاعات اشکال‌زدایی\",\n      \"basic_info\": \"اطلاعات پایه\",\n      \"status\": \"وضعیت\",\n      \"content\": \"محتوا\",\n      \"html_preview\": \"پیش‌نمایش HTML (۱۰۰۰ کاراکتر اول)\",\n      \"summary\": \"خلاصه\",\n      \"url\": \"آدرس\",\n      \"source_url\": \"آدرس منبع\",\n      \"asset_type\": \"نوع دارایی\",\n      \"file_name\": \"نام فایل\",\n      \"owner_user_id\": \"شناسه کاربر صاحب\",\n      \"tagging_status\": \"وضعیت برچسب‌گذاری\",\n      \"summarization_status\": \"وضعیت خلاصه‌سازی\",\n      \"crawl_status\": \"وضعیت خزیدن\",\n      \"crawl_status_code\": \"کد وضعیت HTTP\",\n      \"crawled_at\": \"خزش شده در\",\n      \"recrawl\": \"خزش مجدد\",\n      \"reindex\": \"نمایه‌سازی مجدد\",\n      \"retag\": \"برچسب‌گذاری مجدد\",\n      \"resummarize\": \"خلاصه‌سازی مجدد\",\n      \"bookmark_not_found\": \"نشانک پیدا نشد\",\n      \"action_success\": \"عملیات با موفقیت به پایان رسید\",\n      \"action_failed\": \"عملیات ناموفق بود\",\n      \"recrawl_queued\": \"وظیفه خزیدن مجدد در صف قرار گرفته است\",\n      \"reindex_queued\": \"وظیفه فهرست‌بندی مجدد در صف قرار گرفته است\",\n      \"retag_queued\": \"وظیفه برچسب‌گذاری مجدد در صف قرار گرفته است\",\n      \"resummarize_queued\": \"وظیفه خلاصه‌سازی مجدد در صف قرار گرفته است\",\n      \"view\": \"مشاهده\",\n      \"fetch_error\": \"خطا در دریافت نشانک\"\n    }\n  },\n  \"options\": {\n    \"dark_mode\": \"حالت تیره\",\n    \"light_mode\": \"حالت روشن\",\n    \"apps_extensions\": \"برنامه‌ها و افزونه‌ها\",\n    \"documentation\": \"مستندات\",\n    \"follow_us_on_x\": \"ما را در X دنبال کنید\"\n  },\n  \"lists\": {\n    \"all_lists\": \"تمام فهرست‌ها\",\n    \"favourites\": \"مورد علاقه‌ها\",\n    \"new_list\": \"فهرست جدید\",\n    \"edit_list\": \"ویرایش فهرست\",\n    \"share_list\": \"اشتراک‌گذاری فهرست\",\n    \"new_nested_list\": \"فهرست تودرتوی جدید\",\n    \"merge_list\": \"ادغام فهرست\",\n    \"destination_list\": \"فهرست مقصد\",\n    \"delete_after_merge\": \"حذف فهرست اصلی پس از ادغام\",\n    \"no_destination\": \"بدون مقصد\",\n    \"parent_list\": \"فهرست والدین\",\n    \"no_parent\": \"والد ندارد\",\n    \"list_type\": \"نوع فهرست\",\n    \"manual_list\": \"فهرست دستی\",\n    \"smart_list\": \"فهرست هوشمند\",\n    \"search_query\": \"جستار جستجو\",\n    \"search_query_help\": \"بیشتر در مورد زبان جستار جستجو بیاموزید.\",\n    \"description\": \"توضیحات (اختیاری)\",\n    \"delete_list\": {\n      \"title\": \"حذف فهرست\",\n      \"description\": \"حذف یک فهرست هیچ نشانکی را در آن فهرست حذف نمی‌کند.\",\n      \"delete_children\": \"فهرست‌های فرزند (بازگشتی) را حذف کنید\",\n      \"delete_children_description\": \"اگر علامت‌گذاری نشود، همه فهرست‌های مستقیم فرزند، فهرست ریشه می‌گردند\"\n    },\n    \"rss\": {\n      \"title\": \"خوراک RSS\",\n      \"description\": \"فعال کردن خوراک RSS برای این فهرست\",\n      \"feed_url\": \"آدرس خوراک RSS\"\n    },\n    \"public_list\": {\n      \"title\": \"فهرست همگانی\",\n      \"description\": \"به دیگران اجازه دهید این فهرست را ببینند\",\n      \"share_link\": \"پیوند هم‌رسانی\"\n    },\n    \"shared\": \"مشترک\",\n    \"collaborators\": {\n      \"manage\": \"مدیریت همکاران\",\n      \"view\": \"مشاهده همکاران\",\n      \"collaborators\": \"همکاران\",\n      \"add\": \"افزودن همکار\",\n      \"current\": \"همکاران فعلی\",\n      \"enter_email\": \"آدرس ایمیل را وارد کنید\",\n      \"please_enter_email\": \"لطفا یک آدرس ایمیل وارد کنید\",\n      \"added_successfully\": \"همکار با موفقیت اضافه شد\",\n      \"failed_to_add\": \"اضافه کردن همکار با مشکل مواجه شد\",\n      \"removed\": \"همکار حذف شد\",\n      \"failed_to_remove\": \"حذف همکار با مشکل مواجه شد\",\n      \"role_updated\": \"نقش به‌روزرسانی شد\",\n      \"failed_to_update_role\": \"به‌روزرسانی نقش با مشکل مواجه شد\",\n      \"viewer\": \"بیننده\",\n      \"editor\": \"ویرایشگر\",\n      \"owner\": \"مالک\",\n      \"viewer_description\": \"می‌تواند نشانک‌ها را در لیست مشاهده کند\",\n      \"editor_description\": \"می‌تواند نشانک‌ها را اضافه و حذف کند\",\n      \"no_collaborators\": \"هنوز هیچ همکاری وجود ندارد. برای شروع همکاری، یک نفر را اضافه کنید!\",\n      \"no_collaborators_readonly\": \"هیچ همکاری برای این فهرست وجود ندارد.\",\n      \"people_with_access\": \"افرادی که به این فهرست دسترسی دارند\",\n      \"add_or_remove\": \"اضافه یا حذف کردن افرادی که می‌توانند به این فهرست دسترسی داشته باشند\",\n      \"invitation_sent\": \"دعوت با موفقیت ارسال شد\",\n      \"invitation_revoked\": \"دعوت لغو شد\",\n      \"failed_to_revoke\": \"لغو دعوت ناموفق بود\",\n      \"pending\": \"در انتظار\",\n      \"revoke\": \"لغو\",\n      \"declined\": \"رد شده\"\n    },\n    \"leave_list\": {\n      \"title\": \"ترک لیست\",\n      \"confirm_message\": \"آیا مطمئن هستید که می‌خواهید {{icon}} {{name}} را ترک کنید؟\",\n      \"warning\": \"دیگر قادر به مشاهده یا دسترسی به نشانک‌های درون این فهرست نخواهید بود. در صورت نیاز، صاحب فهرست می‌تواند شما را دوباره اضافه کند.\",\n      \"action\": \"ترک لیست\",\n      \"success\": \"شما «{{icon}} {{name}}» را ترک کرده‌اید\"\n    },\n    \"invitations\": {\n      \"pending\": \"دعوت‌نامه‌های در انتظار\",\n      \"description\": \"دعوت‌نامه‌های همکاری لیست را بررسی و به آنها پاسخ دهید\",\n      \"invited_by\": \"دعوت شده توسط\",\n      \"accept\": \"پذیرفتن\",\n      \"decline\": \"نپذیرفتن\",\n      \"accepted\": \"دعوت پذیرفته شد\",\n      \"declined\": \"دعوت رد شد\",\n      \"failed_to_accept\": \"پذیرش دعوت ناموفق بود\",\n      \"failed_to_decline\": \"رد کردن دعوت ناموفق بود\"\n    },\n    \"shared_lists\": \"لیست های مشترک\"\n  },\n  \"tags\": {\n    \"all_tags\": \"همهٔ برچسب‌ها\",\n    \"your_tags\": \"برچسب‌های شما\",\n    \"your_tags_info\": \"برچسب‌هایی که حداقل یک بار توسط شما پیوست شده‌اند\",\n    \"ai_tags\": \"برچسب‌های هوش مصنوعی\",\n    \"ai_tags_info\": \"برچسب‌هایی که فقط به‌طور خودکار (توسط هوش مصنوعی) پیوست شده‌اند\",\n    \"unused_tags\": \"برچسب‌های استفاده‌نشده\",\n    \"unused_tags_info\": \"برچسب‌هایی که به هیچ نشانکی پیوست نشده‌اند\",\n    \"delete_all_unused_tags\": \"حذف همه برچسب‌های استفاده نشده\",\n    \"drag_and_drop_merging\": \"ادغام کشیدن و رها کردن\",\n    \"drag_and_drop_merging_info\": \"برچسب‌ها را روی هم بکشید و رها کنید تا ادغام شوند\",\n    \"sort_by_name\": \"مرتب سازی بر اساس نام\",\n    \"sort_by_usage\": \"مرتب سازی براساس استفاده\",\n    \"sort_by_relevance\": \"مرتب سازی براساس ارتباط\",\n    \"create_tag\": \"ایجاد برچسب\",\n    \"create_tag_description\": \"یک تگ جدید بدون اتصال به هیچ نشانک ایجاد کنید\",\n    \"tag_name\": \"نام برچسب\",\n    \"enter_tag_name\": \"نام برچسب را وارد کنید\",\n    \"no_custom_tags\": \"هنوز هیچ برچسب سفارشی وجود ندارد\",\n    \"no_ai_tags\": \"هنوز هیچ برچسب هوش مصنوعی وجود ندارد\",\n    \"no_unused_tags\": \"شما هیچ برچسب استفاده نشده ندارید\",\n    \"no_unused_tags_match_your_search\": \"هیچ برچسب استفاده نشده ای با جستجوی شما مطابقت ندارد\",\n    \"no_tags_match_your_search\": \"هیچ برچسبی با جستجوی شما مطابقت نداره\",\n    \"search_placeholder\": \"جستجوی برچسب‌ها...\",\n    \"search_or_create_placeholder\": \"جستجو یا ایجاد برچسب‌ها...\"\n  },\n  \"search\": {\n    \"is_favorited\": \"مورد علاقه‌س\",\n    \"is_not_favorited\": \"مورد علاقه نیست\",\n    \"is_archived\": \"بایگانی شده\",\n    \"is_not_archived\": \"بایگانی نشده\",\n    \"has_any_tag\": \"هر برچسبی داره\",\n    \"has_no_tags\": \"هیچ برچسبی نداره\",\n    \"is_in_any_list\": \"توی هر لیستی هست\",\n    \"is_not_in_any_list\": \"توی هیچ لیستی نیست\",\n    \"created_on_or_after\": \"ایجاد شده در یا بعد از\",\n    \"not_created_on_or_after\": \"ایجاد نشده در یا بعد از\",\n    \"created_on_or_before\": \"ایجاد شده در یا قبل از\",\n    \"not_created_on_or_before\": \"ایجاد نشده در یا قبل از\",\n    \"created_within\": \"ایجاد شده در عرض\",\n    \"created_earlier_than\": \"ایجاد شده زودتر از\",\n    \"day_s\": \" روزها\",\n    \"week_s\": \" هفته‌ها\",\n    \"month_s\": \" ماه ها\",\n    \"year_s\": \" سال ها\",\n    \"day_s_ago\": \" روزها پیش\",\n    \"week_s_ago\": \" هفته(های) پیش\",\n    \"month_s_ago\": \" ماه(های) پیش\",\n    \"year_s_ago\": \" سال(های) قبل\",\n    \"url_contains\": \"آدرس اینترنتی شامل\",\n    \"url_does_not_contain\": \"URL شامل نیست\",\n    \"title_contains\": \"عنوان شامل می‌شود\",\n    \"title_does_not_contain\": \"عنوان شامل نمی‌شود\",\n    \"is_in_list\": \"در لیست هست\",\n    \"is_not_in_list\": \"در لیست نیست\",\n    \"has_tag\": \"برچسب داره\",\n    \"does_not_have_tag\": \"تگ ندارد\",\n    \"full_text_search\": \"جستجوی متن کامل\",\n    \"type_is\": \"نوع اینه\",\n    \"type_is_not\": \"نوع این نیست\",\n    \"is_from_feed\": \"از فید RSS است\",\n    \"is_not_from_feed\": \"از فید RSS نیست\",\n    \"and\": \"و\",\n    \"or\": \"یا\",\n    \"history\": \"جستجوهای اخیر\",\n    \"is_broken_link\": \"لینک خراب دارد\",\n    \"tags\": \"برچسب‌ها\",\n    \"no_suggestions\": \"بدون پیشنهاد‌ها\",\n    \"filters\": \"فیلترها\",\n    \"is_not_broken_link\": \"لینک درست دارد\",\n    \"lists\": \"فهرست‌ها\",\n    \"feeds\": \"فیدها\",\n    \"is_from_source\": \"منبع هست\",\n    \"is_not_from_source\": \"منبع نیست\"\n  },\n  \"preview\": {\n    \"view_original\": \"مشاهده‌ی اصلی\",\n    \"cached_content\": \"محتوای ذخیره شده\",\n    \"reader_view\": \"نمای خواننده\",\n    \"tabs\": {\n      \"content\": \"محتوا\",\n      \"details\": \"جزئیات\"\n    },\n    \"archive_info\": \"ممکنه آرشیوها اگه نیاز به جاوااسکریپت داشته باشن، درست نشون داده نشن. برای بهترین نتیجه، <1>اونو دانلود و تو مرورگر بازش کن</1>.\",\n    \"fetch_error_title\": \"محتوا در دسترس نیست\",\n    \"fetch_error_description\": \"نتونستیم محتوای این لینک رو پیدا کنیم. ممکنه صفحه محافظت شده باشه، نیاز به احراز هویت داشته باشه یا موقتا در دسترس نباشه.\",\n    \"crawling_in_progress\": \"درحال دریافت محتوای صفحه…\",\n    \"continue_reading\": \"همون‌جایی که ولش کردی ادامه بده\",\n    \"continue_reading_percent\": \"همون‌جایی که ولش کردی ادامه بده ({{percent}}%)\",\n    \"continue_button\": \"ادامه بده\"\n  },\n  \"editor\": {\n    \"quickly_focus\": \"با فشردن ⌘ + E می‌توانید به سرعت روی این فیلد تمرکز کنید\",\n    \"multiple_urls_dialog_title\": \"آیا می‌خواهید آدرس‌های اینترنتی را به عنوان نشانک‌های جداگانه وارد کنید؟\",\n    \"multiple_urls_dialog_desc\": \"ورودی شامل چندین آدرس اینترنتی در خطوط جداگانه است. آیا می‌خواهید آن‌ها را به عنوان نشانک‌های جداگانه وارد کنید؟\",\n    \"import_as_text\": \"وارد کردن به عنوان نشانک متنی\",\n    \"import_as_separate_bookmarks\": \"وارد کردن به عنوان نشانک‌های جداگانه\",\n    \"placeholder\": \"یک پیوند یا تصویر را جای‌گذاری کنید، یک نکته بنویسید یا یک تصویر را در اینجا بکشید و رها کنید…\",\n    \"placeholder_v2\": \"یک پیوند را جای‌گذاری کنید، یک یادداشت بنویسید یا یک تصویر را رها کنید…\",\n    \"new_item\": \"مورد جدید\",\n    \"disabled_submissions\": \"ارسال غیرفعال است\",\n    \"text_toolbar\": {\n      \"undo\": \"لغو\",\n      \"redo\": \"باز انجام\",\n      \"bold\": \"درشت\",\n      \"italic\": \"مورب\",\n      \"underline\": \"زیر خط\",\n      \"strikethrough\": \"خط‌خورده\",\n      \"code\": \"کد\",\n      \"highlight\": \"برجسته کردن\",\n      \"align_left\": \"ترازچپ\",\n      \"align_center\": \"ترازوسط\",\n      \"align_right\": \"ترازراست\",\n      \"markdown_shortcuts\": {\n        \"label\": \"میانبرهای Markdown\",\n        \"heading\": {\n          \"label\": \"سرفصل\",\n          \"example\": \"# H1، ## H2، ### H3\"\n        },\n        \"bold\": {\n          \"label\": \"درشت\",\n          \"example\": \"**متن** یا CTRL+b\"\n        },\n        \"italic\": {\n          \"label\": \"مورب\",\n          \"example\": \"*مورب* یا _مورب_ یا CTRL+i\"\n        },\n        \"blockquote\": {\n          \"label\": \"نقل قول\",\n          \"example\": \"> نقل قول\"\n        },\n        \"ordered_list\": {\n          \"label\": \"لیست ترتیب‌دار\",\n          \"example\": \"۱. مورد لیست\"\n        },\n        \"unordered_list\": {\n          \"label\": \"لیست بدون ترتیب\",\n          \"example\": \"- مورد لیست\"\n        },\n        \"inline_code\": {\n          \"label\": \"کد درون‌خطی\",\n          \"example\": \"`کد`\"\n        },\n        \"block_code\": {\n          \"label\": \"کد بلوکی\",\n          \"example\": \"``` + فاصله\"\n        }\n      }\n    }\n  },\n  \"dialogs\": {\n    \"bookmarks\": {\n      \"delete_confirmation_title\": \"پاک کردن نشانک؟\",\n      \"delete_confirmation_description\": \"مطمئنی می‌خوای این نشانک رو پاک کنی؟\"\n    }\n  },\n  \"toasts\": {\n    \"bookmarks\": {\n      \"updated\": \"نشانک به‌روز شد!\",\n      \"deleted\": \"نشانک پاک شد!\",\n      \"refetch\": \"دوباره واکشی به صف اضافه شد!\",\n      \"full_page_archive\": \"ایجاد بایگانی کامل صفحه آغاز شد\",\n      \"delete_from_list\": \"نشانک از فهرست حذف شد\",\n      \"clipboard_copied\": \"لینک به کلیپ‌بورد شما اضافه شد!\",\n      \"preserve_pdf\": \"نگهداری پی‌دی‌اف فعال شده‌است\",\n      \"update_banner\": \"بنر به روز شد!\",\n      \"uploading_banner\": \"بارگذاری بنر...\"\n    },\n    \"lists\": {\n      \"created\": \"فهرست درست شد!\",\n      \"updated\": \"فهرست به‌روز شد!\",\n      \"merged\": \"فهرست ادغام شد!\",\n      \"deleted\": \"فهرست پاک شد!\"\n    },\n    \"tags\": {\n      \"created\": \"برچسب درست شد!\",\n      \"failed_to_create\": \"ایجاد برچسب با مشکل مواجه شد\"\n    }\n  },\n  \"banners\": {\n    \"no_bookmarks\": {\n      \"title\": \"هنوز هیچ نشانکی وجود نداره\",\n      \"description\": \"مقالات، لینک‌ها و صفحه‌های جالب رو ذخیره کن تا بعداً سریع بهشون دسترسی داشته باشی.\"\n    }\n  },\n  \"cleanups\": {\n    \"cleanups\": \"پاک‌سازی‌ها\",\n    \"duplicate_tags\": {\n      \"title\": \"تگ‌های تکراری\",\n      \"merge_all_suggestions\": \"همه پیشنهادها رو ادغام کنم؟\"\n    }\n  },\n  \"bookmark_editor\": {\n    \"title\": \"ویرایش نشانک\",\n    \"subtitle\": \"تغییرات در جزئیات نشانک را انجام دهید. وقتی کارتان تمام شد، ذخیره را بزنید.\",\n    \"author\": \"نویسنده\",\n    \"publisher\": \"ناشر\",\n    \"date_published\": \"تاریخ انتشار\",\n    \"pick_a_date\": \"یک تاریخ انتخاب کنید\",\n    \"save_changes\": \"ذخیره تغییرات\",\n    \"extracted_content\": \"محتوای استخراج شده\"\n  },\n  \"view_options\": {\n    \"title\": \"گزینه‌های نمایش\",\n    \"layout\": \"چیدمان\",\n    \"columns\": \"ستون‌ها\",\n    \"display_options\": \"گزینه‌های نمایش\",\n    \"show_note_previews\": \"نمایش یادداشت‌ها\",\n    \"show_tags\": \"نمایش برچسب‌ها\",\n    \"show_title\": \"نمایش عنوان\",\n    \"image_options\": \"گزینه‌های تصویر\",\n    \"image_fit_cover\": \"روکش (پر کردن)\",\n    \"image_fit_contain\": \"شامل (متناسب)\"\n  },\n  \"version\": {\n    \"new_release_available\": \"یه سری اطلاعیه انتشار جدید اومده\",\n    \"whats_new_title\": \"تو نسخه v{{version}} چه چیزایی جدید داریم؟\",\n    \"release_notes_description\": \"اینم آخرین آپدیتا که از اطلاعیه انتشار گیتهاب برداشته شده.\",\n    \"loading_release_notes\": \"داره اطلاعیه انتشار بارگیری می‌شه…\",\n    \"unable_to_load_release_notes\": \"فعلا نمی‌شه اطلاعیه انتشاری رو بارگیری کرد. لطفا یه کم دیگه تلاش کن.\",\n    \"no_release_notes\": \"واسه این نسخه، اطلاعیه انتشاری منتشر نشده.\",\n    \"release_notes_synced\": \"اطلاعیه‌های انتشار از گیتهاب سینک می‌شن.\",\n    \"view_on_github\": \"دیدن تو گیتهاب\"\n  },\n  \"wrapped\": {\n    \"title\": \"رپ‌شده‌ی {{year}} شما\",\n    \"subtitle\": \"یک سال در کاراکیپ\",\n    \"banner\": {\n      \"title\": \"رپ‌شده‌ی سال ۲۰۲۵ شما آماده است!\",\n      \"description\": \"سال خود را در نشانک‌ها ببینید\",\n      \"view_now\": \"همین حالا ببینید\"\n    },\n    \"button\": \"رپ‌شده ۲۰۲۵\",\n    \"loading\": \"درحال بارگذاری رپ‌شده‌ی شما...\",\n    \"failed_to_load\": \"بارگیری آمار رپ‌شده‌ی شما با مشکل مواجه شد\",\n    \"sections\": {\n      \"total_saves\": {\n        \"prefix\": \"ذخیره کردید\",\n        \"suffix\": \"اقلام امسال\",\n        \"suffix_singular\": \"قلم امسال\"\n      },\n      \"first_bookmark\": {\n        \"title\": \"سفر شما آغاز شد\",\n        \"description\": \"اولین ذخیره از {{year}}:\"\n      },\n      \"top_domains\": \"سایت‌های برتر شما\",\n      \"top_tags\": \"برچسب‌های برتر شما\",\n      \"monthly_activity\": \"سال شما در ذخیره‌ها\",\n      \"most_active_day\": \"فعال‌ترین روز شما\",\n      \"peak_times\": {\n        \"title\": \"چه زمانی ذخیره می‌کنید\",\n        \"peak_hour\": \"ساعت اوج\",\n        \"peak_day\": \"روز اوج\"\n      },\n      \"how_you_save\": \"نحوه ذخیره شما\",\n      \"what_you_saved\": \"آنچه شما ذخیره کردید\",\n      \"summary\": {\n        \"favorites\": \"مورد علاقه‌ها\",\n        \"tags_created\": \"برچسب‌های ایجاد شده\",\n        \"highlights\": \"نکات برجسته\"\n      },\n      \"types\": {\n        \"links\": \"لینک‌ها\",\n        \"notes\": \"یادداشت‌ها\",\n        \"assets\": \"دارایی‌ها\"\n      }\n    },\n    \"footer\": \"ساخته شده با Karakeep\",\n    \"share\": \"اشتراک‌گذاری\",\n    \"download\": \"بارگیری\",\n    \"close\": \"بستن\",\n    \"generating\": \"درحال تولید...\"\n  }\n}\n"
  },
  {
    "path": "apps/web/lib/i18n/locales/fi/translation.json",
    "content": "{\n  \"common\": {\n    \"name\": \"Nimi\",\n    \"email\": \"Sähköposti\",\n    \"password\": \"Salasana\",\n    \"action\": \"Toiminta\",\n    \"actions\": \"Toiminnat\",\n    \"created_at\": \"Luotu klo\",\n    \"updated_at\": \"Päivitetty klo\",\n    \"key\": \"Avain\",\n    \"role\": \"Rooli\",\n    \"type\": \"Tyyppi\",\n    \"size\": \"Koko\",\n    \"roles\": {\n      \"user\": \"Käyttäjä\",\n      \"admin\": \"Järjestelmänvalvoja\"\n    },\n    \"something_went_wrong\": \"Jokin meni pieleen\",\n    \"experimental\": \"Kokeellinen\",\n    \"search\": \"Hae\",\n    \"tags\": \"Tunnisteet\",\n    \"note\": \"Huomautus\",\n    \"attachments\": \"Liitteet\",\n    \"highlights\": \"Kohokohdat\",\n    \"source\": \"Lähde\",\n    \"screenshot\": \"Kuvakaappaus\",\n    \"video\": \"Video\",\n    \"archive\": \"Arkistoi\",\n    \"home\": \"Koti\",\n    \"title\": \"Otsikko\",\n    \"description\": \"Kuvaus\",\n    \"summary\": \"Yhteenveto\",\n    \"bookmark_types\": {\n      \"title\": \"Kirjanmerkin tyyppi\",\n      \"link\": \"Linkki\",\n      \"text\": \"Teksti\",\n      \"media\": \"Media\"\n    },\n    \"url\": \"URL\",\n    \"quota\": \"Kiintiö\",\n    \"bookmarks\": \"Kirjanmerkit\",\n    \"storage\": \"Tallennustila\",\n    \"pdf\": \"Arkistoitu PDF\",\n    \"default\": \"Oletus\",\n    \"id\": \"Tunnus\",\n    \"last_used\": \"Viimeksi käytetty\"\n  },\n  \"layouts\": {\n    \"masonry\": \"Tiililadonta\",\n    \"grid\": \"Ruudukko\",\n    \"list\": \"Lista\",\n    \"compact\": \"Tiivis\"\n  },\n  \"actions\": {\n    \"change_layout\": \"Vaihda asettelua\",\n    \"archive\": \"Arkistoi\",\n    \"unarchive\": \"Poista arkistosta\",\n    \"favorite\": \"Suosikki\",\n    \"unfavorite\": \"Poista suosikeista\",\n    \"delete\": \"Poista\",\n    \"refresh\": \"Päivitä\",\n    \"recrawl\": \"Uudelleenindeksoi\",\n    \"download_full_page_archive\": \"Lataa koko sivun arkisto\",\n    \"edit_tags\": \"Muokkaa tunnisteita\",\n    \"add_to_list\": \"Lisää listaan\",\n    \"select_all\": \"Valitse kaikki\",\n    \"unselect_all\": \"Poista valinnat\",\n    \"copy_link\": \"Kopioi linkki\",\n    \"close_bulk_edit\": \"Sulje joukkomuokkaus\",\n    \"bulk_edit\": \"Massamuokkaus\",\n    \"manage_lists\": \"Hallitse listoja\",\n    \"remove_from_list\": \"Poista listalta\",\n    \"save\": \"Tallenna\",\n    \"add\": \"Lisää\",\n    \"edit\": \"Muokkaa\",\n    \"open_editor\": \"Avaa editori\",\n    \"create\": \"Luo\",\n    \"fetch_now\": \"Hae nyt\",\n    \"summarize_with_ai\": \"Tee yhteenveto tekoälyllä\",\n    \"edit_title\": \"Muokkaa otsikkoa\",\n    \"sign_out\": \"Kirjaudu ulos\",\n    \"close\": \"Sulje\",\n    \"merge\": \"Yhdistä\",\n    \"cancel\": \"Peruuta\",\n    \"apply_all\": \"Käytä kaikkia\",\n    \"ignore\": \"Ohita\",\n    \"sort\": {\n      \"title\": \"Lajittele\",\n      \"newest_first\": \"Uusin ensin\",\n      \"oldest_first\": \"Vanhin ensin\",\n      \"relevant_first\": \"Tärkein ensin\"\n    },\n    \"toggle_show_archived\": \"Näytä arkistoidut\",\n    \"confirm\": \"Vahvista\",\n    \"regenerate\": \"Uudista\",\n    \"load_more\": \"Lataa lisää\",\n    \"edit_notes\": \"Muokkaa muistiinpanoja\",\n    \"preserve_as_pdf\": \"Säilytä PDF-muodossa\",\n    \"offline_copies\": \"Offline-kopiot\",\n    \"preserve_offline_archive\": \"Säilytä offline-arkisto\",\n    \"download_full_page_archive_file\": \"Lataa arkistotiedosto\",\n    \"download_pdf_file\": \"Lataa PDF-tiedosto\",\n    \"remove\": \"Poista\",\n    \"more\": \"Lisää\",\n    \"replace_banner\": \"Korvaa banneri\",\n    \"add_banner\": \"Lisää banneri\",\n    \"download\": \"Lataa\"\n  },\n  \"highlights\": {\n    \"no_highlights\": \"Sulla ei oo vielä yhtään korostusta.\"\n  },\n  \"settings\": {\n    \"back_to_app\": \"Takaisin sovellukseen\",\n    \"user_settings\": \"Käyttäjäasetukset\",\n    \"info\": {\n      \"user_info\": \"Käyttäjätiedot\",\n      \"basic_details\": \"Perustiedot\",\n      \"change_password\": \"Vaihda salasana\",\n      \"current_password\": \"Nykyinen salasana\",\n      \"new_password\": \"Uusi salasana\",\n      \"confirm_new_password\": \"Vahvista uusi salasana\",\n      \"options\": \"Valinnat\",\n      \"interface_lang\": \"Käyttöliittymän kieli\",\n      \"user_settings\": {\n        \"user_settings_updated\": \"Käyttäjäasetukset on päivitetty!\",\n        \"bookmark_click_action\": {\n          \"title\": \"Kirjanmerkin klikkaustoiminto\",\n          \"open_external_url\": \"Avaa alkuperäinen URL\",\n          \"open_bookmark_details\": \"Avaa kirjanmerkin tiedot\"\n        },\n        \"archive_display_behaviour\": {\n          \"title\": \"Arkistoidut kirjanmerkit\",\n          \"show\": \"Näytä arkistoidut kirjanmerkit tunnisteissa ja listoissa\",\n          \"hide\": \"Piilota arkistoidut kirjanmerkit tunnisteissa ja listoissa\"\n        }\n      },\n      \"reader_settings\": {\n        \"local_overrides_title\": \"Laitteen omat asetukset ovat käytössä\",\n        \"using_default\": \"Käytetään asiakkaan oletusarvoa\",\n        \"clear_override_hint\": \"Tyhjennä laitteen ohitus, jotta voit käyttää globaalia asetusta ({{value}})\",\n        \"font_size\": \"Fonttikoko\",\n        \"font_family\": \"Fonttiperhe\",\n        \"preview_inline\": \"(esikatselu)\",\n        \"tooltip_preview\": \"Tallentamattomia esikatselun muutoksia\",\n        \"save_to_all_devices\": \"Kaikissa laitteissa\",\n        \"tooltip_local\": \"Laitteen asetukset poikkeavat globaaleista\",\n        \"reset_preview\": \"Nollaa esikatselu\",\n        \"mono\": \"Monospace\",\n        \"line_height\": \"Rivikorkeus\",\n        \"tooltip_default\": \"Lukemisen asetukset\",\n        \"title\": \"Lukijan asetukset\",\n        \"serif\": \"Serif\",\n        \"preview\": \"Esikatselu\",\n        \"not_set\": \"Ei asetettu\",\n        \"clear_local_overrides\": \"Tyhjennä laitteen asetukset\",\n        \"preview_text\": \"The quick brown fox jumps over the lazy dog. Näin lukijanäkymän tekstisi näkyy.\",\n        \"local_overrides_cleared\": \"Laitteen omat asetukset on tyhjennetty\",\n        \"local_overrides_description\": \"Tässä laitteessa on lukija-asetukset, jotka poikkeavat yleisistä oletusarvoistasi:\",\n        \"clear_defaults\": \"Tyhjennä kaikki oletusarvot\",\n        \"description\": \"Määritä lukijanäkymän oletustekstiasetukset. Nämä asetukset synkronoidaan kaikkien laitteidesi välillä.\",\n        \"defaults_cleared\": \"Lukijan oletusarvot on tyhjennetty\",\n        \"save_hint\": \"Tallenna asetukset vain tälle laitteelle tai synkronoi kaikkiin laitteisiin\",\n        \"save_as_default\": \"Tallenna oletusarvoksi\",\n        \"save_to_device\": \"Tällä laitteella\",\n        \"sans\": \"Sans Serif\",\n        \"tooltip_preview_and_local\": \"Tallentamattomia esikatselun muutoksia; laitteen asetukset poikkeavat globaaleista\",\n        \"adjust_hint\": \"Säädä yllä olevia asetuksia, jotta näet muutokset\"\n      },\n      \"avatar\": {\n        \"upload\": \"Lataa avatar\",\n        \"change\": \"Vaihda avatar\",\n        \"remove_confirm_title\": \"Poistetaanko avatar?\",\n        \"updated\": \"Avatar päivitetty\",\n        \"removed\": \"Avatar poistettu\",\n        \"description\": \"Lataa neliön muotoinen kuva, jota käytetään avatarinasi.\",\n        \"remove_confirm_description\": \"Tämä poistaa nykyisen profiilikuvasi.\",\n        \"title\": \"Profiilikuva\",\n        \"remove\": \"Poista avatar\"\n      }\n    },\n    \"ai\": {\n      \"ai_settings\": \"Tekoälyasetukset\",\n      \"tagging_rules\": \"Taggaussäännöt\",\n      \"tagging_rule_description\": \"Tähän lisäämäsi kehotteet sisällytetään mallin sääntöihin tunnisteiden luonnin aikana. Voit tarkastella lopullisia kehotteita kehotteen esikatseluosiossa.\",\n      \"prompt_preview\": \"Kehote-esikatselu\",\n      \"text_prompt\": \"Tekstikehote\",\n      \"images_prompt\": \"Kuvan kehote\",\n      \"summarization_prompt\": \"Yhteenvedon kehote\",\n      \"all_tagging\": \"Kaikki tägääminen\",\n      \"text_tagging\": \"Tekstin merkitseminen\",\n      \"image_tagging\": \"Kuvien merkitseminen\",\n      \"summarization\": \"Yhteenvedon luonti\",\n      \"tag_style\": \"Tagityyli\",\n      \"auto_summarization_description\": \"Luo kirjanmerkeillesi automaattisesti tiivistelmiä tekoälyn avulla.\",\n      \"auto_tagging\": \"Automaattinen tägääminen\",\n      \"titlecase_spaces\": \"Isot alkukirjaimet ja välilyönnit\",\n      \"lowercase_underscores\": \"Pienet kirjaimet ja alleviivat\",\n      \"inference_language\": \"Päättelykieli\",\n      \"titlecase_hyphens\": \"Isot alkukirjaimet ja yhdysmerkit\",\n      \"lowercase_hyphens\": \"Pienet kirjaimet ja yhdysmerkit\",\n      \"lowercase_spaces\": \"Pienet kirjaimet ja välilyönnit\",\n      \"inference_language_description\": \"Valitse kieli AI-generoiduille tunnisteille ja yhteenvedoille.\",\n      \"tag_style_description\": \"Valitse, miten automaattisesti luotujen tunnisteiden muoto tulisi olla.\",\n      \"auto_tagging_description\": \"Luo kirjanmerkeillesi automaattisesti tägejä tekoälyn avulla.\",\n      \"camelCase\": \"camelCase\",\n      \"auto_summarization\": \"Automaattinen tiivistys\",\n      \"no_preference\": \"Ei toiveita\",\n      \"curated_tags\": \"Kuratoituja tunnisteita\",\n      \"curated_tags_description\": \"Voit halutessasi rajoittaa tekoälyn tunnistuksen käyttämään vain tämän luettelon tunnisteita. Jos tunnisteita ei ole valittu, tekoäly luo tunnisteita vapaasti.\",\n      \"curated_tags_updated\": \"Kuratoituja tunnisteita päivitetty onnistuneesti!\",\n      \"curated_tags_update_failed\": \"Kuratoitujen tunnisteiden päivitys epäonnistui\"\n    },\n    \"feeds\": {\n      \"rss_subscriptions\": \"RSS-tilaukset\",\n      \"add_a_subscription\": \"Lisää tilaus\",\n      \"feed_enabled\": \"RSS-syöte käytössä\",\n      \"feed_disabled\": \"RSS-syöte poistettu käytöstä\"\n    },\n    \"webhooks\": {\n      \"webhooks\": \"Webhookit\",\n      \"description\": \"Voit käyttää webhookeja käynnistämään toimintoja, kun kirjanmerkkejä luodaan, muutetaan tai indeksoidaan.\",\n      \"events\": {\n        \"title\": \"Tapahtumat\",\n        \"crawled\": \"Indeksoitu\",\n        \"created\": \"Luotu\",\n        \"edited\": \"Muokattu\"\n      },\n      \"auth_token\": \"Todennustunnus\",\n      \"add_auth_token\": \"Lisää todennusavain\",\n      \"edit_auth_token\": \"Muokkaa todennustunnusta\",\n      \"create_webhook\": \"Luo webhook\",\n      \"delete_webhook\": \"Poista webhook\",\n      \"delete_webhook_confirmation\": \"Haluatko varmasti poistaa tämän webhookin?\",\n      \"edit_webhook\": \"Muokkaa webhookia\",\n      \"webhook_url\": \"Webhook-URL\"\n    },\n    \"import\": {\n      \"import_export\": \"Tuonti/vienti\",\n      \"import_export_bookmarks\": \"Kirjanmerkkien tuonti / vienti\",\n      \"import_bookmarks_from_html_file\": \"Tuo kirjanmerkkejä HTML-tiedostosta\",\n      \"import_bookmarks_from_pocket_export\": \"Tuo kirjanmerkit Pocket-viennistä\",\n      \"import_bookmarks_from_matter_export\": \"Tuo kirjanmerkit Matter-viennistä\",\n      \"import_bookmarks_from_omnivore_export\": \"Tuo kirjanmerkit Omnivore-viennistä\",\n      \"import_bookmarks_from_linkwarden_export\": \"Tuo kirjanmerkit Linkwarden-viennistä\",\n      \"import_bookmarks_from_hoarder_export\": \"Tuo kirjanmerkit Hoarder-viennistä\",\n      \"export_links_and_notes\": \"Vie linkit ja muistiinpanot\",\n      \"imported_bookmarks\": \"Tuodut kirjanmerkit\",\n      \"import_bookmarks_from_karakeep_export\": \"Tuo kirjanmerkit Karakeep-viennistä\",\n      \"import_bookmarks_from_tab_session_manager_export\": \"Tuo kirjanmerkit Tab Session Managerista\",\n      \"import_bookmarks_from_mymind_export\": \"Tuodaan kirjanmerkit mymind-viennistä\",\n      \"import_bookmarks_from_instapaper_export\": \"Importoi kirjanmerkit Instapaper-viennistä\"\n    },\n    \"api_keys\": {\n      \"key_success_please_copy\": \"Ole hyvä ja kopioi avain ja säilytä se turvallisessa paikassa. Kun suljet valintaikkunan, et voi enää käyttää sitä.\",\n      \"api_keys\": \"API-avaimet\",\n      \"new_api_key\": \"Uusi API-avain\",\n      \"new_api_key_desc\": \"Anna API-avaimellesi yksilöllinen nimi\",\n      \"key_success\": \"Avain luotiin onnistuneesti\",\n      \"regenerate_api_key\": \"Uudista API-avain\",\n      \"key_regenerated\": \"Avain onnistuneesti uusittu\",\n      \"key_regenerated_please_copy\": \"Ole hyvä ja kopioi uusi avain turvalliseen paikkaan. Vanha avain on kumottu eikä toimi enää.\",\n      \"regenerate_warning\": \"Oletko varma, että haluat luoda API-avaimen uudelleen \\\"{{name}}\\\"?\",\n      \"regenerate_confirmation\": \"Tämä peruuttaa nykyisen avaimen ja luo uuden. Kaikki nykyistä avainta käyttävät sovellukset lakkaavat toimimasta.\"\n    },\n    \"broken_links\": {\n      \"broken_links\": \"Rikkinäiset linkit\",\n      \"last_crawled_at\": \"Viimeksi indeksoitu\",\n      \"crawling_status\": \"Indeksointitila\",\n      \"crawling_failed\": \"Ryömintä epäonnistui\"\n    },\n    \"manage_assets\": {\n      \"manage_assets\": \"Hallitse resursseja\",\n      \"delete_asset\": \"Poista assetti\",\n      \"delete_asset_confirmation\": \"Haluatko varmasti poistaa tämän resurssin?\",\n      \"no_assets\": \"Sinulla ei ole vielä yhtään assetteja.\",\n      \"asset_type\": \"Resurssityyppi\",\n      \"bookmark_link\": \"Kirjanmerkin linkki\",\n      \"asset_link\": \"Resurssilinkki\"\n    },\n    \"rules\": {\n      \"whenever\": \"Aina kun...\",\n      \"if\": \"Jos...\",\n      \"enter_rule_name\": \"Anna säännön nimi\",\n      \"describe_what_this_rule_does\": \"Kuvaile, mitä tämä sääntö tekee\",\n      \"conditions_types\": {\n        \"bookmark_type_is\": \"Kirjanmerkin tyyppi on\",\n        \"has_tag\": \"On tunnus\",\n        \"is_favourited\": \"On suosikki\",\n        \"is_archived\": \"On arkistoitu\",\n        \"and\": \"Kaikki seuraavat ovat tosia\",\n        \"always\": \"Aina\",\n        \"url_contains\": \"URL sisältää\",\n        \"imported_from_feed\": \"Tuotu syötteestä\",\n        \"or\": \"Mikä tahansa seuraavista on totta\",\n        \"url_does_not_contain\": \"URL-osoite ei sisällä\",\n        \"title_contains\": \"Otsikon sisältää\",\n        \"title_does_not_contain\": \"Otsikko ei sisällä\"\n      },\n      \"actions_types\": {\n        \"download_full_page_archive\": \"Lataa koko sivun arkisto\",\n        \"favourite_bookmark\": \"Suosikkikirjanmerkki\",\n        \"add_tag\": \"Lisää tagi\",\n        \"remove_tag\": \"Poista tunnus\",\n        \"add_to_list\": \"Lisää listaan\",\n        \"remove_from_list\": \"Poista luettelosta\",\n        \"archive_bookmark\": \"Arkistoi kirjanmerkki\"\n      },\n      \"events_types\": {\n        \"tag_removed\": \"Tämä tunnus poistetaan kirjanmerkistä\",\n        \"added_to_list\": \"Kirjanmerkki lisätään tähän luetteloon\",\n        \"removed_from_list\": \"Kirjanmerkki poistetaan tästä luettelosta\",\n        \"archived\": \"Kirjanmerkki on arkistoitu\",\n        \"bookmark_added\": \"Kirjanmerkki on lisätty\",\n        \"tag_added\": \"Tämä tagi on lisätty kirjanmerkkiin\",\n        \"favourited\": \"Kirjanmerkki on lisätty suosikkeihin\"\n      },\n      \"rules\": \"Sääntömoottori\",\n      \"rule_name\": \"Säännön nimi\",\n      \"description\": \"Voit käyttää sääntöjä käynnistämään toimintoja, kun tapahtuma käynnistyy.\",\n      \"ceate_rule\": \"Luo sääntö\",\n      \"edit_rule\": \"Muokkaa sääntöä\",\n      \"save_rule\": \"Tallenna sääntö\",\n      \"delete_rule\": \"Poista sääntö\",\n      \"delete_rule_confirmation\": \"Haluatko varmasti poistaa tämän säännön?\",\n      \"rule_has_been_created\": \"Sääntö on luotu!\",\n      \"rule_has_been_updated\": \"Sääntö on päivitetty!\",\n      \"rule_has_been_deleted\": \"Sääntö on poistettu!\",\n      \"no_rules_created_yet\": \"Ei vielä luotuja sääntöjä\",\n      \"create_your_first_rule\": \"Luo ensimmäinen sääntösi automatisoidaksesi työnkulkusi\"\n    },\n    \"stats\": {\n      \"usage_statistics\": \"Käyttötilastot\",\n      \"insights_description\": \"Näkemyksiä kirjanmerkintätottumuksiisi ja -kokoelmaasi\",\n      \"failed_to_load\": \"Tilastojen lataaminen epäonnistui\",\n      \"overview\": {\n        \"total_bookmarks\": \"Kirjanmerkkien kokonaismäärä\",\n        \"all_saved_items\": \"Kaikki tallennetut kohteet\",\n        \"favorites\": \"Suosikit\",\n        \"starred_bookmarks\": \"Tähdellä merkityt kirjanmerkit\",\n        \"archived\": \"Arkistoitu\",\n        \"archived_items\": \"Arkistoidut kohteet\",\n        \"tags\": \"Tunnisteet\",\n        \"unique_tags_created\": \"Luotuja yksilöllisiä tunnisteita\",\n        \"lists\": \"Listat\",\n        \"bookmark_collections\": \"Kirjanmerkkikokoelmat\",\n        \"highlights\": \"Kohokohdat\",\n        \"text_highlights\": \"Tekstin korostukset\",\n        \"storage_used\": \"Käytetty tallennustila\",\n        \"total_asset_storage\": \"Omaisuuden kokonaistallennustila\",\n        \"this_month\": \"Tässä kuussa\",\n        \"bookmarks_added\": \"Kirjanmerkit lisätty\"\n      },\n      \"bookmark_types\": {\n        \"title\": \"Kirjanmerkityypit\",\n        \"links\": \"Linkit\",\n        \"text_notes\": \"Tekstimuistiinpanot\",\n        \"assets\": \"Omaisuus\"\n      },\n      \"recent_activity\": {\n        \"title\": \"Viimeaikainen aktiivisuus\",\n        \"this_week\": \"Tällä viikolla\",\n        \"this_month\": \"Tässä kuussa\",\n        \"this_year\": \"Tänä vuonna\"\n      },\n      \"top_domains\": {\n        \"title\": \"Suosituimmat verkkotunnukset\",\n        \"no_domains_found\": \"Verkkotunnuksia ei löytynyt\"\n      },\n      \"most_used_tags\": {\n        \"title\": \"Käytetyimmät tunnisteet\",\n        \"no_tags_found\": \"Tunnisteita ei löytynyt\"\n      },\n      \"activity_patterns\": {\n        \"activity_by_hour\": \"Aktiivisuus tunneittain\",\n        \"activity_by_day\": \"Aktiivisuus päivittäin\"\n      },\n      \"storage_breakdown\": {\n        \"title\": \"Tallennustilan erittely\"\n      },\n      \"bookmark_sources\": {\n        \"title\": \"Kirjanmerkkilähteet\",\n        \"empty\": \"Lähdetietoja ei saatavilla\"\n      }\n    },\n    \"subscription\": {\n      \"subscription\": \"Tilaus\",\n      \"manage_subscription\": \"Hallitse tilaustasi ja laskutustietojasi\",\n      \"current_plan\": \"Nykyinen suunnitelma\",\n      \"billing_period\": \"Laskutuskausi\",\n      \"paid_plan\": \"Maksullinen suunnitelma\",\n      \"unlock_bigger_quota\": \"Hanki suurempi kiintiö ja tue projektia\",\n      \"subscribe_now\": \"Tilaa nyt\",\n      \"manage_billing\": \"Hallitse laskutusta\",\n      \"subscription_canceled\": \"Tilauksesi on peruttu ja päättyy {{date}}. Voit tilata uudelleen milloin tahansa.\",\n      \"usage_quotas\": \"Käyttö ja kiintiöt\",\n      \"track_usage\": \"Seuraa nykyistä käyttöäsi suunnitelmasi rajoja vasten\",\n      \"total_bookmarks_saved\": \"Tallennettujen kirjanmerkkien kokonaismäärä\",\n      \"assets_file_storage\": \"Resurssien ja tiedostojen tallennus\",\n      \"unlimited_usage\": \"Rajoittamaton käyttö\",\n      \"quota_limit_reached\": \"Kiintiöraja on saavutettu\",\n      \"approaching_quota_limit\": \"Kiintiöraja lähestyy\",\n      \"loading_usage\": \"Käyttötietojen lataaminen...\",\n      \"free\": \"Ilmainen\",\n      \"paid\": \"Maksullinen\"\n    },\n    \"import_sessions\": {\n      \"title\": \"Sessioiden tuonti\",\n      \"description\": \"Näytä ja hallitse tuontisessioita joukkona. Sessiot luodaan automaattisesti, kun tuot kirjanmerkkejä.\",\n      \"load_error\": \"Tuontisessioiden lataaminen epäonnistui\",\n      \"no_sessions\": \"Ei vielä tuontisessioita\",\n      \"no_sessions_detail\": \"Tuontisessiot tulevat näkymään tänne automaattisesti, kun tuot kirjanmerkkejä\",\n      \"created_at\": \"Luotu {{time}}\",\n      \"progress\": \"Edistyminen\",\n      \"status\": {\n        \"pending\": \"Odottaa\",\n        \"in_progress\": \"Käynnissä\",\n        \"completed\": \"Valmis\",\n        \"failed\": \"Epäonnistui\",\n        \"processing\": \"Käsitellään\",\n        \"staging\": \"Valmistellaan\",\n        \"running\": \"Käynnissä\",\n        \"paused\": \"Pysäytetty\"\n      },\n      \"badges\": {\n        \"pending\": \"{{count}} odottaa\",\n        \"processing\": \"{{count}} käsitellään\",\n        \"completed\": \"{{count}} valmis\",\n        \"failed\": \"{{count}} epäonnistui\"\n      },\n      \"imported_to\": \"Tuotu kohteeseen:\",\n      \"view_list\": \"Näytä luettelo\",\n      \"delete_dialog_title\": \"Poista tuontisessio\",\n      \"delete_dialog_description\": \"Haluatko varmasti poistaa kohteen \\\"{{name}}\\\"? Tätä toimintoa ei voi perua. Kirjanmerkkejä ei poisteta.\",\n      \"delete_session\": \"Poista istunto\",\n      \"pause_session\": \"Pysäytä\",\n      \"resume_session\": \"Jatka\",\n      \"view_details\": \"Näytä tiedot\",\n      \"detail\": {\n        \"page_title\": \"Tuontisession yksityiskohdat\",\n        \"back_to_import\": \"Takaisin tuontiin\",\n        \"filter_all\": \"Kaikki\",\n        \"filter_accepted\": \"Hyväksytty\",\n        \"filter_rejected\": \"Hylätty\",\n        \"filter_duplicates\": \"Kaksoiskappaleet\",\n        \"filter_pending\": \"Odottaa\",\n        \"table_title\": \"Otsikko / URL\",\n        \"table_type\": \"Tyyppi\",\n        \"table_result\": \"Tulos\",\n        \"table_reason\": \"Syy\",\n        \"table_bookmark\": \"Kirjanmerkki\",\n        \"result_accepted\": \"Hyväksytty\",\n        \"result_rejected\": \"Hylätty\",\n        \"result_skipped_duplicate\": \"Kaksoiskappale\",\n        \"result_pending\": \"Odottaa\",\n        \"result_processing\": \"Käsitellään\",\n        \"no_results\": \"Tällä suodattimella ei löytynyt tuloksia.\",\n        \"view_bookmark\": \"Näytä kirjanmerkki\",\n        \"load_more\": \"Lataa lisää\",\n        \"no_title\": \"Ei otsikkoa\"\n      }\n    },\n    \"backups\": {\n      \"backups\": \"Varmuuskopiot\",\n      \"page_title\": \"Varmuuskopiot\",\n      \"page_description\": \"Luo ja hallitse kirjanmerkkiesi varmuuskopioita automaattisesti. Varmuuskopiot pakataan ja tallennetaan turvallisesti.\",\n      \"configuration\": {\n        \"title\": \"Varmuuskopioiden kokoonpano\",\n        \"enable_automatic_backups\": \"Ota automaattiset varmuuskopiot käyttöön\",\n        \"enable_automatic_backups_description\": \"Luo kirjanmerkeistäsi automaattisesti varmuuskopiot\",\n        \"backup_frequency\": \"Varmuuskopioiden tiheys\",\n        \"backup_frequency_description\": \"Kuinka usein varmuuskopiot tulisi luoda\",\n        \"retention_period\": \"Säilytysaika (päivää)\",\n        \"retention_period_description\": \"Kuinka monta päivää varmuuskopioita säilytetään ennen niiden poistamista\",\n        \"frequency\": {\n          \"daily\": \"Päivittäin\",\n          \"weekly\": \"Viikoittain\"\n        },\n        \"select_frequency\": \"Valitse tiheys\",\n        \"save_settings\": \"Tallenna asetukset\"\n      },\n      \"list\": {\n        \"title\": \"Sinun varmuuskopiosi\",\n        \"create_backup_now\": \"Luo varmuuskopio nyt\",\n        \"no_backups\": \"Sinulla ei ole vielä varmuuskopioita. Ota automaattiset varmuuskopiot käyttöön tai luo sellainen manuaalisesti.\",\n        \"table\": {\n          \"created_at\": \"Luotu\",\n          \"bookmarks\": \"Kirjanmerkit\",\n          \"size\": \"Koko\",\n          \"status\": \"Tila\",\n          \"actions\": \"Toiminnot\"\n        },\n        \"status\": {\n          \"success\": \"Onnistui\",\n          \"failed\": \"Epäonnistui\",\n          \"pending\": \"Odottaa\"\n        },\n        \"actions\": {\n          \"download_backup\": \"Lataa varmuuskopio\",\n          \"delete_backup\": \"Poista varmuuskopio\"\n        }\n      },\n      \"dialogs\": {\n        \"delete_backup_title\": \"Poistetaanko varmuuskopio?\",\n        \"delete_backup_description\": \"Oletko varma, että haluat poistaa tämän varmuuskopion? Tätä toimintoa ei voi peruuttaa.\"\n      },\n      \"toasts\": {\n        \"backup_queued\": \"Varmuuskopiointityö on asetettu jonoon! Se käsitellään pian.\",\n        \"backup_deleted\": \"Varmuuskopio on poistettu!\"\n      }\n    }\n  },\n  \"admin\": {\n    \"admin_settings\": \"Ylläpitäjän asetukset\",\n    \"server_stats\": {\n      \"server_stats\": \"Palvelimen tilastot\",\n      \"total_users\": \"Käyttäjiä yhteensä\",\n      \"total_bookmarks\": \"Kirjanmerkkejä yhteensä\",\n      \"server_version\": \"Palvelimen versio\"\n    },\n    \"background_jobs\": {\n      \"background_jobs\": \"Taustatyöt\",\n      \"crawler_jobs\": \"Indeksointityöt\",\n      \"indexing_jobs\": \"Töiden indeksointi\",\n      \"inference_jobs\": \"Päättelytyöt\",\n      \"tidy_assets_jobs\": \"Siisti resurssityöt\",\n      \"video_jobs\": \"Videon lataustyöt\",\n      \"webhook_jobs\": \"Webhook-työt\",\n      \"asset_preprocessing_jobs\": \"Resurssien esikäsittelytyöt\",\n      \"feed_jobs\": \"RSS-syötetyöt\",\n      \"job\": \"Työ\",\n      \"queued\": \"Jonossa\",\n      \"pending\": \"Odottaa\",\n      \"failed\": \"Epäonnistui\",\n      \"jobs\": {\n        \"crawler\": {\n          \"title\": \"Indeksointityöt\",\n          \"description\": \"Verkkosivujen indeksointi ja sisällön poiminta URL-osoitteista.\"\n        },\n        \"inference\": {\n          \"title\": \"Päättelytyöt\",\n          \"description\": \"Tekoälypohjainen sisällön merkitseminen ja sisällön tiivistys\"\n        },\n        \"indexing\": {\n          \"title\": \"Indeksointityöt\",\n          \"description\": \"Hakemiston päivitykset\"\n        },\n        \"asset_preprocessing\": {\n          \"title\": \"Resurssien esikäsittelytyöt\",\n          \"description\": \"Kuvien ja asiakirjojen esikäsittely (kuvakaappaukset, tekstin poisto jne.)\"\n        },\n        \"tidy_assets\": {\n          \"title\": \"Siisti resurssi -työt\",\n          \"description\": \"Resurssien puhdistus ja tallennustilan optimointi\"\n        },\n        \"video\": {\n          \"title\": \"Videoiden lataustyöt\",\n          \"description\": \"Videon poiminta ja lataus\"\n        },\n        \"webhook\": {\n          \"title\": \"Webhook-työt\",\n          \"description\": \"Ulkoiset webhook-ilmoitukset\"\n        },\n        \"feed\": {\n          \"title\": \"RSS-syötetyöt\",\n          \"description\": \"RSS-syötteen käsittely ja sisällön päivitykset\"\n        },\n        \"admin_maintenance\": {\n          \"title\": \"Ylläpidon huoltotyöt\",\n          \"description\": \"Hallinnollinen siivous ja resurssien ylläpito\"\n        }\n      },\n      \"monitor_and_manage\": \"Seuraa ja hallitse taustatyöjonoja ja järjestelmän käsittelytehtäviä\",\n      \"active\": \"Aktiivinen\",\n      \"available_actions\": \"Käytettävissä olevat toiminnot\",\n      \"status\": {\n        \"title\": \"Työtilojen ymmärtäminen\",\n        \"queued\": {\n          \"title\": \"Jonossa\",\n          \"description\": \"Työt odottavat jonossa käsiteltäväksi. Ne alkavat automaattisesti, kun resursseja on saatavilla.\"\n        },\n        \"unprocessed\": {\n          \"title\": \"Käsittelemättömät\",\n          \"description\": \"Kirjanmerkit, joita ei ole vielä käsitelty. Ne ovat todennäköisesti jo jonossa käsiteltäväksi, jos eivät, sinun on ehkä laitettava ne manuaalisesti uudelleen jonoon.\"\n        },\n        \"failed\": {\n          \"title\": \"Epäonnistunut\",\n          \"description\": \"Kirjanmerkit, joissa tapahtui virheitä käsittelyn aikana. Nämä saattavat vaatia manuaalista tarkastusta.\"\n        }\n      },\n      \"actions\": {\n        \"recrawl_failed_links_only\": \"Ryömi uudelleen vain epäonnistuneet linkit\",\n        \"recrawl_all_links\": \"Ryömi uudelleen kaikki linkit\",\n        \"without_inference\": \"Ilman päättelyä\",\n        \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Luo uudelleen tekoälytunnisteet vain epäonnistuneille kirjanmerkeille\",\n        \"regenerate_ai_tags_for_all_bookmarks\": \"Luo uudelleen tekoälytunnisteet kaikille kirjanmerkeille\",\n        \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Luo uudelleen tekoälyn tiivistelmät vain epäonnistuneille kirjanmerkeille\",\n        \"regenerate_ai_summaries_for_all_bookmarks\": \"Luo uudelleen tekoälyn tiivistelmät kaikille kirjanmerkeille\",\n        \"reindex_all_bookmarks\": \"Indeksoi kaikki kirjanmerkit uudelleen\",\n        \"clean_assets\": \"Siivoa roikkuvat resurssit ja synkronoi metatiedot uudelleen\",\n        \"reprocess_assets_fix_mode\": \"Käsittele käsittelemättömät resurssit uudelleen\",\n        \"migrate_large_link_html_content\": \"Siirrä suurikokoinen sisäinen HTML-sisältö resurssitiedostoihin\",\n        \"recrawl_pending_links_only\": \"Indeksoi odottavat linkit uudelleen\",\n        \"regenerate_ai_tags_for_pending_bookmarks_only\": \"Luo AI-tunnisteet uudelleen vain odottaville kirjanmerkeille\",\n        \"regenerate_ai_summaries_for_pending_bookmarks_only\": \"Luo AI-yhteenvedot uudelleen vain odottaville kirjanmerkeille\"\n      }\n    },\n    \"actions\": {\n      \"recrawl_failed_links_only\": \"Indeksoi uudelleen vain epäonnistuneet linkit\",\n      \"recrawl_all_links\": \"Indeksoi kaikki linkit uudelleen\",\n      \"without_inference\": \"Ilman päättelyä\",\n      \"regenerate_ai_tags_for_all_bookmarks\": \"Luo uudelleen kaikki tekoälytunnisteet kirjanmerkeille\",\n      \"reindex_all_bookmarks\": \"Uudelleenindeksoi kaikki kirjanmerkit\",\n      \"compact_assets\": \"Tiivistetyt resurssit\",\n      \"reprocess_assets_fix_mode\": \"Käsittele resurssit uudelleen (korjaustila)\",\n      \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Luo AI-tageja uudelleen vain epäonnistuneille kirjanmerkeille\",\n      \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Luo uudelleen tekoälyn yhteenvedot vain epäonnistuneille kirjanmerkeille\",\n      \"regenerate_ai_summaries_for_all_bookmarks\": \"Luo uudelleen tekoälyn yhteenvedot kaikille kirjanmerkeille\"\n    },\n    \"users_list\": {\n      \"users_list\": \"Käyttäjäluettelo\",\n      \"create_user\": \"Luo käyttäjä\",\n      \"change_role\": \"Vaihda roolia\",\n      \"reset_password\": \"Nollaa salasana\",\n      \"delete_user\": \"Poista käyttäjä\",\n      \"num_bookmarks\": \"Kirjanmerkkien määrä\",\n      \"asset_sizes\": \"Assetin koot\",\n      \"local_user\": \"Paikallinen käyttäjä\",\n      \"confirm_password\": \"Vahvista salasana\",\n      \"delete_user_confirm_description\": \"Oletko varma, että haluat poistaa käyttäjän \\\"{{name}}\\\"?\",\n      \"unlimited\": \"Rajoittamaton\"\n    },\n    \"service_connections\": {\n      \"title\": \"Palveluyhteydet\",\n      \"description\": \"Valvo ulkoisten järjestelmäriippuvuuksien kuntoa ja yhteyksiä\",\n      \"search_engine\": \"Hakukone\",\n      \"browser\": \"Selain\",\n      \"queue_system\": \"Jonotusjärjestelmä\",\n      \"status\": {\n        \"not_configured\": \"Ei määritetty\",\n        \"connected\": \"Yhdistetty\",\n        \"disconnected\": \"Katkaistu yhteys\"\n      }\n    },\n    \"admin_tools\": {\n      \"admin_tools\": \"Ylläpitäjän työkalut\",\n      \"bookmark_debugger\": \"Kirjanmerkkien virheenkorjaaja\",\n      \"bookmark_id\": \"Kirjanmerkin tunnus\",\n      \"bookmark_id_placeholder\": \"Syötä kirjanmerkin tunnus\",\n      \"lookup\": \"Hae\",\n      \"debug_info\": \"Vianmääritystiedot\",\n      \"basic_info\": \"Perustiedot\",\n      \"status\": \"Tila\",\n      \"content\": \"Sisältö\",\n      \"html_preview\": \"HTML-esikatselu (1000 ensimmäistä merkkiä)\",\n      \"summary\": \"Yhteenveto\",\n      \"url\": \"URL\",\n      \"source_url\": \"Lähde-URL\",\n      \"asset_type\": \"Resurssityyppi\",\n      \"file_name\": \"Tiedoston nimi\",\n      \"owner_user_id\": \"Omistajan käyttäjätunnus\",\n      \"tagging_status\": \"Merkitsemisen tila\",\n      \"summarization_status\": \"Yhteenvedon tila\",\n      \"crawl_status\": \"Indeksoinnin tila\",\n      \"crawl_status_code\": \"HTTP-tilakoodi\",\n      \"crawled_at\": \"Indeksoitu ajankohta\",\n      \"recrawl\": \"Uudelleenindeksointi\",\n      \"reindex\": \"Uudelleenindeksointi\",\n      \"retag\": \"Uudelleentaggaus\",\n      \"resummarize\": \"Uudelleenluonti\",\n      \"bookmark_not_found\": \"Kirjanmerkkiä ei löydy\",\n      \"action_success\": \"Toiminto suoritettu onnistuneesti\",\n      \"action_failed\": \"Toiminto epäonnistui\",\n      \"recrawl_queued\": \"Uudelleenindeksointi on jonossa\",\n      \"reindex_queued\": \"Uudelleenindeksointi on jonossa\",\n      \"retag_queued\": \"Uudelleentaggaus on jonossa\",\n      \"resummarize_queued\": \"Uudelleenluonti on jonossa\",\n      \"view\": \"Näytä\",\n      \"fetch_error\": \"Virhe kirjanmerkin noudossa\"\n    }\n  },\n  \"options\": {\n    \"dark_mode\": \"Tumma tila\",\n    \"light_mode\": \"Vaalea tila\",\n    \"apps_extensions\": \"Sovellukset ja laajennukset\",\n    \"documentation\": \"Dokumentaatio\",\n    \"follow_us_on_x\": \"Seuraa meitä X:ssä\"\n  },\n  \"lists\": {\n    \"all_lists\": \"Kaikki listat\",\n    \"favourites\": \"Suosikit\",\n    \"new_list\": \"Uusi lista\",\n    \"edit_list\": \"Muokkaa listaa\",\n    \"new_nested_list\": \"Uusi sisäkkäinen luettelo\",\n    \"parent_list\": \"Ylälista\",\n    \"no_parent\": \"Ei ylälistaa\",\n    \"list_type\": \"Listan tyyppi\",\n    \"manual_list\": \"Manuaalinen lista\",\n    \"smart_list\": \"Älykäs lista\",\n    \"search_query\": \"Hakukysely\",\n    \"search_query_help\": \"Lue lisää hakukielen syntaksista.\",\n    \"description\": \"Kuvaus (valinnainen)\",\n    \"merge_list\": \"Yhdistä lista\",\n    \"destination_list\": \"Kohdelista\",\n    \"delete_after_merge\": \"Poista alkuperäinen luettelo yhdistämisen jälkeen\",\n    \"no_destination\": \"Ei kohdetta\",\n    \"rss\": {\n      \"title\": \"RSS-syöte\",\n      \"description\": \"Ota RSS-syöte käyttöön tälle listalle\",\n      \"feed_url\": \"RSS-syötteen URL\"\n    },\n    \"public_list\": {\n      \"title\": \"Julkinen lista\",\n      \"description\": \"Salli muiden tarkastella tätä listaa\",\n      \"share_link\": \"Jaa linkki\"\n    },\n    \"share_list\": \"Jaa lista\",\n    \"delete_list\": {\n      \"title\": \"Poista lista\",\n      \"description\": \"Listan poistaminen ei poista yhtään kirjanmerkkiä kyseisestä listasta.\",\n      \"delete_children\": \"Poista alilistat (rekursiivisesti)\",\n      \"delete_children_description\": \"Jos tätä ei valita, kaikki suorat alilistat muuttuvat juurilistoiksi\"\n    },\n    \"shared\": \"Jaettu\",\n    \"collaborators\": {\n      \"manage\": \"Hallitse avustajia\",\n      \"view\": \"Näytä avustajat\",\n      \"collaborators\": \"Avustajat\",\n      \"add\": \"Lisää avustaja\",\n      \"current\": \"Nykyiset avustajat\",\n      \"enter_email\": \"Syötä sähköpostiosoite\",\n      \"please_enter_email\": \"Syötä sähköpostiosoite\",\n      \"added_successfully\": \"Avustaja lisätty onnistuneesti\",\n      \"failed_to_add\": \"Avustajan lisääminen epäonnistui\",\n      \"removed\": \"Avustaja poistettu\",\n      \"failed_to_remove\": \"Avustajan poistaminen epäonnistui\",\n      \"role_updated\": \"Rooli päivitetty\",\n      \"failed_to_update_role\": \"Roolin päivittäminen epäonnistui\",\n      \"viewer\": \"Katsoja\",\n      \"editor\": \"Muokkaaja\",\n      \"owner\": \"Omistaja\",\n      \"viewer_description\": \"Voi tarkastella kirjanmerkkejä luettelossa\",\n      \"editor_description\": \"Voi lisätä ja poistaa kirjanmerkkejä\",\n      \"no_collaborators\": \"Ei vielä avustajia. Lisää joku, että pääsette hommiin!\",\n      \"no_collaborators_readonly\": \"Ei tällä listalla ole muita tekijöitä.\",\n      \"people_with_access\": \"Ihmisillä, joilla on pääsy tälle listalle\",\n      \"add_or_remove\": \"Lisää tai poista ihmisiä, keillä on pääsy tälle listalle.\",\n      \"invitation_sent\": \"Kutsu lähetetty onnistuneesti\",\n      \"invitation_revoked\": \"Kutsu peruttu\",\n      \"failed_to_revoke\": \"Kutsun peruminen epäonnistui\",\n      \"pending\": \"Odottaa\",\n      \"revoke\": \"Peruuta\",\n      \"declined\": \"Hylätty\"\n    },\n    \"leave_list\": {\n      \"title\": \"Poistu listasta\",\n      \"confirm_message\": \"Oletko varma, että haluat poistua listasta {{icon}} {{name}}?\",\n      \"warning\": \"Et voi enää nähdä tai käyttää tämän listan kirjanmerkkejä. Listan omistaja voi lisätä sinut takaisin tarvittaessa.\",\n      \"action\": \"Poistu listasta\",\n      \"success\": \"Olet poistunut listasta \\\"{{icon}} {{name}}\\\"\"\n    },\n    \"invitations\": {\n      \"pending\": \"Odottavat kutsut\",\n      \"description\": \"Tarkista ja vastaa luettelon yhteistyökutsuihin\",\n      \"invited_by\": \"Kutsun lähettäjä\",\n      \"accept\": \"Hyväksy\",\n      \"decline\": \"Hylkää\",\n      \"accepted\": \"Kutsu hyväksytty\",\n      \"declined\": \"Kutsu hylätty\",\n      \"failed_to_accept\": \"Kutsun hyväksyminen epäonnistui\",\n      \"failed_to_decline\": \"Kutsun hylkääminen epäonnistui\"\n    },\n    \"shared_lists\": \"Jaetut listat\"\n  },\n  \"tags\": {\n    \"all_tags\": \"Kaikki tunnisteet\",\n    \"your_tags\": \"Omat tunnisteet\",\n    \"your_tags_info\": \"Tunnisteet, jotka olet liittänyt vähintään kerran\",\n    \"ai_tags\": \"Tekoälytunnisteet\",\n    \"ai_tags_info\": \"Tunnisteet, jotka on liitetty vain automaattisesti (tekoälyn avulla)\",\n    \"unused_tags\": \"Käyttämättömät tunnisteet\",\n    \"unused_tags_info\": \"Tunnisteet, joita ei ole liitetty mihinkään kirjanmerkkiin\",\n    \"delete_all_unused_tags\": \"Poista kaikki käyttämättömät tunnisteet\",\n    \"drag_and_drop_merging\": \"Vedä ja pudota yhdistäminen\",\n    \"drag_and_drop_merging_info\": \"Yhdistä tunnisteita vetämällä ja pudottamalla ne toistensa päälle\",\n    \"sort_by_name\": \"Järjestä nimen mukaan\",\n    \"create_tag\": \"Luo tunnus\",\n    \"create_tag_description\": \"Luo uusi tunnus liittämättä sitä mihinkään kirjanmerkkiin\",\n    \"tag_name\": \"Tunnuksen nimi\",\n    \"enter_tag_name\": \"Kirjoita tunnuksen nimi\",\n    \"sort_by_usage\": \"Järjestä käytön mukaan\",\n    \"sort_by_relevance\": \"Järjestä osuvuuden mukaan\",\n    \"no_custom_tags\": \"Ei vielä omia tunnisteita\",\n    \"no_ai_tags\": \"Ei vielä yhtään tekoälytunnistetta\",\n    \"no_unused_tags\": \"Sinulla ei ole yhtään käyttämätöntä tunnistetta\",\n    \"no_unused_tags_match_your_search\": \"Hakusi ei täsmää yhtään käyttämätöntä tunnistetta\",\n    \"no_tags_match_your_search\": \"Hakusi ei täsmää yhtään tunnistetta\",\n    \"search_placeholder\": \"Etsi tunnisteita...\",\n    \"search_or_create_placeholder\": \"Etsi tai luo tunnisteita...\"\n  },\n  \"search\": {\n    \"is_favorited\": \"On suosikki\",\n    \"is_not_favorited\": \"Ei ole suosikki\",\n    \"is_archived\": \"On arkistoitu\",\n    \"is_not_archived\": \"Ei ole arkistoitu\",\n    \"has_any_tag\": \"On jokin tagi\",\n    \"has_no_tags\": \"Ei tunnistetta\",\n    \"is_in_any_list\": \"On jossain luettelossa\",\n    \"is_not_in_any_list\": \"Ei ole missään listassa\",\n    \"created_on_or_after\": \"Luotu aikaisintaan\",\n    \"not_created_on_or_after\": \"Ei luotu aikaisemmin kuin\",\n    \"created_on_or_before\": \"Luotu aikaisemmin tai silloin\",\n    \"not_created_on_or_before\": \"Ei luotu aikaisemmin kuin\",\n    \"url_contains\": \"URL sisältää\",\n    \"url_does_not_contain\": \"URL ei sisällä\",\n    \"is_in_list\": \"Onko luettelossa\",\n    \"is_not_in_list\": \"Ei ole luettelossa\",\n    \"has_tag\": \"On tunnus\",\n    \"does_not_have_tag\": \"Ei ole tunnistetta\",\n    \"full_text_search\": \"Koko tekstin haku\",\n    \"type_is\": \"Tyyppi on\",\n    \"type_is_not\": \"Tyyppi ei ole\",\n    \"is_from_feed\": \"On RSS-syötteestä\",\n    \"is_not_from_feed\": \"Ei ole RSS-syötteestä\",\n    \"and\": \"Ja\",\n    \"or\": \"Tai\",\n    \"created_within\": \"Luotu aikavälillä\",\n    \"created_earlier_than\": \"Luotu aiemmin kuin\",\n    \"day_s\": \" Päivä(ä)\",\n    \"week_s\": \" Viikko(a)\",\n    \"month_s\": \" Kuukausi(a)\",\n    \"year_s\": \" Vuosi(a)\",\n    \"day_s_ago\": \" Päivä(ä) sitten\",\n    \"week_s_ago\": \" Viikko(a) sitten\",\n    \"month_s_ago\": \" Kuukausi(a) sitten\",\n    \"year_s_ago\": \" Vuosi(a) sitten\",\n    \"history\": \"Viimeaikaiset haut\",\n    \"title_contains\": \"Otsikko sisältää\",\n    \"title_does_not_contain\": \"Otsikko ei sisällä\",\n    \"is_broken_link\": \"On rikkinäinen linkki\",\n    \"tags\": \"Tunnisteet\",\n    \"no_suggestions\": \"Ei ehdotuksia\",\n    \"filters\": \"Suodattimet\",\n    \"is_not_broken_link\": \"On toimiva linkki\",\n    \"lists\": \"Listat\",\n    \"feeds\": \"Syötteet\",\n    \"is_from_source\": \"Lähde on\",\n    \"is_not_from_source\": \"Lähdettä ei ole\"\n  },\n  \"preview\": {\n    \"view_original\": \"Näytä alkuperäinen\",\n    \"cached_content\": \"Välimuistissa oleva sisältö\",\n    \"reader_view\": \"Lukijanäkymä\",\n    \"tabs\": {\n      \"content\": \"Sisältö\",\n      \"details\": \"Tiedot\"\n    },\n    \"archive_info\": \"Arkistot eivät välttämättä hahmotu oikein, jos ne vaativat Javascriptiä. Parhaan tuloksen saat, kun <1>lataat sen ja avaat sen selaimessasi</1>.\",\n    \"fetch_error_title\": \"Sisältö ei ole käytettävissä\",\n    \"fetch_error_description\": \"Emme saaneet haettua tämän linkin sisältöä. Sivu voi olla suojattu, se saattaa vaatia tunnistautumisen tai se on tilapäisesti poissa käytöstä.\",\n    \"crawling_in_progress\": \"Noudetaan sivun sisältöä…\",\n    \"continue_reading\": \"Jatka siitä, mihin jäit\",\n    \"continue_reading_percent\": \"Jatka siitä, mihin jäit ({{percent}} %)\",\n    \"continue_button\": \"Jatka\"\n  },\n  \"editor\": {\n    \"quickly_focus\": \"Voit nopeasti kohdistaa tähän kenttään painamalla ⌘ + E\",\n    \"multiple_urls_dialog_title\": \"Tuodaanko URL-osoitteet erillisinä kirjanmerkkeinä?\",\n    \"multiple_urls_dialog_desc\": \"Syöte sisältää useita URL-osoitteita erillisillä riveillä. Haluatko tuoda ne erillisinä kirjanmerkkeinä?\",\n    \"import_as_text\": \"Tuo tekstikirjanmerkkinä\",\n    \"import_as_separate_bookmarks\": \"Tuo erillisinä kirjanmerkkeinä\",\n    \"placeholder\": \"Liitä linkki tai kuva, kirjoita muistiinpano tai vedä ja pudota kuva tähän…\",\n    \"new_item\": \"UUSI KOHDE\",\n    \"disabled_submissions\": \"Lähetykset on poistettu käytöstä\",\n    \"text_toolbar\": {\n      \"undo\": \"Kumoa\",\n      \"redo\": \"Tee uudelleen\",\n      \"bold\": \"Lihavointi\",\n      \"italic\": \"Kursiivi\",\n      \"underline\": \"Alleviivaa\",\n      \"strikethrough\": \"Yliviivaus\",\n      \"code\": \"Koodi\",\n      \"highlight\": \"Korosta\",\n      \"align_left\": \"Tasaa vasemmalle\",\n      \"align_center\": \"Keskitä tasaus\",\n      \"align_right\": \"Tasaa oikealle\",\n      \"markdown_shortcuts\": {\n        \"label\": \"Markdown-pikakuvakkeet\",\n        \"heading\": {\n          \"label\": \"Otsikko\",\n          \"example\": \"# H1, ## H2, ### H3\"\n        },\n        \"bold\": {\n          \"label\": \"Lihavointi\",\n          \"example\": \"**teksti** tai CTRL+b\"\n        },\n        \"italic\": {\n          \"label\": \"Kursiivi\",\n          \"example\": \"*Kursivoitu* tai _Kursivoitu_ tai CTRL+i\"\n        },\n        \"blockquote\": {\n          \"label\": \"Lainaus\",\n          \"example\": \"> Lainaus\"\n        },\n        \"ordered_list\": {\n          \"label\": \"Järjestetty luettelo\",\n          \"example\": \"1. Listan kohta\"\n        },\n        \"unordered_list\": {\n          \"label\": \"Järjestämätön luettelo\",\n          \"example\": \"- Listan kohde\"\n        },\n        \"inline_code\": {\n          \"label\": \"Tekstin sisäinen koodi\",\n          \"example\": \"`Koodi`\"\n        },\n        \"block_code\": {\n          \"label\": \"Estokoodi\",\n          \"example\": \"``` + välilyönti\"\n        }\n      }\n    },\n    \"placeholder_v2\": \"Liitä linkki, kirjoita muistiinpano tai pudota kuva…\"\n  },\n  \"dialogs\": {\n    \"bookmarks\": {\n      \"delete_confirmation_title\": \"Poistetaanko kirjanmerkki?\",\n      \"delete_confirmation_description\": \"Haluatko varmasti poistaa tämän kirjanmerkin?\"\n    }\n  },\n  \"toasts\": {\n    \"bookmarks\": {\n      \"updated\": \"Kirjanmerkki on päivitetty!\",\n      \"deleted\": \"Kirjanmerkki on poistettu!\",\n      \"refetch\": \"Uudelleennouto on jonossa!\",\n      \"full_page_archive\": \"Koko sivun arkiston luonti on käynnistetty\",\n      \"delete_from_list\": \"Kirjanmerkki on poistettu luettelosta\",\n      \"clipboard_copied\": \"Linkki on lisätty leikepöydälle!\",\n      \"preserve_pdf\": \"PDF:nä säilytys on käynnistetty\",\n      \"update_banner\": \"Banneri on päivitetty!\",\n      \"uploading_banner\": \"Ladataan banneria...\"\n    },\n    \"lists\": {\n      \"created\": \"Lista on luotu!\",\n      \"updated\": \"Lista on päivitetty!\",\n      \"merged\": \"Lista on yhdistetty!\",\n      \"deleted\": \"Lista on poistettu!\"\n    },\n    \"tags\": {\n      \"created\": \"Tunnus on nyt luotu!\",\n      \"failed_to_create\": \"Tunnuksen luominen epäonnistui\"\n    }\n  },\n  \"banners\": {\n    \"no_bookmarks\": {\n      \"title\": \"Ei vielä kirjanmerkkejä\",\n      \"description\": \"Tallenna mielenkiintoisia artikkeleita, linkkejä ja sivuja, jotta voit käyttää niitä nopeasti myöhemmin.\"\n    }\n  },\n  \"cleanups\": {\n    \"cleanups\": \"Siivoukset\",\n    \"duplicate_tags\": {\n      \"title\": \"Päällekkäiset tunnisteet\",\n      \"merge_all_suggestions\": \"Yhdistetäänkö kaikki ehdotukset?\"\n    }\n  },\n  \"bookmark_editor\": {\n    \"title\": \"Muokkaa kirjanmerkkiä\",\n    \"subtitle\": \"Tee muutoksia kirjanmerkin tietoihin. Napsauta tallenna, kun olet valmis.\",\n    \"author\": \"Tekijä\",\n    \"publisher\": \"Julkaisija\",\n    \"date_published\": \"Julkaisupäivämäärä\",\n    \"pick_a_date\": \"Valitse päivämäärä\",\n    \"save_changes\": \"Tallenna muutokset\",\n    \"extracted_content\": \"Purettu sisältö\"\n  },\n  \"view_options\": {\n    \"title\": \"Näytä asetukset\",\n    \"layout\": \"Ulkoasu\",\n    \"columns\": \"Sarakkeet\",\n    \"display_options\": \"Näyttöasetukset\",\n    \"show_note_previews\": \"Näytä muistiinpanot\",\n    \"show_tags\": \"Näytä tunnisteet\",\n    \"show_title\": \"Näytä otsikko\",\n    \"image_options\": \"Kuvan valinnat\",\n    \"image_fit_cover\": \"Peitä (täytä)\",\n    \"image_fit_contain\": \"Sisällä (sovita)\"\n  },\n  \"version\": {\n    \"new_release_available\": \"Uudet julkaisutiedot saatavilla\",\n    \"whats_new_title\": \"Mitä uutta versiossa v{{version}}\",\n    \"release_notes_description\": \"Tässäpä uusimmat päivitykset, jotka on haettu GitHubin julkaisutiedoista.\",\n    \"loading_release_notes\": \"Julkaisutietoja ladataan…\",\n    \"unable_to_load_release_notes\": \"Julkaisutietojen lataaminen ei onnistu juuri nyt. Yritä myöhemmin uudelleen.\",\n    \"no_release_notes\": \"Tälle versiolle ei ole julkaistu julkaisutietoja.\",\n    \"release_notes_synced\": \"Julkaisutiedot synkronoidaan GitHubista.\",\n    \"view_on_github\": \"Näytä GitHubissa\"\n  },\n  \"wrapped\": {\n    \"title\": \"Sun {{year}}-kooste\",\n    \"subtitle\": \"Vuosi Karakeepissä\",\n    \"banner\": {\n      \"title\": \"Sun 2025 -kooste on valmis!\",\n      \"description\": \"Katso sun vuotta kirjanmerkeissä\",\n      \"view_now\": \"Katso nyt\"\n    },\n    \"button\": \"2025 -kooste\",\n    \"loading\": \"Ladataan sun koosteesta...\",\n    \"failed_to_load\": \"Sun koosteen tilastojen lataus epäonnistui\",\n    \"sections\": {\n      \"total_saves\": {\n        \"prefix\": \"Oot tallentanu\",\n        \"suffix\": \"juttua tänä vuonna\",\n        \"suffix_singular\": \"juttu tänä vuonna\"\n      },\n      \"first_bookmark\": {\n        \"title\": \"Matkasi alkoi\",\n        \"description\": \"Ensimmäinen tallennus vuonna {{year}}:\"\n      },\n      \"top_domains\": \"Suosikkisivustosi\",\n      \"top_tags\": \"Suosikkitunnisteet\",\n      \"monthly_activity\": \"Tallennusvuotesi\",\n      \"most_active_day\": \"Aktiivisin päiväsi\",\n      \"peak_times\": {\n        \"title\": \"Milloin tallennat\",\n        \"peak_hour\": \"Vilkkain tunti\",\n        \"peak_day\": \"Vilkkain päivä\"\n      },\n      \"how_you_save\": \"Miten tallennat\",\n      \"what_you_saved\": \"Mitä tallensit\",\n      \"summary\": {\n        \"favorites\": \"Suosikit\",\n        \"tags_created\": \"Luodut tunnisteet\",\n        \"highlights\": \"Kohokohdat\"\n      },\n      \"types\": {\n        \"links\": \"Linkit\",\n        \"notes\": \"Muistiinpanot\",\n        \"assets\": \"Resurssit\"\n      }\n    },\n    \"footer\": \"Tehty Karakeepillä\",\n    \"share\": \"Jaa\",\n    \"download\": \"Lataa\",\n    \"close\": \"Sulje\",\n    \"generating\": \"Luodaan...\"\n  }\n}\n"
  },
  {
    "path": "apps/web/lib/i18n/locales/fr/translation.json",
    "content": "{\n  \"common\": {\n    \"url\": \"URL\",\n    \"name\": \"Nom\",\n    \"email\": \"Email\",\n    \"password\": \"Mot de passe\",\n    \"action\": \"Action\",\n    \"actions\": \"Actions\",\n    \"created_at\": \"Créé le\",\n    \"key\": \"Clé\",\n    \"role\": \"Rôle\",\n    \"roles\": {\n      \"user\": \"Utilisateur\",\n      \"admin\": \"Administrateur\"\n    },\n    \"something_went_wrong\": \"Quelque chose a mal tourné\",\n    \"experimental\": \"Expérimental\",\n    \"search\": \"Recherche\",\n    \"tags\": \"Tags\",\n    \"note\": \"Note\",\n    \"attachments\": \"Pièces jointes\",\n    \"screenshot\": \"Capture d'écran\",\n    \"video\": \"Vidéo\",\n    \"archive\": \"Archive\",\n    \"home\": \"Accueil\",\n    \"source\": \"Source\",\n    \"bookmark_types\": {\n      \"link\": \"Lien\",\n      \"text\": \"Texte\",\n      \"title\": \"Type de marque-page\",\n      \"media\": \"Médias\"\n    },\n    \"highlights\": \"Surlignages\",\n    \"type\": \"Tapez\",\n    \"size\": \"Taille\",\n    \"updated_at\": \"Mis à jour le\",\n    \"title\": \"Titre\",\n    \"description\": \"Description\",\n    \"summary\": \"Résumé\",\n    \"quota\": \"Quota\",\n    \"bookmarks\": \"Marque-pages\",\n    \"storage\": \"Stockage\",\n    \"pdf\": \"PDF archivé\",\n    \"default\": \"Par défaut\",\n    \"id\": \"ID\",\n    \"last_used\": \"Dernière utilisation\"\n  },\n  \"layouts\": {\n    \"masonry\": \"Mosaïque\",\n    \"grid\": \"Grille\",\n    \"list\": \"Liste\",\n    \"compact\": \"Compact\"\n  },\n  \"actions\": {\n    \"change_layout\": \"Changer la disposition\",\n    \"archive\": \"Archiver\",\n    \"unarchive\": \"Désarchiver\",\n    \"favorite\": \"Mettre en favori\",\n    \"unfavorite\": \"Retirer des favoris\",\n    \"delete\": \"Supprimer\",\n    \"refresh\": \"Rafraîchir\",\n    \"download_full_page_archive\": \"Télécharger l'archive de la page complète\",\n    \"edit_tags\": \"Modifier les tags\",\n    \"add_to_list\": \"Ajouter à la liste\",\n    \"select_all\": \"Tout sélectionner\",\n    \"unselect_all\": \"Tout désélectionner\",\n    \"copy_link\": \"Copier le lien\",\n    \"close_bulk_edit\": \"Fermer la modification en masse\",\n    \"bulk_edit\": \"Modification en masse\",\n    \"manage_lists\": \"Gérer les listes\",\n    \"remove_from_list\": \"Retirer de la liste\",\n    \"save\": \"Enregistrer\",\n    \"add\": \"Ajouter\",\n    \"edit\": \"Modifier\",\n    \"create\": \"Créer\",\n    \"fetch_now\": \"Récupérer maintenant\",\n    \"summarize_with_ai\": \"Résumer avec l'IA\",\n    \"edit_title\": \"Modifier le titre\",\n    \"sign_out\": \"Déconnexion\",\n    \"close\": \"Fermer\",\n    \"merge\": \"Fusionner\",\n    \"cancel\": \"Annuler\",\n    \"apply_all\": \"Tout appliquer\",\n    \"ignore\": \"Ignorer\",\n    \"sort\": {\n      \"title\": \"Trier\",\n      \"newest_first\": \"Plus récents d'abord\",\n      \"oldest_first\": \"Plus anciens d'abord\",\n      \"relevant_first\": \"Les plus pertinents en premier\"\n    },\n    \"recrawl\": \"Réexplorer\",\n    \"open_editor\": \"Ouvrir l'éditeur\",\n    \"toggle_show_archived\": \"Afficher les éléments archivés\",\n    \"confirm\": \"Confirmer\",\n    \"regenerate\": \"Régénérer\",\n    \"load_more\": \"En charger plus\",\n    \"edit_notes\": \"Modifier les notes\",\n    \"preserve_as_pdf\": \"Conserver en PDF\",\n    \"offline_copies\": \"Copies hors ligne\",\n    \"preserve_offline_archive\": \"Conserver l’archive hors ligne\",\n    \"download_full_page_archive_file\": \"Télécharger le fichier d’archive\",\n    \"download_pdf_file\": \"Télécharger le fichier PDF\",\n    \"remove\": \"Supprimer\",\n    \"more\": \"Plus\",\n    \"replace_banner\": \"Remplacer la bannière\",\n    \"add_banner\": \"Ajouter une bannière\",\n    \"download\": \"Télécharger\"\n  },\n  \"settings\": {\n    \"back_to_app\": \"Retour à l'application\",\n    \"user_settings\": \"Paramètres utilisateur\",\n    \"info\": {\n      \"user_info\": \"Infos utilisateur\",\n      \"basic_details\": \"Détails de base\",\n      \"change_password\": \"Changer le mot de passe\",\n      \"current_password\": \"Mot de passe actuel\",\n      \"new_password\": \"Nouveau mot de passe\",\n      \"confirm_new_password\": \"Confirmer le nouveau mot de passe\",\n      \"options\": \"Options\",\n      \"interface_lang\": \"Langue de l'interface\",\n      \"user_settings\": {\n        \"archive_display_behaviour\": {\n          \"hide\": \"Masquer les marque-pages archivés dans les tags et les listes\",\n          \"title\": \"Marque-pages archivés\",\n          \"show\": \"Afficher les marque-pages archivés dans les tags et les listes\"\n        },\n        \"user_settings_updated\": \"Les paramètres utilisateur ont été mis à jour !\",\n        \"bookmark_click_action\": {\n          \"title\": \"Action du clic sur un marque-page\",\n          \"open_external_url\": \"Ouvrir l’URL d’origine\",\n          \"open_bookmark_details\": \"Ouvrir les détails du marque-page\"\n        }\n      },\n      \"reader_settings\": {\n        \"local_overrides_title\": \"Paramètres spécifiques à l’appareil actifs\",\n        \"using_default\": \"Utilisation des paramètres par défaut du client\",\n        \"clear_override_hint\": \"Effacer la substitution de l’appareil pour utiliser le paramètre général ({{value}})\",\n        \"font_size\": \"Taille de la police\",\n        \"font_family\": \"Famille de polices\",\n        \"preview_inline\": \"(aperçu)\",\n        \"tooltip_preview\": \"Modifications de l’aperçu non enregistrées\",\n        \"save_to_all_devices\": \"Tous les appareils\",\n        \"tooltip_local\": \"Les paramètres de l’appareil diffèrent des paramètres généraux\",\n        \"reset_preview\": \"Réinitialiser l’aperçu\",\n        \"mono\": \"Monospace\",\n        \"line_height\": \"Hauteur de ligne\",\n        \"tooltip_default\": \"Paramètres de lecture\",\n        \"title\": \"Paramètres du lecteur\",\n        \"serif\": \"Avec empattement\",\n        \"preview\": \"Aperçu\",\n        \"not_set\": \"Non défini\",\n        \"clear_local_overrides\": \"Effacer les paramètres de l’appareil\",\n        \"preview_text\": \"Le rapide renard brun saute par-dessus le chien paresseux. Voici comment apparaîtra le texte de votre affichage de lecteur.\",\n        \"local_overrides_cleared\": \"Les paramètres spécifiques à l’appareil ont été effacés\",\n        \"local_overrides_description\": \"Cet appareil a des paramètres de lecteur qui diffèrent de vos paramètres par défaut globaux :\",\n        \"clear_defaults\": \"Effacer toutes les valeurs par défaut\",\n        \"description\": \"Configurez les paramètres de texte par défaut pour l’affichage du lecteur. Ces paramètres sont synchronisés sur tous vos appareils.\",\n        \"defaults_cleared\": \"Les paramètres par défaut du lecteur ont été supprimés\",\n        \"save_hint\": \"Enregistrer les paramètres pour cet appareil uniquement ou synchroniser avec tous les appareils\",\n        \"save_as_default\": \"Enregistrer comme valeurs par défaut\",\n        \"save_to_device\": \"Cet appareil\",\n        \"sans\": \"Sans empattement\",\n        \"tooltip_preview_and_local\": \"Modifications de l’aperçu non enregistrées ; les paramètres de l’appareil diffèrent des paramètres généraux\",\n        \"adjust_hint\": \"Ajustez les paramètres ci-dessus pour prévisualiser les modifications\"\n      },\n      \"avatar\": {\n        \"upload\": \"Téléverser un avatar\",\n        \"change\": \"Changer d’avatar\",\n        \"remove_confirm_title\": \"Supprimer l’avatar ?\",\n        \"updated\": \"Avatar mis à jour\",\n        \"removed\": \"Avatar supprimé\",\n        \"description\": \"Téléversez une image carrée à utiliser comme avatar.\",\n        \"remove_confirm_description\": \"Cela supprimera votre photo de profil actuelle.\",\n        \"title\": \"Photo de profil\",\n        \"remove\": \"Supprimer l’avatar\"\n      }\n    },\n    \"ai\": {\n      \"ai_settings\": \"Paramètres de l'IA\",\n      \"tagging_rules\": \"Règles de tagging\",\n      \"tagging_rule_description\": \"Les prompts que vous ajoutez ici seront incluses comme règles pour le modèle lors de la génération des tags. Vous pouvez voir les prompts finaux dans la section de prévisualisation des prompts.\",\n      \"prompt_preview\": \"Aperçu du prompt\",\n      \"text_prompt\": \"Prompt texte\",\n      \"images_prompt\": \"Prompt image\",\n      \"summarization_prompt\": \"Prompt de résumé\",\n      \"all_tagging\": \"Tout le tagging\",\n      \"text_tagging\": \"Balises de texte\",\n      \"image_tagging\": \"Marquage d'image\",\n      \"summarization\": \"Résumer\",\n      \"tag_style\": \"Style des balises\",\n      \"auto_summarization_description\": \"Générez automatiquement des résumés pour vos favoris à l’aide de l’IA.\",\n      \"auto_tagging\": \"Attribution automatique de balises\",\n      \"titlecase_spaces\": \"Majuscule en début de mot avec espaces\",\n      \"lowercase_underscores\": \"Minuscules avec traits de soulignement\",\n      \"inference_language\": \"Langue d’inférence\",\n      \"titlecase_hyphens\": \"Majuscule en début de mot avec tirets\",\n      \"lowercase_hyphens\": \"Minuscules avec tirets\",\n      \"lowercase_spaces\": \"Minuscules avec espaces\",\n      \"inference_language_description\": \"Choisissez la langue pour les balises et les résumés générés par l’IA.\",\n      \"tag_style_description\": \"Choisissez le format de vos balises générées automatiquement.\",\n      \"auto_tagging_description\": \"Générez automatiquement des balises pour vos favoris à l’aide de l’IA.\",\n      \"camelCase\": \"camelCase\",\n      \"auto_summarization\": \"Résumés automatiques\",\n      \"no_preference\": \"Aucune préférence\",\n      \"curated_tags\": \"Tags sélectionnés\",\n      \"curated_tags_description\": \"Restreignez éventuellement le balisage de l’IA pour n’utiliser que les balises de cette liste. Lorsqu’aucune balise n’est sélectionnée, l’IA génère les balises librement.\",\n      \"curated_tags_updated\": \"Tags sélectionnés mis à jour avec succès !\",\n      \"curated_tags_update_failed\": \"Échec de la mise à jour des balises sélectionnées\"\n    },\n    \"feeds\": {\n      \"rss_subscriptions\": \"Abonnements RSS\",\n      \"add_a_subscription\": \"Ajouter un abonnement\",\n      \"feed_enabled\": \"Flux RSS activé\",\n      \"feed_disabled\": \"Flux RSS désactivé\"\n    },\n    \"import\": {\n      \"import_export\": \"Importer / Exporter\",\n      \"import_export_bookmarks\": \"Importer / Exporter des favoris\",\n      \"import_bookmarks_from_html_file\": \"Importer des favoris depuis un fichier HTML\",\n      \"import_bookmarks_from_pocket_export\": \"Importer des favoris depuis une exportation Pocket\",\n      \"import_bookmarks_from_matter_export\": \"Importer des favoris depuis une exportation Matter\",\n      \"import_bookmarks_from_omnivore_export\": \"Importer des favoris depuis une exportation Omnivore\",\n      \"import_bookmarks_from_karakeep_export\": \"Importer des favoris depuis une exportation Karakeep\",\n      \"export_links_and_notes\": \"Exporter les liens et les notes\",\n      \"imported_bookmarks\": \"Favoris importés\",\n      \"import_bookmarks_from_linkwarden_export\": \"Importer des marque-pages depuis l'export Linkwarden\",\n      \"import_bookmarks_from_tab_session_manager_export\": \"Importer les marque-pages depuis Tab Session Manager\",\n      \"import_bookmarks_from_mymind_export\": \"Importer les signets depuis l'export mymind\",\n      \"import_bookmarks_from_instapaper_export\": \"Importer les marque-pages à partir de l’exportation Instapaper\"\n    },\n    \"api_keys\": {\n      \"api_keys\": \"Clés API\",\n      \"new_api_key\": \"Nouvelle clé API\",\n      \"new_api_key_desc\": \"Donnez un nom unique à votre clé API\",\n      \"key_success\": \"La clé a été créée avec succès\",\n      \"key_success_please_copy\": \"Veuillez copier la clé et la stocker dans un endroit sûr. Une fois que vous fermerez la fenêtre de dialogue, vous ne pourrez plus y accéder.\",\n      \"regenerate_api_key\": \"Régénérer la clé API\",\n      \"key_regenerated\": \"La clé a été régénérée avec succès\",\n      \"key_regenerated_please_copy\": \"Veuillez copier la nouvelle clé et la conserver dans un endroit sûr. L'ancienne clé a été révoquée et ne fonctionnera plus.\",\n      \"regenerate_warning\": \"Êtes-vous sûr de vouloir régénérer la clé API \\\"{{name}}\\\" ?\",\n      \"regenerate_confirmation\": \"Cela révoquera la clé actuelle et en générera une nouvelle. Toutes les applications utilisant la clé actuelle cesseront de fonctionner.\"\n    },\n    \"webhooks\": {\n      \"auth_token\": \"Jeton d'authentification\",\n      \"webhooks\": \"Webhooks\",\n      \"add_auth_token\": \"Ajouter un jeton d'authentification\",\n      \"edit_webhook\": \"Modifier le webhook\",\n      \"webhook_url\": \"URL du webhook\",\n      \"description\": \"Vous pouvez utiliser des webhooks pour déclencher des actions lorsque des signets sont créés, modifiés ou explorés.\",\n      \"events\": {\n        \"title\": \"Événements\",\n        \"crawled\": \"Exploré\",\n        \"created\": \"Créé\",\n        \"edited\": \"Modifié\"\n      },\n      \"edit_auth_token\": \"Modifier le jeton d'authentification\",\n      \"create_webhook\": \"Créer un webhook\",\n      \"delete_webhook\": \"Supprimer le Webhook\",\n      \"delete_webhook_confirmation\": \"Êtes-vous sûr de vouloir supprimer ce webhook ?\"\n    },\n    \"broken_links\": {\n      \"crawling_status\": \"État de l'exploration\",\n      \"last_crawled_at\": \"Dernier crawlé à\",\n      \"crawling_failed\": \"Exploration échouée\",\n      \"broken_links\": \"Liens brisés\"\n    },\n    \"manage_assets\": {\n      \"manage_assets\": \"Gérer les ressources\",\n      \"no_assets\": \"Vous n'avez pas encore d'actifs.\",\n      \"asset_type\": \"Type d'actif\",\n      \"bookmark_link\": \"Lien de signet\",\n      \"asset_link\": \"Lien de l'actif\",\n      \"delete_asset\": \"Supprimer l'actif\",\n      \"delete_asset_confirmation\": \"Êtes-vous sûr de vouloir supprimer cet actif ?\"\n    },\n    \"rules\": {\n      \"rules\": \"Moteur de règles\",\n      \"rule_name\": \"Nom de la règle\",\n      \"description\": \"Vous pouvez utiliser des règles pour déclencher des actions lorsqu’un événement se produit.\",\n      \"ceate_rule\": \"Créer une règle\",\n      \"edit_rule\": \"Modifier la règle\",\n      \"save_rule\": \"Enregistrer la règle\",\n      \"delete_rule\": \"Supprimer la règle\",\n      \"delete_rule_confirmation\": \"Êtes-vous sûr de vouloir supprimer cette règle ?\",\n      \"whenever\": \"À chaque fois que...\",\n      \"if\": \"Si...\",\n      \"enter_rule_name\": \"Entrez le nom de la règle\",\n      \"describe_what_this_rule_does\": \"Décrivez ce que fait cette règle\",\n      \"rule_has_been_created\": \"La règle a été créée !\",\n      \"rule_has_been_updated\": \"La règle a été mise à jour !\",\n      \"rule_has_been_deleted\": \"La règle a été supprimée !\",\n      \"no_rules_created_yet\": \"Aucune règle n'a encore été créée\",\n      \"create_your_first_rule\": \"Créez votre première règle pour automatiser votre flux de travail\",\n      \"conditions_types\": {\n        \"always\": \"Toujours\",\n        \"url_contains\": \"L'URL contient\",\n        \"imported_from_feed\": \"Importé depuis un flux\",\n        \"bookmark_type_is\": \"Le type de marque-page est\",\n        \"has_tag\": \"A un tag\",\n        \"is_favourited\": \"Est mis en favori\",\n        \"is_archived\": \"Est archivé\",\n        \"and\": \"Toutes les conditions suivantes sont vraies\",\n        \"or\": \"Si l'une des conditions suivantes est remplie\",\n        \"url_does_not_contain\": \"L’URL ne contient pas\",\n        \"title_contains\": \"Le titre contient\",\n        \"title_does_not_contain\": \"Le titre ne contient pas\"\n      },\n      \"actions_types\": {\n        \"add_tag\": \"Ajouter un tag\",\n        \"remove_tag\": \"Supprimer le tag\",\n        \"add_to_list\": \"Ajouter à la liste\",\n        \"remove_from_list\": \"Supprimer de la liste\",\n        \"download_full_page_archive\": \"Télécharger l'archive de la page complète\",\n        \"favourite_bookmark\": \"Marquer un marque-page comme favori\",\n        \"archive_bookmark\": \"Archiver le marque-page\"\n      },\n      \"events_types\": {\n        \"bookmark_added\": \"Un marque-page est ajouté\",\n        \"tag_added\": \"Ce tag est ajouté à un marque-page\",\n        \"tag_removed\": \"Ce tag est supprimé d'un marque-page\",\n        \"added_to_list\": \"Un marque-page est ajouté à cette liste\",\n        \"removed_from_list\": \"Un marque-page est supprimé de cette liste\",\n        \"favourited\": \"Un marque-page est mis en favori\",\n        \"archived\": \"Un marque-page est archivé\"\n      }\n    },\n    \"stats\": {\n      \"usage_statistics\": \"Statistiques d'utilisation\",\n      \"insights_description\": \"Aperçu de vos habitudes de marque-page et collection\",\n      \"failed_to_load\": \"Impossible de charger les statistiques\",\n      \"overview\": {\n        \"total_bookmarks\": \"Nombre total de marque-pages\",\n        \"all_saved_items\": \"Tous les éléments enregistrés\",\n        \"favorites\": \"Favoris\",\n        \"starred_bookmarks\": \"Marque-pages étoilés\",\n        \"archived\": \"Archivé\",\n        \"archived_items\": \"Éléments archivés\",\n        \"tags\": \"Tags\",\n        \"unique_tags_created\": \"Tags uniques créées\",\n        \"lists\": \"Listes\",\n        \"bookmark_collections\": \"Collections de marque-page\",\n        \"highlights\": \"Surlignages\",\n        \"text_highlights\": \"Surlignages de texte\",\n        \"storage_used\": \"Stockage utilisé\",\n        \"total_asset_storage\": \"Stockage total des actifs\",\n        \"this_month\": \"Ce mois-ci\",\n        \"bookmarks_added\": \"Marque-pages ajoutés\"\n      },\n      \"bookmark_types\": {\n        \"title\": \"Types de marque-page\",\n        \"links\": \"Liens\",\n        \"text_notes\": \"Notes texte\",\n        \"assets\": \"Actifs\"\n      },\n      \"recent_activity\": {\n        \"title\": \"Activité récente\",\n        \"this_week\": \"Cette semaine\",\n        \"this_month\": \"Ce mois-ci\",\n        \"this_year\": \"Cette année\"\n      },\n      \"top_domains\": {\n        \"title\": \"Principaux domaines\",\n        \"no_domains_found\": \"Aucun domaine trouvé\"\n      },\n      \"most_used_tags\": {\n        \"title\": \"Tags les plus utilisées\",\n        \"no_tags_found\": \"Aucun tags trouvés\"\n      },\n      \"activity_patterns\": {\n        \"activity_by_hour\": \"Activité par heure\",\n        \"activity_by_day\": \"Activité par jour\"\n      },\n      \"storage_breakdown\": {\n        \"title\": \"Répartition du stockage\"\n      },\n      \"bookmark_sources\": {\n        \"title\": \"Sources des signets\",\n        \"empty\": \"Aucune donnée source disponible\"\n      }\n    },\n    \"subscription\": {\n      \"subscription\": \"Abonnement\",\n      \"manage_subscription\": \"Gérez vos informations d’abonnement et de facturation\",\n      \"current_plan\": \"Forfait actuel\",\n      \"billing_period\": \"Période de facturation\",\n      \"paid_plan\": \"Forfait payant\",\n      \"unlock_bigger_quota\": \"Débloquez un quota plus important et soutenez le projet\",\n      \"subscribe_now\": \"S’abonner maintenant\",\n      \"manage_billing\": \"Gérer la facturation\",\n      \"subscription_canceled\": \"Votre abonnement a été annulé et se terminera le {{date}}. Vous pouvez vous réabonner à tout moment.\",\n      \"usage_quotas\": \"Utilisation et quotas\",\n      \"track_usage\": \"Suivez votre utilisation actuelle par rapport aux limites de votre forfait\",\n      \"total_bookmarks_saved\": \"Nombre total de signets enregistrés\",\n      \"assets_file_storage\": \"Ressources et stockage de fichiers\",\n      \"unlimited_usage\": \"Utilisation illimitée\",\n      \"quota_limit_reached\": \"Limite de quota atteinte\",\n      \"approaching_quota_limit\": \"Limite de quota atteinte\",\n      \"loading_usage\": \"Chargement des informations d’utilisation…\",\n      \"free\": \"Gratuit\",\n      \"paid\": \"Payant\"\n    },\n    \"import_sessions\": {\n      \"title\": \"Importer des sessions\",\n      \"description\": \"Consultez et gérez vos sessions d’importation en bloc. Les sessions sont créées automatiquement lorsque vous importez des signets.\",\n      \"load_error\": \"Échec du chargement des sessions d’importation\",\n      \"no_sessions\": \"Aucune session d’importation pour le moment\",\n      \"no_sessions_detail\": \"Les sessions d’importation apparaîtront ici automatiquement lorsque vous importerez des signets\",\n      \"created_at\": \"Créé {{time}}\",\n      \"progress\": \"Progression\",\n      \"status\": {\n        \"pending\": \"En attente\",\n        \"in_progress\": \"En cours\",\n        \"completed\": \"Terminé\",\n        \"failed\": \"Échoué\",\n        \"processing\": \"Traitement\",\n        \"staging\": \"Préparation\",\n        \"running\": \"En cours d’exécution\",\n        \"paused\": \"En pause\"\n      },\n      \"badges\": {\n        \"pending\": \"{{count}} en attente\",\n        \"processing\": \"{{count}} en cours de traitement\",\n        \"completed\": \"{{count}} terminé\",\n        \"failed\": \"{{count}} échec\"\n      },\n      \"imported_to\": \"Importé vers :\",\n      \"view_list\": \"Voir la liste\",\n      \"delete_dialog_title\": \"Supprimer la session d’importation\",\n      \"delete_dialog_description\": \"Êtes-vous sûr de vouloir supprimer « {{name}} » ? Cette action est irréversible. Les signets eux-mêmes ne seront pas supprimés.\",\n      \"delete_session\": \"Supprimer la session\",\n      \"pause_session\": \"Pause\",\n      \"resume_session\": \"Reprendre\",\n      \"view_details\": \"Voir les détails\",\n      \"detail\": {\n        \"page_title\": \"Détails de la session d'importation\",\n        \"back_to_import\": \"Retour à l'importation\",\n        \"filter_all\": \"Tous\",\n        \"filter_accepted\": \"Accepté\",\n        \"filter_rejected\": \"Rejeté\",\n        \"filter_duplicates\": \"Doublons\",\n        \"filter_pending\": \"En attente\",\n        \"table_title\": \"Titre/URL\",\n        \"table_type\": \"Type\",\n        \"table_result\": \"Résultat\",\n        \"table_reason\": \"Raison\",\n        \"table_bookmark\": \"Marque-page\",\n        \"result_accepted\": \"Accepté\",\n        \"result_rejected\": \"Rejeté\",\n        \"result_skipped_duplicate\": \"Dupliquer\",\n        \"result_pending\": \"En attente\",\n        \"result_processing\": \"En traitement\",\n        \"no_results\": \"Aucun résultat trouvé pour ce filtre.\",\n        \"view_bookmark\": \"Voir le marque-page\",\n        \"load_more\": \"Charger plus\",\n        \"no_title\": \"Pas de titre\"\n      }\n    },\n    \"backups\": {\n      \"backups\": \"Sauvegardes\",\n      \"page_title\": \"Sauvegardes\",\n      \"page_description\": \"Crée et gère automatiquement les sauvegardes de tes marque-pages. Les sauvegardes sont compressées et stockées de manière sécurisée.\",\n      \"configuration\": {\n        \"title\": \"Configuration des sauvegardes\",\n        \"enable_automatic_backups\": \"Activer les sauvegardes automatiques\",\n        \"enable_automatic_backups_description\": \"Crée automatiquement des sauvegardes de tes marque-pages\",\n        \"backup_frequency\": \"Fréquence des sauvegardes\",\n        \"backup_frequency_description\": \"À quelle fréquence les sauvegardes doivent être créées\",\n        \"retention_period\": \"Période de rétention (jours)\",\n        \"retention_period_description\": \"Combien de jours conserver les sauvegardes avant de les supprimer\",\n        \"frequency\": {\n          \"daily\": \"Quotidien\",\n          \"weekly\": \"Hebdomadaire\"\n        },\n        \"select_frequency\": \"Sélectionner la fréquence\",\n        \"save_settings\": \"Enregistrer les paramètres\"\n      },\n      \"list\": {\n        \"title\": \"Tes sauvegardes\",\n        \"create_backup_now\": \"Créer une sauvegarde maintenant\",\n        \"no_backups\": \"Tu n'as pas encore de sauvegardes. Active les sauvegardes automatiques ou crées-en une manuellement.\",\n        \"table\": {\n          \"created_at\": \"Créé à\",\n          \"bookmarks\": \"Marque-pages\",\n          \"size\": \"Taille\",\n          \"status\": \"Statut\",\n          \"actions\": \"Actions\"\n        },\n        \"status\": {\n          \"success\": \"Succès\",\n          \"failed\": \"Échoué\",\n          \"pending\": \"En attente\"\n        },\n        \"actions\": {\n          \"download_backup\": \"Télécharger une sauvegarde\",\n          \"delete_backup\": \"Supprimer une sauvegarde\"\n        }\n      },\n      \"dialogs\": {\n        \"delete_backup_title\": \"Supprimer la sauvegarde ?\",\n        \"delete_backup_description\": \"Êtes-vous sûr de vouloir supprimer cette sauvegarde ? Cette action est irréversible.\"\n      },\n      \"toasts\": {\n        \"backup_queued\": \"La tâche de sauvegarde a été mise en file d’attente ! Elle sera traitée sous peu.\",\n        \"backup_deleted\": \"La sauvegarde a été supprimée !\"\n      }\n    }\n  },\n  \"admin\": {\n    \"admin_settings\": \"Paramètres de l'administrateur\",\n    \"server_stats\": {\n      \"server_stats\": \"Statistiques du serveur\",\n      \"total_users\": \"Nombre total d'utilisateurs\",\n      \"total_bookmarks\": \"Nombre total de favoris\",\n      \"server_version\": \"Version du serveur\"\n    },\n    \"background_jobs\": {\n      \"background_jobs\": \"Tâches en arrière-plan\",\n      \"crawler_jobs\": \"Tâches de crawl\",\n      \"indexing_jobs\": \"Tâches d'indexation\",\n      \"inference_jobs\": \"Tâches d'inférence\",\n      \"tidy_assets_jobs\": \"Tâches de rangement des assets\",\n      \"job\": \"Tâche\",\n      \"queued\": \"En file d'attente\",\n      \"pending\": \"En attente\",\n      \"failed\": \"Échoué\",\n      \"video_jobs\": \"Tâches de téléchargement de vidéos\",\n      \"webhook_jobs\": \"Tâches Webhook\",\n      \"asset_preprocessing_jobs\": \"Tâches de prétraitement des ressources\",\n      \"feed_jobs\": \"Tâches de flux RSS\",\n      \"jobs\": {\n        \"crawler\": {\n          \"title\": \"Tâches d'exploration\",\n          \"description\": \"Exploration du Web et extraction de contenu à partir d'URL\"\n        },\n        \"inference\": {\n          \"title\": \"Tâches d'inférence\",\n          \"description\": \"Attribution de balises et résumé de contenu assistés par l'IA\"\n        },\n        \"indexing\": {\n          \"title\": \"Tâches d'indexation\",\n          \"description\": \"Mises à jour de l'index de recherche\"\n        },\n        \"asset_preprocessing\": {\n          \"title\": \"Tâches de prétraitement des ressources\",\n          \"description\": \"Prétraitement des images et des documents (captures d'écran, extraction de texte, etc.)\"\n        },\n        \"tidy_assets\": {\n          \"title\": \"Tâches concernant le nettoyage des ressources\",\n          \"description\": \"Nettoyage des ressources et optimisation du stockage\"\n        },\n        \"video\": {\n          \"title\": \"Tâches de téléchargement de vidéos\",\n          \"description\": \"Extraction et téléchargement de vidéos\"\n        },\n        \"webhook\": {\n          \"title\": \"Tâches de Webhook\",\n          \"description\": \"Notifications Webhook externes\"\n        },\n        \"feed\": {\n          \"title\": \"Tâches de flux RSS\",\n          \"description\": \"Traitement des flux RSS et mises à jour du contenu\"\n        },\n        \"admin_maintenance\": {\n          \"title\": \"Tâches de maintenance de l’administrateur\",\n          \"description\": \"Nettoyage administratif et maintenance des actifs\"\n        }\n      },\n      \"monitor_and_manage\": \"Surveiller et gérer les files d'attente des tâches en arrière-plan et les tâches de traitement du système\",\n      \"active\": \"Active\",\n      \"available_actions\": \"Actions disponibles\",\n      \"status\": {\n        \"title\": \"Comprendre les états des tâches\",\n        \"queued\": {\n          \"title\": \"En file d'attente\",\n          \"description\": \"Tâches en attente de traitement. Elles démarreront automatiquement lorsque des ressources seront disponibles.\"\n        },\n        \"unprocessed\": {\n          \"title\": \"Non traité\",\n          \"description\": \"Marque-pages qui n'ont pas encore été traités. Ils sont très probablement déjà en file d'attente pour être traités, sinon, vous devrez peut-être les remettre manuellement en file d'attente.\"\n        },\n        \"failed\": {\n          \"title\": \"Échec\",\n          \"description\": \"Marque-pages qui ont rencontré des erreurs lors du traitement. Ils peuvent nécessiter une attention manuelle.\"\n        }\n      },\n      \"actions\": {\n        \"recrawl_failed_links_only\": \"Réexplorer uniquement les liens ayant échoué\",\n        \"recrawl_all_links\": \"Réexplorer tous les liens\",\n        \"without_inference\": \"Sans inférence\",\n        \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Régénérer les balises IA pour les marque-pages ayant échoué uniquement\",\n        \"regenerate_ai_tags_for_all_bookmarks\": \"Régénérer les balises IA pour tous les marque-pages\",\n        \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Régénérer les résumés d'IA pour les marque-pages ayant échoué uniquement\",\n        \"regenerate_ai_summaries_for_all_bookmarks\": \"Régénérer les résumés d'IA pour tous les marque-pages\",\n        \"reindex_all_bookmarks\": \"Réindexer tous les marque-pages\",\n        \"clean_assets\": \"Nettoyer les assets orphelins et resynchroniser les métadonnées\",\n        \"reprocess_assets_fix_mode\": \"Retraiter les assets non traités\",\n        \"migrate_large_link_html_content\": \"Bouger du Grand Contenu HTML En Ligne vers les Ressources\",\n        \"recrawl_pending_links_only\": \"Recréer l'index des liens en attente uniquement\",\n        \"regenerate_ai_tags_for_pending_bookmarks_only\": \"Régénérer les étiquettes d'IA pour les favoris en attente uniquement\",\n        \"regenerate_ai_summaries_for_pending_bookmarks_only\": \"Régénérer les résumés d'IA pour les favoris en attente uniquement\"\n      }\n    },\n    \"actions\": {\n      \"recrawl_failed_links_only\": \"Recrawler uniquement les liens échoués\",\n      \"recrawl_all_links\": \"Recrawler tous les liens\",\n      \"without_inference\": \"Sans inférence\",\n      \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Régénérer les tags AI uniquement pour les favoris échoués\",\n      \"regenerate_ai_tags_for_all_bookmarks\": \"Régénérer les tags AI pour tous les favoris\",\n      \"reindex_all_bookmarks\": \"Réindexer tous les favoris\",\n      \"compact_assets\": \"Compacter les assets\",\n      \"reprocess_assets_fix_mode\": \"Reprocesser les assets (mode fix)\",\n      \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Régénérer les résumés IA uniquement pour les marque-pages ayant échoué\",\n      \"regenerate_ai_summaries_for_all_bookmarks\": \"Régénérer les résumés IA pour tous les marque-pages\"\n    },\n    \"users_list\": {\n      \"users_list\": \"Liste des utilisateurs\",\n      \"create_user\": \"Créer un utilisateur\",\n      \"change_role\": \"Changer le rôle\",\n      \"reset_password\": \"Réinitialiser le mot de passe\",\n      \"delete_user\": \"Supprimer l'utilisateur\",\n      \"num_bookmarks\": \"Nombre de favoris\",\n      \"asset_sizes\": \"Tailles des assets\",\n      \"local_user\": \"Utilisateur local\",\n      \"confirm_password\": \"Confirmer le mot de passe\",\n      \"delete_user_confirm_description\": \"Êtes-vous sûr de vouloir supprimer l'utilisateur \\\"{{name}}\\\" ?\",\n      \"unlimited\": \"Illimité\"\n    },\n    \"service_connections\": {\n      \"title\": \"Connexions de service\",\n      \"description\": \"Surveiller l’état et la connectivité des dépendances du système externe\",\n      \"search_engine\": \"Moteur de recherche\",\n      \"browser\": \"Navigateur\",\n      \"queue_system\": \"Système de file d’attente\",\n      \"status\": {\n        \"not_configured\": \"Non configuré\",\n        \"connected\": \"Connecté\",\n        \"disconnected\": \"Déconnecté\"\n      }\n    },\n    \"admin_tools\": {\n      \"admin_tools\": \"Outils d’administration\",\n      \"bookmark_debugger\": \"Débogueur de signets\",\n      \"bookmark_id\": \"ID de signet\",\n      \"bookmark_id_placeholder\": \"Entrez l’ID de signet\",\n      \"lookup\": \"Rechercher\",\n      \"debug_info\": \"Informations de débogage\",\n      \"basic_info\": \"Informations de base\",\n      \"status\": \"État\",\n      \"content\": \"Contenu\",\n      \"html_preview\": \"Aperçu HTML (1000 premiers caractères)\",\n      \"summary\": \"Résumé\",\n      \"url\": \"URL\",\n      \"source_url\": \"URL source\",\n      \"asset_type\": \"Type d’actif\",\n      \"file_name\": \"Nom de fichier\",\n      \"owner_user_id\": \"ID d’utilisateur du propriétaire\",\n      \"tagging_status\": \"État du balisage\",\n      \"summarization_status\": \"État de la création de résumés\",\n      \"crawl_status\": \"État de l’exploration du Web\",\n      \"crawl_status_code\": \"Code d'état HTTP\",\n      \"crawled_at\": \"Exploré à\",\n      \"recrawl\": \"Nouvel explorage\",\n      \"reindex\": \"Réindexation\",\n      \"retag\": \"Ré-étiquetage\",\n      \"resummarize\": \"Nouveau résumé\",\n      \"bookmark_not_found\": \"Marque-page introuvable\",\n      \"action_success\": \"Action réussie\",\n      \"action_failed\": \"Échec de l'action\",\n      \"recrawl_queued\": \"La tâche de réexploration a été mise en file d'attente\",\n      \"reindex_queued\": \"La tâche de réindexation a été mise en file d'attente\",\n      \"retag_queued\": \"La tâche de ré-étiquetage a été mise en file d'attente\",\n      \"resummarize_queued\": \"La tâche de nouveau résumé a été mise en file d'attente\",\n      \"view\": \"Afficher\",\n      \"fetch_error\": \"Erreur lors de la récupération du favori\"\n    }\n  },\n  \"options\": {\n    \"dark_mode\": \"Mode sombre\",\n    \"light_mode\": \"Mode clair\",\n    \"apps_extensions\": \"Applications et extensions\",\n    \"documentation\": \"Documentation\",\n    \"follow_us_on_x\": \"Suivez-nous sur X\"\n  },\n  \"lists\": {\n    \"all_lists\": \"Toutes les listes\",\n    \"favourites\": \"Favoris\",\n    \"new_list\": \"Nouvelle liste\",\n    \"new_nested_list\": \"Nouvelle liste imbriquée\",\n    \"search_query_help\": \"En savoir plus sur le langage de requête de recherche.\",\n    \"edit_list\": \"Modifier la liste\",\n    \"manual_list\": \"Liste manuelle\",\n    \"smart_list\": \"Liste intelligente\",\n    \"search_query\": \"Requête de recherche\",\n    \"parent_list\": \"Liste parente\",\n    \"no_parent\": \"Pas de parent\",\n    \"list_type\": \"Type de liste\",\n    \"merge_list\": \"Fusionner la liste\",\n    \"destination_list\": \"Liste de destination\",\n    \"delete_after_merge\": \"Supprimer la liste originale après la fusion\",\n    \"no_destination\": \"Pas de destination\",\n    \"description\": \"Description (Optional)\",\n    \"rss\": {\n      \"title\": \"Flux RSS\",\n      \"description\": \"Activer un flux RSS pour cette liste\",\n      \"feed_url\": \"URL du flux RSS\"\n    },\n    \"share_list\": \"Partager la liste\",\n    \"public_list\": {\n      \"title\": \"Liste publique\",\n      \"description\": \"Autoriser les autres à consulter cette liste\",\n      \"share_link\": \"Partager le lien\"\n    },\n    \"delete_list\": {\n      \"title\": \"Supprimer la liste\",\n      \"description\": \"Supprimer une liste ne supprime aucun marque-page dans cette liste.\",\n      \"delete_children\": \"Supprimer les listes enfants (récursivement)\",\n      \"delete_children_description\": \"Si cette case n'est pas cochée, toutes les listes enfants directes deviendront des listes racines\"\n    },\n    \"shared\": \"Partagé\",\n    \"collaborators\": {\n      \"manage\": \"Gérer les collaborateurs\",\n      \"view\": \"Afficher les collaborateurs\",\n      \"collaborators\": \"Collaborateurs\",\n      \"add\": \"Ajouter un collaborateur\",\n      \"current\": \"Collaborateurs actuels\",\n      \"enter_email\": \"Entrer l'adresse e-mail\",\n      \"please_enter_email\": \"Veuillez entrer une adresse e-mail\",\n      \"added_successfully\": \"Collaborateur ajouté avec succès\",\n      \"failed_to_add\": \"Échec de l'ajout du collaborateur\",\n      \"removed\": \"Collaborateur supprimé\",\n      \"failed_to_remove\": \"Échec de la suppression du collaborateur\",\n      \"role_updated\": \"Rôle mis à jour\",\n      \"failed_to_update_role\": \"Échec de la mise à jour du rôle\",\n      \"viewer\": \"Lecteur\",\n      \"editor\": \"Éditeur\",\n      \"owner\": \"Propriétaire\",\n      \"viewer_description\": \"Peut afficher les favoris dans la liste\",\n      \"editor_description\": \"Peut ajouter et supprimer des favoris\",\n      \"no_collaborators\": \"Pas encore de collaborateurs. Ajoutez quelqu'un pour commencer à collaborer !\",\n      \"no_collaborators_readonly\": \"Pas de collaborateurs pour cette liste.\",\n      \"people_with_access\": \"Les personnes qui ont accès à cette liste\",\n      \"add_or_remove\": \"Ajouter ou supprimer des personnes qui peuvent accéder à cette liste\",\n      \"invitation_sent\": \"Invitation envoyée\",\n      \"invitation_revoked\": \"Invitation révoquée\",\n      \"failed_to_revoke\": \"Échec de la révocation de l’invitation\",\n      \"pending\": \"En attente\",\n      \"revoke\": \"Révoquer\",\n      \"declined\": \"Refusée\"\n    },\n    \"leave_list\": {\n      \"title\": \"Quitter la liste\",\n      \"confirm_message\": \"Êtes-vous sûr de vouloir quitter {{icon}} {{name}} ?\",\n      \"warning\": \"Vous ne pourrez plus visualiser ni accéder aux signets dans cette liste. Le propriétaire de la liste peut vous rajouter si nécessaire.\",\n      \"action\": \"Quitter la liste\",\n      \"success\": \"Vous avez quitté « {{icon}} {{name}} »\"\n    },\n    \"invitations\": {\n      \"pending\": \"Invitations en attente\",\n      \"description\": \"Vérifier et répondre aux invitations de collaboration de listes\",\n      \"invited_by\": \"Invité par\",\n      \"accept\": \"Accepter\",\n      \"decline\": \"Refuser\",\n      \"accepted\": \"Invitation acceptée\",\n      \"declined\": \"Invitation refusée\",\n      \"failed_to_accept\": \"Impossible d’accepter l’invitation\",\n      \"failed_to_decline\": \"Impossible de refuser l’invitation\"\n    },\n    \"shared_lists\": \"Listes partagées\"\n  },\n  \"tags\": {\n    \"all_tags\": \"Tous les tags\",\n    \"your_tags\": \"Vos tags\",\n    \"your_tags_info\": \"Tags attachés au moins une fois par vous\",\n    \"ai_tags\": \"Tags AI\",\n    \"ai_tags_info\": \"Tags attachés automatiquement (par AI)\",\n    \"unused_tags\": \"Tags inutilisés\",\n    \"unused_tags_info\": \"Tags non attachés à des favoris\",\n    \"delete_all_unused_tags\": \"Supprimer tous les tags inutilisés\",\n    \"drag_and_drop_merging\": \"Fusion par glisser-déposer\",\n    \"drag_and_drop_merging_info\": \"Glisser-déposer les tags les uns sur les autres pour les fusionner\",\n    \"sort_by_name\": \"Trier par nom\",\n    \"create_tag\": \"Créer un tag\",\n    \"create_tag_description\": \"Créer un nouveau tag sans l’attacher à un marque-page\",\n    \"tag_name\": \"Nom du tag\",\n    \"enter_tag_name\": \"Entrer le nom du tag\",\n    \"sort_by_usage\": \"Trier par utilisation\",\n    \"sort_by_relevance\": \"Trier par pertinence\",\n    \"no_custom_tags\": \"Pas encore de balises personnalisées\",\n    \"no_ai_tags\": \"Pas encore de balises d'IA\",\n    \"no_unused_tags\": \"Vous n'avez pas de balises inutilisées\",\n    \"no_unused_tags_match_your_search\": \"Aucune balise inutilisée ne correspond à votre recherche\",\n    \"no_tags_match_your_search\": \"Aucune balise ne correspond à votre recherche\",\n    \"search_placeholder\": \"Rechercher des tags…\",\n    \"search_or_create_placeholder\": \"Rechercher ou créer des tags…\"\n  },\n  \"preview\": {\n    \"view_original\": \"Voir l'original\",\n    \"cached_content\": \"Contenu en cache\",\n    \"reader_view\": \"Vue Lecteur\",\n    \"tabs\": {\n      \"details\": \"Détails\",\n      \"content\": \"Contenu\"\n    },\n    \"archive_info\": \"Les archives peuvent ne pas s'afficher correctement en ligne si elles nécessitent Javascript. Pour de meilleurs résultats, <1>téléchargez-les et ouvrez-les dans votre navigateur</1>.\",\n    \"fetch_error_title\": \"Contenu non disponible\",\n    \"fetch_error_description\": \"Impossible de récupérer le contenu de ce lien. La page est peut-être protégée, nécessite une authentification ou est temporairement indisponible.\",\n    \"crawling_in_progress\": \"Récupération du contenu de la page…\",\n    \"continue_reading\": \"Continue là où vous vous êtes arrêté\",\n    \"continue_reading_percent\": \"Continue là où vous vous êtes arrêté ({{percent}} %)\",\n    \"continue_button\": \"Continuer\"\n  },\n  \"editor\": {\n    \"quickly_focus\": \"Vous pouvez rapidement vous concentrer sur ce champ en appuyant sur ⌘ + E\",\n    \"multiple_urls_dialog_title\": \"Importer les URLs comme favoris séparés ?\",\n    \"multiple_urls_dialog_desc\": \"L'entrée contient plusieurs URLs sur des lignes séparées. Voulez-vous les importer comme favoris séparés ?\",\n    \"import_as_text\": \"Importer comme favori de texte\",\n    \"import_as_separate_bookmarks\": \"Importer comme favoris séparés\",\n    \"placeholder\": \"Collez un lien ou une image, écrivez une note ou glissez-déposez une image ici ...\",\n    \"new_item\": \"NOUVEL ÉLÉMENT\",\n    \"disabled_submissions\": \"Les soumissions sont désactivées\",\n    \"text_toolbar\": {\n      \"undo\": \"Annuler\",\n      \"redo\": \"Rétablir\",\n      \"bold\": \"Gras\",\n      \"italic\": \"Italique\",\n      \"underline\": \"Souligné\",\n      \"strikethrough\": \"Barré\",\n      \"code\": \"Code\",\n      \"highlight\": \"Surligner\",\n      \"align_left\": \"Aligner à gauche\",\n      \"align_center\": \"Aligner au centre\",\n      \"align_right\": \"Aligner à droite\",\n      \"markdown_shortcuts\": {\n        \"inline_code\": {\n          \"label\": \"Code en ligne\",\n          \"example\": \"`Code`\"\n        },\n        \"unordered_list\": {\n          \"example\": \"- Élément de liste\",\n          \"label\": \"Liste non ordonnée\"\n        },\n        \"block_code\": {\n          \"label\": \"Bloc de code\",\n          \"example\": \"``` + espace\"\n        },\n        \"blockquote\": {\n          \"example\": \"> Citation\",\n          \"label\": \"Citation\"\n        },\n        \"ordered_list\": {\n          \"label\": \"Liste ordonnée\",\n          \"example\": \"1. Élément de la liste\"\n        },\n        \"label\": \"Raccourcis Markdown\",\n        \"heading\": {\n          \"label\": \"En-tête\",\n          \"example\": \"# H1, ## H2, ### H3\"\n        },\n        \"bold\": {\n          \"label\": \"Gras\",\n          \"example\": \"**texte** ou CTRL+b\"\n        },\n        \"italic\": {\n          \"label\": \"Italique\",\n          \"example\": \"*Italique* ou _Italique_ ou CTRL+i\"\n        }\n      }\n    },\n    \"placeholder_v2\": \"Collez un lien, écrivez une note ou déposez une image…\"\n  },\n  \"toasts\": {\n    \"bookmarks\": {\n      \"updated\": \"Le favori a été mis à jour !\",\n      \"deleted\": \"Le favori a été supprimé !\",\n      \"refetch\": \"Re-fetch a été mis en file d'attente !\",\n      \"full_page_archive\": \"La création de l'archive de la page complète a été déclenchée\",\n      \"delete_from_list\": \"Le favori a été supprimé de la liste\",\n      \"clipboard_copied\": \"Le lien a été ajouté à votre presse-papiers !\",\n      \"preserve_pdf\": \"La conservation en PDF a été déclenchée\",\n      \"update_banner\": \"La bannière a été mise à jour!\",\n      \"uploading_banner\": \"Téléversement de la bannière…\"\n    },\n    \"lists\": {\n      \"created\": \"La liste a été créée !\",\n      \"updated\": \"La liste a été mise à jour !\",\n      \"merged\": \"La liste a été fusionnée !\",\n      \"deleted\": \"La liste a été supprimée !\"\n    },\n    \"tags\": {\n      \"created\": \"Le tag a été créé !\",\n      \"failed_to_create\": \"Impossible de créer le tag\"\n    }\n  },\n  \"cleanups\": {\n    \"cleanups\": \"Nettoyages\",\n    \"duplicate_tags\": {\n      \"title\": \"Tags en double\",\n      \"merge_all_suggestions\": \"Fusionner toutes les suggestions ?\"\n    }\n  },\n  \"search\": {\n    \"has_tag\": \"A une balise\",\n    \"is_in_any_list\": \"Est dans n'importe quelle liste\",\n    \"is_not_in_any_list\": \"N'est dans aucune liste\",\n    \"created_on_or_before\": \"Créé le ou avant le\",\n    \"url_does_not_contain\": \"L’URL ne contient pas\",\n    \"does_not_have_tag\": \"N'a pas de tag\",\n    \"type_is_not\": \"Le type n'est pas\",\n    \"or\": \"Ou\",\n    \"is_favorited\": \"Est mis en favoris\",\n    \"is_not_favorited\": \"N'est pas mis en favori\",\n    \"is_not_archived\": \"N'est pas archivé\",\n    \"created_on_or_after\": \"Créé le ou après le\",\n    \"not_created_on_or_after\": \"Pas créé le ou après le\",\n    \"is_in_list\": \"Est dans la liste\",\n    \"is_not_in_list\": \"N'est pas dans la liste\",\n    \"has_any_tag\": \"A n'importe quelle étiquette\",\n    \"has_no_tags\": \"N'a pas de tag\",\n    \"not_created_on_or_before\": \"Pas créé avant ou le\",\n    \"url_contains\": \"L'URL contient\",\n    \"full_text_search\": \"Recherche en texte intégral\",\n    \"type_is\": \"Le type est\",\n    \"is_archived\": \"Est archivé\",\n    \"and\": \"Et\",\n    \"is_not_from_feed\": \"Ne provient pas d'un flux RSS\",\n    \"is_from_feed\": \"Provient d'un flux RSS\",\n    \"created_within\": \"Créé dans\",\n    \"created_earlier_than\": \"Créé avant\",\n    \"day_s\": \" {days} jour(s)\",\n    \"week_s\": \" {weeks} semaine(s)\",\n    \"month_s\": \" {months} mois\",\n    \"year_s\": \" {years} an(s)\",\n    \"day_s_ago\": \" Il y a {days} jour(s)\",\n    \"week_s_ago\": \" Il y a {weeks} semaine(s)\",\n    \"month_s_ago\": \" Il y a {months} mois\",\n    \"year_s_ago\": \" Il y a {years} an(s)\",\n    \"history\": \"Recherches récentes\",\n    \"title_contains\": \"Le titre contient\",\n    \"title_does_not_contain\": \"Le titre ne contient pas\",\n    \"is_broken_link\": \"A un lien brisé\",\n    \"tags\": \"Balises\",\n    \"no_suggestions\": \"Pas de suggestions\",\n    \"filters\": \"Filtres\",\n    \"is_not_broken_link\": \"A un lien fonctionnel\",\n    \"lists\": \"Listes\",\n    \"feeds\": \"Flux\",\n    \"is_from_source\": \"La source est\",\n    \"is_not_from_source\": \"La source n'est pas\"\n  },\n  \"dialogs\": {\n    \"bookmarks\": {\n      \"delete_confirmation_title\": \"Supprimer le signet ?\",\n      \"delete_confirmation_description\": \"T'es sûr de vouloir supprimer ce marque-page ?\"\n    }\n  },\n  \"highlights\": {\n    \"no_highlights\": \"Vous n'avez pas encore de surlignages.\"\n  },\n  \"banners\": {\n    \"no_bookmarks\": {\n      \"title\": \"Pas encore de marque-pages\",\n      \"description\": \"Enregistrez des articles, des liens et des pages intéressants pour y accéder rapidement plus tard.\"\n    }\n  },\n  \"bookmark_editor\": {\n    \"title\": \"Modifier le marque-page\",\n    \"subtitle\": \"Apportez des modifications aux détails du marque-page. Cliquez sur Enregistrer lorsque vous avez terminé.\",\n    \"author\": \"Auteur\",\n    \"publisher\": \"Éditeur\",\n    \"date_published\": \"Date de publication\",\n    \"pick_a_date\": \"Choisir une date\",\n    \"save_changes\": \"Enregistrer les modifications\",\n    \"extracted_content\": \"Contenu extrait\"\n  },\n  \"view_options\": {\n    \"title\": \"Options d'affichage\",\n    \"layout\": \"Disposition\",\n    \"columns\": \"Colonnes\",\n    \"display_options\": \"Options d'affichage\",\n    \"show_note_previews\": \"Voir les notes\",\n    \"show_tags\": \"Afficher les étiquettes\",\n    \"show_title\": \"Afficher le titre\",\n    \"image_options\": \"Options d’image\",\n    \"image_fit_cover\": \"Couverture (remplir)\",\n    \"image_fit_contain\": \"Contenir (adapter)\"\n  },\n  \"version\": {\n    \"new_release_available\": \"Nouvelles notes de version disponibles\",\n    \"whats_new_title\": \"Quoi de neuf dans la v{{version}}\",\n    \"release_notes_description\": \"Voici les dernières mises à jour récupérées des notes de version GitHub.\",\n    \"loading_release_notes\": \"Chargement des notes de version…\",\n    \"unable_to_load_release_notes\": \"Impossible de charger les notes de version pour le moment. Veuillez réessayer plus tard.\",\n    \"no_release_notes\": \"Aucune note de version n’a été publiée pour cette version.\",\n    \"release_notes_synced\": \"Les notes de version sont synchronisées depuis GitHub.\",\n    \"view_on_github\": \"Voir sur GitHub\"\n  },\n  \"wrapped\": {\n    \"title\": \"Ton récap de {{year}}\",\n    \"subtitle\": \"Une année dans Karakeep\",\n    \"banner\": {\n      \"title\": \"Ton récap 2025 est prêt!\",\n      \"description\": \"Consulte ton année en signets\",\n      \"view_now\": \"Afficher maintenant\"\n    },\n    \"button\": \"Récap 2025\",\n    \"loading\": \"Chargement de ton récapitulatif…\",\n    \"failed_to_load\": \"Échec du chargement de tes infos récapitulatives\",\n    \"sections\": {\n      \"total_saves\": {\n        \"prefix\": \"Tu as enregistré\",\n        \"suffix\": \"éléments cette année\",\n        \"suffix_singular\": \"élément cette année\"\n      },\n      \"first_bookmark\": {\n        \"title\": \"Votre aventure commence\",\n        \"description\": \"Premier enregistrement de {{year}}:\"\n      },\n      \"top_domains\": \"Vos meilleurs sites\",\n      \"top_tags\": \"Vos meilleurs tags\",\n      \"monthly_activity\": \"Votre année d'enregistrements\",\n      \"most_active_day\": \"Votre jour le plus actif\",\n      \"peak_times\": {\n        \"title\": \"Quand vous enregistrez\",\n        \"peak_hour\": \"Heure de pointe\",\n        \"peak_day\": \"Jour de pointe\"\n      },\n      \"how_you_save\": \"Comment vous enregistrez\",\n      \"what_you_saved\": \"Ce que vous avez enregistré\",\n      \"summary\": {\n        \"favorites\": \"Favoris\",\n        \"tags_created\": \"Tags créés\",\n        \"highlights\": \"Points forts\"\n      },\n      \"types\": {\n        \"links\": \"Liens\",\n        \"notes\": \"Remarques\",\n        \"assets\": \"Ressources\"\n      }\n    },\n    \"footer\": \"Fait avec Karakeep\",\n    \"share\": \"Partager\",\n    \"download\": \"Télécharger\",\n    \"close\": \"Fermer\",\n    \"generating\": \"Génération en cours...\"\n  }\n}\n"
  },
  {
    "path": "apps/web/lib/i18n/locales/ga/translation.json",
    "content": "{\n  \"common\": {\n    \"updated_at\": \"Nuashonraithe ag\",\n    \"role\": \"Ról\",\n    \"url\": \"URL\",\n    \"name\": \"Ainm\",\n    \"email\": \"Ríomhphost\",\n    \"password\": \"Pasfhocal\",\n    \"action\": \"Gníomh\",\n    \"actions\": \"Gníomhartha\",\n    \"created_at\": \"Cruthaithe Ag\",\n    \"key\": \"Eochair\",\n    \"type\": \"Cineál\",\n    \"size\": \"Méid\",\n    \"roles\": {\n      \"user\": \"Úsáideoir\",\n      \"admin\": \"Riarachán\"\n    },\n    \"something_went_wrong\": \"Tharla rud éigin mícheart\",\n    \"experimental\": \"Turgnamhach\",\n    \"search\": \"Cuardaigh\",\n    \"tags\": \"Clibeanna\",\n    \"note\": \"Nóta\",\n    \"attachments\": \"Iatán\",\n    \"highlights\": \"Buaicphointí\",\n    \"source\": \"Foinse\",\n    \"screenshot\": \"Screenshot\",\n    \"video\": \"Físeán\",\n    \"archive\": \"Cartlann\",\n    \"home\": \"Baile\",\n    \"title\": \"Teideal\",\n    \"description\": \"Cur Síos\",\n    \"summary\": \"Achoimre\",\n    \"bookmark_types\": {\n      \"title\": \"Cineál Leabharcmharc\",\n      \"link\": \"Nasc\",\n      \"text\": \"Téacs\",\n      \"media\": \"Meáin\"\n    },\n    \"quota\": \"Cuóta\",\n    \"bookmarks\": \"Leabhair mharcála\",\n    \"storage\": \"Stóráil\",\n    \"pdf\": \"PDF Cartlainne\",\n    \"default\": \"Réamhshocrú\",\n    \"id\": \"ID\",\n    \"last_used\": \"Úsáidte Deireanach\"\n  },\n  \"actions\": {\n    \"close\": \"Dún\",\n    \"ignore\": \"Déan Neamhaird\",\n    \"change_layout\": \"Athraigh Leagan Amach\",\n    \"archive\": \"Cartlann\",\n    \"unarchive\": \"Dí-chartlannaigh\",\n    \"favorite\": \"Is fearr leat\",\n    \"unfavorite\": \"Dí-roghnaigh\",\n    \"delete\": \"Scrios\",\n    \"toggle_show_archived\": \"Taispeáin Cartlannaithe\",\n    \"refresh\": \"Athnuachan\",\n    \"recrawl\": \"Athraisíoladh\",\n    \"download_full_page_archive\": \"Íoslódáil Cartlann na Leathanaigh Iomláine\",\n    \"edit_tags\": \"Cuir Clibeanna in Eagar\",\n    \"add_to_list\": \"Cuir leis an Liosta\",\n    \"select_all\": \"Roghnaigh Gach Rud\",\n    \"unselect_all\": \"Díroghnaigh Gach Rud\",\n    \"copy_link\": \"Cóipeáil Nasc\",\n    \"close_bulk_edit\": \"Dún an tEagarú Bulc\",\n    \"bulk_edit\": \"Mórchóir Cuir in Eagar\",\n    \"manage_lists\": \"Bainistigh Liostaí\",\n    \"remove_from_list\": \"Bain ón Liosta\",\n    \"save\": \"Sábháil\",\n    \"add\": \"Cuir leis\",\n    \"edit\": \"Cuir in Eagar\",\n    \"open_editor\": \"Oscail an tEagarthóir\",\n    \"create\": \"Cruthaigh\",\n    \"fetch_now\": \"Faigh Anois\",\n    \"summarize_with_ai\": \"Achoimre le hAI\",\n    \"edit_title\": \"Cuir Teideal in Eagar\",\n    \"sign_out\": \"Sínigh Amach\",\n    \"merge\": \"Cumaisc\",\n    \"cancel\": \"Cealaigh\",\n    \"apply_all\": \"Cuir Gach Rud i bhFeidhm\",\n    \"sort\": {\n      \"title\": \"Sórtáil\",\n      \"relevant_first\": \"Is ábhartha ar dtús\",\n      \"newest_first\": \"Is Nuaí ar dtús\",\n      \"oldest_first\": \"Is Sine ar dtús\"\n    },\n    \"confirm\": \"Deimhnigh\",\n    \"regenerate\": \"Athghinigh\",\n    \"load_more\": \"Luchtaigh Níos Mó\",\n    \"edit_notes\": \"Nótaí a Chur in Eagar\",\n    \"preserve_as_pdf\": \"Caomhnaigh mar PDF\",\n    \"offline_copies\": \"Cóipeanna As Líne\",\n    \"preserve_offline_archive\": \"Cartlann As Líne a Chaomhnú\",\n    \"download_full_page_archive_file\": \"Íoslódáil Comhad Cartlainne\",\n    \"download_pdf_file\": \"Íoslódáil Comhad PDF\",\n    \"remove\": \"Bain\",\n    \"more\": \"Tuilleadh\",\n    \"replace_banner\": \"Athsholáthar Meirge\",\n    \"add_banner\": \"Cuir Meirge Leis\",\n    \"download\": \"Íoslódáil\"\n  },\n  \"settings\": {\n    \"ai\": {\n      \"tagging_rule_description\": \"Cuirfear leideanna a chuireann tú leis anseo san áireamh mar rialacha leis an tsamhail le linn giniúint na gclibeanna. Is féidir leat na leideanna deiridh a fheiceáil sa chuid réamhamhairc leideanna.\",\n      \"ai_settings\": \"Socruithe AI\",\n      \"tagging_rules\": \"Rialacha Clibeála\",\n      \"prompt_preview\": \"Réamhamharc Pras\",\n      \"text_prompt\": \"Prómpáil Téacs\",\n      \"images_prompt\": \"Leid Íomhá\",\n      \"summarization_prompt\": \"Pronta Prasúmála\",\n      \"all_tagging\": \"Gach Clibeáil\",\n      \"text_tagging\": \"Clibeáil Téacs\",\n      \"image_tagging\": \"Clibeáil Íomhá\",\n      \"summarization\": \"Achoimre\",\n      \"tag_style\": \"Stíl Clibe\",\n      \"auto_summarization_description\": \"Achoimrí a ghiniúint go huathoibríoch le do leabharmharcanna ag úsáid AI.\",\n      \"auto_tagging\": \"Uathchlibeáil\",\n      \"titlecase_spaces\": \"Cás teidil le spásanna\",\n      \"lowercase_underscores\": \"Cás íseal le fostríocaí\",\n      \"inference_language\": \"Teanga Inbhainte\",\n      \"titlecase_hyphens\": \"Cás teidil le fleiscíní\",\n      \"lowercase_hyphens\": \"Cás íseal le fleiscíní\",\n      \"lowercase_spaces\": \"Cás íseal le spásanna\",\n      \"inference_language_description\": \"Roghnaigh teanga do chlibeanna agus achoimrí arna nginiúint ag AI.\",\n      \"tag_style_description\": \"Roghnaigh conas ar cheart do chlibeanna uathghinte a bheith formáidithe.\",\n      \"auto_tagging_description\": \"Gin clibeanna go huathoibríoch le do leabharmharcanna ag baint úsáide as hintleacht shaorga.\",\n      \"camelCase\": \"Cás camel\",\n      \"auto_summarization\": \"Uathachoimriú\",\n      \"no_preference\": \"Gan aon rogha\",\n      \"curated_tags\": \"Clibeanna Coimeádta\",\n      \"curated_tags_description\": \"Roghnaigh go gcuirfear srian le clibeáil AI chun clibeanna ón liosta seo a úsáid amháin. Nuair nach roghnaítear aon chlibeanna, gineann an AI clibeanna faoi shaoirse.\",\n      \"curated_tags_updated\": \"Clibeanna coimeádta nuashonraithe go rathúil!\",\n      \"curated_tags_update_failed\": \"Theip ar chlibeanna coimeádta a nuashonrú\"\n    },\n    \"webhooks\": {\n      \"webhooks\": \"Crúcaí Gréasáin\",\n      \"description\": \"Is féidir leat crúcaí gréasáin a úsáid chun gníomhartha a spreagadh nuair a chruthaítear, a athraítear nó a scríobhtar leabharmharcanna.\",\n      \"events\": {\n        \"title\": \"Imeachtaí\",\n        \"crawled\": \"Crawlta\",\n        \"created\": \"Cruthaithe\",\n        \"edited\": \"Curtha in Eagar\"\n      },\n      \"auth_token\": \"Comhartha Fíordheimhnithe\",\n      \"add_auth_token\": \"Cuir Comhartha Fíordheimhnithe leis\",\n      \"edit_auth_token\": \"Cuir Comhartha Fíordheimhnithe in Eagar\",\n      \"create_webhook\": \"Cruthaigh Crúca Gréasáin\",\n      \"delete_webhook\": \"Scrios Crúca Gréasáin\",\n      \"delete_webhook_confirmation\": \"An bhfuil tú cinnte gur mhaith leat an crúca gréasáin seo a scriosadh?\",\n      \"edit_webhook\": \"Cuir Crúca Gréasáin in Eagar\",\n      \"webhook_url\": \"URL an Chrúca Gréasáin\"\n    },\n    \"api_keys\": {\n      \"new_api_key\": \"Eochair API Nua\",\n      \"api_keys\": \"Eochracha API\",\n      \"new_api_key_desc\": \"Tabhair ainm uathúil do d'eochair API\",\n      \"key_success\": \"Cruthaíodh an eochair go rathúil\",\n      \"key_success_please_copy\": \"Cóipeáil an eochair agus stóráil in áit shábháilte é. Nuair a dhúnann tú an dialóg, ní bheidh tú in ann rochtain a fháil air arís.\",\n      \"regenerate_api_key\": \"Eochair API a Athghiniúint\",\n      \"key_regenerated\": \"Athghineadh an eochair go rathúil\",\n      \"key_regenerated_please_copy\": \"Cóipeáil an eochair nua agus stóráil in áit shábháilte é, le do thoil. Tá an tseaneochair tarraingthe siar agus ní oibreoidh sí a thuilleadh.\",\n      \"regenerate_warning\": \"An bhfuil tú cinnte gur mhaith leat an eochair API \\\"{{name}}\\\" a athghiniúint?\",\n      \"regenerate_confirmation\": \"Tarraingeoidh sé seo siar an eochair reatha agus ginfear ceann nua. Stadfaidh aon fheidhmchláir a úsáideann an eochair reatha ag obair.\"\n    },\n    \"manage_assets\": {\n      \"asset_link\": \"Nasc Sócmhainne\",\n      \"asset_type\": \"Cineál Sócmhainne\",\n      \"manage_assets\": \"Bainistigh Sócmhainní\",\n      \"no_assets\": \"Níl aon sócmhainní agat fós.\",\n      \"bookmark_link\": \"Nasc Leabharmharc\",\n      \"delete_asset\": \"Scrios Sócmhainn\",\n      \"delete_asset_confirmation\": \"An bhfuil tú cinnte gur mhaith leat an tsócmhainn seo a scriosadh?\"\n    },\n    \"rules\": {\n      \"rules\": \"Inneall Rialacha\",\n      \"edit_rule\": \"Cuir Rialachán in Eagar\",\n      \"save_rule\": \"Sábháil Riail\",\n      \"delete_rule\": \"Scrios an Riail\",\n      \"delete_rule_confirmation\": \"An bhfuil tú cinnte gur mhaith leat an riail seo a scriosadh?\",\n      \"events_types\": {\n        \"bookmark_added\": \"Cuirtear leabhar marc leis\",\n        \"tag_added\": \"Cuirtear an clib seo le leabhar marc\",\n        \"tag_removed\": \"Baintear an clib seo as leabhar marcanna\",\n        \"added_to_list\": \"Cuirtear leabharmharc leis an liosta seo\",\n        \"removed_from_list\": \"Baintear leabhar marcála ón liosta seo\",\n        \"favourited\": \"Tá leabhar marcáilte mar is ansa\",\n        \"archived\": \"Tá leabharmharc cartlannaithe\"\n      },\n      \"rule_name\": \"Ainm na Rialach\",\n      \"description\": \"Is féidir leat rialacha a úsáid chun gníomhartha a spreagadh nuair a tharlaíonn imeacht.\",\n      \"ceate_rule\": \"Cruthaigh Riail\",\n      \"whenever\": \"Aon uair a ...\",\n      \"if\": \"Má ...\",\n      \"enter_rule_name\": \"Iontráil ainm riail\",\n      \"describe_what_this_rule_does\": \"Déan cur síos ar a ndéanann an riail seo\",\n      \"rule_has_been_created\": \"Cruthaíodh riail!\",\n      \"rule_has_been_updated\": \"Nuashonraíodh an riail!\",\n      \"rule_has_been_deleted\": \"Scriosadh an riail!\",\n      \"no_rules_created_yet\": \"Níl aon rialacha cruthaithe fós\",\n      \"create_your_first_rule\": \"Cruthaigh do chéad riail chun d'obair a uathoibriú\",\n      \"conditions_types\": {\n        \"always\": \"I gcónaí\",\n        \"url_contains\": \"Tá sa URL\",\n        \"imported_from_feed\": \"Allmhairithe Ó Fhotha\",\n        \"bookmark_type_is\": \"Is é an Cineál Leabharmharc\",\n        \"has_tag\": \"Tá Clib aige\",\n        \"is_favourited\": \"An bhfuil sé i bhfabhar\",\n        \"is_archived\": \"Archived atá ann\",\n        \"and\": \"Tá gach ceann díobh seo a leanas fíor\",\n        \"or\": \"Tá aon cheann díobh seo a leanas fíor\",\n        \"url_does_not_contain\": \"Níl an URL san Áireamh\",\n        \"title_contains\": \"Tá Teideal Isteach Ann\",\n        \"title_does_not_contain\": \"Níl Sa Teideal\"\n      },\n      \"actions_types\": {\n        \"add_tag\": \"Cuir Clib leis\",\n        \"remove_tag\": \"Clib a Bhaint\",\n        \"add_to_list\": \"Cuir leis an Liosta\",\n        \"remove_from_list\": \"Bain ón Liosta\",\n        \"download_full_page_archive\": \"Íoslódáil Cartlann na Leathanaigh Iomláine\",\n        \"favourite_bookmark\": \"Leabharmharc is Fearr Leat\",\n        \"archive_bookmark\": \"Leabhar Marcála Cartlainne\"\n      }\n    },\n    \"back_to_app\": \"Ar Ais Go dtí an Aip\",\n    \"user_settings\": \"Socruithe Úsáideora\",\n    \"info\": {\n      \"user_info\": \"Eolas Úsáideora\",\n      \"basic_details\": \"Bunsonraí\",\n      \"change_password\": \"Athraigh Pasfhocal\",\n      \"current_password\": \"Pasfhocal Reatha\",\n      \"new_password\": \"Pasfhocal Nua\",\n      \"confirm_new_password\": \"Deimhnigh Pasfhocal Nua\",\n      \"options\": \"Roghanna\",\n      \"interface_lang\": \"Teanga Chomhéadain\",\n      \"user_settings\": {\n        \"user_settings_updated\": \"Tá socruithe an úsáideora nuashonraithe!\",\n        \"bookmark_click_action\": {\n          \"title\": \"Gníomh Cliceáil Leabharmharc\",\n          \"open_external_url\": \"Oscail URL Bunaidh\",\n          \"open_bookmark_details\": \"Oscail Sonraí Leabharcmharc\"\n        },\n        \"archive_display_behaviour\": {\n          \"title\": \"Leabhair Mharc Cartlainne\",\n          \"show\": \"Taispeáin leabhair mharcáilte atá cartlannaithe i gclibeanna agus i liostaí\",\n          \"hide\": \"Folaigh leabharmharcanna cartlannaithe i gclibeanna agus i liostaí\"\n        }\n      },\n      \"reader_settings\": {\n        \"local_overrides_title\": \"Socruithe gléas-sonracha gníomhach\",\n        \"using_default\": \"Ag baint úsáide as réamhshocrú an chliaint\",\n        \"clear_override_hint\": \"Glan sárú gléis chun socrú ginearálta a úsáid ({{value}})\",\n        \"font_size\": \"Méid Cló\",\n        \"font_family\": \"Cló-Aicme\",\n        \"preview_inline\": \"(réamhamharc)\",\n        \"tooltip_preview\": \"Athruithe réamhamhairc neamhshábháilte\",\n        \"save_to_all_devices\": \"Gach gléas\",\n        \"tooltip_local\": \"Tá socruithe gléis difriúil ó shocruithe ginearálta\",\n        \"reset_preview\": \"Athshocraigh réamhamharc\",\n        \"mono\": \"Monaspás\",\n        \"line_height\": \"Airde Líne\",\n        \"tooltip_default\": \"Socruithe léitheoireachta\",\n        \"title\": \"Socruithe Léitheora\",\n        \"serif\": \"Searif\",\n        \"preview\": \"Réamhamharc\",\n        \"not_set\": \"Níl sé socraithe\",\n        \"clear_local_overrides\": \"Glan socruithe gléis\",\n        \"preview_text\": \"Léimeann an sionnach rua tapa thar an madra leisciúil. Seo an chuma a bheidh ar théacs do radhairc léitheora.\",\n        \"local_overrides_cleared\": \"Tá socruithe gléas-sonracha glanta\",\n        \"local_overrides_description\": \"Tá socruithe léitheora ag an ngléas seo atá difriúil ó do réamhshocruithe domhanda:\",\n        \"clear_defaults\": \"Glan gach réamhshocrú\",\n        \"description\": \"Cumraigh socruithe téacs réamhshocraithe do radharc an léitheora. Déantar na socruithe seo a shioncronú ar fud do ghléasanna go léir.\",\n        \"defaults_cleared\": \"Tá réamhshocruithe léitheora glanta\",\n        \"save_hint\": \"Sábháil socruithe don ghléas seo amháin nó sioncronaigh ar gach gléas\",\n        \"save_as_default\": \"Sábháil mar réamhshocrú\",\n        \"save_to_device\": \"An gléas seo\",\n        \"sans\": \"Sans Searif\",\n        \"tooltip_preview_and_local\": \"Athruithe réamhamhairc neamhshábháilte; tá socruithe gléis difriúil ó shocruithe ginearálta\",\n        \"adjust_hint\": \"Coigeartaigh na socruithe thuas chun athruithe a réamhamharc\"\n      },\n      \"avatar\": {\n        \"upload\": \"Uaslódáil avatar\",\n        \"change\": \"Athraigh avatar\",\n        \"remove_confirm_title\": \"Bain avatar?\",\n        \"updated\": \"Nuashonraíodh avatar\",\n        \"removed\": \"Baineadh avatar\",\n        \"description\": \"Uaslódáil íomhá chearnach le húsáid mar avatar.\",\n        \"remove_confirm_description\": \"Glanfaidh sé seo an grianghraf próifíle atá agat faoi láthair.\",\n        \"title\": \"Grianghraf Próifíle\",\n        \"remove\": \"Bain avatar\"\n      }\n    },\n    \"feeds\": {\n      \"rss_subscriptions\": \"Síntiúis RSS\",\n      \"add_a_subscription\": \"Cuir síntiús leis\",\n      \"feed_enabled\": \"Fotha RSS cumasaithe\",\n      \"feed_disabled\": \"Fotha RSS díchumasaithe\"\n    },\n    \"import\": {\n      \"import_export\": \"Iompórtáil / Easpórtáil\",\n      \"import_export_bookmarks\": \"Iompórtáil / Easpórtáil Leabharmharcanna\",\n      \"import_bookmarks_from_html_file\": \"Iompórtáil Leabharmharcanna ó chomhad HTML\",\n      \"import_bookmarks_from_pocket_export\": \"Iompórtáil Leabharmharcanna ó onnmhairiú Pocket\",\n      \"import_bookmarks_from_matter_export\": \"Iompórtáil Leabharmharcanna ó onnmhairiú Matter\",\n      \"import_bookmarks_from_omnivore_export\": \"Iompórtáil Leabharcmharcanna ó onnmhairiú Omnivore\",\n      \"import_bookmarks_from_linkwarden_export\": \"Iompórtáil Leabharmharcanna ó onnmhairiú Linkwarden\",\n      \"import_bookmarks_from_karakeep_export\": \"Iompórtáil Leabharmharcanna ó onnmhairiú Karakeep\",\n      \"import_bookmarks_from_tab_session_manager_export\": \"Iompórtáil Leabhair Mharcála ó Bhainisteoir Seisiúin Cluaisíní\",\n      \"export_links_and_notes\": \"Easpórtáil Naisc agus Nótaí\",\n      \"imported_bookmarks\": \"Leabharmharcanna Allmhairithe\",\n      \"import_bookmarks_from_mymind_export\": \"Iompórtáil Leabharmharcanna ó onnmhairiú mymind\",\n      \"import_bookmarks_from_instapaper_export\": \"Iompórtáil Leabharmharcanna ó onnmhairiú Instapaper\"\n    },\n    \"broken_links\": {\n      \"broken_links\": \"Naisc Bhriste\",\n      \"last_crawled_at\": \"Crawláilte Deireanach Ag\",\n      \"crawling_status\": \"Stádas na hInnéacsaithe\",\n      \"crawling_failed\": \"Crawláil Theipthe\"\n    },\n    \"stats\": {\n      \"usage_statistics\": \"Staitisticí Úsáide\",\n      \"insights_description\": \"Léargais ar do nósanna agus bailiúchán leabharmharcála\",\n      \"failed_to_load\": \"Theip ar staitisticí a luchtú\",\n      \"overview\": {\n        \"total_bookmarks\": \"Iomlán na Leabhar Marcáilte\",\n        \"all_saved_items\": \"Gach mír shábháilte\",\n        \"favorites\": \"Ábhair is Ansa\",\n        \"starred_bookmarks\": \"Leabharmharcanna le réalta\",\n        \"archived\": \"Cartlannaithe\",\n        \"archived_items\": \"Míreanna cartlannaithe\",\n        \"tags\": \"Clibeanna\",\n        \"unique_tags_created\": \"Clibeanna uathúla cruthaithe\",\n        \"lists\": \"Liostaí\",\n        \"bookmark_collections\": \"Bailiúcháin leabharmharcanna\",\n        \"highlights\": \"Aibhsithe\",\n        \"text_highlights\": \"Aibhsithe téacs\",\n        \"storage_used\": \"Stóras a Úsáideadh\",\n        \"total_asset_storage\": \"Stóráil iomlán sócmhainní\",\n        \"this_month\": \"An Mhí Seo\",\n        \"bookmarks_added\": \"Leabhair mharcáilte curtha leis\"\n      },\n      \"bookmark_types\": {\n        \"title\": \"Cineálacha Leabharmharcanna\",\n        \"links\": \"Naisc\",\n        \"text_notes\": \"Nótaí Téacs\",\n        \"assets\": \"Sócmhainní\"\n      },\n      \"recent_activity\": {\n        \"title\": \"Gníomhaíocht le Déanaí\",\n        \"this_week\": \"An tSeachtain Seo\",\n        \"this_month\": \"An Mhí Seo\",\n        \"this_year\": \"An Bhliain Seo\"\n      },\n      \"top_domains\": {\n        \"title\": \"Fearainn Barr\",\n        \"no_domains_found\": \"Ní bhfuarthas aon fhearann\"\n      },\n      \"most_used_tags\": {\n        \"title\": \"Clibeanna is Mó a Úsáidtear\",\n        \"no_tags_found\": \"Ní bhfuarthas clibeanna\"\n      },\n      \"activity_patterns\": {\n        \"activity_by_hour\": \"Gníomhaíocht de réir Uaire\",\n        \"activity_by_day\": \"Gníomhaíocht de réir Lae\"\n      },\n      \"storage_breakdown\": {\n        \"title\": \"Miondealú Stórála\"\n      },\n      \"bookmark_sources\": {\n        \"title\": \"Foinseacha Leabhar Marcanna\",\n        \"empty\": \"Níl aon sonraí foinse ar fáil\"\n      }\n    },\n    \"subscription\": {\n      \"subscription\": \"Síntiús\",\n      \"manage_subscription\": \"Bainistigh d'fhaisnéis síntiúis agus billeála\",\n      \"current_plan\": \"Plean Reatha\",\n      \"billing_period\": \"Tréimhse Bhilleála\",\n      \"paid_plan\": \"Plean Íoctha\",\n      \"unlock_bigger_quota\": \"Cuóta níos mó a dhíghlasáil agus tacú leis an tionscadal\",\n      \"subscribe_now\": \"Liostáil Anois\",\n      \"manage_billing\": \"Bainistigh Billeáil\",\n      \"subscription_canceled\": \"Tá d'íocaíocht ar ceal agus tiocfaidh deireadh leis ar {{date}}. Is féidir leat ath-shuibscríobh am ar bith.\",\n      \"usage_quotas\": \"Úsáid & Cuótaí\",\n      \"track_usage\": \"Lean d'úsáid reatha i gcoinne teorainneacha do phlean a rianú\",\n      \"total_bookmarks_saved\": \"Iomlán na leabharmharcanna a sábháladh\",\n      \"assets_file_storage\": \"Sócmhainní agus stóráil comhad\",\n      \"unlimited_usage\": \"Úsáid neamhtheoranta\",\n      \"quota_limit_reached\": \"Sroicheadh teorainn an chuóta\",\n      \"approaching_quota_limit\": \"Ag druidim le teorainn an chuóta\",\n      \"loading_usage\": \"Ag lódáil faisnéise úsáide...\",\n      \"free\": \"Saor in aisce\",\n      \"paid\": \"Íoctha\"\n    },\n    \"import_sessions\": {\n      \"title\": \"Seisiúin Iompórtála\",\n      \"load_error\": \"Theip ar na seisiúin allmhairithe a lódáil\",\n      \"no_sessions\": \"Gan aon seisiúin allmhairithe fós\",\n      \"no_sessions_detail\": \"Beidh seisiúin allmhairithe le feiceáil anseo go huathoibríoch nuair a allmhairíonn tú leabharmharcanna\",\n      \"created_at\": \"Cruthaithe {{time}}\",\n      \"progress\": \"Dul Chun Cinn\",\n      \"status\": {\n        \"pending\": \"Ar feitheamh\",\n        \"in_progress\": \"Ar siúl\",\n        \"completed\": \"Críochnaithe\",\n        \"failed\": \"Theip air\",\n        \"processing\": \"Próiseáil\",\n        \"staging\": \"Stáitsiú\",\n        \"running\": \"Ag rith\",\n        \"paused\": \"Ar sos\"\n      },\n      \"badges\": {\n        \"pending\": \"{{count}} ar feitheamh\",\n        \"processing\": \"{{count}} phróiseáil\",\n        \"completed\": \"{{count}} críochnaithe\",\n        \"failed\": \"Theip ar {{count}}\"\n      },\n      \"imported_to\": \"Iompórtáilte chuig:\",\n      \"view_list\": \"Féach ar an Liosta\",\n      \"delete_dialog_title\": \"Scrios Seisiún Iompórtála\",\n      \"delete_dialog_description\": \"An bhfuil tú cinnte gur mian leat \\\"{{name}}\\\" a scriosadh? Ní féidir an gníomh seo a chealú. Ní scriosfar na leabharmharcanna féin.\",\n      \"delete_session\": \"Scrios Seisiún\",\n      \"description\": \"Féach ar do sheisiúin allmhairithe mórchóir agus bainistigh iad. Cruthaítear seisiúin go huathoibríoch nuair a allmhairíonn tú leabharmharcanna.\",\n      \"pause_session\": \"Sos\",\n      \"resume_session\": \"Lean ort\",\n      \"view_details\": \"Féach ar Shonraí\",\n      \"detail\": {\n        \"page_title\": \"Sonraí Seisiúin Iompórtála\",\n        \"back_to_import\": \"Ar ais chuig an Iompórtáil\",\n        \"filter_all\": \"Gach\",\n        \"filter_accepted\": \"Glactha\",\n        \"filter_rejected\": \"Diúltaithe\",\n        \"filter_duplicates\": \"Dúblaigh\",\n        \"filter_pending\": \"Ar feitheamh\",\n        \"table_title\": \"Teideal / URL\",\n        \"table_type\": \"Cineál\",\n        \"table_result\": \"Toradh\",\n        \"table_reason\": \"Cúis\",\n        \"table_bookmark\": \"Leabharmharc\",\n        \"result_accepted\": \"Glactha\",\n        \"result_rejected\": \"Diúltaithe\",\n        \"result_skipped_duplicate\": \"Dúblach\",\n        \"result_pending\": \"Ar feitheamh\",\n        \"result_processing\": \"Próiseáil\",\n        \"no_results\": \"Níor aimsíodh aon torthaí don scagaire seo.\",\n        \"view_bookmark\": \"Féach ar Leabharmharc\",\n        \"load_more\": \"Luchtaigh Tuilleadh\",\n        \"no_title\": \"Gan teideal\"\n      }\n    },\n    \"backups\": {\n      \"backups\": \"Cúltaca\",\n      \"page_title\": \"Cúltaca\",\n      \"page_description\": \"Cruthaigh agus bainistigh cúltacaí de do leabharmharcanna go huathoibríoch. Déantar cúltacaí a chomhbhrú agus a stóráil go daingean.\",\n      \"configuration\": {\n        \"title\": \"Cumraíocht Cúltaca\",\n        \"enable_automatic_backups\": \"Cumasaigh Cúltacaí Uathoibríocha\",\n        \"enable_automatic_backups_description\": \"Cruthaigh cúltacaí de do leabharmharcanna go huathoibríoch\",\n        \"backup_frequency\": \"Minicíocht Cúltacaí\",\n        \"backup_frequency_description\": \"Cé chomh minic ba chóir cúltacaí a chruthú\",\n        \"retention_period\": \"Tréimhse Coinneála (laethanta)\",\n        \"retention_period_description\": \"Cé mhéad lá a choinnítear cúltacaí sula scriostar iad\",\n        \"frequency\": {\n          \"daily\": \"Go laethúil\",\n          \"weekly\": \"Go seachtainiúil\"\n        },\n        \"select_frequency\": \"Roghnaigh minicíocht\",\n        \"save_settings\": \"Sábháil Socruithe\"\n      },\n      \"list\": {\n        \"title\": \"Do Chúltacaí\",\n        \"create_backup_now\": \"Cruthaigh Cúltaca Anois\",\n        \"no_backups\": \"Níl aon chúltacaí agat fós. Cumasaigh cúltacaí uathoibríocha nó cruthaigh ceann de láimh.\",\n        \"table\": {\n          \"created_at\": \"Cruthaithe Ag\",\n          \"bookmarks\": \"Leabharmharcanna\",\n          \"size\": \"Méid\",\n          \"status\": \"Stádas\",\n          \"actions\": \"Gníomhartha\"\n        },\n        \"status\": {\n          \"success\": \"Rath\",\n          \"failed\": \"Theip\",\n          \"pending\": \"Ar feitheamh\"\n        },\n        \"actions\": {\n          \"download_backup\": \"Íoslódáil Cúltaca\",\n          \"delete_backup\": \"Scrios Cúltaca\"\n        }\n      },\n      \"dialogs\": {\n        \"delete_backup_title\": \"Scrios an Cúltaca?\",\n        \"delete_backup_description\": \"An bhfuil tú cinnte gur mhaith leat an cóip chúltaca seo a scriosadh? Ní féidir an gníomh seo a chealú.\"\n      },\n      \"toasts\": {\n        \"backup_queued\": \"Cuireadh an post cúltaca sa phróca! Déanfar é a phróiseáil go luath.\",\n        \"backup_deleted\": \"Scriosadh an cúltaca!\"\n      }\n    }\n  },\n  \"lists\": {\n    \"search_query\": \"Iarratas Cuardaigh\",\n    \"search_query_help\": \"Foghlaim tuilleadh faoin teanga cuardaigh.\",\n    \"all_lists\": \"Gach Liosta\",\n    \"favourites\": \"Ceanáin\",\n    \"new_list\": \"Liosta Nua\",\n    \"edit_list\": \"Cuir Liosta in Eagar\",\n    \"share_list\": \"Liosta Comhroinnte\",\n    \"new_nested_list\": \"Liosta Neadaithe Nua\",\n    \"merge_list\": \"Cumaisc Liosta\",\n    \"destination_list\": \"Liosta Ceann Scríbe\",\n    \"delete_after_merge\": \"Scrios an bunliosta tar éis cumaisc\",\n    \"no_destination\": \"Níl Aon Cheann Scríbe Ann\",\n    \"parent_list\": \"Liosta Tuismitheora\",\n    \"no_parent\": \"Níl Aon Tuismitheoir Ann\",\n    \"list_type\": \"Cineál Liosta\",\n    \"manual_list\": \"Liosta Lámhleabhar\",\n    \"smart_list\": \"Cliste Liosta\",\n    \"description\": \"Cur síos (Roghnach)\",\n    \"rss\": {\n      \"title\": \"Fotha RSS\",\n      \"description\": \"Cumasaigh fotha RSS don liosta seo\",\n      \"feed_url\": \"URL Fotha RSS\"\n    },\n    \"public_list\": {\n      \"title\": \"Liosta Poiblí\",\n      \"description\": \"Lig do dhaoine eile an liosta seo a fheiceáil\",\n      \"share_link\": \"Nasc Comhroinnte\"\n    },\n    \"delete_list\": {\n      \"title\": \"Scrios Liosta\",\n      \"description\": \"Ní scriosann liosta a scriosadh aon leabharmharcanna sa liosta sin.\",\n      \"delete_children\": \"Scrios liostaí leanaí (go hathchúrsach)\",\n      \"delete_children_description\": \"Mura bhfuil an bosca seo ticáilte, beidh gach liosta díreach leanaí ina liostaí fréimhe\"\n    },\n    \"shared\": \"Roinnte\",\n    \"collaborators\": {\n      \"manage\": \"Bainistigh Comhoibritheoirí\",\n      \"view\": \"Féach ar Chomhoibritheoirí\",\n      \"collaborators\": \"Comhoibritheoirí\",\n      \"add\": \"Cuir Comhoibritheoir leis\",\n      \"current\": \"Comhoibritheoirí Reatha\",\n      \"enter_email\": \"Cuir isteach seoladh ríomhphoist\",\n      \"please_enter_email\": \"Cuir isteach seoladh ríomhphoist, le do thoil\",\n      \"added_successfully\": \"Comhoibritheoir curtha leis go rathúil\",\n      \"failed_to_add\": \"Theip ar an gcomhoibritheoir a chur leis\",\n      \"removed\": \"Comhoibritheoir bainte\",\n      \"failed_to_remove\": \"Theip ar an gcomhoibritheoir a bhaint\",\n      \"role_updated\": \"Ról nuashonraithe\",\n      \"failed_to_update_role\": \"Theip ar an ról a nuashonrú\",\n      \"viewer\": \"Breathnóir\",\n      \"editor\": \"Eagarthóir\",\n      \"owner\": \"Úinéir\",\n      \"viewer_description\": \"Is féidir leabharmharcanna a fheiceáil sa liosta\",\n      \"editor_description\": \"Is féidir leabharmharcanna a chur leis agus a bhaint\",\n      \"no_collaborators\": \"Níl aon chomhoibritheoirí ann fós. Cuir duine leis chun comhoibriú a thosú!\",\n      \"no_collaborators_readonly\": \"Níl aon chomhoibritheoirí don liosta seo.\",\n      \"people_with_access\": \"Daoine a bhfuil rochtain acu ar an liosta seo\",\n      \"add_or_remove\": \"Cuir daoine leis nó bain daoine ar féidir leo rochtain a fháil ar an liosta seo\",\n      \"invitation_sent\": \"Cuireadh seolta go rathúil\",\n      \"invitation_revoked\": \"Cuireadh ar ceal\",\n      \"failed_to_revoke\": \"Theip ar an gcuireadh a chur ar ceal\",\n      \"pending\": \"Ar feitheamh\",\n      \"revoke\": \"Cuir ar ceal\",\n      \"declined\": \"Diúltaithe\"\n    },\n    \"leave_list\": {\n      \"title\": \"Fág Liosta\",\n      \"confirm_message\": \"An bhfuil tú cinnte gur mhaith leat {{icon}} {{name}} a fhágáil?\",\n      \"warning\": \"Ní bheidh tú in ann cluaisíní a fheiceáil nó rochtain a fháil orthu sa liosta seo a thuilleadh. Is féidir le húinéir an liosta tú a chur ar ais leis más gá.\",\n      \"action\": \"Fág Liosta\",\n      \"success\": \"D'fhág tú \\\"{{icon}} {{name}}\\\"\"\n    },\n    \"invitations\": {\n      \"pending\": \"Cuirí ar Feitheamh\",\n      \"description\": \"Athbhreithnigh agus freagair cuireadh comhoibrithe liosta\",\n      \"invited_by\": \"Ar cuireadh ar aghaidh ag\",\n      \"accept\": \"Glac leis\",\n      \"decline\": \"Diúltaigh\",\n      \"accepted\": \"Glactha leis an gcuireadh\",\n      \"declined\": \"Diúltaíodh don chuireadh\",\n      \"failed_to_accept\": \"Theip ar ghlacadh leis an gcuireadh\",\n      \"failed_to_decline\": \"Theip ar dhiúltú don chuireadh\"\n    },\n    \"shared_lists\": \"Liostaí Comhroinnte\"\n  },\n  \"tags\": {\n    \"unused_tags\": \"Clibeanna Neamhúsáidte\",\n    \"unused_tags_info\": \"Clibeanna nach bhfuil ceangailte le haon leabharmharcanna\",\n    \"your_tags\": \"Do Chlibeanna\",\n    \"ai_tags_info\": \"Clibeanna nár ceanglaíodh ach go huathoibríoch (le hintleacht shaorga)\",\n    \"delete_all_unused_tags\": \"Scrios Gach Clib Neamhúsáidte\",\n    \"drag_and_drop_merging\": \"Cumasc Tarraing & Titim\",\n    \"sort_by_name\": \"Sórtáil de réir Ainm\",\n    \"all_tags\": \"Gach Clib\",\n    \"your_tags_info\": \"Clibeanna a bhí ceangailte uair amháin ar a laghad agat\",\n    \"ai_tags\": \"Clibeanna AI\",\n    \"drag_and_drop_merging_info\": \"Tarraing agus scaoil clibeanna ar a chéile chun iad a chumasc\",\n    \"create_tag\": \"Cruthaigh Clib\",\n    \"create_tag_description\": \"Cruthaigh clib nua gan é a cheangal le haon leabharmharc\",\n    \"tag_name\": \"Ainm an Chlib\",\n    \"enter_tag_name\": \"Cuir isteach ainm an chlib\",\n    \"sort_by_usage\": \"Sórtáil de réir Úsáide\",\n    \"sort_by_relevance\": \"Sórtáil de réir Ábharthachta\",\n    \"no_custom_tags\": \"Níl aon chlibeanna saincheaptha agat fós\",\n    \"no_ai_tags\": \"Níl aon chlibeanna AI agat fós\",\n    \"no_unused_tags\": \"Níl aon chlibeanna neamhúsáidte agat\",\n    \"no_unused_tags_match_your_search\": \"Ní fhreagraíonn aon chlibeanna neamhúsáidte d’inneall cuardaigh duit\",\n    \"no_tags_match_your_search\": \"Ní fhreagraíonn aon chlibeanna d’inneall cuardaigh duit\",\n    \"search_placeholder\": \"Cuardaigh clibeanna...\",\n    \"search_or_create_placeholder\": \"Cuardaigh nó cruthaigh clibeanna...\"\n  },\n  \"search\": {\n    \"is_favorited\": \"Is Fearr\",\n    \"created_on_or_after\": \"Cruthaithe ar nó ina dhiaidh\",\n    \"day_s\": \" Lá(anna)\",\n    \"has_tag\": \"Haischlib\",\n    \"is_not_favorited\": \"Níl sé Marcáilte mar Is Ansa\",\n    \"is_archived\": \"Archived atá ann\",\n    \"is_not_archived\": \"Níl sé Cartlannaithe\",\n    \"has_any_tag\": \"An bhfuil Aon Chlib ann\",\n    \"has_no_tags\": \"Níl Aon Chlib Air\",\n    \"is_in_any_list\": \"An bhfuil sé in Aon Liosta\",\n    \"is_not_in_any_list\": \"Níl sé in Aon Liosta\",\n    \"not_created_on_or_after\": \"Nár Cruthaíodh Ar Aghaidh nó Tar Éis\",\n    \"created_on_or_before\": \"Cruthaithe Ar Nó Roimhe\",\n    \"not_created_on_or_before\": \"Nár Cruthaíodh Ar nó Roimh\",\n    \"created_within\": \"Cruthaithe Laistigh De\",\n    \"created_earlier_than\": \"Cruthaithe Níos Luaithe Ná\",\n    \"week_s\": \" Seachtain(í)\",\n    \"month_s\": \" Míonna\",\n    \"year_s\": \" Bliain/Blianta\",\n    \"day_s_ago\": \" Lá(eth) Ó shin\",\n    \"week_s_ago\": \" Seachtainí Ó Shin\",\n    \"month_s_ago\": \" Míonna ó shin\",\n    \"year_s_ago\": \" bliain/blianta ó shin\",\n    \"url_contains\": \"Tá sa URL\",\n    \"url_does_not_contain\": \"Níl URL Sannta\",\n    \"is_in_list\": \"Sa Liosta É\",\n    \"is_not_in_list\": \"Níl sé Sa Liosta\",\n    \"does_not_have_tag\": \"Níl Clib Air\",\n    \"full_text_search\": \"Cuardach Téacs Iomlán\",\n    \"type_is\": \"Is é an cineál\",\n    \"type_is_not\": \"Níl an cineál\",\n    \"is_from_feed\": \"Ó Fotha RSS atá ann\",\n    \"is_not_from_feed\": \"Níl sé ó Fhotha RSS\",\n    \"and\": \"Agus\",\n    \"or\": \"Nó\",\n    \"history\": \"Cuardaigh Déanaí\",\n    \"title_contains\": \"Tá Teideal I Láthair\",\n    \"title_does_not_contain\": \"Níl Teideal I Láthair\",\n    \"is_broken_link\": \"Tá Nasc Briste Ann\",\n    \"tags\": \"Clibeanna\",\n    \"no_suggestions\": \"Níl moltaí ar bith ann\",\n    \"filters\": \"Scagairí\",\n    \"is_not_broken_link\": \"Tá Nasc Oibre Ann\",\n    \"lists\": \"Liostaí\",\n    \"feeds\": \"Fothaí\",\n    \"is_from_source\": \"Ní foinse é foinse\",\n    \"is_not_from_source\": \"Ní foinse é foinse ní\"\n  },\n  \"editor\": {\n    \"disabled_submissions\": \"Tá aighneachtaí díchumasaithe\",\n    \"text_toolbar\": {\n      \"bold\": \"Trom\",\n      \"markdown_shortcuts\": {\n        \"ordered_list\": {\n          \"label\": \"Liosta Ordaithe\",\n          \"example\": \"1. Mír liosta\"\n        },\n        \"block_code\": {\n          \"example\": \"``` + spás\",\n          \"label\": \"Cód Bloc\"\n        },\n        \"label\": \"Aicearraí Markdown\",\n        \"heading\": {\n          \"label\": \"Ceannteideal\",\n          \"example\": \"# H1, ## H2, ### H3\"\n        },\n        \"bold\": {\n          \"label\": \"Trom\",\n          \"example\": \"**téacs** nó CTRL+b\"\n        },\n        \"italic\": {\n          \"label\": \"Cló Iodálach\",\n          \"example\": \"*Iodálach* nó _Iodálach_ nó CTRL+i\"\n        },\n        \"blockquote\": {\n          \"label\": \"Blocfhocal\",\n          \"example\": \"> Sleachta\"\n        },\n        \"unordered_list\": {\n          \"label\": \"Liosta Neamhordaithe\",\n          \"example\": \"- Mír liosta\"\n        },\n        \"inline_code\": {\n          \"label\": \"Cód Inlíne\",\n          \"example\": \"`Cód`\"\n        }\n      },\n      \"undo\": \"Cealaigh\",\n      \"redo\": \"Athdhéan\",\n      \"italic\": \"Cló Iodálach\",\n      \"underline\": \"Foslíne\",\n      \"strikethrough\": \"Traslíne\",\n      \"code\": \"Cód\",\n      \"highlight\": \"Aibhsigh\",\n      \"align_left\": \"Ailíniú ar Chlé\",\n      \"align_center\": \"Ailíniú sa Lár\",\n      \"align_right\": \"Ailínigh ar Dheis\"\n    },\n    \"quickly_focus\": \"Is féidir leat díriú go tapa ar an réimse seo tríd ⌘ + E a bhrú\",\n    \"multiple_urls_dialog_title\": \"Ag iompórtáil URLanna mar Leabhair Mharcála ar leithligh?\",\n    \"multiple_urls_dialog_desc\": \"Tá URLanna iolracha san ionchur ar línte ar leithligh. Ar mhaith leat iad a iompórtáil mar leabharmharcanna ar leithligh?\",\n    \"import_as_separate_bookmarks\": \"Allmhairigh mar Leabharmharcanna ar leithligh\",\n    \"placeholder\": \"Greamaigh nasc nó íomhá, scríobh nóta nó tarraing agus scaoil íomhá anseo…\",\n    \"placeholder_v2\": \"Greamaigh nasc, scríobh nóta nó scaoil íomhá…\",\n    \"import_as_text\": \"Iompórtáil mar Leabhar Mharc Téacs\",\n    \"new_item\": \"MÍR NUA\"\n  },\n  \"toasts\": {\n    \"bookmarks\": {\n      \"updated\": \"Tá an leabharmharc nuashonraithe!\",\n      \"deleted\": \"Scriosadh an leabharmharc!\",\n      \"refetch\": \"Cuireadh atógáil sa scuaine!\",\n      \"full_page_archive\": \"Tá cruthú Cartlainne Leathanach Iomlán tosaithe\",\n      \"delete_from_list\": \"Scriosadh an leabharmharc ón liosta\",\n      \"clipboard_copied\": \"Tá an nasc curtha le do ghearrthaisce!\",\n      \"preserve_pdf\": \"Tá caomhnú PDF tosaithe\",\n      \"update_banner\": \"Tá an meirge nuashonraithe!\",\n      \"uploading_banner\": \"Ag uaslódáil meirge...\"\n    },\n    \"lists\": {\n      \"created\": \"Cruthaíodh liosta!\",\n      \"updated\": \"Tá an liosta nuashonraithe!\",\n      \"merged\": \"Tá an liosta cumaisc!\",\n      \"deleted\": \"Scriosadh an liosta!\"\n    },\n    \"tags\": {\n      \"created\": \"Tá an clib cruthaithe!\",\n      \"failed_to_create\": \"Theip ar chlib a chruthú\"\n    }\n  },\n  \"admin\": {\n    \"server_stats\": {\n      \"total_bookmarks\": \"Iomlán na Leabharmharcanna\",\n      \"server_version\": \"Leagan Freastalaí\",\n      \"server_stats\": \"Staitisticí Freastalaí\",\n      \"total_users\": \"Iomlán Úsáideoirí\"\n    },\n    \"background_jobs\": {\n      \"background_jobs\": \"Poist Chúlra\",\n      \"crawler_jobs\": \"Poist Crawler\",\n      \"indexing_jobs\": \"Poist Innéacsaithe\",\n      \"inference_jobs\": \"Poist Inbhreathnaithe\",\n      \"tidy_assets_jobs\": \"Poist Sócmhainní néata\",\n      \"video_jobs\": \"Poist Íoslódála Físeáin\",\n      \"webhook_jobs\": \"Poist Ghréasáin\",\n      \"asset_preprocessing_jobs\": \"Poist Réamhphróiseála Sócmhainní\",\n      \"feed_jobs\": \"Poist Fotha RSS\",\n      \"job\": \"Post\",\n      \"queued\": \"Ar feitheamh\",\n      \"pending\": \"Ar feitheamh\",\n      \"failed\": \"Theip air\",\n      \"jobs\": {\n        \"crawler\": {\n          \"title\": \"Poist Chrawlóirí\",\n          \"description\": \"Crawl gréasáin agus eastóscadh ábhair ó URLanna\"\n        },\n        \"inference\": {\n          \"title\": \"Poist Inferrerachta\",\n          \"description\": \"Clibeáil ábhar le hintleacht shaorga agus achoimriúchán\"\n        },\n        \"indexing\": {\n          \"title\": \"Poist Innéacsaithe\",\n          \"description\": \"Nuashonruithe ar innéacs cuardaigh\"\n        },\n        \"asset_preprocessing\": {\n          \"title\": \"Poist Réamhphróiseála Sócmhainní\",\n          \"description\": \"Réamhphróiseáil íomhánna agus doiciméad (scáileáin scáileáin, eastóscadh téacs, etc.)\"\n        },\n        \"tidy_assets\": {\n          \"title\": \"Poist Shlachtaithe Sócmhainní\",\n          \"description\": \"Glanadh sócmhainní agus barrfheabhsú stórála\"\n        },\n        \"video\": {\n          \"title\": \"Poist Íoslódála Físe\",\n          \"description\": \"Eastóscadh agus íoslódáil físe\"\n        },\n        \"webhook\": {\n          \"title\": \"Poist Ghréasáin\",\n          \"description\": \"Fógraí gréasáin seachtracha\"\n        },\n        \"feed\": {\n          \"title\": \"Poist Fotha RSS\",\n          \"description\": \"Próiseáil fotha RSS agus nuashonruithe ábhair\"\n        },\n        \"admin_maintenance\": {\n          \"title\": \"Tascanna Cothabhála Riaracháin\",\n          \"description\": \"Glanadh riaracháin agus cothabháil sócmhainní\"\n        }\n      },\n      \"monitor_and_manage\": \"Déan monatóireacht agus bainistigh scuainí post cúlra agus tascanna próiseála córais\",\n      \"active\": \"Gníomhach\",\n      \"available_actions\": \"Gníomhartha ar Fáil\",\n      \"status\": {\n        \"title\": \"Tuiscint ar Stáit Phóist\",\n        \"queued\": {\n          \"title\": \"Sa scuaine\",\n          \"description\": \"Poist atá ag fanacht sa scuaine le próiseáil. Tosóidh siad go huathoibríoch nuair a bheidh acmhainní ar fáil.\"\n        },\n        \"unprocessed\": {\n          \"title\": \"Neamhphróiseáilte\",\n          \"description\": \"Leabharmharcanna nár próiseáladh go fóill. Is dócha go bhfuil siad sin ar feitheamh cheana féin le próiseáil, mura bhfuil, b'fhéidir go mbeidh ort iad a athsheoladh de láimh.\"\n        },\n        \"failed\": {\n          \"title\": \"Theipt orthu\",\n          \"description\": \"Leabharmharcanna ar tháinig earráidí orthu le linn na próiseála. B'fhéidir go mbeidh gá le haird láimhe orthu seo.\"\n        }\n      },\n      \"actions\": {\n        \"recrawl_failed_links_only\": \"Naisc ar theip orthu amháin a athscríobh\",\n        \"recrawl_all_links\": \"Gach nasc a athscríobh\",\n        \"without_inference\": \"Gan Inbhearú\",\n        \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Clibeanna AI a athghiniúint le haghaidh leabharmharcanna ar theip orthu amháin\",\n        \"regenerate_ai_tags_for_all_bookmarks\": \"Clibeanna AI a athghiniúint le haghaidh gach leabharmharc\",\n        \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Achoimrí AI a athghiniúint le haghaidh leabharmharcanna ar theip orthu amháin\",\n        \"regenerate_ai_summaries_for_all_bookmarks\": \"Achoimrí AI a athghiniúint le haghaidh gach leabharmharc\",\n        \"reindex_all_bookmarks\": \"Gach leabharmharc a athinnéacsú\",\n        \"clean_assets\": \"Sócmhainní crochta a ghlanadh & Meiteashonraí a athshioncronú\",\n        \"reprocess_assets_fix_mode\": \"Sócmhainní neamhphróiseáilte a athphróiseáil\",\n        \"migrate_large_link_html_content\": \"Bog an HTML Mór Inlíne go Sócmhainní\",\n        \"recrawl_pending_links_only\": \"Ath-rianasca na Naisc atá ar Feitheamh Amháin\",\n        \"regenerate_ai_tags_for_pending_bookmarks_only\": \"Athghin Clibeanna AI le haghaidh Leabharmharcanna atá ar Feitheamh Amháin\",\n        \"regenerate_ai_summaries_for_pending_bookmarks_only\": \"Athghin Achoimrí AI le haghaidh Leabharmharcanna atá ar Feitheamh Amháin\"\n      }\n    },\n    \"admin_settings\": \"Socruithe Riaracháin\",\n    \"actions\": {\n      \"recrawl_failed_links_only\": \"Naisc Theipthe Athchrawláilte Amháin\",\n      \"recrawl_all_links\": \"Athghabháil Gach Nasc\",\n      \"without_inference\": \"Gan Inbheartú\",\n      \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Athghin Clibeanna AI le haghaidh Leabharmharcanna Teipthe Amháin\",\n      \"regenerate_ai_tags_for_all_bookmarks\": \"Athghin Clibeanna AI do Gach Leabharcmharc\",\n      \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Athghin Achoimrí AI do Leabharmharcanna Teipthe Amháin\",\n      \"regenerate_ai_summaries_for_all_bookmarks\": \"Athghin Achoimrí AI do Gach Leabharmharc\",\n      \"reindex_all_bookmarks\": \"Athinnéacs Gach Leabharmharc\",\n      \"compact_assets\": \"Dlúthaigh Sócmhainní\",\n      \"reprocess_assets_fix_mode\": \"Athphróiseáil Sócmhainní (Mód Deisithe)\"\n    },\n    \"users_list\": {\n      \"users_list\": \"Liosta Úsáideoirí\",\n      \"create_user\": \"Cruthaigh Úsáideoir\",\n      \"change_role\": \"Athraigh Ról\",\n      \"reset_password\": \"Athshocraigh Pasfhocal\",\n      \"delete_user\": \"Scrios Úsáideoir\",\n      \"num_bookmarks\": \"Líon na Leabharmharcanna\",\n      \"asset_sizes\": \"Méideanna Sócmhainní\",\n      \"local_user\": \"Úsáideoir Áitiúil\",\n      \"confirm_password\": \"Deimhnigh Pasfhocal\",\n      \"delete_user_confirm_description\": \"An bhfuil tú cinnte gur mhaith leat úsáideoir \\\"{{name}}\\\" a scriosadh?\",\n      \"unlimited\": \"Gan teorainn\"\n    },\n    \"service_connections\": {\n      \"title\": \"Naisc Sheirbhíse\",\n      \"description\": \"Déan monatóireacht ar shláinte agus ar nascacht spleáchais córais sheachtraigh\",\n      \"search_engine\": \"Inneall Cuardaigh\",\n      \"browser\": \"Brabhsálaí\",\n      \"queue_system\": \"Córas Scuaine\",\n      \"status\": {\n        \"not_configured\": \"Níl sé Cumraithe\",\n        \"connected\": \"Ceangailte\",\n        \"disconnected\": \"Dícheangailte\"\n      }\n    },\n    \"admin_tools\": {\n      \"admin_tools\": \"Uirlisí Riaracháin\",\n      \"bookmark_debugger\": \"Dífhabhtóir Leabharmharcanna\",\n      \"bookmark_id\": \"ID an Leabharmharc\",\n      \"bookmark_id_placeholder\": \"Iontráil ID an leabharmhairc\",\n      \"lookup\": \"Cuardaigh\",\n      \"debug_info\": \"Faisnéis Dhífhabhtaithe\",\n      \"basic_info\": \"Buneolas\",\n      \"status\": \"Stádas\",\n      \"content\": \"Ábhar\",\n      \"html_preview\": \"Réamhamharc HTML (Na chéad 1000 carachtar)\",\n      \"summary\": \"Achoimre\",\n      \"url\": \"URL\",\n      \"source_url\": \"URL Foinse\",\n      \"asset_type\": \"Cineál Sócmhainne\",\n      \"file_name\": \"Ainm Comhaid\",\n      \"owner_user_id\": \"ID Úsáideora an Úinéara\",\n      \"tagging_status\": \"Stádas Clibeála\",\n      \"summarization_status\": \"Stádas Achoimrithe\",\n      \"crawl_status\": \"Stádas Crawl\",\n      \"crawl_status_code\": \"Cód Stádais HTTP\",\n      \"crawled_at\": \"Crawlta Ag\",\n      \"recrawl\": \"Ath-crawl\",\n      \"reindex\": \"Ath-innéacsú\",\n      \"retag\": \"Ath-chlibeáil\",\n      \"resummarize\": \"Ath-achoimriú\",\n      \"bookmark_not_found\": \"Leabharmharc ar iarraidh\",\n      \"action_success\": \"D'éirigh leis an ngníomh\",\n      \"action_failed\": \"Theip ar an ngníomh\",\n      \"recrawl_queued\": \"Cuireadh post ath-crawl i scuaine\",\n      \"reindex_queued\": \"Cuireadh post ath-innéacsaithe i scuaine\",\n      \"retag_queued\": \"Cuireadh post ath-chlibeála i scuaine\",\n      \"resummarize_queued\": \"Cuireadh post ath-achoimrithe i scuaine\",\n      \"view\": \"Féach\",\n      \"fetch_error\": \"Earráid agus leabharmharc á fháil\"\n    }\n  },\n  \"bookmark_editor\": {\n    \"author\": \"Údar\",\n    \"extracted_content\": \"Ábhar Bainte\",\n    \"title\": \"Cuir Leabharmharc in Eagar\",\n    \"subtitle\": \"Déan athruithe ar shonraí an leabharmharc. Cliceáil ar shábháil nuair atá tú críochnaithe.\",\n    \"publisher\": \"Foilsitheoir\",\n    \"date_published\": \"Dáta Foilsithe\",\n    \"pick_a_date\": \"Roghnaigh dáta\",\n    \"save_changes\": \"Sábháil athruithe\"\n  },\n  \"layouts\": {\n    \"masonry\": \"Saor Cloiche\",\n    \"grid\": \"Eangach\",\n    \"list\": \"Liosta\",\n    \"compact\": \"Dlúth\"\n  },\n  \"highlights\": {\n    \"no_highlights\": \"Níl aird a tharraingt ar bith agat go fóill.\"\n  },\n  \"options\": {\n    \"dark_mode\": \"Mód Dorcha\",\n    \"light_mode\": \"Mód Solais\",\n    \"apps_extensions\": \"Aipeanna & Eisínteachtaí\",\n    \"documentation\": \"Doiciméadú\",\n    \"follow_us_on_x\": \"Lean muid ar X\"\n  },\n  \"preview\": {\n    \"view_original\": \"Féach ar an mBunleagan\",\n    \"cached_content\": \"Ábhar i gCartlann\",\n    \"reader_view\": \"Amharc an Léitheora\",\n    \"tabs\": {\n      \"content\": \"Ábhar\",\n      \"details\": \"Sonraí\"\n    },\n    \"archive_info\": \"Seans nach ndéanfaidh cartlanna rindreáil i gceart inline má tá Javascript ag teastáil uathu. Chun na torthaí is fearr a fháil, <1>íoslódáil é agus oscail i do bhrabhsálaí</1>.\",\n    \"fetch_error_title\": \"Ní féidir Ábhar a Fháil\",\n    \"fetch_error_description\": \"Níorbh fhéidir linn ábhar an nasc seo a thabhairt chun solais. B'fhéidir go bhfuil an leathanach cosanta, go bhfuil gá le fíordheimhniú, nó go bhfuil sé ar fáil go sealadach.\",\n    \"crawling_in_progress\": \"Ag fáil ábhar an leathanaigh…\",\n    \"continue_reading\": \"Lean ar aghaidh san áit ar fágadh thú\",\n    \"continue_reading_percent\": \"Lean ar aghaidh san áit ar fágadh thú ({{percent}}%)\",\n    \"continue_button\": \"Lean ar aghaidh\"\n  },\n  \"dialogs\": {\n    \"bookmarks\": {\n      \"delete_confirmation_title\": \"Scrios Leabharmharc?\",\n      \"delete_confirmation_description\": \"An bhfuil tú cinnte gur mhaith leat an leabhar marcála seo a scriosadh?\"\n    }\n  },\n  \"banners\": {\n    \"no_bookmarks\": {\n      \"title\": \"Níl aon leabharmharcanna ann go fóill\",\n      \"description\": \"Sábháil ailt, naisc agus leathanaigh spéisiúla chun rochtain a fháil orthu go tapa níos déanaí.\"\n    }\n  },\n  \"cleanups\": {\n    \"cleanups\": \"Glanadh\",\n    \"duplicate_tags\": {\n      \"title\": \"Clibeanna Dúblacha\",\n      \"merge_all_suggestions\": \"An bhfuil tú cinnte gur mhaith leat gach moladh a chumasc?\"\n    }\n  },\n  \"view_options\": {\n    \"title\": \"Roghanna Amhairc\",\n    \"layout\": \"Leagan Amach\",\n    \"columns\": \"Colúin\",\n    \"display_options\": \"Roghanna Taispeána\",\n    \"show_note_previews\": \"Nótaí a Thaispeáint\",\n    \"show_tags\": \"Taispeáin Clibeanna\",\n    \"show_title\": \"Taispeáin Teideal\",\n    \"image_options\": \"Roghanna Íomhá\",\n    \"image_fit_cover\": \"Clúdaigh (Líon)\",\n    \"image_fit_contain\": \"Cuimsigh (Oiriúnaigh)\"\n  },\n  \"version\": {\n    \"new_release_available\": \"Nótaí scaoileadh nua ar fáil\",\n    \"whats_new_title\": \"Cad atá nua in v{{version}}\",\n    \"release_notes_description\": \"Seo na nuashonruithe is déanaí a aimsíodh ó nótaí scaoileadh GitHub.\",\n    \"loading_release_notes\": \"Ag luchtú nótaí scaoileadh…\",\n    \"unable_to_load_release_notes\": \"Ní féidir nótaí scaoileadh a luchtú faoi láthair. Bain triail as arís níos déanaí.\",\n    \"no_release_notes\": \"Níor foilsíodh aon nótaí scaoilte don leagan seo.\",\n    \"release_notes_synced\": \"Déantar nótaí scaoilte a shioncronú ó GitHub.\",\n    \"view_on_github\": \"Féach ar GitHub\"\n  },\n  \"wrapped\": {\n    \"title\": \"{{year}} Fillte agat\",\n    \"subtitle\": \"Bliain i gKarakeep\",\n    \"banner\": {\n      \"title\": \"Tá do Fillte 2025 réidh!\",\n      \"description\": \"Féach ar do bhliain i leabharmharcanna\",\n      \"view_now\": \"Féach Anois\"\n    },\n    \"button\": \"Fillte 2025\",\n    \"loading\": \"Ag lódáil do chuid Fillte...\",\n    \"failed_to_load\": \"Theip ort do staitisticí Fillte a lódáil\",\n    \"sections\": {\n      \"total_saves\": {\n        \"prefix\": \"Shábháil tú\",\n        \"suffix\": \"Míreanna i mbliana\",\n        \"suffix_singular\": \"Mír i mbliana\"\n      },\n      \"first_bookmark\": {\n        \"title\": \"Thosaigh do Thuras\",\n        \"description\": \"An chéad sábháil agat in {{year}}:\"\n      },\n      \"top_domains\": \"Na Suímh is Fearr Agat\",\n      \"top_tags\": \"Na Clibeanna is Fearr Agat\",\n      \"monthly_activity\": \"Do Bhliain i Sábhála\",\n      \"most_active_day\": \"An Lá is Gnóthaí Agat\",\n      \"peak_times\": {\n        \"title\": \"Cathain a Shábhálann Tú\",\n        \"peak_hour\": \"Buaicuaire\",\n        \"peak_day\": \"Buaiclá\"\n      },\n      \"how_you_save\": \"Conas a Shábhálann Tú\",\n      \"what_you_saved\": \"Cad a Shábháil Tú\",\n      \"summary\": {\n        \"favorites\": \"Rudaí is Ansa leat\",\n        \"tags_created\": \"Clibeanna Cruthaithe\",\n        \"highlights\": \"Buaicphointí\"\n      },\n      \"types\": {\n        \"links\": \"Naisc\",\n        \"notes\": \"Nótaí\",\n        \"assets\": \"Sócmhainní\"\n      }\n    },\n    \"footer\": \"Déanta le Karakeep\",\n    \"share\": \"Comhroinn\",\n    \"download\": \"Íoslódáil\",\n    \"close\": \"Dún\",\n    \"generating\": \"Ag giniúint...\"\n  }\n}\n"
  },
  {
    "path": "apps/web/lib/i18n/locales/gl/translation.json",
    "content": "{\n  \"common\": {\n    \"action\": \"Acción\",\n    \"roles\": {\n      \"user\": \"Usuario\",\n      \"admin\": \"Administrador\"\n    },\n    \"experimental\": \"Experimental\",\n    \"search\": \"Buscar\",\n    \"tags\": \"Etiquetas\",\n    \"note\": \"Nota\",\n    \"password\": \"Contrasinal\",\n    \"actions\": \"Accións\",\n    \"created_at\": \"Creado\",\n    \"key\": \"Chave\",\n    \"something_went_wrong\": \"Algo saíu mal\",\n    \"archive\": \"Arquivo\",\n    \"name\": \"Nome\",\n    \"url\": \"URL\",\n    \"attachments\": \"Adxuntos\",\n    \"highlights\": \"Destacados\",\n    \"role\": \"Rol\",\n    \"email\": \"Email\",\n    \"screenshot\": \"Captura de pantalla\",\n    \"video\": \"Vídeo\",\n    \"home\": \"Inicio\",\n    \"source\": \"Fonte\",\n    \"bookmark_types\": {\n      \"text\": \"Texto\",\n      \"title\": \"Tipo de marcador\",\n      \"link\": \"Ligazón\",\n      \"media\": \"Multimedia\"\n    },\n    \"type\": \"Escribe\",\n    \"size\": \"Tamaño\",\n    \"updated_at\": \"Actualizado o\",\n    \"title\": \"Título\",\n    \"description\": \"Descrición\",\n    \"summary\": \"Resumo\",\n    \"quota\": \"Cota\",\n    \"bookmarks\": \"Marcadores\",\n    \"storage\": \"Almacenamento\",\n    \"pdf\": \"PDF Arquivado\",\n    \"default\": \"Predeterminado\",\n    \"id\": \"ID\",\n    \"last_used\": \"Usado por última vez\"\n  },\n  \"actions\": {\n    \"favorite\": \"Marcar como favorito\",\n    \"unfavorite\": \"Quitar de favoritos\",\n    \"delete\": \"Eliminar\",\n    \"refresh\": \"Actualizar\",\n    \"edit_tags\": \"Editar etiquetas\",\n    \"select_all\": \"Seleccionar todo\",\n    \"unselect_all\": \"Deseleccionar todo\",\n    \"copy_link\": \"Copiar ligazón\",\n    \"unarchive\": \"Desarquivar\",\n    \"recrawl\": \"Volver a facer un crawl\",\n    \"download_full_page_archive\": \"Descargar Arquivo da Páxina Completa\",\n    \"add_to_list\": \"Agregar á lista\",\n    \"save\": \"Gardar\",\n    \"add\": \"Engadir\",\n    \"edit\": \"Editar\",\n    \"create\": \"Crear\",\n    \"fetch_now\": \"Actualizar agora\",\n    \"summarize_with_ai\": \"Resumir con IA\",\n    \"edit_title\": \"Cambiar título\",\n    \"sign_out\": \"Cerrar sesión\",\n    \"close\": \"Cerrar\",\n    \"merge\": \"Unir\",\n    \"close_bulk_edit\": \"Cerrar editor en masa\",\n    \"bulk_edit\": \"Editar en masa\",\n    \"manage_lists\": \"Administrar listas\",\n    \"remove_from_list\": \"Eliminar da lista\",\n    \"cancel\": \"Cancelar\",\n    \"apply_all\": \"Aplicar todo\",\n    \"ignore\": \"Ignorar\",\n    \"change_layout\": \"Cambiar deseño\",\n    \"archive\": \"Arquivar\",\n    \"sort\": {\n      \"oldest_first\": \"Primeiro os máis antigos\",\n      \"title\": \"Ordenar\",\n      \"newest_first\": \"Primeiro o máis novo\",\n      \"relevant_first\": \"Primeiro os máis relevantes\"\n    },\n    \"open_editor\": \"Abrir editor\",\n    \"toggle_show_archived\": \"Mostrar os arquivados\",\n    \"confirm\": \"Confirmar\",\n    \"regenerate\": \"Rexenerar\",\n    \"load_more\": \"Cargar máis\",\n    \"edit_notes\": \"Editar notas\",\n    \"preserve_as_pdf\": \"Gardar como PDF\",\n    \"offline_copies\": \"Copias sen conexión\",\n    \"preserve_offline_archive\": \"Conservar o arquivo sen conexión\",\n    \"download_full_page_archive_file\": \"Descargar o arquivo\",\n    \"download_pdf_file\": \"Descargar ficheiro PDF\",\n    \"remove\": \"Retirar\",\n    \"more\": \"Máis\",\n    \"replace_banner\": \"Substituír o banner\",\n    \"add_banner\": \"Engadir banner\",\n    \"download\": \"Descargar\"\n  },\n  \"tags\": {\n    \"drag_and_drop_merging_info\": \"Arrastra e solta etiquetas sobre outras para unilas\",\n    \"all_tags\": \"Todas as etiquetas\",\n    \"your_tags\": \"Tuas etiquetas\",\n    \"your_tags_info\": \"Etiquetas que engadiches polo menos unha vez\",\n    \"ai_tags\": \"Etiquetas IA\",\n    \"ai_tags_info\": \"Etiquetas solo usadas automáticamente (por IA)\",\n    \"unused_tags\": \"Etiquetas sen usar\",\n    \"unused_tags_info\": \"Etiquetas sen marcadores\",\n    \"delete_all_unused_tags\": \"Eliminar etiquetas sen usar\",\n    \"drag_and_drop_merging\": \"Fusionar arrastrando e soltando\",\n    \"sort_by_name\": \"Ordenar por nome\",\n    \"create_tag\": \"Crear etiqueta\",\n    \"create_tag_description\": \"Crea unha etiqueta nova sen adxuntala a ningún marcador\",\n    \"tag_name\": \"Nome da etiqueta\",\n    \"enter_tag_name\": \"Introduza o nome da etiqueta\",\n    \"sort_by_usage\": \"Ordenar por uso\",\n    \"sort_by_relevance\": \"Ordenar por relevancia\",\n    \"no_custom_tags\": \"Aínda non hai etiquetas personalizadas\",\n    \"no_ai_tags\": \"Aínda non hai etiquetas de IA\",\n    \"no_unused_tags\": \"Non tes etiquetas sen usar\",\n    \"no_unused_tags_match_your_search\": \"Ningunha etiqueta sen usar coincide coa túa busca\",\n    \"no_tags_match_your_search\": \"Ningunha etiqueta coincide coa túa busca\",\n    \"search_placeholder\": \"Buscar etiquetas...\",\n    \"search_or_create_placeholder\": \"Buscar ou crear etiquetas...\"\n  },\n  \"toasts\": {\n    \"bookmarks\": {\n      \"updated\": \"O marcador actualizouse!\",\n      \"deleted\": \"O marcador eliminouse!\",\n      \"refetch\": \"Solicitouse a actualización!\",\n      \"full_page_archive\": \"Pediuse un Arquivo de Páxina Completa\",\n      \"delete_from_list\": \"O marcador borrouse da lista\",\n      \"clipboard_copied\": \"A ligazón copiouse no teu portapapeis!\",\n      \"preserve_pdf\": \"Activouse a preservación en PDF\",\n      \"update_banner\": \"Banner actualizado!\",\n      \"uploading_banner\": \"Cargando o banner...\"\n    },\n    \"lists\": {\n      \"updated\": \"A lista foi actualizada!\",\n      \"created\": \"Enlace creado correctamente!\",\n      \"merged\": \"A lista foi combinada!\",\n      \"deleted\": \"A lista foi eliminada!\"\n    },\n    \"tags\": {\n      \"created\": \"Creouse a etiqueta!\",\n      \"failed_to_create\": \"Non se puido crear a etiqueta\"\n    }\n  },\n  \"cleanups\": {\n    \"cleanups\": \"Limpezas\",\n    \"duplicate_tags\": {\n      \"title\": \"Etiquetas duplicadas\",\n      \"merge_all_suggestions\": \"Fusionar todas as suxestións?\"\n    }\n  },\n  \"layouts\": {\n    \"list\": \"Lista\",\n    \"compact\": \"Compacto\",\n    \"masonry\": \"Taboleiro\",\n    \"grid\": \"Reixa\"\n  },\n  \"settings\": {\n    \"import\": {\n      \"import_export\": \"Importar / Exportar\",\n      \"import_export_bookmarks\": \"Importar / Exportar marcadores\",\n      \"import_bookmarks_from_html_file\": \"Importar marcadores desde arquivo HTML\",\n      \"import_bookmarks_from_pocket_export\": \"Importar marcadores desde Pocket\",\n      \"import_bookmarks_from_matter_export\": \"Importar marcadores desde Matter\",\n      \"import_bookmarks_from_omnivore_export\": \"Importar marcadores desde Omnivore\",\n      \"import_bookmarks_from_linkwarden_export\": \"Importar marcadores desde Linkwarden\",\n      \"import_bookmarks_from_karakeep_export\": \"Importar marcadores desde Karakeep\",\n      \"export_links_and_notes\": \"Exportar links e notas\",\n      \"imported_bookmarks\": \"Marcadores importados\",\n      \"import_bookmarks_from_tab_session_manager_export\": \"Importar marcadores do xestor de sesións de pestanas\",\n      \"import_bookmarks_from_mymind_export\": \"Importar marcadores da exportación de mymind\",\n      \"import_bookmarks_from_instapaper_export\": \"Importar marcadores da exportación de Instapaper\"\n    },\n    \"back_to_app\": \"Volver á aplicación\",\n    \"user_settings\": \"Axustes de usuario\",\n    \"info\": {\n      \"user_info\": \"Información do usuario\",\n      \"basic_details\": \"Detalles básicos\",\n      \"change_password\": \"Cambiar contrasinal\",\n      \"current_password\": \"Contrasinal actual\",\n      \"new_password\": \"Novo contrasinal\",\n      \"confirm_new_password\": \"Confirmar novo contrasinal\",\n      \"options\": \"Axustes\",\n      \"interface_lang\": \"Idioma de la interface\",\n      \"user_settings\": {\n        \"user_settings_updated\": \"Actualizáronse os axustes do usuario!\",\n        \"bookmark_click_action\": {\n          \"title\": \"Acción ao premer no marcador\",\n          \"open_external_url\": \"Abrir o URL orixinal\",\n          \"open_bookmark_details\": \"Abrir os detalles do marcador\"\n        },\n        \"archive_display_behaviour\": {\n          \"title\": \"Marcadores arquivados\",\n          \"show\": \"Mostrar os marcadores arquivados en etiquetas e listas\",\n          \"hide\": \"Ocultar os marcadores arquivados en etiquetas e listas\"\n        }\n      },\n      \"reader_settings\": {\n        \"local_overrides_title\": \"Axustes específicos do dispositivo activos\",\n        \"using_default\": \"Usando o predeterminado do cliente\",\n        \"clear_override_hint\": \"Limpa a anulación do dispositivo para usar a configuración global ({{value}})\",\n        \"font_size\": \"Tamaño da letra\",\n        \"font_family\": \"Familia tipográfica\",\n        \"preview_inline\": \"(vista previa)\",\n        \"tooltip_preview\": \"Cambios da vista previa sen gardar\",\n        \"save_to_all_devices\": \"Todos os dispositivos\",\n        \"tooltip_local\": \"Os axustes do dispositivo difiren dos globais\",\n        \"reset_preview\": \"Restabelecer a vista previa\",\n        \"mono\": \"Monoespazo\",\n        \"line_height\": \"Alto de liña\",\n        \"tooltip_default\": \"Axustes de lectura\",\n        \"title\": \"Axustes do Reader\",\n        \"serif\": \"Con serifas\",\n        \"preview\": \"Vista previa\",\n        \"not_set\": \"Sen axustar\",\n        \"clear_local_overrides\": \"Eliminar axustes do dispositivo\",\n        \"preview_text\": \"A raposa marrón rápida salta sobre o can preguiceiro. Así é como aparecerá o texto da vista do lector.\",\n        \"local_overrides_cleared\": \"Elimináronse os axustes específicos do dispositivo\",\n        \"local_overrides_description\": \"Este dispositivo ten parámetros de lector que difieren dos teus predeterminados globais:\",\n        \"clear_defaults\": \"Borrar todos os predeterminados\",\n        \"description\": \"Configure os axustes de texto predeterminados para a vista do lector. Estes axustes sincronízanse en todos os teus dispositivos.\",\n        \"defaults_cleared\": \"Elimináronse os valores predeterminados do lector\",\n        \"save_hint\": \"Garda os axustes só para este dispositivo ou sincronízaos en todos os dispositivos\",\n        \"save_as_default\": \"Gardar como predeterminado\",\n        \"save_to_device\": \"Este dispositivo\",\n        \"sans\": \"Sen serifas\",\n        \"tooltip_preview_and_local\": \"Cambios da vista previa sen gardar; os axustes do dispositivo difiren dos globais\",\n        \"adjust_hint\": \"Axusta os axustes de arriba para previsualizar os cambios\"\n      },\n      \"avatar\": {\n        \"upload\": \"Subir avatar\",\n        \"change\": \"Cambiar o avatar\",\n        \"remove_confirm_title\": \"Queres eliminar o avatar?\",\n        \"updated\": \"Avatar actualizado\",\n        \"removed\": \"Avatar eliminado\",\n        \"description\": \"Sube unha imaxe cadrada para usar como avatar.\",\n        \"remove_confirm_description\": \"Isto borrará a túa foto de perfil actual.\",\n        \"title\": \"Foto de perfil\",\n        \"remove\": \"Eliminar o avatar\"\n      }\n    },\n    \"ai\": {\n      \"ai_settings\": \"Axustes de la IA\",\n      \"tagging_rules\": \"Regras de etiquetado\",\n      \"tagging_rule_description\": \"Os prompts que engadas aquí incluiranse como regras durante la xeración de etiquetas. Podes comprobar o prompt final na sección de previsualización.\",\n      \"prompt_preview\": \"Previsualización\",\n      \"text_prompt\": \"Prompt para texto\",\n      \"images_prompt\": \"Prompt para Imaxen\",\n      \"summarization_prompt\": \"Solicitude de resumo\",\n      \"all_tagging\": \"Todas as etiquetas\",\n      \"text_tagging\": \"Etiquetaxe de texto\",\n      \"image_tagging\": \"Etiquetaxe de imaxes\",\n      \"summarization\": \"Resumo\",\n      \"tag_style\": \"Estilo da etiqueta\",\n      \"auto_summarization_description\": \"Xera automaticamente resumos para os teus marcadores usando a intelixencia artificial.\",\n      \"auto_tagging\": \"Etiquetado automático\",\n      \"titlecase_spaces\": \"Maiúsculas e minúsculas con espazos\",\n      \"lowercase_underscores\": \"Minúsculas con guións baixos\",\n      \"inference_language\": \"Linguaxe dedución\",\n      \"titlecase_hyphens\": \"Maiúsculas só na primeira palabra con guións\",\n      \"lowercase_hyphens\": \"Minúsculas con guións\",\n      \"lowercase_spaces\": \"Minúsculas con espazos\",\n      \"inference_language_description\": \"Elixe a lingua para as etiquetas e os resumos xerados pola IA.\",\n      \"tag_style_description\": \"Elixe como se deben formatar as etiquetas xeradas automaticamente.\",\n      \"auto_tagging_description\": \"Xera automaticamente etiquetas para os teus marcadores usando a intelixencia artificial.\",\n      \"camelCase\": \"camelCase (a primeira palabra en minúsculas e as seguintes en maiúsculas)\",\n      \"auto_summarization\": \"Resumo automático\",\n      \"no_preference\": \"Sen preferencias\",\n      \"curated_tags\": \"Etiquetas seleccionadas\",\n      \"curated_tags_description\": \"De xeito opcional, restrinxe o etiquetado da IA para que só utilice etiquetas desta lista. Cando non hai etiquetas seleccionadas, a IA xera etiquetas libremente.\",\n      \"curated_tags_updated\": \"Etiquetas seleccionadas actualizadas correctamente!\",\n      \"curated_tags_update_failed\": \"Non se puideron actualizar as etiquetas seleccionadas\"\n    },\n    \"feeds\": {\n      \"rss_subscriptions\": \"Subscricións RSS\",\n      \"add_a_subscription\": \"Engadir subscrición\",\n      \"feed_enabled\": \"Fonte RSS activada\",\n      \"feed_disabled\": \"Fonte RSS desactivada\"\n    },\n    \"api_keys\": {\n      \"api_keys\": \"Chaves APIs\",\n      \"new_api_key\": \"Crear chave API\",\n      \"new_api_key_desc\": \"Ponlle a chave API un nome único\",\n      \"key_success\": \"Chave creada correctamente\",\n      \"key_success_please_copy\": \"Copia a chave e garda a nun lugar seguro. Unha vez cerres este mensaxe, non o poderás volver a ver.\",\n      \"regenerate_api_key\": \"Rexenerar a chave de API\",\n      \"key_regenerated\": \"A chave rexenerouse correctamente\",\n      \"key_regenerated_please_copy\": \"Copia a nova chave e gárdaa nalgún lugar seguro. Revogouse a chave antiga e xa non funcionará.\",\n      \"regenerate_warning\": \"Seguro que queres rexenerar a clave de API \\\"{{name}}\\\"?\",\n      \"regenerate_confirmation\": \"Isto revogará a clave actual e xerará unha nova. Calquera aplicación que use a clave actual deixará de funcionar.\"\n    },\n    \"broken_links\": {\n      \"broken_links\": \"Ligazóns rotos\",\n      \"last_crawled_at\": \"Indexado por ultima vez\",\n      \"crawling_status\": \"Estado da indexación\",\n      \"crawling_failed\": \"Indexado errado\"\n    },\n    \"webhooks\": {\n      \"delete_webhook\": \"Borrar o webhook\",\n      \"delete_webhook_confirmation\": \"Seguro que queres eliminar este webhook?\",\n      \"edit_webhook\": \"Editar webhook\",\n      \"webhook_url\": \"URL do webhook\",\n      \"edit_auth_token\": \"Editar o token de autenticación\",\n      \"description\": \"Podes usar webhooks para activar accións cando se crean, cambian ou rastrexan marcadores.\",\n      \"events\": {\n        \"title\": \"Eventos\",\n        \"crawled\": \"Rastrexado\",\n        \"created\": \"Creado\",\n        \"edited\": \"Editado\"\n      },\n      \"auth_token\": \"Token de autenticación\",\n      \"add_auth_token\": \"Engadir token de autenticación\",\n      \"create_webhook\": \"Crear webhook\",\n      \"webhooks\": \"Webhooks\"\n    },\n    \"manage_assets\": {\n      \"asset_link\": \"Ligazón do recurso\",\n      \"delete_asset\": \"Eliminar recurso\",\n      \"delete_asset_confirmation\": \"Seguro que queres eliminar este recurso?\",\n      \"manage_assets\": \"Xestionar recursos\",\n      \"no_assets\": \"Aínda non tes ningún recurso.\",\n      \"asset_type\": \"Tipo de recurso\",\n      \"bookmark_link\": \"Ligazón do marcador\"\n    },\n    \"rules\": {\n      \"rules\": \"Motor de regras\",\n      \"rule_name\": \"Nome da regra\",\n      \"description\": \"Podes usar regras para activar accións cando se activa un evento.\",\n      \"ceate_rule\": \"Crear regra\",\n      \"edit_rule\": \"Editar regra\",\n      \"save_rule\": \"Gardar regra\",\n      \"delete_rule\": \"Eliminar regra\",\n      \"delete_rule_confirmation\": \"Seguro que queres eliminar esta regra?\",\n      \"whenever\": \"Sempre que ...\",\n      \"if\": \"Se ...\",\n      \"enter_rule_name\": \"Introduza o nome da regra\",\n      \"describe_what_this_rule_does\": \"Describe o que fai esta regra\",\n      \"rule_has_been_created\": \"Creouse a regra!\",\n      \"rule_has_been_updated\": \"A regra actualizouse!\",\n      \"rule_has_been_deleted\": \"Eliminouse a regra!\",\n      \"no_rules_created_yet\": \"Aínda non se crearon regras\",\n      \"create_your_first_rule\": \"Crea a túa primeira regra para automatizar o teu fluxo de traballo\",\n      \"conditions_types\": {\n        \"always\": \"Sempre\",\n        \"url_contains\": \"O URL contén\",\n        \"imported_from_feed\": \"Importado da fonte\",\n        \"bookmark_type_is\": \"O tipo de marcador é\",\n        \"has_tag\": \"Ten etiqueta\",\n        \"is_favourited\": \"Está marcado como favorito\",\n        \"is_archived\": \"Está arquivado\",\n        \"and\": \"Todo o seguinte é certo\",\n        \"or\": \"Calquera das seguintes é verdadeira\",\n        \"url_does_not_contain\": \"O URL non contén\",\n        \"title_contains\": \"O título contén\",\n        \"title_does_not_contain\": \"O título non contén\"\n      },\n      \"actions_types\": {\n        \"add_tag\": \"Engadir etiqueta\",\n        \"remove_tag\": \"Eliminar etiqueta\",\n        \"add_to_list\": \"Engadir á lista\",\n        \"remove_from_list\": \"Eliminar da lista\",\n        \"download_full_page_archive\": \"Descargar o arquivo da páxina completa\",\n        \"favourite_bookmark\": \"Marcar marcador como favorito\",\n        \"archive_bookmark\": \"Gardar marcador\"\n      },\n      \"events_types\": {\n        \"bookmark_added\": \"Engadiuse un marcador\",\n        \"tag_added\": \"Esta etiqueta engádese a un marcador\",\n        \"tag_removed\": \"Esta etiqueta elimínase dun marcador\",\n        \"added_to_list\": \"Engádese un marcador a esta lista\",\n        \"removed_from_list\": \"Eliminouse un marcador desta lista\",\n        \"favourited\": \"Un marcador está marcado como favorito\",\n        \"archived\": \"Un marcador gárdase\"\n      }\n    },\n    \"stats\": {\n      \"usage_statistics\": \"Estatísticas de uso\",\n      \"insights_description\": \"Información sobre os teus hábitos e colección de marcadores\",\n      \"failed_to_load\": \"Non se puideron cargar as estatísticas\",\n      \"overview\": {\n        \"total_bookmarks\": \"Total de marcadores\",\n        \"all_saved_items\": \"Todos os elementos gardados\",\n        \"favorites\": \"Favoritos\",\n        \"starred_bookmarks\": \"Marcadores destacados\",\n        \"archived\": \"Arquivado\",\n        \"archived_items\": \"Elementos arquivados\",\n        \"tags\": \"Etiquetas\",\n        \"unique_tags_created\": \"Etiquetas únicas creadas\",\n        \"lists\": \"Listas\",\n        \"bookmark_collections\": \"Coleccións de marcadores\",\n        \"highlights\": \"Destacados\",\n        \"text_highlights\": \"Resaltados de texto\",\n        \"storage_used\": \"Almacenamento usado\",\n        \"total_asset_storage\": \"Almacenamento total de recursos\",\n        \"this_month\": \"Este mes\",\n        \"bookmarks_added\": \"Marcadores engadidos\"\n      },\n      \"bookmark_types\": {\n        \"title\": \"Tipos de marcadores\",\n        \"links\": \"Ligazóns\",\n        \"text_notes\": \"Notas de texto\",\n        \"assets\": \"Recursos\"\n      },\n      \"recent_activity\": {\n        \"title\": \"Actividade recente\",\n        \"this_week\": \"Esta semana\",\n        \"this_month\": \"Este mes\",\n        \"this_year\": \"Este ano\"\n      },\n      \"top_domains\": {\n        \"title\": \"Dominios principais\",\n        \"no_domains_found\": \"Non se atoparon dominios\"\n      },\n      \"most_used_tags\": {\n        \"title\": \"Etiquetas máis usadas\",\n        \"no_tags_found\": \"Non se atoparon etiquetas\"\n      },\n      \"activity_patterns\": {\n        \"activity_by_hour\": \"Actividade por hora\",\n        \"activity_by_day\": \"Actividade por día\"\n      },\n      \"storage_breakdown\": {\n        \"title\": \"Distribución do almacenamento\"\n      },\n      \"bookmark_sources\": {\n        \"title\": \"Fonte de marcadores\",\n        \"empty\": \"Non hai datos fonte dispoñibles\"\n      }\n    },\n    \"subscription\": {\n      \"subscription\": \"Subscrición\",\n      \"manage_subscription\": \"Xestiona a túa subscrición e información de facturación\",\n      \"current_plan\": \"Plan actual\",\n      \"billing_period\": \"Período de facturación\",\n      \"paid_plan\": \"Plan de pago\",\n      \"unlock_bigger_quota\": \"Desbloquea unha cota maior e apoia o proxecto\",\n      \"subscribe_now\": \"Subscríbete agora\",\n      \"manage_billing\": \"Xestionar a facturación\",\n      \"subscription_canceled\": \"A túa subscrición foi cancelada e rematará o {{date}}. Podes volver subscribirte en calquera momento.\",\n      \"usage_quotas\": \"Uso e cotas\",\n      \"track_usage\": \"Fai un seguimento do teu uso actual fronte aos límites do teu plan\",\n      \"total_bookmarks_saved\": \"Total de marcadores gardados\",\n      \"assets_file_storage\": \"Almacenamento de ficheiros e recursos\",\n      \"unlimited_usage\": \"Uso ilimitado\",\n      \"quota_limit_reached\": \"Alcanzouse o límite de cota\",\n      \"approaching_quota_limit\": \"Achegándose ao límite da cota\",\n      \"loading_usage\": \"Cargando información de uso...\",\n      \"free\": \"De balde\",\n      \"paid\": \"De pago\"\n    },\n    \"import_sessions\": {\n      \"title\": \"Importar sesións\",\n      \"description\": \"Consulta e xestiona as túas sesións de importación masiva. As sesións créanse automaticamente cando importas marcadores.\",\n      \"load_error\": \"Non se puido cargar as sesións de importación\",\n      \"no_sessions\": \"Aínda non hai sesións de importación\",\n      \"no_sessions_detail\": \"As sesións de importación aparecerán aquí automaticamente cando importes marcadores\",\n      \"created_at\": \"Creado {{time}}\",\n      \"progress\": \"Progreso\",\n      \"status\": {\n        \"pending\": \"Pendente\",\n        \"in_progress\": \"En progreso\",\n        \"completed\": \"Completado\",\n        \"failed\": \"Fallou\",\n        \"processing\": \"Procesando\",\n        \"staging\": \"Preparando\",\n        \"running\": \"Executando\",\n        \"paused\": \"En pausa\"\n      },\n      \"badges\": {\n        \"pending\": \"{{count}} pendentes\",\n        \"processing\": \"{{count}} procesando\",\n        \"completed\": \"{{count}} completadas\",\n        \"failed\": \"{{count}} fallidas\"\n      },\n      \"imported_to\": \"Importado a:\",\n      \"view_list\": \"Ver lista\",\n      \"delete_dialog_title\": \"Eliminar sesión de importación\",\n      \"delete_dialog_description\": \"Seguro que queres borrar «{{name}}»? Esta acción non se pode desfacer. Os marcadores en si non se borrarán.\",\n      \"delete_session\": \"Eliminar sesión\",\n      \"pause_session\": \"Pausar\",\n      \"resume_session\": \"Reanudar\",\n      \"view_details\": \"Ver detalles\",\n      \"detail\": {\n        \"page_title\": \"Ver detalles da sesión de importación\",\n        \"back_to_import\": \"Volver á importación\",\n        \"filter_all\": \"Todo\",\n        \"filter_accepted\": \"Aceptado\",\n        \"filter_rejected\": \"Rexeitado\",\n        \"filter_duplicates\": \"Duplicados\",\n        \"filter_pending\": \"Pendentes\",\n        \"table_title\": \"Título/URL\",\n        \"table_type\": \"Tipo\",\n        \"table_result\": \"Resultado\",\n        \"table_reason\": \"Motivo\",\n        \"table_bookmark\": \"Marcador\",\n        \"result_accepted\": \"Aceptado\",\n        \"result_rejected\": \"Rexeitado\",\n        \"result_skipped_duplicate\": \"Duplicado\",\n        \"result_pending\": \"Pendentes\",\n        \"result_processing\": \"Procesando\",\n        \"no_results\": \"Non se atoparon resultados para este filtro.\",\n        \"view_bookmark\": \"Ver o marcador\",\n        \"load_more\": \"Cargar máis\",\n        \"no_title\": \"Sen título\"\n      }\n    },\n    \"backups\": {\n      \"backups\": \"Copias de seguridade\",\n      \"page_title\": \"Copias de seguridade\",\n      \"page_description\": \"Crea e xestiona copias de seguridade dos teus marcadores automaticamente. As copias de seguridade comprímense e gárdanse de forma segura.\",\n      \"configuration\": {\n        \"title\": \"Configuración das copias de seguridade\",\n        \"enable_automatic_backups\": \"Activar as copias de seguridade automáticas\",\n        \"enable_automatic_backups_description\": \"Crear automaticamente copias de seguridade dos teus marcadores\",\n        \"backup_frequency\": \"Frecuencia das copias de seguridade\",\n        \"backup_frequency_description\": \"Cada canto tempo se deberían crear as copias de seguridade\",\n        \"retention_period\": \"Período de retención (días)\",\n        \"retention_period_description\": \"Cantos días se deben gardar as copias de seguridade antes de seren eliminadas\",\n        \"frequency\": {\n          \"daily\": \"Diariamente\",\n          \"weekly\": \"Semanalmente\"\n        },\n        \"select_frequency\": \"Seleccione a frecuencia\",\n        \"save_settings\": \"Gardar a configuración\"\n      },\n      \"list\": {\n        \"title\": \"As túas copias de seguridade\",\n        \"create_backup_now\": \"Crear unha copia de seguridade agora\",\n        \"no_backups\": \"Aínda non tes ningunha copia de seguridade. Activa as copias de seguridade automáticas ou crea unha manualmente.\",\n        \"table\": {\n          \"created_at\": \"Creado en\",\n          \"bookmarks\": \"Marcadores\",\n          \"size\": \"Tamaño\",\n          \"status\": \"Estado\",\n          \"actions\": \"Accións\"\n        },\n        \"status\": {\n          \"success\": \"Éxito\",\n          \"failed\": \"Fallou\",\n          \"pending\": \"Pendente\"\n        },\n        \"actions\": {\n          \"download_backup\": \"Descargar copia de seguranza\",\n          \"delete_backup\": \"Borrar copia de seguranza\"\n        }\n      },\n      \"dialogs\": {\n        \"delete_backup_title\": \"Queres borrar a copia de seguranza?\",\n        \"delete_backup_description\": \"Estás seguro de que queres borrar esta copia de seguranza? Esta acción non se pode desfacer.\"\n      },\n      \"toasts\": {\n        \"backup_queued\": \"O traballo de copia de seguranza foi posto en cola! Será procesado en breve.\",\n        \"backup_deleted\": \"Copia de seguranza borrada!\"\n      }\n    }\n  },\n  \"highlights\": {\n    \"no_highlights\": \"Inda non tés nada destacado.\"\n  },\n  \"admin\": {\n    \"admin_settings\": \"Axustes de Administrador\",\n    \"server_stats\": {\n      \"server_stats\": \"Estatísticas do servidor\",\n      \"total_users\": \"Número de usuarios\",\n      \"total_bookmarks\": \"Número de marcadores\",\n      \"server_version\": \"Versión do servidor\"\n    },\n    \"background_jobs\": {\n      \"background_jobs\": \"Operacións en segundo plano\",\n      \"crawler_jobs\": \"Traballos de crawling\",\n      \"indexing_jobs\": \"Traballos de indexación\",\n      \"inference_jobs\": \"Traballos de inferencia\",\n      \"tidy_assets_jobs\": \"Traballos de refinación\",\n      \"job\": \"Traballo\",\n      \"queued\": \"Na cola\",\n      \"pending\": \"Pendente\",\n      \"failed\": \"Erro\",\n      \"asset_preprocessing_jobs\": \"Traballos de preprocesamento de recursos\",\n      \"feed_jobs\": \"Traballos de fontes RSS\",\n      \"webhook_jobs\": \"Traballos de webhook\",\n      \"video_jobs\": \"Traballos de descarga de vídeo\",\n      \"jobs\": {\n        \"crawler\": {\n          \"title\": \"Traballos do rastreador\",\n          \"description\": \"Rastrexo web e extracción de contido de URL\"\n        },\n        \"inference\": {\n          \"title\": \"Traballos de inferencia\",\n          \"description\": \"Etiquetado e resumo de contido impulsados por intelixencia artificial\"\n        },\n        \"indexing\": {\n          \"title\": \"Traballos de indexación\",\n          \"description\": \"Actualizacións do índice de busca\"\n        },\n        \"asset_preprocessing\": {\n          \"title\": \"Traballos de preprocesamento de recursos\",\n          \"description\": \"Preprocesamento de imaxes e documentos (capturas de pantalla, extracción de texto, etc.)\"\n        },\n        \"tidy_assets\": {\n          \"title\": \"Traballos de activos ordenados\",\n          \"description\": \"Limpeza de activos e optimización de almacenamento\"\n        },\n        \"video\": {\n          \"title\": \"Traballos de descarga de vídeo\",\n          \"description\": \"Extracción e descarga de vídeo\"\n        },\n        \"webhook\": {\n          \"title\": \"Traballos de Webhook\",\n          \"description\": \"Notificacións de webhook externas\"\n        },\n        \"feed\": {\n          \"title\": \"Traballos de fonte RSS\",\n          \"description\": \"Procesamento de fontes RSS e actualizacións de contido\"\n        },\n        \"admin_maintenance\": {\n          \"title\": \"Traballos de mantemento do administrador\",\n          \"description\": \"Limpeza administrativa e mantemento de activos\"\n        }\n      },\n      \"monitor_and_manage\": \"Supervisar e xestionar as colas de traballos en segundo plano e as tarefas de procesamento do sistema\",\n      \"active\": \"Activas\",\n      \"available_actions\": \"Accións dispoñibles\",\n      \"status\": {\n        \"title\": \"Comprensión dos estados dos traballos\",\n        \"queued\": {\n          \"title\": \"En cola\",\n          \"description\": \"Tarefas en cola á espera de ser procesadas. Iniciaranse automaticamente cando os recursos estean dispoñibles.\"\n        },\n        \"unprocessed\": {\n          \"title\": \"Sen procesar\",\n          \"description\": \"Marcadores que aínda non foron procesados. É moi probable que xa estean en cola para o seu procesamento, se non, quizais teñas que volver poñelos en cola manualmente.\"\n        },\n        \"failed\": {\n          \"title\": \"Fallido\",\n          \"description\": \"Marcadores que atoparon erros durante o procesamento. Estes poden necesitar atención manual.\"\n        }\n      },\n      \"actions\": {\n        \"recrawl_failed_links_only\": \"Volver rastrexar só as ligazóns erradas\",\n        \"recrawl_all_links\": \"Volver rastrexar todas as ligazóns\",\n        \"without_inference\": \"Sen inferencia\",\n        \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Volver xerar etiquetas da IA só para os marcadores errados\",\n        \"regenerate_ai_tags_for_all_bookmarks\": \"Volver xerar etiquetas da IA para todos os marcadores\",\n        \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Volver xerar resumos da IA só para os marcadores errados\",\n        \"regenerate_ai_summaries_for_all_bookmarks\": \"Volver xerar resumos da IA para todos os marcadores\",\n        \"reindex_all_bookmarks\": \"Volver indexar todos os marcadores\",\n        \"clean_assets\": \"Limpar os recursos colgantes e volver sincronizar os metadatos\",\n        \"reprocess_assets_fix_mode\": \"Volver procesar os recursos sen procesar\",\n        \"migrate_large_link_html_content\": \"Mover o contido HTML en liña grande aos recursos\",\n        \"recrawl_pending_links_only\": \"Volver a rastrexar só as ligazóns pendentes\",\n        \"regenerate_ai_tags_for_pending_bookmarks_only\": \"Volver a xerar as etiquetas de intelixencia artificial só para os marcadores pendentes\",\n        \"regenerate_ai_summaries_for_pending_bookmarks_only\": \"Só volver a xerar os resumos de IA para os marcadores pendentes\"\n      }\n    },\n    \"actions\": {\n      \"recrawl_failed_links_only\": \"Recrawlear só ligazóns erradas\",\n      \"recrawl_all_links\": \"Recrawlear todas as ligazóns\",\n      \"without_inference\": \"Sen inferencia\",\n      \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Rexenerar etiquetas IA so en marcadores errados\",\n      \"regenerate_ai_tags_for_all_bookmarks\": \"Rexenerar etiquetas IA para todos os marcadores\",\n      \"reindex_all_bookmarks\": \"Reindexar marcadores\",\n      \"compact_assets\": \"Optimizar multimedia\",\n      \"reprocess_assets_fix_mode\": \"Reprocesar assets (modo fixo)\",\n      \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Volver xerar os resumos da IA só para os marcadores fallidos\",\n      \"regenerate_ai_summaries_for_all_bookmarks\": \"Volver xerar os resumos da IA para todos os marcadores\"\n    },\n    \"users_list\": {\n      \"users_list\": \"Listado de usuarios\",\n      \"create_user\": \"Crear usuario\",\n      \"change_role\": \"Cambiar rol\",\n      \"reset_password\": \"Reiniciar contrasinal\",\n      \"delete_user\": \"Borrar usuario\",\n      \"local_user\": \"Usuario local\",\n      \"confirm_password\": \"Confirmar contrasinal\",\n      \"num_bookmarks\": \"Núm. Marcadores\",\n      \"asset_sizes\": \"Espacio utilizado\",\n      \"delete_user_confirm_description\": \"Seguro que queres eliminar o usuario \\\"{{name}}\\\"?\",\n      \"unlimited\": \"Ilimitado\"\n    },\n    \"service_connections\": {\n      \"title\": \"Conexións de servizo\",\n      \"description\": \"Monitorizar a saúde e a conectividade das dependencias do sistema externo\",\n      \"search_engine\": \"Motor de busca\",\n      \"browser\": \"Navegador\",\n      \"queue_system\": \"Sistema de cola\",\n      \"status\": {\n        \"not_configured\": \"Sen configurar\",\n        \"connected\": \"Conectado\",\n        \"disconnected\": \"Desconectado\"\n      }\n    },\n    \"admin_tools\": {\n      \"admin_tools\": \"Ferramentas de administración\",\n      \"bookmark_debugger\": \"Depurador de marcadores\",\n      \"bookmark_id\": \"ID de marcador\",\n      \"bookmark_id_placeholder\": \"Introduza o ID do marcador\",\n      \"lookup\": \"Buscar\",\n      \"debug_info\": \"Información de depuración\",\n      \"basic_info\": \"Información básica\",\n      \"status\": \"Estado\",\n      \"content\": \"Contido\",\n      \"html_preview\": \"Vista previa HTML (primeiros 1000 caracteres)\",\n      \"summary\": \"Resumo\",\n      \"url\": \"URL\",\n      \"source_url\": \"URL fonte\",\n      \"asset_type\": \"Tipo de activo\",\n      \"file_name\": \"Nome do ficheiro\",\n      \"owner_user_id\": \"ID de usuario propietario\",\n      \"tagging_status\": \"Estado de etiquetaxe\",\n      \"summarization_status\": \"Estado de resumo\",\n      \"crawl_status\": \"Estado de rastrexo\",\n      \"crawl_status_code\": \"Código de estado HTTP\",\n      \"crawled_at\": \"Rastrexado en\",\n      \"recrawl\": \"Volver rastrexar\",\n      \"reindex\": \"Volver indexar\",\n      \"retag\": \"Volver etiquetar\",\n      \"resummarize\": \"Volver resumir\",\n      \"bookmark_not_found\": \"Marcador non atopado\",\n      \"action_success\": \"A acción completouse correctamente\",\n      \"action_failed\": \"A acción fallou\",\n      \"recrawl_queued\": \"O traballo de volver rastrexar foi posto en cola\",\n      \"reindex_queued\": \"O traballo de volver indexar foi posto en cola\",\n      \"retag_queued\": \"O traballo de volver etiquetar foi posto en cola\",\n      \"resummarize_queued\": \"O traballo de volver resumir foi posto en cola\",\n      \"view\": \"Ver\",\n      \"fetch_error\": \"Erro ao obter o marcador\"\n    }\n  },\n  \"options\": {\n    \"dark_mode\": \"Modo escuro\",\n    \"light_mode\": \"Modo claro\",\n    \"apps_extensions\": \"Aplicacións e extensións\",\n    \"documentation\": \"Documentación\",\n    \"follow_us_on_x\": \"Síguenos en X\"\n  },\n  \"lists\": {\n    \"all_lists\": \"Todas as listas\",\n    \"favourites\": \"Favoritos\",\n    \"new_list\": \"Nova lista\",\n    \"new_nested_list\": \"Nova lista aniñada\",\n    \"smart_list\": \"Lista intelixente\",\n    \"search_query\": \"Consulta de busca\",\n    \"manual_list\": \"Lista manual\",\n    \"parent_list\": \"Lista de pais\",\n    \"no_parent\": \"Sen pai\",\n    \"list_type\": \"Tipo de lista\",\n    \"search_query_help\": \"Aprende máis sobre a linguaxe de consulta de busca.\",\n    \"edit_list\": \"Editar lista\",\n    \"merge_list\": \"Combinar lista\",\n    \"destination_list\": \"Lista de destino\",\n    \"delete_after_merge\": \"Eliminar a lista orixinal despois da combinación\",\n    \"no_destination\": \"Sen destino\",\n    \"description\": \"Descrición (Opcional)\",\n    \"share_list\": \"Compartir lista\",\n    \"rss\": {\n      \"title\": \"Fonte RSS\",\n      \"description\": \"Activar unha fonte RSS para esta lista\",\n      \"feed_url\": \"URL da fonte RSS\"\n    },\n    \"public_list\": {\n      \"title\": \"Lista pública\",\n      \"description\": \"Permitir que outros vexan esta lista\",\n      \"share_link\": \"Compartir ligazón\"\n    },\n    \"delete_list\": {\n      \"title\": \"Eliminar lista\",\n      \"description\": \"Eliminar unha lista non elimina ningún marcador desa lista.\",\n      \"delete_children\": \"Eliminar as listas fillas (recursivamente)\",\n      \"delete_children_description\": \"Se non está marcada, todas as listas fillas directas converteranse en listas raíz\"\n    },\n    \"shared\": \"Compartido\",\n    \"collaborators\": {\n      \"manage\": \"Xestionar colaboradores\",\n      \"view\": \"Ver os colaboradores\",\n      \"collaborators\": \"Colaboradores\",\n      \"add\": \"Engadir un colaborador\",\n      \"current\": \"Colaboradores actuais\",\n      \"enter_email\": \"Introduza o enderezo de correo electrónico\",\n      \"please_enter_email\": \"Introduza un enderezo de correo electrónico\",\n      \"added_successfully\": \"Colaborador engadido correctamente\",\n      \"failed_to_add\": \"Non se puido engadir o colaborador\",\n      \"removed\": \"Colaborador eliminado\",\n      \"failed_to_remove\": \"Non se puido eliminar o colaborador\",\n      \"role_updated\": \"Función actualizada\",\n      \"failed_to_update_role\": \"Non se puido actualizar o rol\",\n      \"viewer\": \"Visor\",\n      \"editor\": \"Editor\",\n      \"owner\": \"Propietario\",\n      \"viewer_description\": \"Pode ver os marcadores na lista\",\n      \"editor_description\": \"Pode engadir e eliminar marcadores\",\n      \"no_collaborators\": \"Aínda non hai colaboradores. Engade alguén para comezar a colaborar!\",\n      \"no_collaborators_readonly\": \"Non hai colaboradores nesta lista.\",\n      \"people_with_access\": \"Xente que ten acceso a esta lista\",\n      \"add_or_remove\": \"Engadir ou eliminar xente que pode acceder a esta lista\",\n      \"invitation_sent\": \"Invitación enviada correctamente\",\n      \"invitation_revoked\": \"Invitación revogada\",\n      \"failed_to_revoke\": \"Produciuse un erro ao revogar a invitación\",\n      \"pending\": \"Pendente\",\n      \"revoke\": \"Revogar\",\n      \"declined\": \"Rexeitada\"\n    },\n    \"leave_list\": {\n      \"title\": \"Deixar a lista\",\n      \"confirm_message\": \"Seguro que queres deixar {{icon}} {{name}}?\",\n      \"warning\": \"Xa non poderás nin ver nin acceder aos marcadores desta lista. O propietario da lista pode volver a engadirte se é necesario.\",\n      \"action\": \"Deixar a lista\",\n      \"success\": \"Deixaches «{{icon}} {{name}}»\"\n    },\n    \"invitations\": {\n      \"pending\": \"Invitacións pendentes\",\n      \"description\": \"Revisa e responde ás invitacións de colaboración na lista\",\n      \"invited_by\": \"Invitado por\",\n      \"accept\": \"Aceptar\",\n      \"decline\": \"Rexeitar\",\n      \"accepted\": \"Invitación aceptada\",\n      \"declined\": \"Invitación rexeitada\",\n      \"failed_to_accept\": \"Produciuse un erro ao aceptar a invitación\",\n      \"failed_to_decline\": \"Produciuse un erro ao rexeitar a invitación\"\n    },\n    \"shared_lists\": \"Listas compartidas\"\n  },\n  \"preview\": {\n    \"view_original\": \"Ver orixinal\",\n    \"cached_content\": \"Contendo cacheado\",\n    \"reader_view\": \"Vista de lectura\",\n    \"tabs\": {\n      \"content\": \"Contido\",\n      \"details\": \"Detalles\"\n    },\n    \"archive_info\": \"É posible que os arquivos non se representen correctamente en liña se requiren Javascript. Para obter os mellores resultados, <1>descárgueo e ábreo no navegador</1>.\",\n    \"fetch_error_title\": \"Contido non dispoñible\",\n    \"fetch_error_description\": \"Non puidemos obter o contido desta ligazón. A páxina pode estar protexida, requirir autenticación ou non estar dispoñible temporalmente.\",\n    \"crawling_in_progress\": \"Obtendo o contido da páxina…\",\n    \"continue_reading\": \"Continúa onde o deixaches\",\n    \"continue_reading_percent\": \"Continúa onde o deixaches ({{percent}}%)\",\n    \"continue_button\": \"Continúa\"\n  },\n  \"editor\": {\n    \"quickly_focus\": \"Podes enfocar este campo pulsando ⌘ + E\",\n    \"multiple_urls_dialog_title\": \"¿Importar URLs como marcadores independentes?\",\n    \"multiple_urls_dialog_desc\": \"A entrada contén varias URLs en liñas independentes. ¿Queres importalas como distintos marcadores?\",\n    \"import_as_text\": \"Importar como marcador de texto\",\n    \"import_as_separate_bookmarks\": \"Importar como distintos marcadores\",\n    \"placeholder\": \"Pega un enlace ou imaxe, escribe unha nota ou arrastra unha imaxe aquí…\",\n    \"new_item\": \"NOVA ENTRADA\",\n    \"disabled_submissions\": \"Entradas deshabilitadas\",\n    \"text_toolbar\": {\n      \"undo\": \"Desfacer\",\n      \"redo\": \"Refacer\",\n      \"bold\": \"Grosa\",\n      \"italic\": \"Cursiva\",\n      \"underline\": \"Subliñado\",\n      \"strikethrough\": \"Tachado\",\n      \"code\": \"Código\",\n      \"highlight\": \"Destacar\",\n      \"align_left\": \"Aliñar á esquerda\",\n      \"align_center\": \"Aliñar ao centro\",\n      \"align_right\": \"Aliñar á dereita\",\n      \"markdown_shortcuts\": {\n        \"label\": \"Atallos de Markdown\",\n        \"heading\": {\n          \"label\": \"Cabeceira\",\n          \"example\": \"# H1, ## H2, ### H3\"\n        },\n        \"bold\": {\n          \"label\": \"Grosa\",\n          \"example\": \"**texto** o CTRL+b\"\n        },\n        \"italic\": {\n          \"label\": \"Cursiva\",\n          \"example\": \"*texto*, _texto_ o CTRL+i\"\n        },\n        \"blockquote\": {\n          \"label\": \"Cita en Bloque\",\n          \"example\": \"> Texto\"\n        },\n        \"ordered_list\": {\n          \"label\": \"Lista ordenada\",\n          \"example\": \"1. Texto\"\n        },\n        \"unordered_list\": {\n          \"label\": \"Lista sen orden\",\n          \"example\": \"- Texto\"\n        },\n        \"inline_code\": {\n          \"label\": \"Código en liña\",\n          \"example\": \"`Texto`\"\n        },\n        \"block_code\": {\n          \"label\": \"Código en bloque\",\n          \"example\": \"``` + espacio\"\n        }\n      }\n    },\n    \"placeholder_v2\": \"Pega unha ligazón, escribe unha nota ou solta unha imaxe…\"\n  },\n  \"search\": {\n    \"is_not_archived\": \"Non está arquivado\",\n    \"is_in_any_list\": \"Está en calquera lista\",\n    \"does_not_have_tag\": \"Non ten etiqueta\",\n    \"full_text_search\": \"Busca de texto completo\",\n    \"has_no_tags\": \"Non ten etiqueta\",\n    \"created_on_or_before\": \"Creado en ou antes\",\n    \"not_created_on_or_before\": \"Non creado en ou antes\",\n    \"url_does_not_contain\": \"O URL non contén\",\n    \"is_in_list\": \"Está na lista\",\n    \"is_not_in_list\": \"Non está na lista\",\n    \"has_tag\": \"Ten etiqueta\",\n    \"type_is\": \"O tipo é\",\n    \"type_is_not\": \"O tipo non é\",\n    \"and\": \"E\",\n    \"or\": \"Ou\",\n    \"is_favorited\": \"Está marcado como favorito\",\n    \"is_not_favorited\": \"Non está entre os favoritos\",\n    \"is_archived\": \"Está arquivado\",\n    \"is_not_in_any_list\": \"Non está en ningunha lista\",\n    \"created_on_or_after\": \"Creado en ou despois do\",\n    \"not_created_on_or_after\": \"Non creado en ou despois de\",\n    \"url_contains\": \"O URL contén\",\n    \"has_any_tag\": \"Ten algunha etiqueta\",\n    \"is_from_feed\": \"É dunha fonte RSS\",\n    \"is_not_from_feed\": \"Non é dunha fonte RSS\",\n    \"created_within\": \"Creado dentro de\",\n    \"created_earlier_than\": \"Creado antes de\",\n    \"day_s\": \" Día(s)\",\n    \"week_s\": \" Semana(s)\",\n    \"month_s\": \" Mes(es)\",\n    \"year_s\": \" Ano(s)\",\n    \"day_s_ago\": \" Hai días\",\n    \"week_s_ago\": \" Hai semanas\",\n    \"month_s_ago\": \" Hai meses\",\n    \"year_s_ago\": \" Hai anos\",\n    \"history\": \"Buscas recentes\",\n    \"title_contains\": \"O título contén\",\n    \"title_does_not_contain\": \"O título non contén\",\n    \"is_broken_link\": \"Ten Ligazón Rota\",\n    \"tags\": \"Etiquetas\",\n    \"no_suggestions\": \"Sen suxestións\",\n    \"filters\": \"Filtros\",\n    \"is_not_broken_link\": \"Ten Ligazón Válida\",\n    \"lists\": \"Listas\",\n    \"feeds\": \"Fontes\",\n    \"is_from_source\": \"Orixe é\",\n    \"is_not_from_source\": \"A orixe non é\"\n  },\n  \"dialogs\": {\n    \"bookmarks\": {\n      \"delete_confirmation_title\": \"Queres eliminar o marcador?\",\n      \"delete_confirmation_description\": \"Seguro que queres borrar este marcador?\"\n    }\n  },\n  \"banners\": {\n    \"no_bookmarks\": {\n      \"title\": \"Aínda non hai marcadores\",\n      \"description\": \"Garda artigos, ligazóns e páxinas interesantes para acceder a eles rapidamente máis tarde.\"\n    }\n  },\n  \"bookmark_editor\": {\n    \"title\": \"Editar marcador\",\n    \"subtitle\": \"Fai cambios nos detalles do marcador. Cando remates, fai clic en gardar.\",\n    \"author\": \"Autor\",\n    \"publisher\": \"Editor\",\n    \"date_published\": \"Data de publicación\",\n    \"pick_a_date\": \"Escolle unha data\",\n    \"save_changes\": \"Gardar cambios\",\n    \"extracted_content\": \"Contido extraído\"\n  },\n  \"view_options\": {\n    \"title\": \"Ver Opcións\",\n    \"layout\": \"Disposición\",\n    \"columns\": \"Columnas\",\n    \"display_options\": \"Opcións de visualización\",\n    \"show_note_previews\": \"Amosar notas\",\n    \"show_tags\": \"Mostrar etiquetas\",\n    \"show_title\": \"Mostrar título\",\n    \"image_options\": \"Opcións da imaxe\",\n    \"image_fit_cover\": \"Cubrir (encher)\",\n    \"image_fit_contain\": \"Contido (axustar)\"\n  },\n  \"version\": {\n    \"new_release_available\": \"Novas notas da versión dispoñíbeis\",\n    \"whats_new_title\": \"Que hai de novo na v{{version}}\",\n    \"release_notes_description\": \"Aquí están as últimas actualizacións obtidas das notas da versión de GitHub.\",\n    \"loading_release_notes\": \"Cargando as notas da versión…\",\n    \"unable_to_load_release_notes\": \"Non se poden cargar as notas da versión neste momento. Inténtao de novo máis tarde.\",\n    \"no_release_notes\": \"Non se publicaron notas da versión para esta versión.\",\n    \"release_notes_synced\": \"As notas da versión sincronízanse desde GitHub.\",\n    \"view_on_github\": \"Ver en GitHub\"\n  },\n  \"wrapped\": {\n    \"title\": \"O teu {{year}} resumido\",\n    \"subtitle\": \"Un ano en Karakeep\",\n    \"banner\": {\n      \"title\": \"Xa está listo o teu resumo de 2025!\",\n      \"description\": \"Consulta o teu ano en marcadores\",\n      \"view_now\": \"Ver agora\"\n    },\n    \"button\": \"Resumo de 2025\",\n    \"loading\": \"Cargando o teu resumo...\",\n    \"failed_to_load\": \"Non se puideron cargar as estatísticas do teu resumo\",\n    \"sections\": {\n      \"total_saves\": {\n        \"prefix\": \"Gardaches\",\n        \"suffix\": \"elementos este ano\",\n        \"suffix_singular\": \"elemento este ano\"\n      },\n      \"first_bookmark\": {\n        \"title\": \"Comeza a túa viaxe\",\n        \"description\": \"Primeiro gardado de {{year}}:\"\n      },\n      \"top_domains\": \"Os teus sitios principais\",\n      \"top_tags\": \"As túas etiquetas principais\",\n      \"monthly_activity\": \"O teu ano en gardados\",\n      \"most_active_day\": \"O teu día de maior actividade\",\n      \"peak_times\": {\n        \"title\": \"Cando gardas\",\n        \"peak_hour\": \"Hora de maior actividade\",\n        \"peak_day\": \"Día de maior actividade\"\n      },\n      \"how_you_save\": \"Como gardas\",\n      \"what_you_saved\": \"Que gardaches\",\n      \"summary\": {\n        \"favorites\": \"Favoritos\",\n        \"tags_created\": \"Etiquetas creadas\",\n        \"highlights\": \"Momentos salientables\"\n      },\n      \"types\": {\n        \"links\": \"Ligazóns\",\n        \"notes\": \"Notas\",\n        \"assets\": \"Activos\"\n      }\n    },\n    \"footer\": \"Feito con Karakeep\",\n    \"share\": \"Compartir\",\n    \"download\": \"Descargar\",\n    \"close\": \"Pechar\",\n    \"generating\": \"Xerando...\"\n  }\n}\n"
  },
  {
    "path": "apps/web/lib/i18n/locales/hr/translation.json",
    "content": "{\n  \"admin\": {\n    \"users_list\": {\n      \"num_bookmarks\": \"Broj oznaka\",\n      \"reset_password\": \"Ponovno postavljanje lozinke\",\n      \"local_user\": \"Lokalni korisnik\",\n      \"users_list\": \"Popis korisnika\",\n      \"delete_user\": \"Izbriši korisnika\",\n      \"confirm_password\": \"Potvrdi lozinku\",\n      \"create_user\": \"Kreiraj korisnika\",\n      \"change_role\": \"Promijeni ulogu\",\n      \"asset_sizes\": \"Veličine resursa\",\n      \"delete_user_confirm_description\": \"Jesi ziher da želiš izbrisati korisnika \\\"{{name}}\\\"?\",\n      \"unlimited\": \"Neograničeno\"\n    },\n    \"admin_settings\": \"Postavke administratora\",\n    \"server_stats\": {\n      \"server_stats\": \"Statistike servera\",\n      \"total_users\": \"Ukupno korisnika\",\n      \"total_bookmarks\": \"Ukupno oznaka\",\n      \"server_version\": \"Verzija servera\"\n    },\n    \"background_jobs\": {\n      \"background_jobs\": \"Pozadinski zadaci\",\n      \"crawler_jobs\": \"Zadaci pregledavanja\",\n      \"indexing_jobs\": \"Zadaci indeksiranja\",\n      \"job\": \"Zadatak\",\n      \"pending\": \"Na čekanju\",\n      \"queued\": \"U čekanju\",\n      \"failed\": \"Neuspješno\",\n      \"inference_jobs\": \"Zadaci zaključivanja\",\n      \"tidy_assets_jobs\": \"Zadaci čišćenja resursa\",\n      \"video_jobs\": \"Poslovi preuzimanja videozapisa\",\n      \"asset_preprocessing_jobs\": \"Poslovi preprocesiranja resursa\",\n      \"feed_jobs\": \"RSS Feed poslovi\",\n      \"webhook_jobs\": \"Webhook poslovi\",\n      \"jobs\": {\n        \"feed\": {\n          \"title\": \"Poslovi RSS feeda\",\n          \"description\": \"Obrada RSS feeda i ažuriranja sadržaja\"\n        },\n        \"crawler\": {\n          \"title\": \"Poslovi indeksiranja\",\n          \"description\": \"Web crawling i izdvajanje sadržaja iz URL-ova\"\n        },\n        \"inference\": {\n          \"title\": \"Poslovi zaključivanja\",\n          \"description\": \"Označavanje i sažimanje sadržaja pomoću umjetne inteligencije\"\n        },\n        \"indexing\": {\n          \"title\": \"Poslovi indeksiranja\",\n          \"description\": \"Ažuriranja indeksa pretraživanja\"\n        },\n        \"asset_preprocessing\": {\n          \"title\": \"Poslovi preprocesiranja sredstava\",\n          \"description\": \"Pretprocesiranje slika i dokumenata (snimke zaslona, izdvajanje teksta, itd.)\"\n        },\n        \"tidy_assets\": {\n          \"title\": \"Poslovi za sređivanje sredstava\",\n          \"description\": \"Čišćenje sredstava i optimizacija pohrane\"\n        },\n        \"video\": {\n          \"title\": \"Poslovi preuzimanja videozapisa\",\n          \"description\": \"Izdvajanje i preuzimanje videozapisa\"\n        },\n        \"webhook\": {\n          \"title\": \"Webhook poslovi\",\n          \"description\": \"Vanjske webhook obavijesti\"\n        },\n        \"admin_maintenance\": {\n          \"title\": \"Poslovi održavanja administratora\",\n          \"description\": \"Administrativno čišćenje i održavanje imovine\"\n        }\n      },\n      \"monitor_and_manage\": \"Pratite i upravljajte redovima pozadinskih poslova i zadacima obrade sustava\",\n      \"active\": \"Aktivan\",\n      \"available_actions\": \"Dostupne radnje\",\n      \"status\": {\n        \"title\": \"Razumijevanje stanja posla\",\n        \"queued\": {\n          \"title\": \"U redu čekanja\",\n          \"description\": \"Poslovi čekaju u redu za obradu. Automatski će se pokrenuti kad budu dostupni resursi.\"\n        },\n        \"unprocessed\": {\n          \"title\": \"Neobrađeno\",\n          \"description\": \"Oznake koje još nisu obrađene. Najvjerojatnije su već u redu čekanja za obradu, ako ne, možda ćete ih morati ručno ponovno staviti u red čekanja.\"\n        },\n        \"failed\": {\n          \"title\": \"Neuspjelo\",\n          \"description\": \"Oznake koje su naišle na pogreške tijekom obrade. Možda će trebati ručnu intervenciju.\"\n        }\n      },\n      \"actions\": {\n        \"recrawl_failed_links_only\": \"Ponovno indeksiraj samo neuspjele poveznice\",\n        \"recrawl_all_links\": \"Ponovno indeksiraj sve poveznice\",\n        \"without_inference\": \"Bez zaključivanja\",\n        \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Ponovno generiraj AI oznake samo za neuspjele oznake\",\n        \"regenerate_ai_tags_for_all_bookmarks\": \"Ponovno generiraj AI oznake za sve oznake\",\n        \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Ponovno generiraj AI sažetke samo za neuspjele oznake\",\n        \"regenerate_ai_summaries_for_all_bookmarks\": \"Ponovno generiraj AI sažetke za sve oznake\",\n        \"reindex_all_bookmarks\": \"Ponovno indeksiraj sve oznake\",\n        \"clean_assets\": \"Očisti viseću imovinu i ponovno sinkroniziraj metapodatke\",\n        \"reprocess_assets_fix_mode\": \"Ponovno obradi neobrađenu imovinu\",\n        \"migrate_large_link_html_content\": \"Premjesti veliki Inline HTML sadržaj u resurse\",\n        \"recrawl_pending_links_only\": \"Ponovno indeksiraj samo poveznice na čekanju\",\n        \"regenerate_ai_tags_for_pending_bookmarks_only\": \"Generiraj AI oznake samo za knjižne oznake na čekanju\",\n        \"regenerate_ai_summaries_for_pending_bookmarks_only\": \"Generiraj AI sažetke samo za knjižne oznake na čekanju\"\n      }\n    },\n    \"actions\": {\n      \"recrawl_failed_links_only\": \"Ponovno pregledavanje samo neuspjelih veza\",\n      \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Ponovno generiranje AI oznaka samo za neuspjele oznake\",\n      \"reindex_all_bookmarks\": \"Ponovno indeksiranje svih oznaka\",\n      \"recrawl_all_links\": \"Ponovno pregledavanje svih veza\",\n      \"regenerate_ai_tags_for_all_bookmarks\": \"Ponovno generiranje AI oznaka za sve oznake\",\n      \"without_inference\": \"Bez zaključivanja\",\n      \"compact_assets\": \"Kompaktiranje resursa\",\n      \"reprocess_assets_fix_mode\": \"Ponovno postupanje s resursima (fiksni mod)\",\n      \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Ponovno generiraj AI sažetke samo za neuspjele oznake\",\n      \"regenerate_ai_summaries_for_all_bookmarks\": \"Ponovno generiraj AI sažetke za sve oznake\"\n    },\n    \"service_connections\": {\n      \"title\": \"Servisne veze\",\n      \"description\": \"Prati zdravlje i povezanost vanjskih sistemskih ovisnosti\",\n      \"search_engine\": \"Tražilica\",\n      \"browser\": \"Preglednik\",\n      \"queue_system\": \"Sustav čekanja u redu\",\n      \"status\": {\n        \"not_configured\": \"Nije konfigurirano\",\n        \"connected\": \"Spojeno\",\n        \"disconnected\": \"Odspojeno\"\n      }\n    },\n    \"admin_tools\": {\n      \"admin_tools\": \"Admin Alati\",\n      \"bookmark_debugger\": \"Ispravljač pogrešaka oznake\",\n      \"bookmark_id\": \"ID oznake\",\n      \"bookmark_id_placeholder\": \"Unesi ID oznake\",\n      \"lookup\": \"Traži\",\n      \"debug_info\": \"Informacije za ispravljanje pogrešaka\",\n      \"basic_info\": \"Osnovne informacije\",\n      \"status\": \"Status\",\n      \"content\": \"Sadržaj\",\n      \"html_preview\": \"HTML Pregled (Prvih 1000 znakova)\",\n      \"summary\": \"Sažetak\",\n      \"url\": \"URL\",\n      \"source_url\": \"Izvorni URL\",\n      \"asset_type\": \"Vrsta Imovine\",\n      \"file_name\": \"Naziv datoteke\",\n      \"owner_user_id\": \"ID korisnika vlasnika\",\n      \"tagging_status\": \"Status označavanja\",\n      \"summarization_status\": \"Status sažimanja\",\n      \"crawl_status\": \"Status indeksiranja\",\n      \"crawl_status_code\": \"HTTP status kod\",\n      \"crawled_at\": \"Puzano u\",\n      \"recrawl\": \"Ponovno puzanje\",\n      \"reindex\": \"Ponovna indeksacija\",\n      \"retag\": \"Ponovno označavanje\",\n      \"resummarize\": \"Ponovno sažimanje\",\n      \"bookmark_not_found\": \"Oznaka nije pronađena\",\n      \"action_success\": \"Radnja je uspješno dovršena\",\n      \"action_failed\": \"Radnja nije uspjela\",\n      \"recrawl_queued\": \"Posao ponovnog puzanja je u redu čekanja\",\n      \"reindex_queued\": \"Posao ponovne indeksacije je u redu čekanja\",\n      \"retag_queued\": \"Posao ponovnog označavanja je u redu čekanja\",\n      \"resummarize_queued\": \"Posao ponovnog sažimanja je u redu čekanja\",\n      \"view\": \"Prikaži\",\n      \"fetch_error\": \"Greška pri dohvaćanju oznake\"\n    }\n  },\n  \"layouts\": {\n    \"compact\": \"Kompaktan\",\n    \"grid\": \"Mreža\",\n    \"masonry\": \"Masonerija\",\n    \"list\": \"Popis\"\n  },\n  \"common\": {\n    \"roles\": {\n      \"admin\": \"Administrator\",\n      \"user\": \"Korisnik\"\n    },\n    \"name\": \"Ime\",\n    \"email\": \"Mail\",\n    \"password\": \"Lozinka\",\n    \"action\": \"Akcija\",\n    \"actions\": \"Akcije\",\n    \"created_at\": \"Kreirano\",\n    \"key\": \"Kljuc\",\n    \"role\": \"Uloga\",\n    \"something_went_wrong\": \"Greška se desila\",\n    \"experimental\": \"Eksperimentalno\",\n    \"search\": \"Potraži\",\n    \"url\": \"URL\",\n    \"note\": \"Napomena\",\n    \"attachments\": \"Privici\",\n    \"highlights\": \"Istaknuto\",\n    \"source\": \"Izvor\",\n    \"screenshot\": \"Snimka ekrana\",\n    \"video\": \"Video\",\n    \"archive\": \"Arhiva\",\n    \"home\": \"Početna\",\n    \"tags\": \"Oznake\",\n    \"type\": \"Upiši\",\n    \"size\": \"Veličina\",\n    \"bookmark_types\": {\n      \"media\": \"Mediji\",\n      \"title\": \"Vrsta oznake\",\n      \"link\": \"Poveznica\",\n      \"text\": \"Tekst\"\n    },\n    \"updated_at\": \"Ažurirano\",\n    \"title\": \"Naslov\",\n    \"description\": \"Opis\",\n    \"summary\": \"Sažetak\",\n    \"quota\": \"Kvota\",\n    \"bookmarks\": \"Oznake\",\n    \"storage\": \"Pohrana\",\n    \"pdf\": \"Arhivirani PDF\",\n    \"default\": \"Zadano\",\n    \"id\": \"ID\",\n    \"last_used\": \"Zadnje korišteno\"\n  },\n  \"settings\": {\n    \"ai\": {\n      \"tagging_rules\": \"Pravila označavanja\",\n      \"tagging_rule_description\": \"Upiti koje ovdje dodate bit će uključeni kao pravila modelu tijekom generiranja oznaka. Konačne upite možete pregledati u odjeljku za pregled upita.\",\n      \"ai_settings\": \"AI Postavke\",\n      \"prompt_preview\": \"Pregled upita\",\n      \"images_prompt\": \"Vizualni upit\",\n      \"text_prompt\": \"Tekst upit\",\n      \"summarization_prompt\": \"Sažeti upit\",\n      \"text_tagging\": \"Označavanje teksta\",\n      \"image_tagging\": \"Označavanje slika\",\n      \"summarization\": \"Sažetak\",\n      \"all_tagging\": \"Sve oznake\",\n      \"tag_style\": \"Stil oznake\",\n      \"auto_summarization_description\": \"Automatski generiraj sažetke za svoje knjižne oznake pomoću AI-ja.\",\n      \"auto_tagging\": \"Automatsko označavanje\",\n      \"titlecase_spaces\": \"Veliko početno slovo s razmacima\",\n      \"lowercase_underscores\": \"Mala slova s podvlakama\",\n      \"inference_language\": \"Jezik zaključka\",\n      \"titlecase_hyphens\": \"Veliko početno slovo s crticama\",\n      \"lowercase_hyphens\": \"Mala slova s crticama\",\n      \"lowercase_spaces\": \"Mala slova s razmacima\",\n      \"inference_language_description\": \"Odaberi jezik za oznake i sažetke generirane pomoću AI-a.\",\n      \"tag_style_description\": \"Odaberi kako će tvoje automatski generirane oznake biti formatirane.\",\n      \"auto_tagging_description\": \"Automatski generiraj oznake za svoje knjižne oznake pomoću AI-ja.\",\n      \"camelCase\": \"camelCase\",\n      \"auto_summarization\": \"Automatsko sažimanje\",\n      \"no_preference\": \"Bez preferencija\",\n      \"curated_tags\": \"Odabrane oznake\",\n      \"curated_tags_description\": \"Po želji ograniči AI označavanje samo na upotrebu oznaka s ovog popisa. Kada nema odabranih oznaka, AI slobodno generira oznake.\",\n      \"curated_tags_updated\": \"Odabrane oznake su uspješno ažurirane!\",\n      \"curated_tags_update_failed\": \"Nije uspjelo ažuriranje odabranih oznaka\"\n    },\n    \"import\": {\n      \"import_bookmarks_from_html_file\": \"Import knjižnih oznaka iz HTML datoteke\",\n      \"import_export\": \"Import / Export\",\n      \"import_export_bookmarks\": \"Import / Export knjižnih oznaka\",\n      \"import_bookmarks_from_linkwarden_export\": \"Import oznaka iz Linkwarden exporta\",\n      \"import_bookmarks_from_pocket_export\": \"Import oznaka iz Pocket exporta\",\n      \"import_bookmarks_from_matter_export\": \"Import oznaka iz Matter exporta\",\n      \"import_bookmarks_from_karakeep_export\": \"Import oznaka iz Karakeep exporta\",\n      \"export_links_and_notes\": \"Export veza i bilješki\",\n      \"imported_bookmarks\": \"Importirane oznake\",\n      \"import_bookmarks_from_omnivore_export\": \"Import oznaka iz Omnivore exporta\",\n      \"import_bookmarks_from_tab_session_manager_export\": \"Uvezi oznake iz Tab Session Managera\",\n      \"import_bookmarks_from_mymind_export\": \"Uvezi oznake iz mymind izvoza\",\n      \"import_bookmarks_from_instapaper_export\": \"Uvezi oznake iz eksporta Instapaper računa\"\n    },\n    \"back_to_app\": \"Povratak u aplikaciju\",\n    \"user_settings\": \"Postavke korisnika\",\n    \"info\": {\n      \"user_info\": \"Informacije o korisniku\",\n      \"basic_details\": \"Osnovni podaci\",\n      \"change_password\": \"Promijeni lozinku\",\n      \"current_password\": \"Trenutna lozinka\",\n      \"new_password\": \"Nova lozinka\",\n      \"confirm_new_password\": \"Potvrdi novu lozinku\",\n      \"options\": \"Opcije\",\n      \"interface_lang\": \"Jezik sučelja\",\n      \"user_settings\": {\n        \"user_settings_updated\": \"Postavke korisnika su ažurirane!\",\n        \"bookmark_click_action\": {\n          \"title\": \"Radnja klika na oznaku\",\n          \"open_external_url\": \"Otvori izvorni URL\",\n          \"open_bookmark_details\": \"Otvori detalje oznake\"\n        },\n        \"archive_display_behaviour\": {\n          \"title\": \"Arhivirane oznake\",\n          \"show\": \"Prikaži arhivirane oznake u oznakama i popisima\",\n          \"hide\": \"Sakrij arhivirane oznake u oznakama i popisima\"\n        }\n      },\n      \"reader_settings\": {\n        \"local_overrides_title\": \"Aktivne postavke specifične za uređaj\",\n        \"using_default\": \"Korištenje zadanih postavki klijenta\",\n        \"clear_override_hint\": \"Obriši nadjačavanje uređaja za korištenje globalne postavke ({{value}})\",\n        \"font_size\": \"Veličina fonta\",\n        \"font_family\": \"Vrsta fonta\",\n        \"preview_inline\": \"(pregled)\",\n        \"tooltip_preview\": \"Nespremljene promjene pregleda\",\n        \"save_to_all_devices\": \"Svi uređaji\",\n        \"tooltip_local\": \"Postavke uređaja razlikuju se od globalnih\",\n        \"reset_preview\": \"Resetiraj pregled\",\n        \"mono\": \"Monospace\",\n        \"line_height\": \"Visina retka\",\n        \"tooltip_default\": \"Postavke čitanja\",\n        \"title\": \"Postavke čitača\",\n        \"serif\": \"Serif\",\n        \"preview\": \"Pregled\",\n        \"not_set\": \"Nije postavljeno\",\n        \"clear_local_overrides\": \"Očisti postavke uređaja\",\n        \"preview_text\": \"Smeđi lisac brzo skače preko lijenog psa. Ovako će izgledati tekst u prikazu čitača.\",\n        \"local_overrides_cleared\": \"Postavke specifične za uređaj su očišćene\",\n        \"local_overrides_description\": \"Ovaj uređaj ima postavke čitanja koje se razlikuju od tvojih globalnih zadanih postavki:\",\n        \"clear_defaults\": \"Očisti sve zadane vrijednosti\",\n        \"description\": \"Konfiguriraj zadane postavke teksta za prikaz čitača. Ove se postavke sinkroniziraju na svim tvojim uređajima.\",\n        \"defaults_cleared\": \"Zadane postavke čitača su očišćene\",\n        \"save_hint\": \"Spremi postavke samo za ovaj uređaj ili sinkroniziraj na svim uređajima\",\n        \"save_as_default\": \"Spremi kao zadane\",\n        \"save_to_device\": \"Ovaj uređaj\",\n        \"sans\": \"Sans Serif\",\n        \"tooltip_preview_and_local\": \"Nespremljene promjene pregleda; postavke uređaja razlikuju se od globalnih\",\n        \"adjust_hint\": \"Prilagodite postavke iznad za pregled promjena\"\n      },\n      \"avatar\": {\n        \"upload\": \"Učitaj avatar\",\n        \"change\": \"Promijeni avatar\",\n        \"remove_confirm_title\": \"Ukloniti avatar?\",\n        \"updated\": \"Avatar ažuriran\",\n        \"removed\": \"Avatar uklonjen\",\n        \"description\": \"Učitaj kvadratnu sliku koju ćeš koristiti kao avatar.\",\n        \"remove_confirm_description\": \"Ovim ćeš ukloniti trenutnu fotku profila.\",\n        \"title\": \"Fotka profila\",\n        \"remove\": \"Ukloni avatar\"\n      }\n    },\n    \"api_keys\": {\n      \"api_keys\": \"API ključevi\",\n      \"new_api_key\": \"Novi API ključ\",\n      \"new_api_key_desc\": \"Dajte svom API ključu jedinstveno ime\",\n      \"key_success\": \"Ključ je uspješno kreiran\",\n      \"key_success_please_copy\": \"Molimo kopirajte ključ i saćuvajte ga na sigurnom mjestu. Nakon što zatvorite dijalog, više ga nećete moći ponovo pristupiti.\",\n      \"regenerate_api_key\": \"Ponovo stvori API ključ\",\n      \"key_regenerated\": \"Ključ je uspješno ponovo stvoren\",\n      \"key_regenerated_please_copy\": \"Kopiraj novi ključ i spremi ga na sigurno mjesto. Stari ključ je povučen i više neće raditi.\",\n      \"regenerate_warning\": \"Jesi li siguran da želiš ponovo stvoriti API ključ \\\"{{name}}\\\"?\",\n      \"regenerate_confirmation\": \"Ovo će povući trenutni ključ i stvoriti novi. Sve aplikacije koje koriste trenutni ključ prestat će raditi.\"\n    },\n    \"broken_links\": {\n      \"broken_links\": \"Pokidane veze\",\n      \"last_crawled_at\": \"Posljednjo pregledano u\",\n      \"crawling_status\": \"Status pregledavanja\",\n      \"crawling_failed\": \"Pregledavanje nije uspjelo\"\n    },\n    \"feeds\": {\n      \"rss_subscriptions\": \"RSS pretplate\",\n      \"add_a_subscription\": \"Dodaj pretplatu\",\n      \"feed_enabled\": \"RSS feed omogućen\",\n      \"feed_disabled\": \"RSS feed onemogućen\"\n    },\n    \"webhooks\": {\n      \"edit_webhook\": \"Uredi Webhook\",\n      \"webhook_url\": \"Webhook URL\",\n      \"webhooks\": \"Web-dojave\",\n      \"description\": \"Možete koristiti web-dojave za pokretanje radnji kada se stvaraju, mijenjaju ili indeksiraju oznake.\",\n      \"events\": {\n        \"title\": \"Događaji\",\n        \"crawled\": \"Puzano\",\n        \"created\": \"Kreirano\",\n        \"edited\": \"Uređeno\"\n      },\n      \"auth_token\": \"Token za autentifikaciju\",\n      \"add_auth_token\": \"Dodaj Auth Token\",\n      \"edit_auth_token\": \"Uredi Auth Token\",\n      \"create_webhook\": \"Napravi Webhook\",\n      \"delete_webhook\": \"Izbriši Webhook\",\n      \"delete_webhook_confirmation\": \"Jesi li siguran da želiš izbrisati ovaj webhook?\"\n    },\n    \"manage_assets\": {\n      \"manage_assets\": \"Upravljanje resursima\",\n      \"no_assets\": \"Još nemaš nikakvih sredstava.\",\n      \"asset_type\": \"Vrsta resursa\",\n      \"bookmark_link\": \"Poveznica bookmarka\",\n      \"asset_link\": \"Poveznica resursa\",\n      \"delete_asset\": \"Izbriši sredstvo\",\n      \"delete_asset_confirmation\": \"Jesi li siguran da želiš izbrisati ovaj resurs?\"\n    },\n    \"rules\": {\n      \"rules\": \"Mehanizam pravila\",\n      \"conditions_types\": {\n        \"is_archived\": \"Arhivirano je\",\n        \"and\": \"Sve od navedenog je istinito\",\n        \"or\": \"Bilo što od sljedećeg je istinito\",\n        \"always\": \"Uvijek\",\n        \"url_contains\": \"URL sadrži\",\n        \"imported_from_feed\": \"Uvezeno iz feeda\",\n        \"bookmark_type_is\": \"Vrsta bookmaka je\",\n        \"has_tag\": \"Ima oznaku\",\n        \"is_favourited\": \"Je li označeno kao omiljeno\",\n        \"url_does_not_contain\": \"URL ne sadrži\",\n        \"title_contains\": \"Naslov sadrži\",\n        \"title_does_not_contain\": \"Naslov ne sadrži\"\n      },\n      \"actions_types\": {\n        \"add_tag\": \"Dodaj oznaku\",\n        \"remove_tag\": \"Ukloni oznaku\",\n        \"add_to_list\": \"Dodaj na popis\",\n        \"remove_from_list\": \"Ukloni s popisa\",\n        \"download_full_page_archive\": \"Preuzmi arhivu cijele stranice\",\n        \"favourite_bookmark\": \"Bookmark favorita\",\n        \"archive_bookmark\": \"Arhiviraj oznaku\"\n      },\n      \"events_types\": {\n        \"bookmark_added\": \"Bookmark je dodan\",\n        \"tag_added\": \"Ova oznaka je dodana bookmarku\",\n        \"tag_removed\": \"Ova oznaka je uklonjena s oznake\",\n        \"added_to_list\": \"Oznaka je dodana na ovaj popis\",\n        \"removed_from_list\": \"Oznaka je uklonjena s ovog popisa\",\n        \"favourited\": \"Označen bookmark kao favorit\",\n        \"archived\": \"Oznaka je arhivirana\"\n      },\n      \"rule_name\": \"Naziv pravila\",\n      \"description\": \"Možeš koristiti pravila za pokretanje radnji kad se dogodi neki događaj.\",\n      \"ceate_rule\": \"Stvori pravilo\",\n      \"edit_rule\": \"Uredi pravilo\",\n      \"save_rule\": \"Spremi pravilo\",\n      \"delete_rule\": \"Izbriši pravilo\",\n      \"delete_rule_confirmation\": \"Jesi ziher da želiš izbrisati ovo pravilo?\",\n      \"whenever\": \"Kad god ...\",\n      \"if\": \"Ako ...\",\n      \"enter_rule_name\": \"Unesite naziv pravila\",\n      \"describe_what_this_rule_does\": \"Opiši što ovo pravilo radi\",\n      \"rule_has_been_created\": \"Pravilo je stvoreno!\",\n      \"rule_has_been_updated\": \"Pravilo je ažurirano!\",\n      \"rule_has_been_deleted\": \"Pravilo je izbrisano!\",\n      \"no_rules_created_yet\": \"Još nema stvorenih pravila\",\n      \"create_your_first_rule\": \"Napravi svoje prvo pravilo za automatizaciju radnog procesa\"\n    },\n    \"stats\": {\n      \"usage_statistics\": \"Statistika korištenja\",\n      \"insights_description\": \"Uvid u tvoje navike označavanja i kolekciju\",\n      \"failed_to_load\": \"Nije uspjelo učitavanje statistike\",\n      \"overview\": {\n        \"total_bookmarks\": \"Ukupno oznaka\",\n        \"all_saved_items\": \"Svi spremljeni predmeti\",\n        \"favorites\": \"Favoriti\",\n        \"starred_bookmarks\": \"Označene knjižne oznake\",\n        \"archived\": \"Arhivirano\",\n        \"archived_items\": \"Arhivirane stavke\",\n        \"tags\": \"Oznake\",\n        \"unique_tags_created\": \"Stvorene jedinstvene oznake\",\n        \"lists\": \"Popisi\",\n        \"bookmark_collections\": \"Zbirke oznaka\",\n        \"highlights\": \"Istaknuto\",\n        \"text_highlights\": \"Istaknuti tekstovi\",\n        \"storage_used\": \"Iskorištena pohrana\",\n        \"total_asset_storage\": \"Ukupna pohrana imovine\",\n        \"this_month\": \"Ovaj mjesec\",\n        \"bookmarks_added\": \"Dodane oznake\"\n      },\n      \"bookmark_types\": {\n        \"title\": \"Vrste oznaka\",\n        \"links\": \"Poveznice\",\n        \"text_notes\": \"Tekstualne bilješke\",\n        \"assets\": \"Sredstva\"\n      },\n      \"recent_activity\": {\n        \"title\": \"Nedavna aktivnost\",\n        \"this_week\": \"Ovaj tjedan\",\n        \"this_month\": \"Ovaj mjesec\",\n        \"this_year\": \"Ove godine\"\n      },\n      \"top_domains\": {\n        \"title\": \"Vrhunske domene\",\n        \"no_domains_found\": \"Nema pronađenih domena\"\n      },\n      \"most_used_tags\": {\n        \"title\": \"Najčešće korištene oznake\",\n        \"no_tags_found\": \"Nema pronađenih oznaka\"\n      },\n      \"activity_patterns\": {\n        \"activity_by_hour\": \"Aktivnost po satu\",\n        \"activity_by_day\": \"Aktivnost po danu\"\n      },\n      \"storage_breakdown\": {\n        \"title\": \"Raščlamba pohrane\"\n      },\n      \"bookmark_sources\": {\n        \"title\": \"Izvori oznaka\",\n        \"empty\": \"Nema dostupnih izvornih podataka\"\n      }\n    },\n    \"subscription\": {\n      \"subscription\": \"Pretplata\",\n      \"manage_subscription\": \"Upravljaj svojom pretplatom i informacijama o naplati\",\n      \"current_plan\": \"Trenutni plan\",\n      \"billing_period\": \"Razdoblje naplate\",\n      \"paid_plan\": \"Plaćeni plan\",\n      \"unlock_bigger_quota\": \"Otključaj veću kvotu i podrži projekt\",\n      \"subscribe_now\": \"Pretplati se odmah\",\n      \"manage_billing\": \"Upravljaj naplatom\",\n      \"subscription_canceled\": \"Tvoja pretplata je otkazana i završit će {{date}}. Možeš se ponovno pretplatiti bilo kada.\",\n      \"usage_quotas\": \"Korištenje i kvote\",\n      \"track_usage\": \"Prati svoje trenutno korištenje u odnosu na ograničenja plana\",\n      \"total_bookmarks_saved\": \"Ukupno spremljenih oznaka\",\n      \"assets_file_storage\": \"Spremanje datoteka i imovine\",\n      \"unlimited_usage\": \"Neograničeno korištenje\",\n      \"quota_limit_reached\": \"Dosegnuta granica kvote\",\n      \"approaching_quota_limit\": \"Približavam se granici kvote\",\n      \"loading_usage\": \"Učitavam informacije o korištenju...\",\n      \"free\": \"Besplatno\",\n      \"paid\": \"Plaćeno\"\n    },\n    \"import_sessions\": {\n      \"title\": \"Uvoz sesija\",\n      \"description\": \"Pregledaj i upravljaj svojim sesijama grupnog uvoza. Sesije se automatski stvaraju kada uvoziš oznake.\",\n      \"load_error\": \"Nisam uspio učitati sesije uvoza\",\n      \"no_sessions\": \"Još nema sesija uvoza\",\n      \"no_sessions_detail\": \"Sesije uvoza će se ovdje automatski pojaviti kad uvoziš oznake\",\n      \"created_at\": \"Napravljeno {{time}}\",\n      \"progress\": \"Napredak\",\n      \"status\": {\n        \"pending\": \"U tijeku\",\n        \"in_progress\": \"U tijeku\",\n        \"completed\": \"Završeno\",\n        \"failed\": \"Neuspjelo\",\n        \"processing\": \"Obrada\",\n        \"staging\": \"Priprema\",\n        \"running\": \"U tijeku\",\n        \"paused\": \"Pauzirano\"\n      },\n      \"badges\": {\n        \"pending\": \"{{count}} na čekanju\",\n        \"processing\": \"{{count}} se obrađuje\",\n        \"completed\": \"{{count}} završeno\",\n        \"failed\": \"{{count}} neuspjelo\"\n      },\n      \"imported_to\": \"Uvezeno u:\",\n      \"view_list\": \"Vidite popis\",\n      \"delete_dialog_title\": \"Izbriši sesiju uvoza\",\n      \"delete_dialog_description\": \"Jesi li ziher da želiš izbrisati \\\"{{name}}\\\"? Ne možeš to poništiti. Same oznake neće biti izbrisane.\",\n      \"delete_session\": \"Izbriši sesiju\",\n      \"pause_session\": \"Pauziraj\",\n      \"resume_session\": \"Nastavi\",\n      \"view_details\": \"Pogledaj detalje\",\n      \"detail\": {\n        \"page_title\": \"Detalji sesije uvoza\",\n        \"back_to_import\": \"Natrag na uvoz\",\n        \"filter_all\": \"Sve\",\n        \"filter_accepted\": \"Prihvaćeno\",\n        \"filter_rejected\": \"Odbijeno\",\n        \"filter_duplicates\": \"Duplikati\",\n        \"filter_pending\": \"Na čekanju\",\n        \"table_title\": \"Naslov / URL\",\n        \"table_type\": \"Vrsta\",\n        \"table_result\": \"Rezultat\",\n        \"table_reason\": \"Razlog\",\n        \"table_bookmark\": \"Oznaka\",\n        \"result_accepted\": \"Prihvaćeno\",\n        \"result_rejected\": \"Odbijeno\",\n        \"result_skipped_duplicate\": \"Duplikat\",\n        \"result_pending\": \"Na čekanju\",\n        \"result_processing\": \"Obrada\",\n        \"no_results\": \"Nema rezultata za ovaj filtar.\",\n        \"view_bookmark\": \"Pogledaj oznaku\",\n        \"load_more\": \"Učitaj više\",\n        \"no_title\": \"Bez naslova\"\n      }\n    },\n    \"backups\": {\n      \"backups\": \"Sigurnosne kopije\",\n      \"page_title\": \"Sigurnosne kopije\",\n      \"page_description\": \"Automatski kreiraj i upravljaj sigurnosnim kopijama svojih oznaka. Sigurnosne kopije su komprimirane i sigurno pohranjene.\",\n      \"configuration\": {\n        \"title\": \"Konfiguracija sigurnosnih kopija\",\n        \"enable_automatic_backups\": \"Omogući automatske sigurnosne kopije\",\n        \"enable_automatic_backups_description\": \"Automatski kreiraj sigurnosne kopije tvojih oznaka\",\n        \"backup_frequency\": \"Učestalost sigurnosnih kopija\",\n        \"backup_frequency_description\": \"Koliko često bi se trebale kreirati sigurnosne kopije\",\n        \"retention_period\": \"Period zadržavanja (dana)\",\n        \"retention_period_description\": \"Koliko dana čuvati sigurnosne kopije prije nego što ih izbrišeš\",\n        \"frequency\": {\n          \"daily\": \"Dnevno\",\n          \"weekly\": \"Tjedno\"\n        },\n        \"select_frequency\": \"Odaberi učestalost\",\n        \"save_settings\": \"Spremi postavke\"\n      },\n      \"list\": {\n        \"title\": \"Tvoje sigurnosne kopije\",\n        \"create_backup_now\": \"Napravi sigurnosnu kopiju sada\",\n        \"no_backups\": \"Još nemaš nikakvih sigurnosnih kopija. Omogući automatske sigurnosne kopije ili napravi jednu ručno.\",\n        \"table\": {\n          \"created_at\": \"Napravljeno\",\n          \"bookmarks\": \"Oznake\",\n          \"size\": \"Veličina\",\n          \"status\": \"Status\",\n          \"actions\": \"Radnje\"\n        },\n        \"status\": {\n          \"success\": \"Uspjeh\",\n          \"failed\": \"Neuspješno\",\n          \"pending\": \"Na čekanju\"\n        },\n        \"actions\": {\n          \"download_backup\": \"Preuzmi sigurnosnu kopiju\",\n          \"delete_backup\": \"Izbriši sigurnosnu kopiju\"\n        }\n      },\n      \"dialogs\": {\n        \"delete_backup_title\": \"Izbrisati sigurnosnu kopiju?\",\n        \"delete_backup_description\": \"Jesi li siguran da želiš izbrisati ovu sigurnosnu kopiju? Ova radnja se ne može poništiti.\"\n      },\n      \"toasts\": {\n        \"backup_queued\": \"Posao sigurnosne kopije je u redu čekanja! Bit će obrađen uskoro.\",\n        \"backup_deleted\": \"Sigurnosna kopija je izbrisana!\"\n      }\n    }\n  },\n  \"actions\": {\n    \"manage_lists\": \"Upravljanje popisima\",\n    \"create\": \"Kreiraj\",\n    \"ignore\": \"Ignoriraj\",\n    \"favorite\": \"Omiljeno\",\n    \"change_layout\": \"Promijeni raspored\",\n    \"archive\": \"Arhiva\",\n    \"unarchive\": \"Raz-arhiviraj\",\n    \"unfavorite\": \"Oduzmi omiljeno\",\n    \"delete\": \"Izbriši\",\n    \"refresh\": \"Osvježi\",\n    \"recrawl\": \"Ponovno indeksiraj\",\n    \"download_full_page_archive\": \"Preuzmi cijeli arhivirani sadržaj stranice\",\n    \"edit_tags\": \"Uredi oznake\",\n    \"add_to_list\": \"Dodaj na popis\",\n    \"select_all\": \"Odaberi sve\",\n    \"unselect_all\": \"Oduzmi sve oznake\",\n    \"copy_link\": \"Kopiraj link\",\n    \"close_bulk_edit\": \"Zatvori masovno uređivanje\",\n    \"bulk_edit\": \"Masovno uređivanje\",\n    \"remove_from_list\": \"Ukloni s popisa\",\n    \"save\": \"Spremi\",\n    \"add\": \"Dodaj\",\n    \"edit\": \"Uredi\",\n    \"fetch_now\": \"Preuzmi odmah\",\n    \"summarize_with_ai\": \"Sažmi pomoću AI\",\n    \"edit_title\": \"Uredi naslov\",\n    \"sign_out\": \"Odjavi se\",\n    \"close\": \"Zatvori\",\n    \"merge\": \"Spoji\",\n    \"cancel\": \"Otkaži\",\n    \"apply_all\": \"Primijeni sve\",\n    \"sort\": {\n      \"title\": \"Sortiraj\",\n      \"newest_first\": \"Najnovije prvo\",\n      \"oldest_first\": \"Najstarije prvo\",\n      \"relevant_first\": \"Najrelevantnije prvo\"\n    },\n    \"open_editor\": \"Otvori uređivač\",\n    \"toggle_show_archived\": \"Prikaži arhivirano\",\n    \"confirm\": \"Potvrdi\",\n    \"regenerate\": \"Ponovo stvori\",\n    \"load_more\": \"Učitaj više\",\n    \"edit_notes\": \"Uredi bilješke\",\n    \"preserve_as_pdf\": \"Spremi kao PDF\",\n    \"offline_copies\": \"Izvanmrežne kopije\",\n    \"preserve_offline_archive\": \"Spremi izvanmrežnu arhivu\",\n    \"download_full_page_archive_file\": \"Preuzmi arhivsku datoteku\",\n    \"download_pdf_file\": \"Preuzmi PDF datoteku\",\n    \"remove\": \"Ukloni\",\n    \"more\": \"Više\",\n    \"replace_banner\": \"Zamijeni banner\",\n    \"add_banner\": \"Dodaj banner\",\n    \"download\": \"Preuzmi\"\n  },\n  \"highlights\": {\n    \"no_highlights\": \"Još nemate nijednu istaknutu stavku.\"\n  },\n  \"options\": {\n    \"dark_mode\": \"Dark Mode\",\n    \"light_mode\": \"Light Mode\",\n    \"apps_extensions\": \"Aplikacije i proširenja\",\n    \"documentation\": \"Dokumentacija\",\n    \"follow_us_on_x\": \"Prati nas na X-u\"\n  },\n  \"lists\": {\n    \"all_lists\": \"Svi popisi\",\n    \"favourites\": \"Omiljeni\",\n    \"new_list\": \"Novi popis\",\n    \"new_nested_list\": \"Novi ugniježđeni popis\",\n    \"edit_list\": \"Uredi popis\",\n    \"manual_list\": \"Ručni popis\",\n    \"smart_list\": \"Pametni popis\",\n    \"search_query\": \"Upit za pretraživanje\",\n    \"search_query_help\": \"Saznajte više o jeziku upita za pretraživanje.\",\n    \"parent_list\": \"Lista roditelja\",\n    \"no_parent\": \"Nema roditelja\",\n    \"list_type\": \"Vrsta popisa\",\n    \"merge_list\": \"Spoji listu\",\n    \"destination_list\": \"Odredišna lista\",\n    \"delete_after_merge\": \"Izbriši izvornu listu nakon spajanja\",\n    \"no_destination\": \"Nema odredišta\",\n    \"description\": \"Opis (neobavezno)\",\n    \"public_list\": {\n      \"share_link\": \"Podijeli vezu\",\n      \"title\": \"Javni popis\",\n      \"description\": \"Dopusti drugima da vide ovaj popis\"\n    },\n    \"share_list\": \"Podijeli popis\",\n    \"rss\": {\n      \"title\": \"RSS feed\",\n      \"description\": \"Omogući RSS feed za ovaj popis\",\n      \"feed_url\": \"URL RSS feeda\"\n    },\n    \"delete_list\": {\n      \"title\": \"Izbriši popis\",\n      \"description\": \"Brisanjem popisa ne brišu se oznake unutar tog popisa.\",\n      \"delete_children\": \"Izbriši podređene popise (rekurzivno)\",\n      \"delete_children_description\": \"Ako nije označeno, svi izravno podređeni popisi postat će korijenski popisi\"\n    },\n    \"shared\": \"Zajedničko\",\n    \"collaborators\": {\n      \"manage\": \"Upravljaj suradnicima\",\n      \"view\": \"Prikaži suradnike\",\n      \"collaborators\": \"Suradnici\",\n      \"add\": \"Dodaj suradnika\",\n      \"current\": \"Trenutni suradnici\",\n      \"enter_email\": \"Unesi e-mail adresu\",\n      \"please_enter_email\": \"Molim te, unesi e-mail adresu\",\n      \"added_successfully\": \"Suradnik uspješno dodan\",\n      \"failed_to_add\": \"Nije uspjelo dodavanje suradnika\",\n      \"removed\": \"Suradnik uklonjen\",\n      \"failed_to_remove\": \"Nije uspjelo uklanjanje suradnika\",\n      \"role_updated\": \"Uloga ažurirana\",\n      \"failed_to_update_role\": \"Nije uspjelo ažuriranje uloge\",\n      \"viewer\": \"Preglednik\",\n      \"editor\": \"Urednik\",\n      \"owner\": \"Vlasnik\",\n      \"viewer_description\": \"Može vidjeti oznake u popisu\",\n      \"editor_description\": \"Može dodavati i uklanjati oznake\",\n      \"no_collaborators\": \"Još nema suradnika. Dodaj nekoga da bi započeo suradnju!\",\n      \"no_collaborators_readonly\": \"Nema suradnika za ovaj popis.\",\n      \"people_with_access\": \"Ljudi koji imaju pristup ovom popisu\",\n      \"add_or_remove\": \"Dodaj ili ukloni osobe koje imaju pristup ovom popisu\",\n      \"invitation_sent\": \"Uspješno poslana pozivnica\",\n      \"invitation_revoked\": \"Pozivnica povučena\",\n      \"failed_to_revoke\": \"Nije uspjelo povlačenje pozivnice\",\n      \"pending\": \"Na čekanju\",\n      \"revoke\": \"Opozovi\",\n      \"declined\": \"Odbijeno\"\n    },\n    \"leave_list\": {\n      \"title\": \"Napusti popis\",\n      \"confirm_message\": \"Jesi li siguran da želiš napustiti {{icon}} {{name}}?\",\n      \"warning\": \"Više nećeš moći vidjeti ni pristupati oznakama na ovom popisu. Vlasnik popisa te može ponovno dodati ako treba.\",\n      \"action\": \"Napusti popis\",\n      \"success\": \"Napustio si \\\"{{icon}} {{name}}\\\"\"\n    },\n    \"invitations\": {\n      \"pending\": \"Pozivnice na čekanju\",\n      \"description\": \"Pregledaj i odgovori na pozivnice za suradnju na popisu\",\n      \"invited_by\": \"Poziv poslao\",\n      \"accept\": \"Prihvati\",\n      \"decline\": \"Odbij\",\n      \"accepted\": \"Poziv prihvaćen\",\n      \"declined\": \"Poziv odbijen\",\n      \"failed_to_accept\": \"Nije uspjelo prihvatiti pozivnicu\",\n      \"failed_to_decline\": \"Nije uspjelo odbiti pozivnicu\"\n    },\n    \"shared_lists\": \"Zajednički popisi\"\n  },\n  \"tags\": {\n    \"all_tags\": \"Sve oznake\",\n    \"your_tags\": \"Tvoje oznake\",\n    \"your_tags_info\": \"Oznake koje si barem jednom priložio ti\",\n    \"ai_tags\": \"AI oznake\",\n    \"ai_tags_info\": \"Oznake koje su bile priložene samo automatski (od AI-a)\",\n    \"unused_tags\": \"Nekorištene oznake\",\n    \"drag_and_drop_merging\": \"Spajanje priko drag & drop\",\n    \"drag_and_drop_merging_info\": \"Drag and drop oznake jedna na drugu kako biste ih spojili\",\n    \"sort_by_name\": \"Sortiraj po imenu\",\n    \"delete_all_unused_tags\": \"Izbriši sve neiskorištene oznake\",\n    \"unused_tags_info\": \"Oznake koje nisu priložene nijednoj oznaci\",\n    \"create_tag\": \"Kreiraj oznaku\",\n    \"create_tag_description\": \"Napravi novu oznaku bez da ju spojiš s nekom knjižnom oznakom\",\n    \"tag_name\": \"Ime oznake\",\n    \"enter_tag_name\": \"Upiši ime oznake\",\n    \"sort_by_usage\": \"Sortiraj po upotrebi\",\n    \"sort_by_relevance\": \"Sortiraj po važnosti\",\n    \"no_custom_tags\": \"Još nema prilagođenih oznaka\",\n    \"no_ai_tags\": \"Još nema AI oznaka\",\n    \"no_unused_tags\": \"Nemaš neiskorištenih oznaka\",\n    \"no_unused_tags_match_your_search\": \"Nijedna neiskorištena oznaka ne odgovara pretrazi\",\n    \"no_tags_match_your_search\": \"Nijedna oznaka ne odgovara pretrazi\",\n    \"search_placeholder\": \"Pretraži oznake...\",\n    \"search_or_create_placeholder\": \"Pretraži ili stvori oznake...\"\n  },\n  \"preview\": {\n    \"view_original\": \"Pogledaj orginal\",\n    \"cached_content\": \"Keširani sadržaj\",\n    \"reader_view\": \"Prikaz čitača\",\n    \"tabs\": {\n      \"content\": \"Sadržaj\",\n      \"details\": \"Detalji\"\n    },\n    \"archive_info\": \"Arhive se možda neće ispravno prikazati inline ako zahtijevaju Javascript. Za najbolje rezultate, <1>preuzmite ih i otvorite u svom pregledniku</1>.\",\n    \"fetch_error_title\": \"Sadržaj nije dostupan\",\n    \"fetch_error_description\": \"Nismo uspjeli dohvatiti sadržaj za ovu poveznicu. Stranica je možda zaštićena, zahtijeva autentifikaciju ili je privremeno nedostupna.\",\n    \"crawling_in_progress\": \"Dohvaćam sadržaj stranice…\",\n    \"continue_reading\": \"Nastavi gdje si stao\",\n    \"continue_reading_percent\": \"Nastavi gdje si stao ({{percent}}%)\",\n    \"continue_button\": \"Nastavi\"\n  },\n  \"editor\": {\n    \"quickly_focus\": \"Možete brzo fokusirati ovo polje pritiskanjem ⌘ + E\",\n    \"import_as_text\": \"Uvoz kao tekstualnu oznaku\",\n    \"import_as_separate_bookmarks\": \"Uvoz kao odvojene oznake\",\n    \"multiple_urls_dialog_title\": \"Uvoz URL-ova kao odvojene oznake?\",\n    \"multiple_urls_dialog_desc\": \"Unos sadrži više URL-ova na odvojenim redovima. Želite li ih uvesti kao odvojene oznake?\",\n    \"placeholder\": \"Zalijepite vezu ili sliku, napišite bilješku ili povucite i ispustite sliku ovdje...\",\n    \"new_item\": \"NOVI ITEM\",\n    \"disabled_submissions\": \"Podnošenja su onemogućena\",\n    \"text_toolbar\": {\n      \"undo\": \"Poništi\",\n      \"redo\": \"Ponovi\",\n      \"bold\": \"Podebljano\",\n      \"italic\": \"Kurziv\",\n      \"underline\": \"Podcrtano\",\n      \"strikethrough\": \"Prekriženo\",\n      \"code\": \"Kod\",\n      \"highlight\": \"Označeno\",\n      \"align_left\": \"Poravnaj lijevo\",\n      \"align_center\": \"Poravnaj centar\",\n      \"align_right\": \"Poravnaj desno\",\n      \"markdown_shortcuts\": {\n        \"label\": \"Prečaci za Markdown\",\n        \"heading\": {\n          \"label\": \"Naslov\",\n          \"example\": \"# H1, ## H2, ### H3\"\n        },\n        \"blockquote\": {\n          \"label\": \"Citat blokova\",\n          \"example\": \"> Citat blokova\"\n        },\n        \"ordered_list\": {\n          \"label\": \"Numerirana lista\",\n          \"example\": \"1. Stavka liste\"\n        },\n        \"unordered_list\": {\n          \"label\": \"Nenumirirana lista\",\n          \"example\": \"- Stavka liste\"\n        },\n        \"inline_code\": {\n          \"label\": \"Inline kod\",\n          \"example\": \"`Kod`\"\n        },\n        \"block_code\": {\n          \"label\": \"Blok kod\",\n          \"example\": \"``` + razmak\"\n        },\n        \"italic\": {\n          \"example\": \"*Italic* ili _Italic_ ili CTRL+i\",\n          \"label\": \"Kurziv\"\n        },\n        \"bold\": {\n          \"example\": \"**text** ili CTRL+b\",\n          \"label\": \"Podebljano\"\n        }\n      }\n    },\n    \"placeholder_v2\": \"Zalijepi poveznicu, napiši bilješku ili ubaci sliku…\"\n  },\n  \"toasts\": {\n    \"bookmarks\": {\n      \"updated\": \"Oznaka je ažurirana!\",\n      \"deleted\": \"Oznaka je izbrisana!\",\n      \"refetch\": \"Ponovno preuzimanje je stavljeno u čekanje!\",\n      \"full_page_archive\": \"Pokrenuto je stvaranje potpune arhive stranice\",\n      \"delete_from_list\": \"Oznaka je izbrisana s popisa\",\n      \"clipboard_copied\": \"Veza je dodana u vaš međuspremnik!\",\n      \"preserve_pdf\": \"Spremanje u PDF formatu je pokrenuto\",\n      \"update_banner\": \"Banner je ažuriran!\",\n      \"uploading_banner\": \"Učitavanje bannera...\"\n    },\n    \"lists\": {\n      \"created\": \"Popis je kreiran!\",\n      \"updated\": \"Popis je ažuriran!\",\n      \"merged\": \"Lista je spojena!\",\n      \"deleted\": \"Lista je izbrisana!\"\n    },\n    \"tags\": {\n      \"created\": \"Oznaka je kreirana!\",\n      \"failed_to_create\": \"Nije uspjelo kreiranje oznake\"\n    }\n  },\n  \"cleanups\": {\n    \"cleanups\": \"Čišćenje\",\n    \"duplicate_tags\": {\n      \"title\": \"Duplikatne oznake\",\n      \"merge_all_suggestions\": \"Spojiti sve prijedloge?\"\n    }\n  },\n  \"search\": {\n    \"is_favorited\": \"Je li označeno kao omiljeno\",\n    \"created_on_or_after\": \"Stvoreno na ili nakon\",\n    \"not_created_on_or_after\": \"Nije kreirano na ili nakon\",\n    \"created_on_or_before\": \"Kreirano na ili prije\",\n    \"not_created_on_or_before\": \"Nije stvoreno na ili prije\",\n    \"url_contains\": \"URL sadrži\",\n    \"is_in_list\": \"Je na listi\",\n    \"has_tag\": \"Ima oznaku\",\n    \"does_not_have_tag\": \"Nema oznaku\",\n    \"full_text_search\": \"Pretraživanje cijelog teksta\",\n    \"is_not_favorited\": \"Nije označen kao favorit\",\n    \"is_archived\": \"Je arhivirano\",\n    \"has_no_tags\": \"Nema oznaku\",\n    \"is_in_any_list\": \"Je li na bilo kojem popisu\",\n    \"is_not_in_list\": \"Nije na popisu\",\n    \"or\": \"Ili\",\n    \"has_any_tag\": \"Ima bilo koji tag\",\n    \"is_not_archived\": \"Nije arhivirano\",\n    \"is_not_in_any_list\": \"Nije ni na jednom popisu\",\n    \"url_does_not_contain\": \"URL ne sadrži\",\n    \"type_is\": \"Vrsta je\",\n    \"type_is_not\": \"Tip nije\",\n    \"and\": \"I\",\n    \"is_from_feed\": \"Je iz RSS feeda\",\n    \"is_not_from_feed\": \"Nije iz RSS feeda\",\n    \"created_within\": \"Stvoreno unutar\",\n    \"created_earlier_than\": \"Stvoreno prije\",\n    \"day_s\": \" Dana(e)\",\n    \"week_s\": \" Tjedna(i)\",\n    \"month_s\": \" Mjeseca(i)\",\n    \"year_s\": \" Godina(e)\",\n    \"day_s_ago\": \" Dana(e) prije\",\n    \"week_s_ago\": \" Tjedna(i) prije\",\n    \"month_s_ago\": \" Mjeseca(i) prije\",\n    \"year_s_ago\": \" Godina(e) prije\",\n    \"history\": \"Nedavne pretrage\",\n    \"title_contains\": \"Naslov sadrži\",\n    \"title_does_not_contain\": \"Naslov ne sadrži\",\n    \"is_broken_link\": \"Ima pokvareni link\",\n    \"tags\": \"Oznake\",\n    \"no_suggestions\": \"Nema prijedloga\",\n    \"filters\": \"Filtri\",\n    \"is_not_broken_link\": \"Ima radni link\",\n    \"lists\": \"Popisi\",\n    \"feeds\": \"Kanali\",\n    \"is_from_source\": \"Izvor je\",\n    \"is_not_from_source\": \"Izvor nije\"\n  },\n  \"dialogs\": {\n    \"bookmarks\": {\n      \"delete_confirmation_title\": \"Izbrisati oznaku?\",\n      \"delete_confirmation_description\": \"Jesi ziher da želiš izbrisati ovaj bookmark?\"\n    }\n  },\n  \"banners\": {\n    \"no_bookmarks\": {\n      \"title\": \"Još nema oznaka\",\n      \"description\": \"Spremi zanimljive članke, poveznice i stranice da im brzo pristupiš kasnije.\"\n    }\n  },\n  \"bookmark_editor\": {\n    \"title\": \"Uredi oznaku\",\n    \"subtitle\": \"Napravi promjene u detaljima oznake. Klikni spremi kad si gotov.\",\n    \"author\": \"Autor\",\n    \"publisher\": \"Izdavač\",\n    \"date_published\": \"Datum objave\",\n    \"pick_a_date\": \"Odaberi datum\",\n    \"save_changes\": \"Spremi promjene\",\n    \"extracted_content\": \"Izdvojeni sadržaj\"\n  },\n  \"view_options\": {\n    \"title\": \"Opcije prikaza\",\n    \"layout\": \"Izgled\",\n    \"columns\": \"Stupci\",\n    \"display_options\": \"Opcije prikaza\",\n    \"show_note_previews\": \"Prikaži bilješke\",\n    \"show_tags\": \"Pokaži oznake\",\n    \"show_title\": \"Pokaži naslov\",\n    \"image_options\": \"Opcije slike\",\n    \"image_fit_cover\": \"Naslovnica (ispuni)\",\n    \"image_fit_contain\": \"Sadrži (stane)\"\n  },\n  \"version\": {\n    \"new_release_available\": \"Nove bilješke o izdanju su dostupne\",\n    \"whats_new_title\": \"Što je novo u verziji {{version}}\",\n    \"release_notes_description\": \"Evo najnovijih ažuriranja preuzetih iz bilješki o izdanju na GitHubu.\",\n    \"loading_release_notes\": \"Učitavanje bilješki o izdanju…\",\n    \"unable_to_load_release_notes\": \"Trenutno nije moguće učitati bilješke o izdanju. Pokušaj ponovno kasnije.\",\n    \"no_release_notes\": \"Za ovu verziju nisu objavljene bilješke o izdanju.\",\n    \"release_notes_synced\": \"Bilješke o izdanju sinkronizirane su s GitHuba.\",\n    \"view_on_github\": \"Pogledaj na GitHubu\"\n  },\n  \"wrapped\": {\n    \"title\": \"Tvoj {{year}} sažetak\",\n    \"subtitle\": \"Godina u Karakeepu\",\n    \"banner\": {\n      \"title\": \"Tvoj 2025 sažetak je spreman!\",\n      \"description\": \"Pogledajte svoju godinu u oznakama\",\n      \"view_now\": \"Pogledaj sada\"\n    },\n    \"button\": \"2025 Sažetak\",\n    \"loading\": \"Učitavanje tvog sažetka...\",\n    \"failed_to_load\": \"Nije uspjelo učitavanje statistike tvog sažetka\",\n    \"sections\": {\n      \"total_saves\": {\n        \"prefix\": \"Spremio si\",\n        \"suffix\": \"artikala ove godine\",\n        \"suffix_singular\": \"artikl ove godine\"\n      },\n      \"first_bookmark\": {\n        \"title\": \"Tvoje putovanje je počelo\",\n        \"description\": \"Prvo spremanje {{year}}.:\"\n      },\n      \"top_domains\": \"Tvoje najjače stranice\",\n      \"top_tags\": \"Tvoje najjače oznake\",\n      \"monthly_activity\": \"Tvoja godina spremanja\",\n      \"most_active_day\": \"Tvoj najaktivniji dan\",\n      \"peak_times\": {\n        \"title\": \"Kada spremaš\",\n        \"peak_hour\": \"Vrhunac sata\",\n        \"peak_day\": \"Vrhunac dana\"\n      },\n      \"how_you_save\": \"Kako spremaš\",\n      \"what_you_saved\": \"Što si spremio\",\n      \"summary\": {\n        \"favorites\": \"Favoriti\",\n        \"tags_created\": \"Kreirane oznake\",\n        \"highlights\": \"Najbitnije\"\n      },\n      \"types\": {\n        \"links\": \"Linkovi\",\n        \"notes\": \"Bilješke\",\n        \"assets\": \"Sredstva\"\n      }\n    },\n    \"footer\": \"Napravljeno s Karakeepom\",\n    \"share\": \"Podijeli\",\n    \"download\": \"Preuzmi\",\n    \"close\": \"Zatvori\",\n    \"generating\": \"Generiram...\"\n  }\n}\n"
  },
  {
    "path": "apps/web/lib/i18n/locales/hu/translation.json",
    "content": "{\n  \"actions\": {\n    \"add\": \"Hozzáad\",\n    \"sort\": {\n      \"newest_first\": \"Legújabb elől\",\n      \"oldest_first\": \"Legrégebbi elől\",\n      \"title\": \"Rendezés\",\n      \"relevant_first\": \"Legrelevánsabb elöl\"\n    },\n    \"unfavorite\": \"Kedvencség visszavonása\",\n    \"refresh\": \"Frissítés\",\n    \"edit_tags\": \"Címkék szerkesztése\",\n    \"create\": \"Létrehozás\",\n    \"bulk_edit\": \"Tömeges szerkesztés\",\n    \"sign_out\": \"Kijelentkezés\",\n    \"change_layout\": \"Elrendezés változtatása\",\n    \"archive\": \"Archívum\",\n    \"favorite\": \"Kedvenc\",\n    \"unarchive\": \"Archiváltság visszavonása\",\n    \"delete\": \"Törlés\",\n    \"recrawl\": \"Újratérképezés\",\n    \"download_full_page_archive\": \"Teljes oldal letöltése\",\n    \"add_to_list\": \"Hozzáadás listához\",\n    \"select_all\": \"Összes kiválasztása\",\n    \"unselect_all\": \"Összes kiválasztásának megszüntetése\",\n    \"copy_link\": \"Link Másolása\",\n    \"manage_lists\": \"Listák kezelése\",\n    \"remove_from_list\": \"Levétel listáról\",\n    \"edit\": \"Szerkesztés\",\n    \"fetch_now\": \"Begyűjtés most\",\n    \"summarize_with_ai\": \"Összegzés MI segítségével\",\n    \"edit_title\": \"Cím szerkesztése\",\n    \"merge\": \"Egyesítés\",\n    \"apply_all\": \"Alkalmazás mindenre\",\n    \"ignore\": \"Figyelmen kívül hagyás\",\n    \"cancel\": \"Visszavonás\",\n    \"close\": \"Bezárás\",\n    \"close_bulk_edit\": \"Tömeges szerkesztés bezárása\",\n    \"save\": \"Mentés\",\n    \"open_editor\": \"Szerkesztő megnyitása\",\n    \"toggle_show_archived\": \"Archiváltak megjelenítése\",\n    \"confirm\": \"Megerősít\",\n    \"regenerate\": \"Újragenerálás\",\n    \"load_more\": \"Továbbiak betöltése\",\n    \"edit_notes\": \"Jegyzetek szerkesztése\",\n    \"preserve_as_pdf\": \"Mentés PDF-ként\",\n    \"offline_copies\": \"Offline példányok\",\n    \"preserve_offline_archive\": \"Offline archívum megőrzése\",\n    \"download_full_page_archive_file\": \"Archív fájl letöltése\",\n    \"download_pdf_file\": \"PDF fájl letöltése\",\n    \"remove\": \"Eltávolítás\",\n    \"more\": \"Tovább\",\n    \"replace_banner\": \"Banner cseréje\",\n    \"add_banner\": \"Banner hozzáadása\",\n    \"download\": \"Letöltés\"\n  },\n  \"settings\": {\n    \"user_settings\": \"Felhasználói beállítások\",\n    \"feeds\": {\n      \"rss_subscriptions\": \"RSS feliratkozások\",\n      \"add_a_subscription\": \"Feliratkozás hozzáadása\",\n      \"feed_enabled\": \"RSS-hírfolyam engedélyezve\",\n      \"feed_disabled\": \"RSS-hírfolyam letiltva\"\n    },\n    \"info\": {\n      \"confirm_new_password\": \"Új jelszó megerősítése\",\n      \"user_info\": \"Felhasználói adatok\",\n      \"basic_details\": \"Alap adatok\",\n      \"change_password\": \"Jelszó megváltoztatása\",\n      \"current_password\": \"Aktuális jelszó\",\n      \"new_password\": \"Új jelszó\",\n      \"options\": \"Beállítások\",\n      \"interface_lang\": \"Felület nyelve\",\n      \"user_settings\": {\n        \"user_settings_updated\": \"A felhasználói beállítások frissítve lettek!\",\n        \"bookmark_click_action\": {\n          \"title\": \"Könyvjelzőre kattintás művelete\",\n          \"open_external_url\": \"Eredeti URL megnyitása\",\n          \"open_bookmark_details\": \"Könyvjelző részleteinek megnyitása\"\n        },\n        \"archive_display_behaviour\": {\n          \"title\": \"Archivált könyvjelzők\",\n          \"show\": \"Archivált könyvjelzők megjelenítése címkékben és listákban\",\n          \"hide\": \"Archivált könyvjelzők elrejtése címkékben és listákban\"\n        }\n      },\n      \"reader_settings\": {\n        \"local_overrides_title\": \"Eszközspecifikus beállítások aktívak\",\n        \"using_default\": \"Ügyfél alapértelmezettjének használata\",\n        \"clear_override_hint\": \"Eszközfelülírás törlése a globális beállítás ({{value}}) használatához\",\n        \"font_size\": \"Betűméret\",\n        \"font_family\": \"Betűtípus családja\",\n        \"preview_inline\": \"(előnézet)\",\n        \"tooltip_preview\": \"El nem mentett előnézeti módosítások\",\n        \"save_to_all_devices\": \"Minden eszköz\",\n        \"tooltip_local\": \"Az eszköz beállításai eltérnek a globálistól\",\n        \"reset_preview\": \"Előnézet visszaállítása\",\n        \"mono\": \"Monospace\",\n        \"line_height\": \"Sortávolság\",\n        \"tooltip_default\": \"Olvasási beállítások\",\n        \"title\": \"Olvasó beállításai\",\n        \"serif\": \"Serif\",\n        \"preview\": \"Előnézet\",\n        \"not_set\": \"Nincs beállítva\",\n        \"clear_local_overrides\": \"Eszközbeállítások törlése\",\n        \"preview_text\": \"A gyors barna róka átugorja a lusta kutyát. Így fog megjelenni az olvasónézeti szöveg.\",\n        \"local_overrides_cleared\": \"Az eszközspecifikus beállítások törölve lettek\",\n        \"local_overrides_description\": \"Ennek az eszköznek az olvasási beállításai eltérnek a globális alapértelmezésektől:\",\n        \"clear_defaults\": \"Összes alapértelmezett törlése\",\n        \"description\": \"Az olvasónézet alapértelmezett szövegbeállításainak konfigurálása. Ezek a beállítások szinkronizálva vannak az összes eszközén.\",\n        \"defaults_cleared\": \"Az olvasó alapértelmezései törölve\",\n        \"save_hint\": \"Beállítások mentése csak ehhez az eszközhöz, vagy szinkronizálás minden eszközre\",\n        \"save_as_default\": \"Mentés alapértelmezettként\",\n        \"save_to_device\": \"Ez az eszköz\",\n        \"sans\": \"Sans Serif\",\n        \"tooltip_preview_and_local\": \"El nem mentett előnézeti módosítások; az eszköz beállításai eltérnek a globálistól\",\n        \"adjust_hint\": \"A módosítások előnézetéhez állítsa be a fenti beállításokat\"\n      },\n      \"avatar\": {\n        \"upload\": \"Avatár feltöltése\",\n        \"change\": \"Avatár módosítása\",\n        \"remove_confirm_title\": \"Avatár eltávolítása?\",\n        \"updated\": \"Avatár frissítve\",\n        \"removed\": \"Avatár eltávolítva\",\n        \"description\": \"Tölts fel egy négyzet alakú képet, amit avatárként használhatsz.\",\n        \"remove_confirm_description\": \"Ezzel törlöd a jelenlegi profilképed.\",\n        \"title\": \"Profilkép\",\n        \"remove\": \"Avatár eltávolítása\"\n      }\n    },\n    \"webhooks\": {\n      \"description\": \"A webhook-ok segítségével műveleteket indíthatsz a könyvjelzők létrehozásakor, módosításakor és újratérképezésekor.\",\n      \"events\": {\n        \"edited\": \"Módosítva\",\n        \"title\": \"Események\",\n        \"crawled\": \"Feltérképezve\",\n        \"created\": \"Létrehozva\"\n      },\n      \"create_webhook\": \"Webhook létrehozása\",\n      \"delete_webhook\": \"Webhook törlése\",\n      \"webhooks\": \"Webhook-ok\",\n      \"auth_token\": \"Hitelesítő token\",\n      \"add_auth_token\": \"Hitelesítő token hozzáadása\",\n      \"edit_auth_token\": \"Hitelesítő token módosítása\",\n      \"delete_webhook_confirmation\": \"Biztos vagy a webhook törlésében?\",\n      \"edit_webhook\": \"Webhook múdosítása\",\n      \"webhook_url\": \"Webhook URL címe\"\n    },\n    \"ai\": {\n      \"tagging_rule_description\": \"Az ide írt utasításokat a modell szabályként fogja használni címke generáláskor. A végső utasításokat megtalálhatók a utasítás előnézetben.\",\n      \"summarization_prompt\": \"Összegző utasítás\",\n      \"all_tagging\": \"Minden címkézés\",\n      \"ai_settings\": \"MI beállítások\",\n      \"tagging_rules\": \"Címkézési szabályok\",\n      \"prompt_preview\": \"Utasítás előnézet\",\n      \"text_prompt\": \"Szöveges utasítás\",\n      \"images_prompt\": \"Utasítás képpel\",\n      \"text_tagging\": \"Szöveg címkézés\",\n      \"image_tagging\": \"Kép címkézés\",\n      \"summarization\": \"Összesítés\",\n      \"tag_style\": \"Címke stílusa\",\n      \"auto_summarization_description\": \"A MI használatával automatikusan összefoglalókat generálhatsz a könyvjelzőidhez.\",\n      \"auto_tagging\": \"Automatikus címkézés\",\n      \"titlecase_spaces\": \"Címzett nagybetűs, szóközökkel\",\n      \"lowercase_underscores\": \"Kisbetűs, aláhúzásokkal\",\n      \"inference_language\": \"Következtetési nyelv\",\n      \"titlecase_hyphens\": \"Címzett nagybetűs, kötőjelekkel\",\n      \"lowercase_hyphens\": \"Kisbetűs, kötőjelekkel\",\n      \"lowercase_spaces\": \"Kisbetűs, szóközökkel\",\n      \"inference_language_description\": \"Válaszd ki az AI által generált címkék és összefoglalók nyelvét.\",\n      \"tag_style_description\": \"Válaszd ki, hogyan legyenek formázva az automatikusan létrehozott címkék.\",\n      \"auto_tagging_description\": \"A MI használatával automatikusan címkéket generálhatsz a könyvjelzőidhez.\",\n      \"camelCase\": \"camelCase\",\n      \"auto_summarization\": \"Automatikus összefoglalás\",\n      \"no_preference\": \"Nincs preferencia\",\n      \"curated_tags\": \"Kurált címkék\",\n      \"curated_tags_description\": \"Opcionálisan korlátozhatod a mesterséges intelligencia címkézését, hogy csak a lista címkéit használja. Ha nincsenek címkék kiválasztva, a mesterséges intelligencia szabadon generál címkéket.\",\n      \"curated_tags_updated\": \"A kurált címkék frissítése sikeresen megtörtént!\",\n      \"curated_tags_update_failed\": \"Nem sikerült frissíteni a kurált címkéket\"\n    },\n    \"api_keys\": {\n      \"new_api_key\": \"Új API kulcs\",\n      \"key_success_please_copy\": \"Másold le a kulcsot és tartsd biztonságos helyen! Az ablak becsukását követően már nem fogsz tudni visszatérni hozzá.\",\n      \"api_keys\": \"API kulcsok\",\n      \"new_api_key_desc\": \"Adj egyedi nevet az API kulcsodnak\",\n      \"key_success\": \"A kulcs létrehozás sikeres\",\n      \"regenerate_api_key\": \"API-kulcs újragenerálása\",\n      \"key_regenerated\": \"A kulcs sikeresen újragenerálva\",\n      \"key_regenerated_please_copy\": \"Kérlek, másold ki az új kulcsot, és tárold biztonságos helyen. A régi kulcs visszavonásra került, és nem fog többé működni.\",\n      \"regenerate_warning\": \"Biztosan újra akarod generálni a(z) „{{name}}” API-kulcsot?\",\n      \"regenerate_confirmation\": \"Ez visszavonja a jelenlegi kulcsot, és generál egy újat. A jelenlegi kulcsot használó alkalmazások nem fognak működni.\"\n    },\n    \"import\": {\n      \"import_bookmarks_from_karakeep_export\": \"Könyvjelző importálása Karakeep-ből\",\n      \"import_bookmarks_from_omnivore_export\": \"Könyvjelző importálása Omnivore-ból\",\n      \"import_export\": \"Importálás / Exportálás\",\n      \"import_export_bookmarks\": \"Könyvjelző importálása / exportálása\",\n      \"import_bookmarks_from_html_file\": \"Könyvjelző importálása HTML fájlból\",\n      \"import_bookmarks_from_pocket_export\": \"Könyvjelző importálása Pocket-ből\",\n      \"import_bookmarks_from_matter_export\": \"Könyvjelző importálása Matter-ből\",\n      \"import_bookmarks_from_linkwarden_export\": \"Könyvjelző importálása Linkwarden-ből\",\n      \"export_links_and_notes\": \"Jegyzetek és hivatkozások exportálása\",\n      \"imported_bookmarks\": \"Importált könyvjelzők\",\n      \"import_bookmarks_from_tab_session_manager_export\": \"Könyvjelzők importálása a Tab Session Managerből\",\n      \"import_bookmarks_from_mymind_export\": \"Könyvjelzők importálása a mymind exportálásából\",\n      \"import_bookmarks_from_instapaper_export\": \"Könyvjelzők importálása Instapaper exportból\"\n    },\n    \"broken_links\": {\n      \"crawling_status\": \"Feltérképezési állapot\",\n      \"last_crawled_at\": \"Utoljára feltérképezve\",\n      \"broken_links\": \"Hibás hivatkozások\",\n      \"crawling_failed\": \"Feltérképezési hiba\"\n    },\n    \"back_to_app\": \"Vissza az alkalmazáshoz\",\n    \"manage_assets\": {\n      \"no_assets\": \"Még nincs semmilyen elemed.\",\n      \"asset_type\": \"Elem Típus\",\n      \"asset_link\": \"Elem Hivatkozás\",\n      \"delete_asset_confirmation\": \"Biztosan törölni akarod ezt az elemet?\",\n      \"delete_asset\": \"Elem Törlése\",\n      \"manage_assets\": \"Elemek Kezelése\",\n      \"bookmark_link\": \"Könyvjelző Hivatkozás\"\n    },\n    \"rules\": {\n      \"rules\": \"Szabálymotor\",\n      \"actions_types\": {\n        \"download_full_page_archive\": \"Teljes oldalarchívum letöltése\",\n        \"favourite_bookmark\": \"Könyvjelző kedvencként jelölése\",\n        \"add_tag\": \"Címke hozzáadása\",\n        \"remove_tag\": \"Címke eltávolítása\",\n        \"add_to_list\": \"Hozzáadás a listához\",\n        \"remove_from_list\": \"Eltávolítás a listáról\",\n        \"archive_bookmark\": \"Könyvjelző archiválása\"\n      },\n      \"rule_name\": \"Szabály neve\",\n      \"description\": \"A szabályok segítségével műveleteket indíthatsz el, amikor egy esemény bekövetkezik.\",\n      \"ceate_rule\": \"Szabály létrehozása\",\n      \"edit_rule\": \"Szabály szerkesztése\",\n      \"save_rule\": \"Szabály mentése\",\n      \"delete_rule\": \"Szabály törlése\",\n      \"delete_rule_confirmation\": \"Biztosan törölni akarod ezt a szabályt?\",\n      \"whenever\": \"Amikor ...\",\n      \"if\": \"Ha ...\",\n      \"enter_rule_name\": \"Add meg a szabály nevét\",\n      \"describe_what_this_rule_does\": \"Írd le, mit csinál ez a szabály\",\n      \"rule_has_been_created\": \"A szabály létre lett hozva!\",\n      \"rule_has_been_updated\": \"A szabály frissítve lett!\",\n      \"rule_has_been_deleted\": \"A szabály törölve lett!\",\n      \"no_rules_created_yet\": \"Még nincsenek szabályok létrehozva\",\n      \"create_your_first_rule\": \"Hozd létre az első szabályodat a munkafolyamat automatizálásához\",\n      \"conditions_types\": {\n        \"always\": \"Mindig\",\n        \"url_contains\": \"URL tartalmazza\",\n        \"imported_from_feed\": \"Hírfolyamból importálva\",\n        \"bookmark_type_is\": \"A könyvjelző típusa\",\n        \"has_tag\": \"Van címkéje\",\n        \"and\": \"A következők mindegyike igaz\",\n        \"is_favourited\": \"Kedvenc\",\n        \"or\": \"Az alábbiak bármelyike igaz\",\n        \"is_archived\": \"Archiválva\",\n        \"url_does_not_contain\": \"Az URL nem tartalmazza\",\n        \"title_contains\": \"A cím tartalmazza\",\n        \"title_does_not_contain\": \"A cím nem tartalmazza\"\n      },\n      \"events_types\": {\n        \"bookmark_added\": \"Könyvjelző hozzáadva\",\n        \"tag_added\": \"Ez a címke hozzá van adva egy könyvjelzőhöz\",\n        \"tag_removed\": \"Ez a címke el lett távolítva egy könyvjelzőről\",\n        \"added_to_list\": \"Egy könyvjelző hozzá lett adva ehhez a listához\",\n        \"removed_from_list\": \"Egy könyvjelző el lett távolítva ebből a listából\",\n        \"favourited\": \"A könyvjelző kedvencként van megjelölve\",\n        \"archived\": \"Egy könyvjelző archiválva van\"\n      }\n    },\n    \"stats\": {\n      \"usage_statistics\": \"Használati statisztikák\",\n      \"insights_description\": \"Betekintés a könyvjelzőzési szokásaidba és gyűjteményedbe\",\n      \"failed_to_load\": \"Nem sikerült betölteni a statisztikákat\",\n      \"overview\": {\n        \"total_bookmarks\": \"Könyvjelzők összesen\",\n        \"all_saved_items\": \"Minden mentett elem\",\n        \"favorites\": \"Kedvencek\",\n        \"starred_bookmarks\": \"Csillagozott könyvjelzők\",\n        \"archived\": \"Archivált\",\n        \"archived_items\": \"Archivált elemek\",\n        \"tags\": \"Címkék\",\n        \"unique_tags_created\": \"Egyedi címkék létrehozva\",\n        \"lists\": \"Listák\",\n        \"bookmark_collections\": \"Könyvjelzőgyűjtemények\",\n        \"highlights\": \"Kiemelések\",\n        \"text_highlights\": \"Szövegkiemelések\",\n        \"storage_used\": \"Felhasznált tárhely\",\n        \"total_asset_storage\": \"Eszközök teljes tárolókapacitása\",\n        \"this_month\": \"Ebben a hónapban\",\n        \"bookmarks_added\": \"Könyvjelzők hozzáadva\"\n      },\n      \"bookmark_types\": {\n        \"title\": \"Könyvjelzőtípusok\",\n        \"links\": \"Linkek\",\n        \"text_notes\": \"Szöveges jegyzetek\",\n        \"assets\": \"Eszközök\"\n      },\n      \"recent_activity\": {\n        \"title\": \"Legutóbbi tevékenység\",\n        \"this_week\": \"Ezen a héten\",\n        \"this_month\": \"Ebben a hónapban\",\n        \"this_year\": \"Ebben az évben\"\n      },\n      \"top_domains\": {\n        \"title\": \"Legnépszerűbb domainek\",\n        \"no_domains_found\": \"Nem található domain\"\n      },\n      \"most_used_tags\": {\n        \"title\": \"Leggyakrabban használt címkék\",\n        \"no_tags_found\": \"Nem találhatók címkék\"\n      },\n      \"activity_patterns\": {\n        \"activity_by_hour\": \"Tevékenység óránként\",\n        \"activity_by_day\": \"Napi aktivitás\"\n      },\n      \"storage_breakdown\": {\n        \"title\": \"Tárhely lebontása\"\n      },\n      \"bookmark_sources\": {\n        \"title\": \"Könyvjelző források\",\n        \"empty\": \"Nincs elérhető forrásadat\"\n      }\n    },\n    \"subscription\": {\n      \"subscription\": \"Előfizetés\",\n      \"manage_subscription\": \"Az előfizetésed és a számlázási adataid kezelése\",\n      \"current_plan\": \"Jelenlegi csomag\",\n      \"billing_period\": \"Számlázási időszak\",\n      \"paid_plan\": \"Fizetős csomag\",\n      \"unlock_bigger_quota\": \"Szerezz nagyobb kvótát és támogasd a projektet\",\n      \"subscribe_now\": \"Iratkozz fel most\",\n      \"manage_billing\": \"Számlázás kezelése\",\n      \"subscription_canceled\": \"Az előfizetésed le lett mondva, és {{date}}-kor fog véget érni. Bármikor újra előfizethetsz.\",\n      \"usage_quotas\": \"Használat és kvóták\",\n      \"track_usage\": \"Kövesd nyomon a jelenlegi használatodat a csomagod korlátaihoz képest\",\n      \"total_bookmarks_saved\": \"Összes mentett könyvjelző\",\n      \"assets_file_storage\": \"Eszközök és fájltárolás\",\n      \"unlimited_usage\": \"Korlátlan használat\",\n      \"quota_limit_reached\": \"Elérted a kvóta korlátot\",\n      \"approaching_quota_limit\": \"Közeledik a kvóta korlát\",\n      \"loading_usage\": \"A használati adatok betöltése...\",\n      \"free\": \"Ingyenes\",\n      \"paid\": \"Fizetős\"\n    },\n    \"import_sessions\": {\n      \"title\": \"Importálási munkamenetek\",\n      \"description\": \"Itt nézheted meg és kezelheted a tömeges importálási munkameneteidet. A munkamenetek automatikusan létrejönnek, amikor könyvjelzőket importálsz.\",\n      \"load_error\": \"Nem sikerült betölteni az importálási munkameneteket\",\n      \"no_sessions\": \"Még nincsenek importálási munkamenetek\",\n      \"no_sessions_detail\": \"Az importálási munkamenetek automatikusan itt fognak megjelenni, amikor könyvjelzőket importálsz\",\n      \"created_at\": \"Létrehozva: {{time}}\",\n      \"progress\": \"Folyamat\",\n      \"status\": {\n        \"pending\": \"Függőben\",\n        \"in_progress\": \"Folyamatban\",\n        \"completed\": \"Befejezve\",\n        \"failed\": \"Sikertelen\",\n        \"processing\": \"Feldolgozás\",\n        \"staging\": \"Előkészítés\",\n        \"running\": \"Futás\",\n        \"paused\": \"Szüneteltetve\"\n      },\n      \"badges\": {\n        \"pending\": \"{{count}} függőben\",\n        \"processing\": \"{{count}} feldolgozás alatt\",\n        \"completed\": \"{{count}} befejezve\",\n        \"failed\": \"{{count}} sikertelen\"\n      },\n      \"imported_to\": \"Importálva ide:\",\n      \"view_list\": \"Lista megtekintése\",\n      \"delete_dialog_title\": \"Importálási munkamenet törlése\",\n      \"delete_dialog_description\": \"Biztosan törölni akarod a következőt: „{{name}}”? Ez a művelet nem vonható vissza. Maguk a könyvjelzők nem lesznek törölve.\",\n      \"delete_session\": \"Munkamenet törlése\",\n      \"pause_session\": \"Szüneteltetés\",\n      \"resume_session\": \"Folytatás\",\n      \"view_details\": \"Részletek megtekintése\",\n      \"detail\": {\n        \"page_title\": \"Importálási munkamenet részletei\",\n        \"back_to_import\": \"Vissza az importáláshoz\",\n        \"filter_all\": \"Összes\",\n        \"filter_accepted\": \"Elfogadva\",\n        \"filter_rejected\": \"Elutasítva\",\n        \"filter_duplicates\": \"Duplikátumok\",\n        \"filter_pending\": \"Függőben\",\n        \"table_title\": \"Cím / URL\",\n        \"table_type\": \"Típus\",\n        \"table_result\": \"Eredmény\",\n        \"table_reason\": \"Ok\",\n        \"table_bookmark\": \"Könyvjelző\",\n        \"result_accepted\": \"Elfogadva\",\n        \"result_rejected\": \"Elutasítva\",\n        \"result_skipped_duplicate\": \"Duplikátum\",\n        \"result_pending\": \"Függőben\",\n        \"result_processing\": \"Feldolgozás\",\n        \"no_results\": \"Nincsenek találatok ehhez a szűrőhöz.\",\n        \"view_bookmark\": \"Könyvjelző megtekintése\",\n        \"load_more\": \"Tölts be többet\",\n        \"no_title\": \"Nincs cím\"\n      }\n    },\n    \"backups\": {\n      \"backups\": \"Biztonsági mentések\",\n      \"page_title\": \"Biztonsági mentések\",\n      \"page_description\": \"Automatikusan hozz létre és kezelj biztonsági mentéseket a könyvjelzőidről. A biztonsági mentések tömörítve és biztonságosan tárolva vannak.\",\n      \"configuration\": {\n        \"title\": \"Biztonsági mentés beállításai\",\n        \"enable_automatic_backups\": \"Automatikus biztonsági mentések engedélyezése\",\n        \"enable_automatic_backups_description\": \"Automatikusan készíts biztonsági másolatot a könyvjelzőidről\",\n        \"backup_frequency\": \"Biztonsági mentés gyakorisága\",\n        \"backup_frequency_description\": \"Milyen gyakran kell biztonsági mentéseket készíteni?\",\n        \"retention_period\": \"Megőrzési idő (nap)\",\n        \"retention_period_description\": \"Hány napig tartsa meg a biztonsági mentéseket a törlés előtt?\",\n        \"frequency\": {\n          \"daily\": \"Naponta\",\n          \"weekly\": \"Hetente\"\n        },\n        \"select_frequency\": \"Gyakoriság kiválasztása\",\n        \"save_settings\": \"Beállítások mentése\"\n      },\n      \"list\": {\n        \"title\": \"Biztonsági másolataid\",\n        \"create_backup_now\": \"Biztonsági mentés létrehozása most\",\n        \"no_backups\": \"Még nincsenek biztonsági másolataid. Engedélyezd az automatikus biztonsági mentéseket, vagy hozz létre egyet manuálisan.\",\n        \"table\": {\n          \"created_at\": \"Létrehozva:\",\n          \"bookmarks\": \"Könyvjelzők\",\n          \"size\": \"Méret\",\n          \"status\": \"Állapot\",\n          \"actions\": \"Műveletek\"\n        },\n        \"status\": {\n          \"success\": \"Siker\",\n          \"failed\": \"Sikertelen\",\n          \"pending\": \"Függőben\"\n        },\n        \"actions\": {\n          \"download_backup\": \"Biztonsági mentés letöltése\",\n          \"delete_backup\": \"Biztonsági mentés törlése\"\n        }\n      },\n      \"dialogs\": {\n        \"delete_backup_title\": \"Biztonsági mentés törlése?\",\n        \"delete_backup_description\": \"Biztos, hogy törölni akarja ezt a biztonsági mentést? Ez a művelet nem vonható vissza.\"\n      },\n      \"toasts\": {\n        \"backup_queued\": \"A biztonsági mentési feladat várólistára került! Hamarosan feldolgozásra kerül.\",\n        \"backup_deleted\": \"A biztonsági mentés törölve lett!\"\n      }\n    }\n  },\n  \"common\": {\n    \"archive\": \"Archívum\",\n    \"bookmark_types\": {\n      \"link\": \"Hivatkozás\",\n      \"title\": \"Könyvjelző típusa\",\n      \"text\": \"Szöveg\",\n      \"media\": \"Média\"\n    },\n    \"roles\": {\n      \"admin\": \"Admin\",\n      \"user\": \"Felhasználó\"\n    },\n    \"url\": \"URL\",\n    \"email\": \"Email\",\n    \"name\": \"Név\",\n    \"password\": \"Jelszó\",\n    \"action\": \"Művelet\",\n    \"actions\": \"Műveletek\",\n    \"created_at\": \"Létrehozás Ideje\",\n    \"role\": \"Szerep\",\n    \"something_went_wrong\": \"Hiba történt\",\n    \"experimental\": \"Kísérleti\",\n    \"tags\": \"Címkék\",\n    \"note\": \"Jegyzet\",\n    \"attachments\": \"Csatolmányok\",\n    \"highlights\": \"Kiemelések\",\n    \"source\": \"Forrás\",\n    \"screenshot\": \"Képernyőkép\",\n    \"video\": \"Videó\",\n    \"home\": \"Otthon\",\n    \"key\": \"Kulcs\",\n    \"search\": \"Keresés\",\n    \"type\": \"Típus\",\n    \"size\": \"Méret\",\n    \"updated_at\": \"Frissítve\",\n    \"title\": \"Cím\",\n    \"description\": \"Leírás\",\n    \"summary\": \"Összegzés\",\n    \"quota\": \"Keret\",\n    \"bookmarks\": \"Könyvjelzők\",\n    \"storage\": \"Tárhely\",\n    \"pdf\": \"Archivált PDF\",\n    \"default\": \"Alapértelmezett\",\n    \"id\": \"Azonosító\",\n    \"last_used\": \"Utoljára használt\"\n  },\n  \"editor\": {\n    \"import_as_text\": \"Importálás szöveges könyvjelzőként\",\n    \"text_toolbar\": {\n      \"markdown_shortcuts\": {\n        \"blockquote\": {\n          \"label\": \"Idézetblokk\",\n          \"example\": \"> Blockquote\"\n        },\n        \"block_code\": {\n          \"example\": \"``` + space\",\n          \"label\": \"Kód tömb\"\n        },\n        \"heading\": {\n          \"label\": \"Fejléc\",\n          \"example\": \"# H1, ## H2, ### H3\"\n        },\n        \"unordered_list\": {\n          \"label\": \"Rendezetlen lista\",\n          \"example\": \"- lista elem\"\n        },\n        \"bold\": {\n          \"label\": \"Félkövér\",\n          \"example\": \"**text** vagy CTRL+b\"\n        },\n        \"italic\": {\n          \"label\": \"Dőlt\",\n          \"example\": \"*Italic* vagy _Italic_ vagy CTRL+i\"\n        },\n        \"label\": \"Markdown rövidítés\",\n        \"ordered_list\": {\n          \"label\": \"Rendezett lista\",\n          \"example\": \"A lista 1. eleme\"\n        },\n        \"inline_code\": {\n          \"label\": \"Inline kód\",\n          \"example\": \"`Code`\"\n        }\n      },\n      \"strikethrough\": \"Áthúzott\",\n      \"underline\": \"Aláhúzott\",\n      \"code\": \"Kód\",\n      \"align_right\": \"Jobbra igazított\",\n      \"redo\": \"Előrelépés\",\n      \"undo\": \"Visszalépés\",\n      \"bold\": \"Félkövér\",\n      \"italic\": \"Dőlt\",\n      \"highlight\": \"Kiemelés\",\n      \"align_left\": \"Balra igazított\",\n      \"align_center\": \"Középre igazított\"\n    },\n    \"multiple_urls_dialog_desc\": \"A beadott szöveg több URL-t tartalmat különböző sorokban. Külön könyvjelzőként importálod őket?\",\n    \"placeholder\": \"Illesszen be hivatkozást vagy képet, írjon jegyzetet vagy húzzon be ide képet…\",\n    \"quickly_focus\": \"A ⌘ + E megnyomásával gyorsan erre a mezőre fókuszálhatsz\",\n    \"multiple_urls_dialog_title\": \"Az URL címeket külön könyvjelzőként importálod?\",\n    \"import_as_separate_bookmarks\": \"Importálás külön könyvjelzőkként\",\n    \"new_item\": \"ÚJ TÁRGY\",\n    \"disabled_submissions\": \"Az alárendelések le vannak tiltva\",\n    \"placeholder_v2\": \"Illessz be egy linket, írj egy jegyzetet vagy húzz ide egy képet…\"\n  },\n  \"search\": {\n    \"is_in_any_list\": \"Szerepel bármely listán\",\n    \"and\": \"És\",\n    \"type_is\": \"A típusa\",\n    \"type_is_not\": \"A típusa nem\",\n    \"is_not_archived\": \"Nem archivált\",\n    \"is_not_favorited\": \"Nincs kedvencekhez adva\",\n    \"is_archived\": \"Archivált\",\n    \"is_not_in_any_list\": \"Nem szerepel bármely listán\",\n    \"is_favorited\": \"Kedvencekhez van adva\",\n    \"has_any_tag\": \"Van hozzáadott címke\",\n    \"has_no_tags\": \"Nincs hozzáadott címke\",\n    \"created_on_or_after\": \"Létrehozva akkor vagy azóta\",\n    \"not_created_on_or_after\": \"Nem létrehozva akkor vagy azóta\",\n    \"created_on_or_before\": \"Létrehozva akkor vagy előtte\",\n    \"not_created_on_or_before\": \"Nem létrehozva akkor vagy előtte\",\n    \"url_contains\": \"Az URL tartalmazza\",\n    \"url_does_not_contain\": \"Az URL nem tartalmazza\",\n    \"is_in_list\": \"Szerepel listán\",\n    \"is_not_in_list\": \"Nem szerepel listán\",\n    \"has_tag\": \"Van címkéje\",\n    \"does_not_have_tag\": \"Nincs címkéje\",\n    \"full_text_search\": \"Teljes szöveg keresése\",\n    \"or\": \"Vagy\",\n    \"is_from_feed\": \"RSS-hírcsatornából származik\",\n    \"is_not_from_feed\": \"Nem RSS-hírcsatornából származik\",\n    \"created_within\": \"Létrehozva ezen belül\",\n    \"created_earlier_than\": \"Korábban létrehozva, mint\",\n    \"year_s\": \" Év(ek)\",\n    \"day_s\": \" Nap(ok)\",\n    \"week_s\": \" Hét(ek)\",\n    \"month_s\": \" Hónap(ok)\",\n    \"day_s_ago\": \" Nap(ok)kal ezelőtt\",\n    \"week_s_ago\": \" Hét(ek)kel ezelőtt\",\n    \"month_s_ago\": \" Hónap(ok)kal ezelőtt\",\n    \"year_s_ago\": \" Év(ek)kel ezelőtt\",\n    \"history\": \"Legutóbbi keresések\",\n    \"title_contains\": \"A cím tartalmazza\",\n    \"title_does_not_contain\": \"A cím nem tartalmazza\",\n    \"is_broken_link\": \"Van hibás link\",\n    \"tags\": \"Címkék\",\n    \"no_suggestions\": \"Nincsenek javaslatok\",\n    \"filters\": \"Szűrők\",\n    \"is_not_broken_link\": \"Van működő link\",\n    \"lists\": \"Listák\",\n    \"feeds\": \"Hírcsatornák\",\n    \"is_from_source\": \"A forrás a következő:\",\n    \"is_not_from_source\": \"A forrás nem\"\n  },\n  \"lists\": {\n    \"manual_list\": \"Manuális lista\",\n    \"new_nested_list\": \"Új beágyazott lista\",\n    \"favourites\": \"Kedvencek\",\n    \"all_lists\": \"Minden lista\",\n    \"new_list\": \"Új lista\",\n    \"edit_list\": \"Lista módosítása\",\n    \"parent_list\": \"Szülő lista\",\n    \"no_parent\": \"Nincs szülő\",\n    \"list_type\": \"Lista típusa\",\n    \"smart_list\": \"Okos lista\",\n    \"search_query\": \"Kereső lekérdezés\",\n    \"search_query_help\": \"Tudj meg többet a keresési lekérdező nyelvről.\",\n    \"merge_list\": \"Lista egyesítése\",\n    \"destination_list\": \"Céllista\",\n    \"delete_after_merge\": \"Az eredeti lista törlése az egyesítés után\",\n    \"no_destination\": \"Nincs cél\",\n    \"description\": \"Leírás (opcionális)\",\n    \"share_list\": \"Lista megosztása\",\n    \"rss\": {\n      \"title\": \"RSS-hírcsatorna\",\n      \"description\": \"RSS-hírcsatorna engedélyezése ehhez a listához\",\n      \"feed_url\": \"RSS-hírcsatorna URL-je\"\n    },\n    \"public_list\": {\n      \"title\": \"Nyilvános lista\",\n      \"description\": \"Mások megtekinthetik ezt a listát\",\n      \"share_link\": \"Link megosztása\"\n    },\n    \"delete_list\": {\n      \"title\": \"Lista törlése\",\n      \"description\": \"A lista törlése nem törli a listában lévő könyvjelzőket.\",\n      \"delete_children\": \"Gyermeklisták törlése (rekurzív módon)\",\n      \"delete_children_description\": \"Ha nincs bejelölve, az összes közvetlen gyermeklista gyökérlista lesz\"\n    },\n    \"shared\": \"Megosztva\",\n    \"collaborators\": {\n      \"manage\": \"Közreműködők kezelése\",\n      \"view\": \"Közreműködők megtekintése\",\n      \"collaborators\": \"Közreműködők\",\n      \"add\": \"Közreműködő hozzáadása\",\n      \"current\": \"Jelenlegi közreműködők\",\n      \"enter_email\": \"Add meg az e-mail címet\",\n      \"please_enter_email\": \"Kérlek, add meg az e-mail címet\",\n      \"added_successfully\": \"A közreműködő sikeresen hozzáadva\",\n      \"failed_to_add\": \"A közreműködő hozzáadása sikertelen\",\n      \"removed\": \"A közreműködő eltávolítva\",\n      \"failed_to_remove\": \"A közreműködő eltávolítása sikertelen\",\n      \"role_updated\": \"A szerep frissítve\",\n      \"failed_to_update_role\": \"A szerep frissítése sikertelen\",\n      \"viewer\": \"Megtekintő\",\n      \"editor\": \"Szerkesztő\",\n      \"owner\": \"Tulajdonos\",\n      \"viewer_description\": \"Megtekintheti a könyvjelzőket a listában\",\n      \"editor_description\": \"Könyvjelzők hozzáadása és eltávolítása\",\n      \"no_collaborators\": \"Még nincsenek közreműködők. Adj hozzá valakit az együttműködéshez!\",\n      \"no_collaborators_readonly\": \"Ehhez a listához nincsenek közreműködők.\",\n      \"people_with_access\": \"Emberek, akik hozzáférhetnek ehhez a listához\",\n      \"add_or_remove\": \"Adj hozzá vagy távolíts el embereket, akik hozzáférhetnek ehhez a listához\",\n      \"invitation_sent\": \"Meghívó sikeresen elküldve\",\n      \"invitation_revoked\": \"Meghívó visszavonva\",\n      \"failed_to_revoke\": \"Nem sikerült visszavonni a meghívót\",\n      \"pending\": \"Függőben\",\n      \"revoke\": \"Visszavonás\",\n      \"declined\": \"Elutasítva\"\n    },\n    \"leave_list\": {\n      \"title\": \"Lista elhagyása\",\n      \"confirm_message\": \"Biztosan el akarod hagyni a(z) {{icon}} {{name}} listát?\",\n      \"warning\": \"Többé nem fogod tudni megtekinteni vagy elérni a könyvjelzőket ebben a listában. A lista tulajdonosa újra hozzáadhat, ha szükséges.\",\n      \"action\": \"Lista elhagyása\",\n      \"success\": \"Elhagytad a(z) \\\"{{icon}} {{name}}\\\" listát\"\n    },\n    \"invitations\": {\n      \"pending\": \"Függőben lévő meghívók\",\n      \"description\": \"Lista együttműködési meghívások áttekintése és fogadása\",\n      \"invited_by\": \"Meghívó küldője\",\n      \"accept\": \"Elfogadás\",\n      \"decline\": \"Elutasítás\",\n      \"accepted\": \"Meghívó elfogadva\",\n      \"declined\": \"Meghívó elutasítva\",\n      \"failed_to_accept\": \"Nem sikerült elfogadni a meghívást\",\n      \"failed_to_decline\": \"Nem sikerült elutasítani a meghívást\"\n    },\n    \"shared_lists\": \"Megosztott listák\"\n  },\n  \"admin\": {\n    \"background_jobs\": {\n      \"crawler_jobs\": \"Feltérképezési feladatok\",\n      \"background_jobs\": \"Háttér feladatok\",\n      \"inference_jobs\": \"Becslési feladatok\",\n      \"indexing_jobs\": \"Indexelési feladatok\",\n      \"tidy_assets_jobs\": \"Tiszta tulajdon feladatok\",\n      \"job\": \"Feladatok\",\n      \"queued\": \"Ütemezve\",\n      \"pending\": \"Folyamatban van\",\n      \"failed\": \"Hibá történt\",\n      \"video_jobs\": \"Videóletöltési feladatok\",\n      \"webhook_jobs\": \"Webhook feladatok\",\n      \"asset_preprocessing_jobs\": \"Eszköz-előfeldolgozási feladatok\",\n      \"feed_jobs\": \"RSS-hírcsatorna feladatok\",\n      \"jobs\": {\n        \"crawler\": {\n          \"title\": \"Crawler munkák\",\n          \"description\": \"Weboldal feltérképezése és tartalom kinyerése URL-ekből\"\n        },\n        \"inference\": {\n          \"title\": \"Következtetési munkák\",\n          \"description\": \"AI-alapú címkézés és tartalom összefoglalása\"\n        },\n        \"indexing\": {\n          \"title\": \"Indexelő munkák\",\n          \"description\": \"Keresési index frissítések\"\n        },\n        \"asset_preprocessing\": {\n          \"title\": \"Eszközök előfeldolgozási munkái\",\n          \"description\": \"Kép- és dokumentumfeldolgozás (képernyőképek, szövegkinyerés stb.)\"\n        },\n        \"tidy_assets\": {\n          \"title\": \"Eszközrendezési munkák\",\n          \"description\": \"Eszközök tisztítása és tárolás optimalizálása\"\n        },\n        \"video\": {\n          \"title\": \"Videóletöltési munkák\",\n          \"description\": \"Videók kivonása és letöltése\"\n        },\n        \"webhook\": {\n          \"title\": \"Webhook munkák\",\n          \"description\": \"Külső webhook értesítések\"\n        },\n        \"feed\": {\n          \"title\": \"RSS feed munkák\",\n          \"description\": \"RSS feed feldolgozása és tartalomfrissítések\"\n        },\n        \"admin_maintenance\": {\n          \"title\": \"Admin karbantartási feladatok\",\n          \"description\": \"Adminisztratív takarítás és eszközkarbantartás\"\n        }\n      },\n      \"monitor_and_manage\": \"Háttérfeladat-sorok és rendszerfeldolgozási feladatok figyelése és kezelése\",\n      \"active\": \"Aktív\",\n      \"available_actions\": \"Elérhető műveletek\",\n      \"status\": {\n        \"title\": \"A munkák állapotának megértése\",\n        \"queued\": {\n          \"title\": \"Sorban\",\n          \"description\": \"Feldolgozásra váró feladatok. Automatikusan elindulnak, amint rendelkezésre állnak erőforrások.\"\n        },\n        \"unprocessed\": {\n          \"title\": \"Feldolgozatlan\",\n          \"description\": \"A még fel nem dolgozott könyvjelzők. Valószínűleg már sorban állnak a feldolgozásra, ha nem, akkor manuálisan újra kell sorba állítani őket.\"\n        },\n        \"failed\": {\n          \"title\": \"Sikertelen\",\n          \"description\": \"Könyvjelzők, amelyek hibákba ütköztek a feldolgozás során. Ezek manuális beavatkozást igényelhetnek.\"\n        }\n      },\n      \"actions\": {\n        \"recrawl_failed_links_only\": \"Csak a sikertelen hivatkozások újbóli feltérképezése\",\n        \"recrawl_all_links\": \"Összes hivatkozás újbóli feltérképezése\",\n        \"without_inference\": \"Következtetés nélkül\",\n        \"regenerate_ai_tags_for_failed_bookmarks_only\": \"AI-címkék újragenerálása csak a sikertelen könyvjelzőkhöz\",\n        \"regenerate_ai_tags_for_all_bookmarks\": \"AI-címkék újragenerálása az összes könyvjelzőhöz\",\n        \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"AI-összefoglalók újragenerálása csak a sikertelen könyvjelzőkhöz\",\n        \"regenerate_ai_summaries_for_all_bookmarks\": \"AI-összefoglalók újragenerálása az összes könyvjelzőhöz\",\n        \"reindex_all_bookmarks\": \"Összes könyvjelző újraindexelése\",\n        \"clean_assets\": \"Lógó elemek tisztítása és metaadatok újraszinkronizálása\",\n        \"reprocess_assets_fix_mode\": \"Feldolgozatlan elemek újrafeldolgozása\",\n        \"migrate_large_link_html_content\": \"Nagy méretű beágyazott HTML-tartalom áthelyezése az eszközökbe\",\n        \"recrawl_pending_links_only\": \"Csak a függőben lévő linkek újbóli feltérképezése\",\n        \"regenerate_ai_tags_for_pending_bookmarks_only\": \"AI-címkék újragenerálása csak a függőben lévő könyvjelzőkhöz\",\n        \"regenerate_ai_summaries_for_pending_bookmarks_only\": \"AI-összefoglalók újragenerálása csak a függőben lévő könyvjelzőkhöz\"\n      }\n    },\n    \"actions\": {\n      \"recrawl_failed_links_only\": \"Hibás hivatkozások újratérképezése\",\n      \"recrawl_all_links\": \"Minden hivatkozások újratérképezése\",\n      \"without_inference\": \"Nincs becslés\",\n      \"regenerate_ai_tags_for_all_bookmarks\": \"Minden könyvjelző MI címkéjének lecserélése\",\n      \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Hibás könyvjelzők MI címkéjének lecserélése\",\n      \"reindex_all_bookmarks\": \"Minden könyvjelző újraindexelése\",\n      \"compact_assets\": \"Kompakt tulajdonok\",\n      \"reprocess_assets_fix_mode\": \"Tulajdonok függvényezése (Fix Mod)\",\n      \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"AI-összefoglalók újragenerálása csak a sikertelen könyvjelzőkhöz\",\n      \"regenerate_ai_summaries_for_all_bookmarks\": \"AI-összefoglalók újragenerálása az összes könyvjelzőhöz\"\n    },\n    \"users_list\": {\n      \"asset_sizes\": \"Tulajdon méretek\",\n      \"users_list\": \"Felhasználók listája\",\n      \"local_user\": \"Helyi felhasználó\",\n      \"create_user\": \"Felhasználó létrehozása\",\n      \"change_role\": \"Szerepkör módosítása\",\n      \"reset_password\": \"Jelszó alaphelyzetbe állítása\",\n      \"delete_user\": \"Felhasználó törlése\",\n      \"num_bookmarks\": \"Számozott könyvjelzők\",\n      \"confirm_password\": \"Jelszó megerősítése\",\n      \"delete_user_confirm_description\": \"Biztosan törölni akarod a(z) \\\"{{name}}\\\" felhasználót?\",\n      \"unlimited\": \"Korlátlan\"\n    },\n    \"admin_settings\": \"Rendszergazdai beállítások\",\n    \"server_stats\": {\n      \"server_stats\": \"Szerver statisztika\",\n      \"total_users\": \"Összes felhasználó\",\n      \"total_bookmarks\": \"Összes könyvjelző\",\n      \"server_version\": \"Szerver verzió\"\n    },\n    \"service_connections\": {\n      \"title\": \"Szolgáltatási kapcsolatok\",\n      \"description\": \"Külső rendszerfüggőségek állapotának és kapcsolatának figyelése\",\n      \"search_engine\": \"Keresőmotor\",\n      \"browser\": \"Böngésző\",\n      \"queue_system\": \"Üzenetsor kezelő rendszer\",\n      \"status\": {\n        \"not_configured\": \"Nincs konfigurálva\",\n        \"connected\": \"Csatlakoztatva\",\n        \"disconnected\": \"Szétkapcsolva\"\n      }\n    },\n    \"admin_tools\": {\n      \"admin_tools\": \"Adminisztrációs eszközök\",\n      \"bookmark_debugger\": \"Könyvjelző hibakereső\",\n      \"bookmark_id\": \"Könyvjelző azonosító\",\n      \"bookmark_id_placeholder\": \"Add meg a könyvjelző azonosítóját\",\n      \"lookup\": \"Keresés\",\n      \"debug_info\": \"Hibakeresési információk\",\n      \"basic_info\": \"Alapvető információk\",\n      \"status\": \"Állapot\",\n      \"content\": \"Tartalom\",\n      \"html_preview\": \"HTML előnézet (első 1000 karakter)\",\n      \"summary\": \"Összegzés\",\n      \"url\": \"URL\",\n      \"source_url\": \"Forrás URL\",\n      \"asset_type\": \"Eszköz típusa\",\n      \"file_name\": \"Fájlnév\",\n      \"owner_user_id\": \"Tulajdonos felhasználói azonosítója\",\n      \"tagging_status\": \"Címkézési állapot\",\n      \"summarization_status\": \"Összefoglalás állapota\",\n      \"crawl_status\": \"Feltérképezés állapota\",\n      \"crawl_status_code\": \"HTTP-állapotkód\",\n      \"crawled_at\": \"Bejárás időpontja\",\n      \"recrawl\": \"Újra bejárás\",\n      \"reindex\": \"Újraindexelés\",\n      \"retag\": \"Újracímkézés\",\n      \"resummarize\": \"Újraösszefoglalás\",\n      \"bookmark_not_found\": \"A könyvjelző nem található\",\n      \"action_success\": \"A művelet sikeresen befejeződött\",\n      \"action_failed\": \"A művelet sikertelen\",\n      \"recrawl_queued\": \"Az újra bejárási feladat várólistára került\",\n      \"reindex_queued\": \"Az újraindexelési feladat várólistára került\",\n      \"retag_queued\": \"Az újracímkézési feladat várólistára került\",\n      \"resummarize_queued\": \"Az újraösszefoglalási feladat várólistára került\",\n      \"view\": \"Megtekintés\",\n      \"fetch_error\": \"Hiba a könyvjelző lekérésekor\"\n    }\n  },\n  \"options\": {\n    \"dark_mode\": \"Sötét mód\",\n    \"light_mode\": \"Világos mód\",\n    \"apps_extensions\": \"Appok és kiterjesztések\",\n    \"documentation\": \"Dokumentáció\",\n    \"follow_us_on_x\": \"Kövess minket az X-en\"\n  },\n  \"tags\": {\n    \"your_tags\": \"Salyát címkék\",\n    \"drag_and_drop_merging\": \"Egyesítés odahúzással\",\n    \"all_tags\": \"Minden címke\",\n    \"your_tags_info\": \"Címkék amiket legalább egyszer használtál\",\n    \"ai_tags_info\": \"Csak automatikusan (MI) hozzáadott címkék\",\n    \"unused_tags_info\": \"Semelyik könyvjelzőhöz sem hozzáadott címkék\",\n    \"delete_all_unused_tags\": \"Minden nem használt címke törlése\",\n    \"drag_and_drop_merging_info\": \"Húzza egymásra a címkéket az egyesítéshez\",\n    \"sort_by_name\": \"Rendezés név szerint\",\n    \"ai_tags\": \"MI címkék\",\n    \"unused_tags\": \"Nem használt címkék\",\n    \"create_tag\": \"Címke létrehozása\",\n    \"create_tag_description\": \"Új címke létrehozása anélkül, hogy bármilyen könyvjelzőhöz lenne csatolva\",\n    \"tag_name\": \"Címke neve\",\n    \"enter_tag_name\": \"Írd be a címke nevét\",\n    \"sort_by_usage\": \"Rendezés használat szerint\",\n    \"sort_by_relevance\": \"Rendezés relevancia szerint\",\n    \"no_custom_tags\": \"Még nincsenek egyéni címkék\",\n    \"no_ai_tags\": \"Még nincsenek mesterséges intelligencia címkék\",\n    \"no_unused_tags\": \"Nincsenek fel nem használt címkéid\",\n    \"no_unused_tags_match_your_search\": \"Nincsenek a keresésednek megfelelő fel nem használt címkék\",\n    \"no_tags_match_your_search\": \"Nincsenek a keresésednek megfelelő címkék\",\n    \"search_placeholder\": \"Címkék keresése…\",\n    \"search_or_create_placeholder\": \"Címkék keresése vagy létrehozása…\"\n  },\n  \"layouts\": {\n    \"masonry\": \"Falazat\",\n    \"grid\": \"Rács\",\n    \"list\": \"Lista\",\n    \"compact\": \"Kompakt\"\n  },\n  \"preview\": {\n    \"view_original\": \"Eredeti megjelenítése\",\n    \"cached_content\": \"Gyorsítótárazott tartalmak\",\n    \"reader_view\": \"Olvasó nézet\",\n    \"tabs\": {\n      \"content\": \"Tartalom\",\n      \"details\": \"Részletek\"\n    },\n    \"archive_info\": \"Lehetséges, hogy a JavaScriptet igénylő archívumok nem jelennek meg helyesen beágyazva. A legjobb eredmény érdekében <1>töltsd le és nyisd meg a böngésződben</1>.\",\n    \"fetch_error_title\": \"A tartalom nem érhető el\",\n    \"fetch_error_description\": \"Ezt a linket nem tudtuk betölteni. Lehet, hogy az oldal védett, hitelesítést igényel, vagy átmenetileg nem elérhető.\",\n    \"crawling_in_progress\": \"Oldaltartalom lekérése…\",\n    \"continue_reading\": \"Folytasd, ahol abbahagytad\",\n    \"continue_reading_percent\": \"Folytasd, ahol abbahagytad ({{percent}}%)\",\n    \"continue_button\": \"Folytatás\"\n  },\n  \"dialogs\": {\n    \"bookmarks\": {\n      \"delete_confirmation_title\": \"Törli a könyvjelzőt?\",\n      \"delete_confirmation_description\": \"Biztos benne, hogy törli a könyvjelzőt?\"\n    }\n  },\n  \"toasts\": {\n    \"bookmarks\": {\n      \"updated\": \"A könyvjelző frissítve lett!\",\n      \"deleted\": \"A könyvjelző törölve lett!\",\n      \"refetch\": \"Újra begyűjtés beütemezve!\",\n      \"full_page_archive\": \"Minden oldal lecserélése beütemezésre került\",\n      \"delete_from_list\": \"A könyvjelző törlődött a listából\",\n      \"clipboard_copied\": \"A hivatkozás kimásolva a memóriába!\",\n      \"preserve_pdf\": \"A PDF archiválás elindult.\",\n      \"update_banner\": \"A banner frissítve lett!\",\n      \"uploading_banner\": \"Banner feltöltése...\"\n    },\n    \"lists\": {\n      \"created\": \"A hivatkozás létrejött!\",\n      \"updated\": \"A hivatkozás frissült!\",\n      \"merged\": \"A lista összevonásra került!\",\n      \"deleted\": \"A lista törölve lett!\"\n    },\n    \"tags\": {\n      \"created\": \"A címke létrejött!\",\n      \"failed_to_create\": \"Nem sikerült létrehozni a címkét\"\n    }\n  },\n  \"cleanups\": {\n    \"cleanups\": \"Tisztítás\",\n    \"duplicate_tags\": {\n      \"title\": \"Duplikált címkék\",\n      \"merge_all_suggestions\": \"Minden javasolt elemet egyesít?\"\n    }\n  },\n  \"highlights\": {\n    \"no_highlights\": \"Még nem emeltél kis semmit.\"\n  },\n  \"banners\": {\n    \"no_bookmarks\": {\n      \"title\": \"Még nincsenek könyvjelzők\",\n      \"description\": \"Mentsd el az érdekes cikkeket, linkeket és oldalakat, hogy később gyorsan elérhesd őket.\"\n    }\n  },\n  \"bookmark_editor\": {\n    \"title\": \"Könyvjelző szerkesztése\",\n    \"subtitle\": \"Végezz módosításokat a könyvjelző részletein. Ha kész, kattints a mentés gombra.\",\n    \"author\": \"Szerző\",\n    \"publisher\": \"Kiadó\",\n    \"date_published\": \"Közzététel dátuma\",\n    \"pick_a_date\": \"Válassz egy dátumot\",\n    \"save_changes\": \"Változások mentése\",\n    \"extracted_content\": \"Kinyert tartalom\"\n  },\n  \"view_options\": {\n    \"title\": \"Nézet beállításai\",\n    \"layout\": \"Elrendezés\",\n    \"columns\": \"Oszlopok\",\n    \"display_options\": \"Megjelenítési beállítások\",\n    \"show_note_previews\": \"Jegyzetek megjelenítése\",\n    \"show_tags\": \"Címkék mutatása\",\n    \"show_title\": \"Cím mutatása\",\n    \"image_options\": \"Kép beállításai\",\n    \"image_fit_cover\": \"Borító (kitöltés)\",\n    \"image_fit_contain\": \"Tartalmaz (illesztés)\"\n  },\n  \"version\": {\n    \"new_release_available\": \"Új kiadási megjegyzések érhetők el\",\n    \"whats_new_title\": \"Újdonságok a {{version}} verzióban\",\n    \"release_notes_description\": \"Itt vannak a GitHub kiadási megjegyzéseiből begyűjtött legújabb frissítések.\",\n    \"loading_release_notes\": \"Kiadási megjegyzések betöltése…\",\n    \"unable_to_load_release_notes\": \"Jelenleg nem sikerült betölteni a kiadási megjegyzéseket. Kérlek, próbáld meg később.\",\n    \"no_release_notes\": \"Ehhez a verzióhoz nem lettek kiadási megjegyzések közzétéve.\",\n    \"release_notes_synced\": \"A kiadási megjegyzések a GitHubról vannak szinkronizálva.\",\n    \"view_on_github\": \"Megtekintés a GitHubon\"\n  },\n  \"wrapped\": {\n    \"title\": \"A te {{year}} összegzésed\",\n    \"subtitle\": \"Egy év Karakeepben\",\n    \"banner\": {\n      \"title\": \"A 2025-ös összegzésed kész!\",\n      \"description\": \"Nézd meg az éved a könyvjelzőkben\",\n      \"view_now\": \"Megtekintés most\"\n    },\n    \"button\": \"2025 Összegzés\",\n    \"loading\": \"Összegzés betöltése...\",\n    \"failed_to_load\": \"Nem sikerült betölteni az összegzés statisztikáidat\",\n    \"sections\": {\n      \"total_saves\": {\n        \"prefix\": \"Mentetted\",\n        \"suffix\": \"cuccok idén\",\n        \"suffix_singular\": \"cucc idén\"\n      },\n      \"first_bookmark\": {\n        \"title\": \"A te utad elkezdődött\",\n        \"description\": \"Első mentés {{year}}-ben:\"\n      },\n      \"top_domains\": \"A te legjobb oldalaid\",\n      \"top_tags\": \"A te legjobb címkéid\",\n      \"monthly_activity\": \"A te éved mentésekben\",\n      \"most_active_day\": \"A te legaktívabb napod\",\n      \"peak_times\": {\n        \"title\": \"Mikor mentesz\",\n        \"peak_hour\": \"Csúcs óra\",\n        \"peak_day\": \"Csúcsnap\"\n      },\n      \"how_you_save\": \"Hogyan mentesz\",\n      \"what_you_saved\": \"Mit mentettél el\",\n      \"summary\": {\n        \"favorites\": \"Kedvencek\",\n        \"tags_created\": \"Címkék létrehozva\",\n        \"highlights\": \"Kiemelések\"\n      },\n      \"types\": {\n        \"links\": \"Linkek\",\n        \"notes\": \"Jegyzetek\",\n        \"assets\": \"Eszközök\"\n      }\n    },\n    \"footer\": \"Készült a Karakeep-pel\",\n    \"share\": \"Megosztás\",\n    \"download\": \"Letöltés\",\n    \"close\": \"Bezárás\",\n    \"generating\": \"Generálás...\"\n  }\n}\n"
  },
  {
    "path": "apps/web/lib/i18n/locales/it/translation.json",
    "content": "{\n  \"actions\": {\n    \"unfavorite\": \"Rimuovi dai preferiti\",\n    \"edit\": \"Modifica\",\n    \"delete\": \"Elimina\",\n    \"ignore\": \"Ignora\",\n    \"unselect_all\": \"Deseleziona tutto\",\n    \"change_layout\": \"Cambia layout\",\n    \"select_all\": \"Seleziona tutto\",\n    \"archive\": \"Archivia\",\n    \"unarchive\": \"Rimuovi dall'archivio\",\n    \"favorite\": \"Preferito\",\n    \"refresh\": \"Aggiorna\",\n    \"recrawl\": \"Ricontrolla\",\n    \"download_full_page_archive\": \"Scarica un archivio della pagina completa\",\n    \"edit_tags\": \"Modifica tags\",\n    \"add_to_list\": \"Aggiungi alla lista\",\n    \"close_bulk_edit\": \"Chiudi modifica massiva\",\n    \"bulk_edit\": \"Modifica massiva\",\n    \"manage_lists\": \"Gestisci liste\",\n    \"remove_from_list\": \"Rimuovi dalla lista\",\n    \"save\": \"Salva\",\n    \"add\": \"Aggiungi\",\n    \"create\": \"Crea\",\n    \"fetch_now\": \"Recupera ora\",\n    \"summarize_with_ai\": \"Riassumi con AI\",\n    \"edit_title\": \"Modifica titolo\",\n    \"sign_out\": \"Esci\",\n    \"close\": \"Chiudi\",\n    \"merge\": \"Unisci\",\n    \"cancel\": \"Cancella\",\n    \"apply_all\": \"Applica a tutto\",\n    \"copy_link\": \"Copia link\",\n    \"sort\": {\n      \"title\": \"Ordina\",\n      \"newest_first\": \"Prima i più recenti\",\n      \"oldest_first\": \"Prima i più vecchi\",\n      \"relevant_first\": \"Più rilevanti prima\"\n    },\n    \"open_editor\": \"Apri editor\",\n    \"toggle_show_archived\": \"Mostra archiviati\",\n    \"confirm\": \"Conferma\",\n    \"regenerate\": \"Rigenera\",\n    \"load_more\": \"Carica altro\",\n    \"edit_notes\": \"Modifica note\",\n    \"preserve_as_pdf\": \"Salva come PDF\",\n    \"offline_copies\": \"Copie offline\",\n    \"preserve_offline_archive\": \"Conserva l'archivio offline\",\n    \"download_full_page_archive_file\": \"Scarica il file di archivio\",\n    \"download_pdf_file\": \"Scarica il file PDF\",\n    \"remove\": \"Rimuovi\",\n    \"more\": \"Altro\",\n    \"replace_banner\": \"Sostituisci il banner\",\n    \"add_banner\": \"Aggiungi un banner\",\n    \"download\": \"Scarica\"\n  },\n  \"common\": {\n    \"attachments\": \"Allegati\",\n    \"something_went_wrong\": \"Qualcosa è andato storto\",\n    \"roles\": {\n      \"user\": \"Utente\",\n      \"admin\": \"Admin\"\n    },\n    \"video\": \"Video\",\n    \"name\": \"Nome\",\n    \"email\": \"Email\",\n    \"password\": \"Password\",\n    \"action\": \"Azione\",\n    \"actions\": \"Azioni\",\n    \"created_at\": \"Creato il\",\n    \"key\": \"Chiave\",\n    \"role\": \"Ruolo\",\n    \"experimental\": \"Sperimentale\",\n    \"search\": \"Cerca\",\n    \"tags\": \"Tags\",\n    \"note\": \"Note\",\n    \"screenshot\": \"Screenshot\",\n    \"archive\": \"Archivio\",\n    \"home\": \"Home\",\n    \"url\": \"URL\",\n    \"type\": \"Digita\",\n    \"bookmark_types\": {\n      \"link\": \"Collegamento\",\n      \"text\": \"Testo\",\n      \"media\": \"Media\",\n      \"title\": \"Tipo di segnalibro\"\n    },\n    \"size\": \"Dimensione\",\n    \"highlights\": \"Evidenziazioni\",\n    \"source\": \"Origine\",\n    \"updated_at\": \"Aggiornato il\",\n    \"title\": \"Titolo\",\n    \"description\": \"Descrizione\",\n    \"summary\": \"Riepilogo\",\n    \"quota\": \"Quota\",\n    \"bookmarks\": \"Segnalibri\",\n    \"storage\": \"Archiviazione\",\n    \"pdf\": \"PDF archiviato\",\n    \"default\": \"Predefinito\",\n    \"id\": \"ID\",\n    \"last_used\": \"Ultimo utilizzo\"\n  },\n  \"settings\": {\n    \"broken_links\": {\n      \"broken_links\": \"Link rotti\",\n      \"last_crawled_at\": \"Ultimo recupero il\",\n      \"crawling_status\": \"Stato recupero\",\n      \"crawling_failed\": \"Recupero fallito\"\n    },\n    \"info\": {\n      \"confirm_new_password\": \"Conferma nuova password\",\n      \"interface_lang\": \"Lingua interfaccia\",\n      \"user_info\": \"Info utente\",\n      \"basic_details\": \"Informazioni base\",\n      \"change_password\": \"Cambia password\",\n      \"current_password\": \"Password attuale\",\n      \"new_password\": \"Nuova password\",\n      \"options\": \"Opzioni\",\n      \"user_settings\": {\n        \"user_settings_updated\": \"Le impostazioni utente sono state aggiornate!\",\n        \"bookmark_click_action\": {\n          \"title\": \"Azione al clic sul segnalibro\",\n          \"open_external_url\": \"Apri URL originale\",\n          \"open_bookmark_details\": \"Apri dettagli segnalibro\"\n        },\n        \"archive_display_behaviour\": {\n          \"title\": \"Segnalibri archiviati\",\n          \"show\": \"Mostra i segnalibri archiviati in tag e liste\",\n          \"hide\": \"Nascondi i segnalibri archiviati in tag e liste\"\n        }\n      },\n      \"reader_settings\": {\n        \"local_overrides_title\": \"Impostazioni specifiche del dispositivo attive\",\n        \"using_default\": \"Utilizzo predefinito del client\",\n        \"clear_override_hint\": \"Cancella la sostituzione del dispositivo per utilizzare l'impostazione globale ({{value}})\",\n        \"font_size\": \"Dimensione del font\",\n        \"font_family\": \"Famiglia di caratteri\",\n        \"preview_inline\": \"(anteprima)\",\n        \"tooltip_preview\": \"Modifiche all'anteprima non salvate\",\n        \"save_to_all_devices\": \"Tutti i dispositivi\",\n        \"tooltip_local\": \"Le impostazioni del dispositivo differiscono da quelle globali\",\n        \"reset_preview\": \"Ripristina l'anteprima\",\n        \"mono\": \"Monospace\",\n        \"line_height\": \"Altezza della linea\",\n        \"tooltip_default\": \"Impostazioni di lettura\",\n        \"title\": \"Impostazioni lettore\",\n        \"serif\": \"Serif\",\n        \"preview\": \"Anteprima\",\n        \"not_set\": \"Non impostato\",\n        \"clear_local_overrides\": \"Cancella impostazioni del dispositivo\",\n        \"preview_text\": \"The quick brown fox jumps over the lazy dog. Ecco come apparirà il testo nella visualizzazione del lettore.\",\n        \"local_overrides_cleared\": \"Le impostazioni specifiche del dispositivo sono state cancellate\",\n        \"local_overrides_description\": \"Questo dispositivo ha impostazioni del lettore diverse da quelle predefinite globali:\",\n        \"clear_defaults\": \"Cancella tutti i predefiniti\",\n        \"description\": \"Configura le impostazioni di testo predefinite per la visualizzazione del lettore. Queste impostazioni si sincronizzano su tutti i tuoi dispositivi.\",\n        \"defaults_cleared\": \"Le impostazioni predefinite del lettore sono state cancellate\",\n        \"save_hint\": \"Salva le impostazioni solo per questo dispositivo o sincronizza su tutti i dispositivi\",\n        \"save_as_default\": \"Salva come predefinito\",\n        \"save_to_device\": \"Questo dispositivo\",\n        \"sans\": \"Sans Serif\",\n        \"tooltip_preview_and_local\": \"Modifiche all'anteprima non salvate; le impostazioni del dispositivo differiscono da quelle globali\",\n        \"adjust_hint\": \"Regola le impostazioni sopra per visualizzare l'anteprima delle modifiche\"\n      },\n      \"avatar\": {\n        \"upload\": \"Carica avatar\",\n        \"change\": \"Cambia avatar\",\n        \"remove_confirm_title\": \"Rimuovere l'avatar?\",\n        \"updated\": \"Avatar aggiornato\",\n        \"removed\": \"Avatar rimosso\",\n        \"description\": \"Carica un'immagine quadrata da usare come avatar.\",\n        \"remove_confirm_description\": \"Ehm... rimuoverai la tua attuale foto del profilo.\",\n        \"title\": \"Foto profilo\",\n        \"remove\": \"Rimuovi avatar\"\n      }\n    },\n    \"back_to_app\": \"Torna all'App\",\n    \"user_settings\": \"Impostazioni utente\",\n    \"ai\": {\n      \"ai_settings\": \"Impostazioni AI\",\n      \"tagging_rules\": \"Regole dei tag\",\n      \"prompt_preview\": \"Anteprima prompt\",\n      \"text_prompt\": \"Prompt testo\",\n      \"images_prompt\": \"Prompt immagini\",\n      \"tagging_rule_description\": \"Il testo che scriverai qui verrà incluso come regole al modello AI durante la generazione dei tag. Puoi vedere il prompt finale nella sezione anteprima.\",\n      \"summarization_prompt\": \"Prompt di riepilogo\",\n      \"image_tagging\": \"Tagging immagini\",\n      \"text_tagging\": \"Tagging testo\",\n      \"all_tagging\": \"Tutte le etichette\",\n      \"summarization\": \"Riassunto\",\n      \"tag_style\": \"Stile etichetta\",\n      \"auto_summarization_description\": \"Genera automaticamente riassunti per i tuoi segnalibri usando l'AI.\",\n      \"auto_tagging\": \"Tagging automatico\",\n      \"titlecase_spaces\": \"Maiuscola con spazi\",\n      \"lowercase_underscores\": \"Minuscolo con trattini bassi\",\n      \"inference_language\": \"Lingua di inferenza\",\n      \"titlecase_hyphens\": \"Maiuscola con trattini\",\n      \"lowercase_hyphens\": \"Minuscolo con trattini\",\n      \"lowercase_spaces\": \"Minuscolo con spazi\",\n      \"inference_language_description\": \"Scegli la lingua per i tag e i riepiloghi generati dall'AI.\",\n      \"tag_style_description\": \"Scegli come formattare le etichette generate automaticamente.\",\n      \"auto_tagging_description\": \"Genera automaticamente i tag per i tuoi segnalibri usando l'AI.\",\n      \"camelCase\": \"camelCase\",\n      \"auto_summarization\": \"Riassunto automatico\",\n      \"no_preference\": \"Nessuna preferenza\",\n      \"curated_tags\": \"Tag curati\",\n      \"curated_tags_description\": \"Limita facoltativamente l'assegnazione di tag AI all'uso esclusivo di tag provenienti da questo elenco. Quando non sono selezionati tag, l'AI genera tag liberamente.\",\n      \"curated_tags_updated\": \"Tag curati aggiornati con successo!\",\n      \"curated_tags_update_failed\": \"Impossibile aggiornare i tag curati\"\n    },\n    \"feeds\": {\n      \"rss_subscriptions\": \"Iscrizione RSS\",\n      \"add_a_subscription\": \"Aggiungi un'iscrizione\",\n      \"feed_enabled\": \"Feed RSS abilitato\",\n      \"feed_disabled\": \"Feed RSS disabilitato\"\n    },\n    \"import\": {\n      \"import_export\": \"Importa / Esporta\",\n      \"import_bookmarks_from_pocket_export\": \"Importa segnalibri da esportazione Pocket\",\n      \"import_bookmarks_from_matter_export\": \"Importa segnalibri da esportazione Matter\",\n      \"import_bookmarks_from_karakeep_export\": \"Importa segnalibri da esportazione Karakeep\",\n      \"export_links_and_notes\": \"Esporta link e note\",\n      \"imported_bookmarks\": \"Segnalibri importati\",\n      \"import_bookmarks_from_html_file\": \"Importa segnalibri da file HTML\",\n      \"import_export_bookmarks\": \"Importa / Esporta segnalibri\",\n      \"import_bookmarks_from_omnivore_export\": \"Importa segnalibri da esportazione Omnivore\",\n      \"import_bookmarks_from_linkwarden_export\": \"Importa i segnalibri dall'esportazione di Linkwarden\",\n      \"import_bookmarks_from_tab_session_manager_export\": \"Importa i segnalibri da Tab Session Manager\",\n      \"import_bookmarks_from_mymind_export\": \"Importa i segnalibri dall'esportazione di mymind\",\n      \"import_bookmarks_from_instapaper_export\": \"Importa i segnalibri dall'esportazione di Instapaper\"\n    },\n    \"api_keys\": {\n      \"api_keys\": \"Chiavi API\",\n      \"new_api_key_desc\": \"Dai un nome univoco alla chiave API\",\n      \"key_success\": \"Chiave creata con successo\",\n      \"key_success_please_copy\": \"Copia la chiave e salvala in modo sicuro. Una volta chiuso questo messaggio, non potrai accederci di nuovo.\",\n      \"new_api_key\": \"Nuova chiave API\",\n      \"regenerate_api_key\": \"Rigenera chiave API\",\n      \"key_regenerated\": \"Chiave rigenerata con successo\",\n      \"key_regenerated_please_copy\": \"Copia la nuova chiave e conservala in un luogo sicuro. La vecchia chiave non è più valida e non funzionerà più.\",\n      \"regenerate_warning\": \"Sei sicuro di voler rigenerare la chiave API \\\"{{name}}\\\"?\",\n      \"regenerate_confirmation\": \"Questa operazione revocherà la chiave attuale e ne genererà una nuova. Tutte le applicazioni che utilizzano la chiave attuale smetteranno di funzionare.\"\n    },\n    \"manage_assets\": {\n      \"no_assets\": \"Non hai ancora nessun asset.\",\n      \"asset_type\": \"Tipo di risorsa\",\n      \"bookmark_link\": \"Collegamento segnalibro\",\n      \"asset_link\": \"Link risorsa\",\n      \"delete_asset\": \"Elimina risorsa\",\n      \"delete_asset_confirmation\": \"Sei sicuro di voler eliminare questa risorsa?\",\n      \"manage_assets\": \"Gestisci risorse\"\n    },\n    \"webhooks\": {\n      \"description\": \"Puoi usare i webhook per attivare azioni quando i segnalibri vengono creati, modificati o scansionati.\",\n      \"events\": {\n        \"crawled\": \"Scansionato\",\n        \"created\": \"Creato\",\n        \"title\": \"Eventi\",\n        \"edited\": \"Modificato\"\n      },\n      \"auth_token\": \"Token di autenticazione\",\n      \"add_auth_token\": \"Aggiungi token di autenticazione\",\n      \"edit_auth_token\": \"Modifica token di autenticazione\",\n      \"create_webhook\": \"Crea Webhook\",\n      \"delete_webhook\": \"Elimina webhook\",\n      \"edit_webhook\": \"Modifica Webhook\",\n      \"webhook_url\": \"URL del webhook\",\n      \"delete_webhook_confirmation\": \"Sei sicuro di voler eliminare questo webhook?\",\n      \"webhooks\": \"Webhook\"\n    },\n    \"rules\": {\n      \"conditions_types\": {\n        \"url_contains\": \"L'URL contiene\",\n        \"imported_from_feed\": \"Importato dal feed\",\n        \"always\": \"Sempre\",\n        \"bookmark_type_is\": \"Il tipo di segnalibro è\",\n        \"has_tag\": \"Ha il tag\",\n        \"is_favourited\": \"È tra i preferiti\",\n        \"is_archived\": \"Archiviato\",\n        \"and\": \"Tutte le seguenti sono vere\",\n        \"or\": \"Se una qualsiasi delle seguenti condizioni è vera\",\n        \"url_does_not_contain\": \"L'URL non contiene\",\n        \"title_contains\": \"Il titolo contiene\",\n        \"title_does_not_contain\": \"Il titolo non contiene\"\n      },\n      \"events_types\": {\n        \"tag_removed\": \"Questo tag viene rimosso da un segnalibro\",\n        \"added_to_list\": \"Un segnalibro viene aggiunto a questo elenco\",\n        \"removed_from_list\": \"Un segnalibro viene rimosso da questo elenco\",\n        \"favourited\": \"Un segnalibro è tra i preferiti\",\n        \"bookmark_added\": \"Un segnalibro viene aggiunto\",\n        \"tag_added\": \"Questo tag viene aggiunto a un segnalibro\",\n        \"archived\": \"Un segnalibro è archiviato\"\n      },\n      \"description\": \"Puoi usare le regole per attivare azioni quando viene attivato un evento.\",\n      \"rules\": \"Motore di regole\",\n      \"rule_name\": \"Nome regola\",\n      \"ceate_rule\": \"Crea regola\",\n      \"edit_rule\": \"Modifica regola\",\n      \"save_rule\": \"Salva regola\",\n      \"delete_rule\": \"Elimina regola\",\n      \"delete_rule_confirmation\": \"Sei sicuro di voler eliminare questa regola?\",\n      \"whenever\": \"Quando...\",\n      \"if\": \"Se...\",\n      \"enter_rule_name\": \"Inserisci il nome della regola\",\n      \"describe_what_this_rule_does\": \"Descrivi cosa fa questa regola\",\n      \"rule_has_been_created\": \"Regola creata!\",\n      \"rule_has_been_updated\": \"Regola aggiornata!\",\n      \"rule_has_been_deleted\": \"Regola eliminata!\",\n      \"no_rules_created_yet\": \"Nessuna regola creata finora\",\n      \"create_your_first_rule\": \"Crea la tua prima regola per automatizzare il tuo flusso di lavoro\",\n      \"actions_types\": {\n        \"add_to_list\": \"Aggiungi alla lista\",\n        \"remove_from_list\": \"Rimuovi dalla lista\",\n        \"download_full_page_archive\": \"Scarica l'archivio della pagina completa\",\n        \"favourite_bookmark\": \"Segnalibro preferito\",\n        \"archive_bookmark\": \"Archivia segnalibro\",\n        \"add_tag\": \"Aggiungi tag\",\n        \"remove_tag\": \"Rimuovi tag\"\n      }\n    },\n    \"stats\": {\n      \"usage_statistics\": \"Statistiche di utilizzo\",\n      \"insights_description\": \"Approfondimenti sulle tue abitudini di bookmarking e sulla tua collezione\",\n      \"failed_to_load\": \"Impossibile caricare le statistiche\",\n      \"overview\": {\n        \"total_bookmarks\": \"Segnalibri totali\",\n        \"all_saved_items\": \"Tutti gli elementi salvati\",\n        \"favorites\": \"Preferiti\",\n        \"starred_bookmarks\": \"Segnalibri preferiti\",\n        \"archived\": \"Archiviato\",\n        \"archived_items\": \"Elementi archiviati\",\n        \"tags\": \"Tag\",\n        \"unique_tags_created\": \"Tag univoci creati\",\n        \"lists\": \"Liste\",\n        \"bookmark_collections\": \"Raccolte di segnalibri\",\n        \"highlights\": \"Evidenziazioni\",\n        \"text_highlights\": \"Evidenziazioni di testo\",\n        \"storage_used\": \"Spazio di archiviazione utilizzato\",\n        \"total_asset_storage\": \"Archiviazione totale degli asset\",\n        \"this_month\": \"Questo mese\",\n        \"bookmarks_added\": \"Segnalibri aggiunti\"\n      },\n      \"bookmark_types\": {\n        \"title\": \"Tipi di segnalibri\",\n        \"links\": \"Link\",\n        \"text_notes\": \"Note di testo\",\n        \"assets\": \"Asset\"\n      },\n      \"recent_activity\": {\n        \"title\": \"Attività recente\",\n        \"this_week\": \"Questa settimana\",\n        \"this_month\": \"Questo mese\",\n        \"this_year\": \"Quest'anno\"\n      },\n      \"top_domains\": {\n        \"title\": \"Domini principali\",\n        \"no_domains_found\": \"Nessun dominio trovato\"\n      },\n      \"most_used_tags\": {\n        \"title\": \"Tag più usati\",\n        \"no_tags_found\": \"Nessun tag trovato\"\n      },\n      \"activity_patterns\": {\n        \"activity_by_hour\": \"Attività per ora\",\n        \"activity_by_day\": \"Attività per giorno\"\n      },\n      \"storage_breakdown\": {\n        \"title\": \"Ripartizione archiviazione\"\n      },\n      \"bookmark_sources\": {\n        \"title\": \"Origini segnalibro\",\n        \"empty\": \"Nessun dato di origine disponibile\"\n      }\n    },\n    \"subscription\": {\n      \"subscription\": \"Abbonamento\",\n      \"manage_subscription\": \"Gestisci il tuo abbonamento e le informazioni di fatturazione\",\n      \"current_plan\": \"Piano attuale\",\n      \"billing_period\": \"Periodo di fatturazione\",\n      \"paid_plan\": \"Piano a pagamento\",\n      \"unlock_bigger_quota\": \"Sblocca una quota maggiore e supporta il progetto\",\n      \"subscribe_now\": \"Iscriviti ora\",\n      \"manage_billing\": \"Gestisci la fatturazione\",\n      \"subscription_canceled\": \"Il tuo abbonamento è stato annullato e terminerà il {{date}}. Puoi riabbonarti in qualsiasi momento.\",\n      \"usage_quotas\": \"Utilizzo e quote\",\n      \"track_usage\": \"Tieni traccia del tuo utilizzo attuale rispetto ai limiti del tuo piano\",\n      \"total_bookmarks_saved\": \"Totale segnalibri salvati\",\n      \"assets_file_storage\": \"Risorse e archiviazione file\",\n      \"unlimited_usage\": \"Utilizzo illimitato\",\n      \"quota_limit_reached\": \"Limite di quota raggiunto\",\n      \"approaching_quota_limit\": \"Limite di quota in avvicinamento\",\n      \"loading_usage\": \"Caricamento delle informazioni sull'utilizzo...\",\n      \"free\": \"Gratuito\",\n      \"paid\": \"A pagamento\"\n    },\n    \"import_sessions\": {\n      \"title\": \"Importa sessioni\",\n      \"description\": \"Visualizza e gestisci le tue sessioni di importazione in blocco. Le sessioni vengono create automaticamente quando importi i segnalibri.\",\n      \"load_error\": \"Impossibile caricare le sessioni di importazione\",\n      \"no_sessions\": \"Ancora nessuna sessione di importazione\",\n      \"no_sessions_detail\": \"Le sessioni di importazione appariranno qui automaticamente quando importi i segnalibri\",\n      \"created_at\": \"Creato {{time}}\",\n      \"progress\": \"Avanzamento\",\n      \"status\": {\n        \"pending\": \"In sospeso\",\n        \"in_progress\": \"In corso\",\n        \"completed\": \"Completato\",\n        \"failed\": \"Fallito\",\n        \"processing\": \"Elaborazione\",\n        \"staging\": \"In preparazione\",\n        \"running\": \"In esecuzione\",\n        \"paused\": \"In pausa\"\n      },\n      \"badges\": {\n        \"pending\": \"{{count}} in sospeso\",\n        \"processing\": \"{{count}} in elaborazione\",\n        \"completed\": \"{{count}} completati\",\n        \"failed\": \"{{count}} falliti\"\n      },\n      \"imported_to\": \"Importato in:\",\n      \"view_list\": \"Visualizza elenco\",\n      \"delete_dialog_title\": \"Elimina sessione di importazione\",\n      \"delete_dialog_description\": \"Sei sicuro di voler eliminare \\\"{{name}}\\\"? Questa azione non può essere annullata. I segnalibri stessi non verranno eliminati.\",\n      \"delete_session\": \"Elimina sessione\",\n      \"pause_session\": \"Pausa\",\n      \"resume_session\": \"Riprendi\",\n      \"view_details\": \"Visualizza dettagli\",\n      \"detail\": {\n        \"page_title\": \"Dettagli sessione di importazione\",\n        \"back_to_import\": \"Torna a Importa\",\n        \"filter_all\": \"Tutto\",\n        \"filter_accepted\": \"Accettato\",\n        \"filter_rejected\": \"Rifiutato\",\n        \"filter_duplicates\": \"Duplicati\",\n        \"filter_pending\": \"In attesa\",\n        \"table_title\": \"Titolo/URL\",\n        \"table_type\": \"Tipo\",\n        \"table_result\": \"Risultato\",\n        \"table_reason\": \"Motivo\",\n        \"table_bookmark\": \"Segnalibro\",\n        \"result_accepted\": \"Accettato\",\n        \"result_rejected\": \"Rifiutato\",\n        \"result_skipped_duplicate\": \"Duplicato\",\n        \"result_pending\": \"In attesa\",\n        \"result_processing\": \"In elaborazione\",\n        \"no_results\": \"Nessun risultato trovato per questo filtro.\",\n        \"view_bookmark\": \"Visualizza segnalibro\",\n        \"load_more\": \"Carica altro\",\n        \"no_title\": \"Nessun titolo\"\n      }\n    },\n    \"backups\": {\n      \"backups\": \"Backup\",\n      \"page_title\": \"Backup\",\n      \"page_description\": \"Crea e gestisci automaticamente i backup dei tuoi segnalibri. I backup sono compressi e archiviati in modo sicuro.\",\n      \"configuration\": {\n        \"title\": \"Configurazione backup\",\n        \"enable_automatic_backups\": \"Abilita backup automatici\",\n        \"enable_automatic_backups_description\": \"Crea automaticamente backup dei tuoi segnalibri\",\n        \"backup_frequency\": \"Frequenza backup\",\n        \"backup_frequency_description\": \"Ogni quanto tempo creare i backup\",\n        \"retention_period\": \"Periodo di conservazione (giorni)\",\n        \"retention_period_description\": \"Per quanti giorni conservare i backup prima di eliminarli\",\n        \"frequency\": {\n          \"daily\": \"Giornalmente\",\n          \"weekly\": \"Settimanalmente\"\n        },\n        \"select_frequency\": \"Seleziona frequenza\",\n        \"save_settings\": \"Salva impostazioni\"\n      },\n      \"list\": {\n        \"title\": \"I tuoi backup\",\n        \"create_backup_now\": \"Crea backup ora\",\n        \"no_backups\": \"Non hai ancora nessun backup. Abilita i backup automatici o creane uno manualmente.\",\n        \"table\": {\n          \"created_at\": \"Creato il\",\n          \"bookmarks\": \"Segnalibri\",\n          \"size\": \"Dimensione\",\n          \"status\": \"Stato\",\n          \"actions\": \"Azioni\"\n        },\n        \"status\": {\n          \"success\": \"Successo\",\n          \"failed\": \"Fallito\",\n          \"pending\": \"In sospeso\"\n        },\n        \"actions\": {\n          \"download_backup\": \"Scarica backup\",\n          \"delete_backup\": \"Elimina backup\"\n        }\n      },\n      \"dialogs\": {\n        \"delete_backup_title\": \"Eliminare il backup?\",\n        \"delete_backup_description\": \"Sei sicuro di voler eliminare questo backup? Questa azione non può essere annullata.\"\n      },\n      \"toasts\": {\n        \"backup_queued\": \"Il processo di backup è stato messo in coda! Verrà elaborato a breve.\",\n        \"backup_deleted\": \"Il backup è stato eliminato!\"\n      }\n    }\n  },\n  \"editor\": {\n    \"text_toolbar\": {\n      \"markdown_shortcuts\": {\n        \"ordered_list\": {\n          \"label\": \"Lista ordinata\",\n          \"example\": \"1. Elemento\"\n        },\n        \"label\": \"Comandi brevi Markdown\",\n        \"heading\": {\n          \"example\": \"# H1, ## H2, ### H3\",\n          \"label\": \"Intestazione\"\n        },\n        \"bold\": {\n          \"example\": \"**text** o CTRL+b\",\n          \"label\": \"Grassetto\"\n        },\n        \"italic\": {\n          \"example\": \"*Italic* o _Italic_ o CTRL+i\",\n          \"label\": \"Corsivo\"\n        },\n        \"blockquote\": {\n          \"label\": \"Citazione\",\n          \"example\": \"> Citazione\"\n        },\n        \"unordered_list\": {\n          \"label\": \"Lista non ordinata\",\n          \"example\": \"- Elemento\"\n        },\n        \"inline_code\": {\n          \"label\": \"Codice in linea\",\n          \"example\": \"`Codice`\"\n        },\n        \"block_code\": {\n          \"label\": \"Blocco di codice\",\n          \"example\": \"``` + spazio\"\n        }\n      },\n      \"undo\": \"Annulla\",\n      \"bold\": \"Grassetto\",\n      \"italic\": \"Corsivo\",\n      \"underline\": \"Sottolineato\",\n      \"strikethrough\": \"Barrato\",\n      \"code\": \"Codice\",\n      \"highlight\": \"Evidenziato\",\n      \"align_left\": \"Allinea a sinistra\",\n      \"align_center\": \"Allinea al centro\",\n      \"align_right\": \"Allinea a destra\",\n      \"redo\": \"Ripeti\"\n    },\n    \"multiple_urls_dialog_title\": \"Importare gli URL come segnalibri separati?\",\n    \"multiple_urls_dialog_desc\": \"L'input contiene diversi URL su linee separate. Vuoi importarli come segnalibri diversi?\",\n    \"import_as_text\": \"Importa come segnalibro di testo\",\n    \"placeholder\": \"Incolla un link o un'immagine, scrivi una nota o trascina un'immagine qui…\",\n    \"disabled_submissions\": \"Le iscrizioni sono disattivate\",\n    \"new_item\": \"NUOVO ELEMENTO\",\n    \"quickly_focus\": \"Puoi mettere il focus qui premendo ⌘ + E\",\n    \"import_as_separate_bookmarks\": \"Importa come segnalibri separati\",\n    \"placeholder_v2\": \"Incolla un link, scrivi una nota o rilascia un'immagine…\"\n  },\n  \"layouts\": {\n    \"masonry\": \"Masonry\",\n    \"grid\": \"Griglia\",\n    \"list\": \"Lista\",\n    \"compact\": \"Compatto\"\n  },\n  \"admin\": {\n    \"admin_settings\": \"Impostazioni amministratore\",\n    \"server_stats\": {\n      \"server_stats\": \"Statistiche server\",\n      \"total_bookmarks\": \"Segnalibri totali\",\n      \"server_version\": \"Versione server\",\n      \"total_users\": \"Utenti totali\"\n    },\n    \"background_jobs\": {\n      \"background_jobs\": \"Lavori in background\",\n      \"crawler_jobs\": \"Lavori di recupero\",\n      \"indexing_jobs\": \"Lavori di indicizzazione\",\n      \"job\": \"Lavoro\",\n      \"queued\": \"In coda\",\n      \"pending\": \"In attesa\",\n      \"failed\": \"Fallito\",\n      \"tidy_assets_jobs\": \"Lavori di pulizia Asset\",\n      \"inference_jobs\": \"Lavori di generazione testo\",\n      \"feed_jobs\": \"Lavori feed RSS\",\n      \"webhook_jobs\": \"Lavori Webhook\",\n      \"asset_preprocessing_jobs\": \"Processi di pre-elaborazione asset\",\n      \"video_jobs\": \"Lavori di download video\",\n      \"jobs\": {\n        \"crawler\": {\n          \"title\": \"Processi di crawling\",\n          \"description\": \"Web crawling ed estrazione di contenuti dagli URL\"\n        },\n        \"inference\": {\n          \"title\": \"Processi di inferenza\",\n          \"description\": \"Tagging e riepilogo dei contenuti basati sull'intelligenza artificiale\"\n        },\n        \"indexing\": {\n          \"title\": \"Processi di indicizzazione\",\n          \"description\": \"Aggiornamenti dell'indice di ricerca\"\n        },\n        \"asset_preprocessing\": {\n          \"title\": \"Processi di pre-elaborazione degli asset\",\n          \"description\": \"Pre-elaborazione di immagini e documenti (screenshot, estrazione di testo, ecc.)\"\n        },\n        \"tidy_assets\": {\n          \"title\": \"Processi di pulizia degli asset\",\n          \"description\": \"Pulizia degli asset e ottimizzazione dello storage\"\n        },\n        \"video\": {\n          \"title\": \"Processi di download video\",\n          \"description\": \"Estrazione e download di video\"\n        },\n        \"webhook\": {\n          \"title\": \"Processi webhook\",\n          \"description\": \"Notifiche webhook esterne\"\n        },\n        \"feed\": {\n          \"title\": \"Processi di Feed RSS\",\n          \"description\": \"Elaborazione dei feed RSS e aggiornamenti dei contenuti\"\n        },\n        \"admin_maintenance\": {\n          \"title\": \"Lavori di manutenzione dell'amministratore\",\n          \"description\": \"Pulizia amministrativa e manutenzione degli asset\"\n        }\n      },\n      \"monitor_and_manage\": \"Monitora e gestisci le code dei processi in background e le attività di elaborazione del sistema\",\n      \"active\": \"Attivo\",\n      \"available_actions\": \"Azioni disponibili\",\n      \"status\": {\n        \"title\": \"Comprensione degli stati di un processo\",\n        \"queued\": {\n          \"title\": \"In coda\",\n          \"description\": \"Lavori in attesa di essere elaborati. Inizieranno automaticamente quando le risorse saranno disponibili.\"\n        },\n        \"unprocessed\": {\n          \"title\": \"Non elaborati\",\n          \"description\": \"Segnalibri che non sono ancora stati elaborati. Molto probabilmente sono già in coda per l'elaborazione, in caso contrario, potrebbe essere necessario rimetterli in coda manualmente.\"\n        },\n        \"failed\": {\n          \"title\": \"Falliti\",\n          \"description\": \"Segnalibri che hanno riscontrato errori durante l'elaborazione. Questi potrebbero richiedere un intervento manuale.\"\n        }\n      },\n      \"actions\": {\n        \"recrawl_failed_links_only\": \"Riesamina solo i link falliti\",\n        \"recrawl_all_links\": \"Riesamina tutti i link\",\n        \"without_inference\": \"Senza inferenza\",\n        \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Rigenera i tag AI solo per i segnalibri falliti\",\n        \"regenerate_ai_tags_for_all_bookmarks\": \"Rigenera i tag AI per tutti i segnalibri\",\n        \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Rigenera i riepiloghi AI solo per i segnalibri falliti\",\n        \"regenerate_ai_summaries_for_all_bookmarks\": \"Rigenera i riepiloghi AI per tutti i segnalibri\",\n        \"reindex_all_bookmarks\": \"Reindicizza tutti i segnalibri\",\n        \"clean_assets\": \"Pulisci le risorse orfane e risincronizza i metadati\",\n        \"reprocess_assets_fix_mode\": \"Rielabora le risorse non elaborate\",\n        \"migrate_large_link_html_content\": \"Sposta grandi contenuti HTML incorporati negli asset\",\n        \"recrawl_pending_links_only\": \"Nuova scansione solo dei link in sospeso\",\n        \"regenerate_ai_tags_for_pending_bookmarks_only\": \"Rigenera i tag AI solo per i segnalibri in sospeso\",\n        \"regenerate_ai_summaries_for_pending_bookmarks_only\": \"Rigenera i riassunti AI solo per i segnalibri in sospeso\"\n      }\n    },\n    \"actions\": {\n      \"recrawl_failed_links_only\": \"Recupera solo link falliti\",\n      \"recrawl_all_links\": \"Recupera tutti i link\",\n      \"without_inference\": \"Senza generazione testo\",\n      \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Rigenera tag AI solo per i segnalibri falliti\",\n      \"regenerate_ai_tags_for_all_bookmarks\": \"Rigenera tag AI per tutti i segnalibri\",\n      \"compact_assets\": \"Compatta asset\",\n      \"reindex_all_bookmarks\": \"Reindicizza tutti i segnalibri\",\n      \"reprocess_assets_fix_mode\": \"Riprocessa asset (modalità fissa)\",\n      \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Rigenera i riassunti AI solo per i segnalibri falliti\",\n      \"regenerate_ai_summaries_for_all_bookmarks\": \"Rigenera i riassunti AI per tutti i segnalibri\"\n    },\n    \"users_list\": {\n      \"users_list\": \"Lista utenti\",\n      \"change_role\": \"Cambia ruolo\",\n      \"num_bookmarks\": \"Num segnalibri\",\n      \"asset_sizes\": \"Dimensione Asset\",\n      \"local_user\": \"Utente locale\",\n      \"confirm_password\": \"Conferma password\",\n      \"create_user\": \"Crea utente\",\n      \"delete_user\": \"Elimina utente\",\n      \"reset_password\": \"Ripristina password\",\n      \"delete_user_confirm_description\": \"Sei sicuro di voler eliminare l'utente \\\"{{name}}\\\"?\",\n      \"unlimited\": \"Illimitato\"\n    },\n    \"service_connections\": {\n      \"title\": \"Connessioni di servizio\",\n      \"description\": \"Monitora lo stato e la connettività delle dipendenze del sistema esterno\",\n      \"search_engine\": \"Motore di ricerca\",\n      \"browser\": \"Browser\",\n      \"queue_system\": \"Sistema di code\",\n      \"status\": {\n        \"not_configured\": \"Non configurato\",\n        \"connected\": \"Connesso\",\n        \"disconnected\": \"Disconnesso\"\n      }\n    },\n    \"admin_tools\": {\n      \"admin_tools\": \"Strumenti di amministrazione\",\n      \"bookmark_debugger\": \"Debugger segnalibro\",\n      \"bookmark_id\": \"ID segnalibro\",\n      \"bookmark_id_placeholder\": \"Inserisci ID segnalibro\",\n      \"lookup\": \"Ricerca\",\n      \"debug_info\": \"Informazioni di debug\",\n      \"basic_info\": \"Informazioni di base\",\n      \"status\": \"Stato\",\n      \"content\": \"Contenuto\",\n      \"html_preview\": \"Anteprima HTML (primi 1000 caratteri)\",\n      \"summary\": \"Riepilogo\",\n      \"url\": \"URL\",\n      \"source_url\": \"URL di origine\",\n      \"asset_type\": \"Tipo di risorsa\",\n      \"file_name\": \"Nome file\",\n      \"owner_user_id\": \"ID utente proprietario\",\n      \"tagging_status\": \"Stato dell'etichettatura\",\n      \"summarization_status\": \"Stato di riepilogo\",\n      \"crawl_status\": \"Stato della scansione\",\n      \"crawl_status_code\": \"Codice di stato HTTP\",\n      \"crawled_at\": \"Scansionato il\",\n      \"recrawl\": \"Ri-scansiona\",\n      \"reindex\": \"Ri-indicizza\",\n      \"retag\": \"Ri-etichetta\",\n      \"resummarize\": \"Ri-sommarizza\",\n      \"bookmark_not_found\": \"Segnalibro non trovato\",\n      \"action_success\": \"Azione completata con successo\",\n      \"action_failed\": \"Azione fallita\",\n      \"recrawl_queued\": \"Il processo di ri-scansione è stato accodato\",\n      \"reindex_queued\": \"Il processo di ri-indicizzazione è stato accodato\",\n      \"retag_queued\": \"Il processo di ri-etichettatura è stato accodato\",\n      \"resummarize_queued\": \"Il processo di ri-sommarizzazione è stato accodato\",\n      \"view\": \"Visualizza\",\n      \"fetch_error\": \"Errore durante il recupero del segnalibro\"\n    }\n  },\n  \"options\": {\n    \"dark_mode\": \"Modalità scura\",\n    \"light_mode\": \"Modalità chiara\",\n    \"apps_extensions\": \"App e estensioni\",\n    \"documentation\": \"Documentazione\",\n    \"follow_us_on_x\": \"Seguici su X\"\n  },\n  \"lists\": {\n    \"all_lists\": \"Tutte le liste\",\n    \"favourites\": \"Preferiti\",\n    \"new_list\": \"Nuova lista\",\n    \"new_nested_list\": \"Nuova sottolista\",\n    \"search_query_help\": \"Ulteriori informazioni sul linguaggio delle query di ricerca.\",\n    \"manual_list\": \"Lista manuale\",\n    \"smart_list\": \"Lista intelligente\",\n    \"search_query\": \"Query di ricerca\",\n    \"parent_list\": \"Elenco elementi principali\",\n    \"no_parent\": \"Nessun elemento principale\",\n    \"list_type\": \"Tipo di elenco\",\n    \"edit_list\": \"Modifica elenco\",\n    \"merge_list\": \"Unisci elenco\",\n    \"destination_list\": \"Elenco di destinazione\",\n    \"delete_after_merge\": \"Elimina l'elenco originale dopo l'unione\",\n    \"no_destination\": \"Nessuna destinazione\",\n    \"description\": \"Descrizione (facoltativa)\",\n    \"rss\": {\n      \"title\": \"Feed RSS\",\n      \"description\": \"Abilita un feed RSS per questa lista\",\n      \"feed_url\": \"URL del feed RSS\"\n    },\n    \"public_list\": {\n      \"description\": \"Consenti ad altri di visualizzare questa lista\",\n      \"title\": \"Lista pubblica\",\n      \"share_link\": \"Condividi link\"\n    },\n    \"share_list\": \"Condividi lista\",\n    \"delete_list\": {\n      \"title\": \"Elimina lista\",\n      \"description\": \"Eliminare una lista non elimina i segnalibri al suo interno.\",\n      \"delete_children\": \"Elimina le liste figlie (ricorsivamente)\",\n      \"delete_children_description\": \"Se non selezionato, tutte le liste figlie dirette diventeranno liste radice\"\n    },\n    \"shared\": \"Condiviso\",\n    \"collaborators\": {\n      \"manage\": \"Gestisci collaboratori\",\n      \"view\": \"Visualizza collaboratori\",\n      \"collaborators\": \"Collaboratori\",\n      \"add\": \"Aggiungi collaboratore\",\n      \"current\": \"Collaboratori attuali\",\n      \"enter_email\": \"Inserisci indirizzo email\",\n      \"please_enter_email\": \"Inserisci un indirizzo email\",\n      \"added_successfully\": \"Collaboratore aggiunto correttamente\",\n      \"failed_to_add\": \"Impossibile aggiungere il collaboratore\",\n      \"removed\": \"Collaboratore rimosso\",\n      \"failed_to_remove\": \"Impossibile rimuovere il collaboratore\",\n      \"role_updated\": \"Ruolo aggiornato\",\n      \"failed_to_update_role\": \"Impossibile aggiornare il ruolo\",\n      \"viewer\": \"Visualizzatore\",\n      \"editor\": \"Editor\",\n      \"owner\": \"Proprietario\",\n      \"viewer_description\": \"Può visualizzare i segnalibri nella lista\",\n      \"editor_description\": \"Può aggiungere e rimuovere segnalibri\",\n      \"no_collaborators\": \"Ancora nessun collaboratore. Aggiungi qualcuno per iniziare a collaborare!\",\n      \"no_collaborators_readonly\": \"Nessun collaboratore per questo elenco.\",\n      \"people_with_access\": \"Persone che hanno accesso a questo elenco\",\n      \"add_or_remove\": \"Aggiungi o rimuovi persone che possono accedere a questo elenco\",\n      \"invitation_sent\": \"Invito inviato correttamente\",\n      \"invitation_revoked\": \"Invito revocato\",\n      \"failed_to_revoke\": \"Impossibile revocare l'invito\",\n      \"pending\": \"In sospeso\",\n      \"revoke\": \"Revoca\",\n      \"declined\": \"Rifiutato\"\n    },\n    \"leave_list\": {\n      \"title\": \"Abbandona l'elenco\",\n      \"confirm_message\": \"Sei sicuro di voler abbandonare {{icon}} {{name}}?\",\n      \"warning\": \"Non potrai più visualizzare o accedere ai segnalibri in questo elenco. Il proprietario dell'elenco potrà aggiungerti di nuovo se necessario.\",\n      \"action\": \"Abbandona l'elenco\",\n      \"success\": \"Hai abbandonato \\\"{{icon}} {{name}}\\\"\"\n    },\n    \"invitations\": {\n      \"pending\": \"Inviti in sospeso\",\n      \"description\": \"Esamina e rispondi agli inviti di collaborazione nell'elenco\",\n      \"invited_by\": \"Invitato da\",\n      \"accept\": \"Accetta\",\n      \"decline\": \"Rifiuta\",\n      \"accepted\": \"Invito accettato\",\n      \"declined\": \"Invito rifiutato\",\n      \"failed_to_accept\": \"Impossibile accettare l'invito\",\n      \"failed_to_decline\": \"Impossibile rifiutare l'invito\"\n    },\n    \"shared_lists\": \"Elenchi condivisi\"\n  },\n  \"tags\": {\n    \"your_tags\": \"I tuoi tag\",\n    \"your_tags_info\": \"Tag che sono stati utilizzati almeno una volta da te\",\n    \"ai_tags\": \"Tag AI\",\n    \"ai_tags_info\": \"Tag che sono solo stati usati automaticamente (dall'AI)\",\n    \"unused_tags\": \"Tag inutilizzati\",\n    \"unused_tags_info\": \"Tag che non sono collegati a nessun segnalibro\",\n    \"drag_and_drop_merging\": \"Unione con Drag&Drop\",\n    \"drag_and_drop_merging_info\": \"Trascina e rilascia i tag per unirli\",\n    \"sort_by_name\": \"Ordina per nome\",\n    \"all_tags\": \"Tutti i tag\",\n    \"delete_all_unused_tags\": \"Elimina tutti i tag inutilizzati\",\n    \"create_tag\": \"Crea tag\",\n    \"create_tag_description\": \"Crea un nuovo tag senza collegarlo ad alcun segnalibro\",\n    \"tag_name\": \"Nome tag\",\n    \"enter_tag_name\": \"Inserisci il nome del tag\",\n    \"sort_by_usage\": \"Ordina per utilizzo\",\n    \"sort_by_relevance\": \"Ordina per rilevanza\",\n    \"no_custom_tags\": \"Ancora nessun tag personalizzato\",\n    \"no_ai_tags\": \"Ancora nessun tag IA\",\n    \"no_unused_tags\": \"Non hai tag inutilizzati\",\n    \"no_unused_tags_match_your_search\": \"Nessun tag inutilizzato corrisponde alla tua ricerca\",\n    \"no_tags_match_your_search\": \"Nessun tag corrisponde alla tua ricerca\",\n    \"search_placeholder\": \"Cerca tag...\",\n    \"search_or_create_placeholder\": \"Cerca o crea tag...\"\n  },\n  \"preview\": {\n    \"view_original\": \"Vedi originale\",\n    \"cached_content\": \"Contenuto salvato in cache\",\n    \"reader_view\": \"Visualizzazione Reader\",\n    \"tabs\": {\n      \"content\": \"Contenuto\",\n      \"details\": \"Dettagli\"\n    },\n    \"archive_info\": \"Gli archivi potrebbero non essere visualizzati correttamente in linea se richiedono Javascript. Per risultati ottimali, <1>scaricalo e aprilo nel tuo browser</1>.\",\n    \"fetch_error_title\": \"Contenuto non disponibile\",\n    \"fetch_error_description\": \"Impossibile ottenere il contenuto per questo link. La pagina potrebbe essere protetta, richiedere l'autenticazione o essere temporaneamente non disponibile.\",\n    \"crawling_in_progress\": \"Recupero del contenuto della pagina…\",\n    \"continue_reading\": \"Continua da dove hai lasciato\",\n    \"continue_reading_percent\": \"Continua da dove hai lasciato ({{percent}}%)\",\n    \"continue_button\": \"Continua\"\n  },\n  \"toasts\": {\n    \"bookmarks\": {\n      \"updated\": \"Il segnalibro è stato aggiornato!\",\n      \"deleted\": \"Il segnalibro è stato eliminato!\",\n      \"refetch\": \"L'aggiornamento è stato messo in coda!\",\n      \"full_page_archive\": \"L'archivio della pagina completa è stato attivato\",\n      \"delete_from_list\": \"Il segnalibro è stato eliminato dalla lista\",\n      \"clipboard_copied\": \"Il link è stato copiato!\",\n      \"preserve_pdf\": \"È stato attivato il salvataggio in PDF\",\n      \"update_banner\": \"Banner aggiornato!\",\n      \"uploading_banner\": \"Caricamento del banner...\"\n    },\n    \"lists\": {\n      \"created\": \"Lista creata!\",\n      \"updated\": \"Lista aggiornata!\",\n      \"merged\": \"L'elenco è stato unito!\",\n      \"deleted\": \"L'elenco è stato eliminato!\"\n    },\n    \"tags\": {\n      \"created\": \"Tag creato!\",\n      \"failed_to_create\": \"Impossibile creare il tag\"\n    }\n  },\n  \"cleanups\": {\n    \"cleanups\": \"Pulizie\",\n    \"duplicate_tags\": {\n      \"title\": \"Tag duplicati\",\n      \"merge_all_suggestions\": \"Unire tutti i suggerimenti?\"\n    }\n  },\n  \"search\": {\n    \"or\": \"Oppure\",\n    \"and\": \"E\",\n    \"is_in_any_list\": \"È in qualsiasi lista\",\n    \"is_not_in_any_list\": \"Non è in nessun elenco\",\n    \"not_created_on_or_after\": \"Non creato in o dopo\",\n    \"has_tag\": \"Ha tag\",\n    \"full_text_search\": \"Ricerca di testo completo\",\n    \"does_not_have_tag\": \"Non ha il tag\",\n    \"has_any_tag\": \"Ha qualsiasi tag\",\n    \"not_created_on_or_before\": \"Non creato in o prima di\",\n    \"url_contains\": \"L'URL contiene\",\n    \"is_in_list\": \"È in elenco\",\n    \"is_not_in_list\": \"Non è nella lista\",\n    \"type_is\": \"Il tipo è\",\n    \"is_not_favorited\": \"Non è tra i preferiti\",\n    \"is_archived\": \"È archiviato\",\n    \"is_not_archived\": \"Non è archiviato\",\n    \"has_no_tags\": \"Non ha tag\",\n    \"created_on_or_after\": \"Creato il o dopo il\",\n    \"is_favorited\": \"È tra i preferiti\",\n    \"created_on_or_before\": \"Creato in data o prima del\",\n    \"type_is_not\": \"Il tipo non è\",\n    \"url_does_not_contain\": \"L'URL non contiene\",\n    \"is_from_feed\": \"Proviene da un feed RSS\",\n    \"is_not_from_feed\": \"Non proviene da un feed RSS\",\n    \"created_within\": \"Creato entro\",\n    \"created_earlier_than\": \"Creato prima di\",\n    \"day_s\": \" Giorni\",\n    \"week_s\": \" Settimane\",\n    \"month_s\": \" Mesi\",\n    \"year_s\": \" Anni\",\n    \"day_s_ago\": \" Giorni fa\",\n    \"week_s_ago\": \" Settimane fa\",\n    \"month_s_ago\": \" Mesi fa\",\n    \"year_s_ago\": \" Anni fa\",\n    \"history\": \"Ricerche recenti\",\n    \"title_contains\": \"Il titolo contiene\",\n    \"title_does_not_contain\": \"Il titolo non contiene\",\n    \"is_broken_link\": \"Ha Link Non Funzionante\",\n    \"tags\": \"Tag\",\n    \"no_suggestions\": \"Nessun suggerimento\",\n    \"filters\": \"Filtri\",\n    \"is_not_broken_link\": \"Ha Link Funzionante\",\n    \"lists\": \"Elenchi\",\n    \"feeds\": \"Feed\",\n    \"is_from_source\": \"L'origine è\",\n    \"is_not_from_source\": \"L'origine non è\"\n  },\n  \"dialogs\": {\n    \"bookmarks\": {\n      \"delete_confirmation_description\": \"Sei sicuro di voler eliminare questo segnalibro?\",\n      \"delete_confirmation_title\": \"Eliminare il segnalibro?\"\n    }\n  },\n  \"highlights\": {\n    \"no_highlights\": \"Non hai ancora nessun evidenziatore.\"\n  },\n  \"banners\": {\n    \"no_bookmarks\": {\n      \"description\": \"Salva articoli, link e pagine interessanti per accedervi rapidamente in seguito.\",\n      \"title\": \"Ancora nessun segnalibro\"\n    }\n  },\n  \"bookmark_editor\": {\n    \"subtitle\": \"Apporta modifiche ai dettagli del segnalibro. Clicca su salva quando hai finito.\",\n    \"author\": \"Autore\",\n    \"publisher\": \"Editore\",\n    \"date_published\": \"Data di pubblicazione\",\n    \"pick_a_date\": \"Scegli una data\",\n    \"save_changes\": \"Salva le modifiche\",\n    \"title\": \"Modifica segnalibro\",\n    \"extracted_content\": \"Contenuto estratto\"\n  },\n  \"view_options\": {\n    \"title\": \"Visualizza opzioni\",\n    \"layout\": \"Disposizione\",\n    \"columns\": \"Colonne\",\n    \"display_options\": \"Opzioni di visualizzazione\",\n    \"show_note_previews\": \"Mostra note\",\n    \"show_tags\": \"Mostra tag\",\n    \"show_title\": \"Mostra titolo\",\n    \"image_options\": \"Opzioni immagine\",\n    \"image_fit_cover\": \"Copri (riempi)\",\n    \"image_fit_contain\": \"Contieni (adatta)\"\n  },\n  \"version\": {\n    \"new_release_available\": \"Nuove note di rilascio disponibili\",\n    \"whats_new_title\": \"Cosa c'è di nuovo nella v{{version}}\",\n    \"release_notes_description\": \"Ecco gli ultimi aggiornamenti presi dalle note di rilascio di GitHub.\",\n    \"loading_release_notes\": \"Caricamento delle note di rilascio…\",\n    \"unable_to_load_release_notes\": \"Impossibile caricare le note di rilascio al momento. Riprova più tardi.\",\n    \"no_release_notes\": \"Nessuna nota di rilascio è stata pubblicata per questa versione.\",\n    \"release_notes_synced\": \"Le note di rilascio sono sincronizzate da GitHub.\",\n    \"view_on_github\": \"Visualizza su GitHub\"\n  },\n  \"wrapped\": {\n    \"title\": \"Il tuo riepilogo {{year}}\",\n    \"subtitle\": \"Un anno su Karakeep\",\n    \"banner\": {\n      \"title\": \"Il tuo riepilogo 2025 è pronto!\",\n      \"description\": \"Guarda il tuo anno in segnalibri\",\n      \"view_now\": \"Visualizza ora\"\n    },\n    \"button\": \"Riepilogo 2025\",\n    \"loading\": \"Caricamento del tuo riepilogo...\",\n    \"failed_to_load\": \"Impossibile caricare le statistiche del riepilogo\",\n    \"sections\": {\n      \"total_saves\": {\n        \"prefix\": \"Hai salvato\",\n        \"suffix\": \"elementi quest'anno\",\n        \"suffix_singular\": \"elemento quest'anno\"\n      },\n      \"first_bookmark\": {\n        \"title\": \"Il tuo viaggio è iniziato\",\n        \"description\": \"Primo salvataggio del {{year}}:\"\n      },\n      \"top_domains\": \"I tuoi siti principali\",\n      \"top_tags\": \"I tuoi tag principali\",\n      \"monthly_activity\": \"Il tuo anno di salvataggi\",\n      \"most_active_day\": \"Il tuo giorno più attivo\",\n      \"peak_times\": {\n        \"title\": \"Quando salvi\",\n        \"peak_hour\": \"Ora di picco\",\n        \"peak_day\": \"Giorno di picco\"\n      },\n      \"how_you_save\": \"Come salvi\",\n      \"what_you_saved\": \"Cosa hai salvato\",\n      \"summary\": {\n        \"favorites\": \"Preferiti\",\n        \"tags_created\": \"Tag creati\",\n        \"highlights\": \"In evidenza\"\n      },\n      \"types\": {\n        \"links\": \"Link\",\n        \"notes\": \"Note\",\n        \"assets\": \"Risorse\"\n      }\n    },\n    \"footer\": \"Realizzato con Karakeep\",\n    \"share\": \"Condividi\",\n    \"download\": \"Scarica\",\n    \"close\": \"Chiudi\",\n    \"generating\": \"Generazione in corso...\"\n  }\n}\n"
  },
  {
    "path": "apps/web/lib/i18n/locales/ja/translation.json",
    "content": "{\n  \"actions\": {\n    \"add_to_list\": \"リストに追加\",\n    \"apply_all\": \"全て適用\",\n    \"create\": \"作成\",\n    \"fetch_now\": \"フェッチする\",\n    \"summarize_with_ai\": \"AIで要約する\",\n    \"add\": \"追加\",\n    \"delete\": \"削除\",\n    \"archive\": \"アーカイブ\",\n    \"favorite\": \"お気に入り\",\n    \"edit_tags\": \"タグを編集\",\n    \"select_all\": \"全選択\",\n    \"unselect_all\": \"全ての選択を解除\",\n    \"bulk_edit\": \"一括編集\",\n    \"manage_lists\": \"リストの管理\",\n    \"remove_from_list\": \"リストから削除\",\n    \"save\": \"保存\",\n    \"edit_title\": \"タイトルの編集\",\n    \"sign_out\": \"サインアウト\",\n    \"close\": \"閉じる\",\n    \"merge\": \"マージする\",\n    \"close_bulk_edit\": \"一括編集を閉じる\",\n    \"unarchive\": \"アーカイブを解除\",\n    \"copy_link\": \"リンクのコピー\",\n    \"change_layout\": \"レイアウトの変更\",\n    \"edit\": \"編集\",\n    \"unfavorite\": \"お気に入りを解除\",\n    \"refresh\": \"リフレッシュ\",\n    \"recrawl\": \"再クロール\",\n    \"ignore\": \"無視する\",\n    \"cancel\": \"キャンセル\",\n    \"download_full_page_archive\": \"全てのページアーカイブをダウンロード\",\n    \"sort\": {\n      \"title\": \"並び替え\",\n      \"newest_first\": \"新しい順\",\n      \"oldest_first\": \"古い順\",\n      \"relevant_first\": \"関連性の高い順\"\n    },\n    \"open_editor\": \"エディタを開く\",\n    \"toggle_show_archived\": \"アーカイブを表示\",\n    \"confirm\": \"確認\",\n    \"regenerate\": \"再生成\",\n    \"load_more\": \"もっと読み込む\",\n    \"edit_notes\": \"注釈を編集\",\n    \"preserve_as_pdf\": \"PDFとして保存する\",\n    \"offline_copies\": \"オフラインコピー\",\n    \"preserve_offline_archive\": \"オフラインアーカイブを保存\",\n    \"download_full_page_archive_file\": \"アーカイブファイルをダウンロード\",\n    \"download_pdf_file\": \"PDF ファイルをダウンロード\",\n    \"remove\": \"削除\",\n    \"more\": \"もっと見る\",\n    \"replace_banner\": \"バナーを置き換える\",\n    \"add_banner\": \"バナーを追加\",\n    \"download\": \"ダウンロード\"\n  },\n  \"admin\": {\n    \"actions\": {\n      \"regenerate_ai_tags_for_failed_bookmarks_only\": \"失敗したブックマークのみの AI タグを再生成する\",\n      \"regenerate_ai_tags_for_all_bookmarks\": \"すべてのブックマークの AI タグを再生成\",\n      \"without_inference\": \"推論なし\",\n      \"reindex_all_bookmarks\": \"すべてのブックマークを再インデックスする\",\n      \"recrawl_failed_links_only\": \"失敗したリンクのみ再クロール\",\n      \"recrawl_all_links\": \"すべてのリンクを再クロール\",\n      \"compact_assets\": \"コンパクトアセット\",\n      \"reprocess_assets_fix_mode\": \"アセットの再処理（修正モード）\",\n      \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"失敗したブックマークのみAI要約を再生成\",\n      \"regenerate_ai_summaries_for_all_bookmarks\": \"すべてのブックマークのAI要約を再生成\"\n    },\n    \"server_stats\": {\n      \"server_version\": \"サーバーバージョン\",\n      \"server_stats\": \"サーバー統計情報\",\n      \"total_bookmarks\": \"総ブックマーク\",\n      \"total_users\": \"総ユーザ数\"\n    },\n    \"background_jobs\": {\n      \"job\": \"ジョブ\",\n      \"failed\": \"失敗\",\n      \"background_jobs\": \"バックグラウンドジョブ\",\n      \"inference_jobs\": \"推論ジョブ\",\n      \"tidy_assets_jobs\": \"Tidy アセットジョブ\",\n      \"queued\": \"キュー\",\n      \"indexing_jobs\": \"インデックス作成ジョブ\",\n      \"crawler_jobs\": \"クローラージョブ\",\n      \"pending\": \"保留中\",\n      \"video_jobs\": \"動画ダウンロードジョブ\",\n      \"webhook_jobs\": \"Webhookジョブ\",\n      \"asset_preprocessing_jobs\": \"アセットのプリプロセスジョブ\",\n      \"feed_jobs\": \"RSSフィードジョブ\",\n      \"jobs\": {\n        \"crawler\": {\n          \"title\": \"クローラーのジョブ\",\n          \"description\": \"URLからのウェブクローリングとコンテンツ抽出\"\n        },\n        \"inference\": {\n          \"title\": \"推論ジョブ\",\n          \"description\": \"AIを活用したコンテンツのタグ付けと要約\"\n        },\n        \"indexing\": {\n          \"title\": \"インデックスジョブ\",\n          \"description\": \"検索インデックスの更新\"\n        },\n        \"asset_preprocessing\": {\n          \"title\": \"アセットのプレ処理ジョブ\",\n          \"description\": \"画像とドキュメントの前処理（スクリーンショット、テキスト抽出など）\"\n        },\n        \"tidy_assets\": {\n          \"title\": \"アセット整理ジョブ\",\n          \"description\": \"アセットのクリーンアップとストレージの最適化\"\n        },\n        \"video\": {\n          \"title\": \"ビデオダウンロードジョブ\",\n          \"description\": \"動画の抽出とダウンロード\"\n        },\n        \"webhook\": {\n          \"title\": \"Webhookジョブ\",\n          \"description\": \"外部Webhook通知\"\n        },\n        \"feed\": {\n          \"title\": \"RSSフィードジョブ\",\n          \"description\": \"RSSフィードの処理とコンテンツの更新\"\n        },\n        \"admin_maintenance\": {\n          \"title\": \"管理メンテナンスジョブ\",\n          \"description\": \"管理クリーンアップとアセットメンテナンス\"\n        }\n      },\n      \"monitor_and_manage\": \"バックグラウンドジョブキューとシステム処理タスクの監視および管理\",\n      \"active\": \"アクティブ\",\n      \"available_actions\": \"利用可能なアクション\",\n      \"status\": {\n        \"title\": \"ジョブの状態について\",\n        \"queued\": {\n          \"title\": \"キューに追加済み\",\n          \"description\": \"処理待ちのジョブが並んでいます。リソースが利用可能になると自動的に開始されます。\"\n        },\n        \"unprocessed\": {\n          \"title\": \"未処理\",\n          \"description\": \"まだ処理されていないブックマーク。ほとんどの場合、すでに処理待ちのキューに入っています。そうでない場合は、手動で再キューする必要があるかもしれません。\"\n        },\n        \"failed\": {\n          \"title\": \"失敗\",\n          \"description\": \"処理中にエラーが発生したブックマーク。手動での対応が必要になる場合があります。\"\n        }\n      },\n      \"actions\": {\n        \"recrawl_failed_links_only\": \"失敗したリンクのみを再クロール\",\n        \"recrawl_all_links\": \"すべてのリンクを再クロール\",\n        \"without_inference\": \"推論なし\",\n        \"regenerate_ai_tags_for_failed_bookmarks_only\": \"失敗したブックマークのみAIタグを再生成\",\n        \"regenerate_ai_tags_for_all_bookmarks\": \"すべてのブックマークのAIタグを再生成\",\n        \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"失敗したブックマークのみAI要約を再生成\",\n        \"regenerate_ai_summaries_for_all_bookmarks\": \"すべてのブックマークのAI要約を再生成\",\n        \"reindex_all_bookmarks\": \"すべてのブックマークを再インデックス\",\n        \"clean_assets\": \"宙ぶらりんのアセットをクリーンアップしてメタデータを再同期\",\n        \"reprocess_assets_fix_mode\": \"未処理のアセットを再処理\",\n        \"migrate_large_link_html_content\": \"大規模なインラインHTMLコンテンツをアセットに移動\",\n        \"recrawl_pending_links_only\": \"保留中のリンクのみを再クロールする\",\n        \"regenerate_ai_tags_for_pending_bookmarks_only\": \"保留中のブックマークのみを対象にAIタグを再生成する\",\n        \"regenerate_ai_summaries_for_pending_bookmarks_only\": \"保留中のブックマークのみを対象にAI要約を再生成する\"\n      }\n    },\n    \"admin_settings\": \"管理者設定\",\n    \"users_list\": {\n      \"asset_sizes\": \"アセットサイズ\",\n      \"local_user\": \"ローカルユーザー\",\n      \"confirm_password\": \"パスワードの確認\",\n      \"create_user\": \"ユーザーを作成\",\n      \"change_role\": \"ロールの変更\",\n      \"num_bookmarks\": \"ブックマーク数\",\n      \"users_list\": \"ユーザーリスト\",\n      \"reset_password\": \"パスワードをリセット\",\n      \"delete_user\": \"ユーザーを削除\",\n      \"delete_user_confirm_description\": \"ほんまにユーザー「{{name}}」を削除してええん？\",\n      \"unlimited\": \"無制限\"\n    },\n    \"service_connections\": {\n      \"title\": \"サービス接続\",\n      \"description\": \"外部システム依存関係のヘルスと接続性を監視する\",\n      \"search_engine\": \"検索エンジン\",\n      \"browser\": \"ブラウザ\",\n      \"queue_system\": \"キューシステム\",\n      \"status\": {\n        \"not_configured\": \"未構成\",\n        \"connected\": \"接続済み\",\n        \"disconnected\": \"切断されました\"\n      }\n    },\n    \"admin_tools\": {\n      \"admin_tools\": \"管理ツール\",\n      \"bookmark_debugger\": \"ブックマークデバッガー\",\n      \"bookmark_id\": \"ブックマークID\",\n      \"bookmark_id_placeholder\": \"ブックマークIDを入力してや\",\n      \"lookup\": \"検索\",\n      \"debug_info\": \"デバッグ情報\",\n      \"basic_info\": \"基本情報\",\n      \"status\": \"ステータス\",\n      \"content\": \"コンテンツ\",\n      \"html_preview\": \"HTMLプレビュー（最初の1000文字）\",\n      \"summary\": \"サマリー\",\n      \"url\": \"URL\",\n      \"source_url\": \"ソースURL\",\n      \"asset_type\": \"アセットタイプ\",\n      \"file_name\": \"ファイル名\",\n      \"owner_user_id\": \"オーナーユーザーID\",\n      \"tagging_status\": \"タギングステータス\",\n      \"summarization_status\": \"要約ステータス\",\n      \"crawl_status\": \"クロールステータス\",\n      \"crawl_status_code\": \"HTTPステータスコード\",\n      \"crawled_at\": \"クロール日時\",\n      \"recrawl\": \"再クロール\",\n      \"reindex\": \"再インデックス\",\n      \"retag\": \"再タグ付け\",\n      \"resummarize\": \"再要約\",\n      \"bookmark_not_found\": \"ブックマークが見つかりません\",\n      \"action_success\": \"アクションは正常に完了しました\",\n      \"action_failed\": \"アクション失敗\",\n      \"recrawl_queued\": \"再クロールジョブをキューに追加しました\",\n      \"reindex_queued\": \"再インデックスジョブをキューに追加しました\",\n      \"retag_queued\": \"再タグ付けジョブをキューに追加しました\",\n      \"resummarize_queued\": \"再要約ジョブをキューに追加しました\",\n      \"view\": \"表示\",\n      \"fetch_error\": \"ブックマークの取得エラー\"\n    }\n  },\n  \"settings\": {\n    \"info\": {\n      \"user_info\": \"ユーザ情報\",\n      \"new_password\": \"新しいパスワード\",\n      \"basic_details\": \"基本情報\",\n      \"change_password\": \"パスワードを変更する\",\n      \"current_password\": \"現在のパスワード\",\n      \"confirm_new_password\": \"新しいパスワードの確認\",\n      \"options\": \"オプション\",\n      \"interface_lang\": \"インターフェース言語\",\n      \"user_settings\": {\n        \"user_settings_updated\": \"ユーザー設定が更新されました！\",\n        \"bookmark_click_action\": {\n          \"title\": \"ブックマーククリック時の動作\",\n          \"open_external_url\": \"元のURLを開く\",\n          \"open_bookmark_details\": \"ブックマークの詳細を開く\"\n        },\n        \"archive_display_behaviour\": {\n          \"show\": \"タグとリストにアーカイブされたブックマークを表示する\",\n          \"title\": \"アーカイブされたブックマーク\",\n          \"hide\": \"タグとリストにアーカイブされたブックマークを非表示にする\"\n        }\n      },\n      \"reader_settings\": {\n        \"local_overrides_title\": \"デバイス固有の設定が有効\",\n        \"using_default\": \"クライアントの既定を使用中\",\n        \"clear_override_hint\": \"デバイスのオーバーライドをクリアして、全体設定 ({{value}}) を使用します\",\n        \"font_size\": \"フォントサイズ\",\n        \"font_family\": \"フォントファミリー\",\n        \"preview_inline\": \"(プレビュー)\",\n        \"tooltip_preview\": \"未保存のプレビュー変更\",\n        \"save_to_all_devices\": \"すべてのデバイス\",\n        \"tooltip_local\": \"デバイス設定が全体設定と異なります\",\n        \"reset_preview\": \"プレビューをリセット\",\n        \"mono\": \"等幅\",\n        \"line_height\": \"行の高さ\",\n        \"tooltip_default\": \"リーディング設定\",\n        \"title\": \"リーダー設定\",\n        \"serif\": \"セリフ\",\n        \"preview\": \"プレビュー\",\n        \"not_set\": \"未設定\",\n        \"clear_local_overrides\": \"デバイス設定をクリア\",\n        \"preview_text\": \"すばやい茶色のキツネがのろまな犬を飛び越えます。リーダー表示のテキストはこんな感じになります。\",\n        \"local_overrides_cleared\": \"デバイス固有の設定がクリアされました\",\n        \"local_overrides_description\": \"このデバイスには、グローバル既定と異なるリーダー設定があります。\",\n        \"clear_defaults\": \"すべての既定をクリア\",\n        \"description\": \"リーダー表示の既定のテキスト設定を構成します。これらの設定は、すべてのデバイス間で同期されます。\",\n        \"defaults_cleared\": \"リーダーの既定がクリアされました\",\n        \"save_hint\": \"このデバイスのみの設定を保存するか、すべてのデバイス間で同期します\",\n        \"save_as_default\": \"既定として保存\",\n        \"save_to_device\": \"このデバイス\",\n        \"sans\": \"サンセリフ\",\n        \"tooltip_preview_and_local\": \"未保存のプレビュー変更; デバイス設定が全体設定と異なります\",\n        \"adjust_hint\": \"変更をプレビューするには、上記の設定を調整してください\"\n      },\n      \"avatar\": {\n        \"upload\": \"アバターをアップロードする\",\n        \"change\": \"アバターを変更する\",\n        \"remove_confirm_title\": \"アバターを削除する？\",\n        \"updated\": \"アバターを更新したで\",\n        \"removed\": \"アバターを削除したで\",\n        \"description\": \"アバターとして使う正方形の画像をアップロードしてちょ。\",\n        \"remove_confirm_description\": \"現在のプロフィール写真が消去されるけど、ええんか？\",\n        \"title\": \"プロフィール画像\",\n        \"remove\": \"アバターを削除する\"\n      }\n    },\n    \"ai\": {\n      \"prompt_preview\": \"プロンプトプレビュー\",\n      \"images_prompt\": \"画像プロンプト\",\n      \"tagging_rule_description\": \"ここで追加したプロンプトは、タグ生成時にモデルのルールとして組み込まれます。最終的なプロンプトは、プロンプト プレビュー セクションで確認できます。\",\n      \"ai_settings\": \"AI設定\",\n      \"text_prompt\": \"テキストプロンプト\",\n      \"tagging_rules\": \"タグ付けルール\",\n      \"summarization_prompt\": \"要約プロンプト\",\n      \"all_tagging\": \"すべてのタグ付け\",\n      \"text_tagging\": \"テキストタグ付け\",\n      \"image_tagging\": \"画像タグ付け\",\n      \"summarization\": \"要約\",\n      \"tag_style\": \"タグのスタイル\",\n      \"auto_summarization_description\": \"AIを使ってブックマークの要約を自動生成する。\",\n      \"auto_tagging\": \"自動タグ付け\",\n      \"titlecase_spaces\": \"タイトルケース、スペース区切り\",\n      \"lowercase_underscores\": \"小文字、アンダースコア区切り\",\n      \"inference_language\": \"推論言語\",\n      \"titlecase_hyphens\": \"タイトルケース、ハイフン区切り\",\n      \"lowercase_hyphens\": \"小文字、ハイフン区切り\",\n      \"lowercase_spaces\": \"小文字、スペース区切り\",\n      \"inference_language_description\": \"AIが生成するタグや概要の言語を選んでくれ。\",\n      \"tag_style_description\": \"自動生成されるタグの書式を選んでくれ。\",\n      \"auto_tagging_description\": \"AIを使ってブックマークのタグを自動生成する。\",\n      \"camelCase\": \"camelCase\",\n      \"auto_summarization\": \"自動要約\",\n      \"no_preference\": \"指定なし\",\n      \"curated_tags\": \"厳選されたタグ\",\n      \"curated_tags_description\": \"必要に応じて、AI タグ付けをこのリストのタグのみを使用するように制限します。タグが選択されていない場合、AI は自由にタグを生成します。\",\n      \"curated_tags_updated\": \"厳選されたタグが正常に更新されました。\",\n      \"curated_tags_update_failed\": \"厳選されたタグの更新に失敗しました\"\n    },\n    \"import\": {\n      \"import_export_bookmarks\": \"ブックマークのインポート/エクスポート\",\n      \"import_bookmarks_from_html_file\": \"HTML ファイルからブックマークをインポートする\",\n      \"import_bookmarks_from_karakeep_export\": \"Karakeep エクスポートからブックマークをインポート\",\n      \"imported_bookmarks\": \"インポートされたブックマーク\",\n      \"import_bookmarks_from_pocket_export\": \"Pocketのエクスポートからブックマークをインポート\",\n      \"import_bookmarks_from_matter_export\": \"Matterのエクスポートからブックマークをインポート\",\n      \"import_bookmarks_from_omnivore_export\": \"Omnivoreエクスポートからブックマークをインポート\",\n      \"export_links_and_notes\": \"リンクとメモをエクスポートする\",\n      \"import_export\": \"インポート/エクスポート\",\n      \"import_bookmarks_from_linkwarden_export\": \"Linkwardenのエクスポートからブックマークをインポート\",\n      \"import_bookmarks_from_tab_session_manager_export\": \"Tab Session Managerからブックマークをインポート\",\n      \"import_bookmarks_from_mymind_export\": \"mymind エクスポートからブックマークをインポート\",\n      \"import_bookmarks_from_instapaper_export\": \"Instapaperのエクスポートからブックマークをインポートする\"\n    },\n    \"api_keys\": {\n      \"key_success_please_copy\": \"キーをコピーして安全な場所に保管してください。ダイアログを閉じると、再度アクセスできなくなります。\",\n      \"new_api_key_desc\": \"APIキーに一意の名前を付ける\",\n      \"key_success\": \"キーが正常に作成されました\",\n      \"api_keys\": \"APIキー\",\n      \"new_api_key\": \"新しいAPIキー\",\n      \"regenerate_api_key\": \"APIキーを再生成する\",\n      \"key_regenerated\": \"キーの再生成に成功しました\",\n      \"key_regenerated_please_copy\": \"新しいキーをコピーして安全な場所に保管してください。古いキーは無効になり、機能しなくなります。\",\n      \"regenerate_warning\": \"APIキー \\\"{{name}}\\\" を再生成してもよろしいですか？\",\n      \"regenerate_confirmation\": \"実行すると、現在のキーが無効になり、新しいキーが生成されます。現在のキーを使用しているアプリケーションは動作を停止します。\"\n    },\n    \"broken_links\": {\n      \"broken_links\": \"リンク切れ\",\n      \"last_crawled_at\": \"最終クロール日時\",\n      \"crawling_status\": \"クロールステータス\",\n      \"crawling_failed\": \"クロールに失敗しました\"\n    },\n    \"back_to_app\": \"アプリに戻る\",\n    \"user_settings\": \"ユーザ設定\",\n    \"feeds\": {\n      \"add_a_subscription\": \"サブスクリプションを追加する\",\n      \"rss_subscriptions\": \"RSS 購読\",\n      \"feed_enabled\": \"RSSフィードは有効です\",\n      \"feed_disabled\": \"RSSフィードは無効です\"\n    },\n    \"manage_assets\": {\n      \"manage_assets\": \"アセットの管理\",\n      \"no_assets\": \"まだアセットがありません。\",\n      \"asset_type\": \"アセットタイプ\",\n      \"bookmark_link\": \"ブックマークリンク\",\n      \"asset_link\": \"アセットリンク\",\n      \"delete_asset\": \"アセットを削除\",\n      \"delete_asset_confirmation\": \"このアセットを削除してもよろしいですか？\"\n    },\n    \"webhooks\": {\n      \"webhooks\": \"ウェブフック\",\n      \"description\": \"Webhook を使用して、ブックマークの作成、変更、またはクロール時にアクションをトリガーできます。\",\n      \"events\": {\n        \"title\": \"イベント\",\n        \"crawled\": \"クロール済み\",\n        \"created\": \"作成済み\",\n        \"edited\": \"編集済み\"\n      },\n      \"auth_token\": \"認証トークン\",\n      \"add_auth_token\": \"認証トークンを追加\",\n      \"edit_auth_token\": \"認証トークンを編集\",\n      \"create_webhook\": \"Webhookを作成\",\n      \"delete_webhook\": \"Webhookを削除\",\n      \"delete_webhook_confirmation\": \"このWebhookを削除してもよろしいですか？\",\n      \"edit_webhook\": \"Webhook を編集\",\n      \"webhook_url\": \"Webhook URL\"\n    },\n    \"rules\": {\n      \"if\": \"もし…\",\n      \"rules\": \"ルールエンジン\",\n      \"rule_name\": \"ルール名\",\n      \"description\": \"ルールを使用すると、イベントが発生したときにアクションをトリガーできます。\",\n      \"ceate_rule\": \"ルールを作成\",\n      \"edit_rule\": \"ルールを編集\",\n      \"save_rule\": \"ルールを保存\",\n      \"delete_rule\": \"ルールを削除\",\n      \"delete_rule_confirmation\": \"このルールを削除してもよろしいですか？\",\n      \"whenever\": \"いつでも…\",\n      \"enter_rule_name\": \"ルール名を入力\",\n      \"describe_what_this_rule_does\": \"このルールの内容を説明してください\",\n      \"rule_has_been_created\": \"ルールが作成されたぞ！\",\n      \"rule_has_been_updated\": \"ルールを更新しました！\",\n      \"rule_has_been_deleted\": \"ルールが削除されたぞ！\",\n      \"no_rules_created_yet\": \"まだルールは作られてないぞ\",\n      \"create_your_first_rule\": \"最初のルールを作成して、ワークフローを自動化しましょう\",\n      \"conditions_types\": {\n        \"always\": \"常に\",\n        \"url_contains\": \"URLに以下を含む\",\n        \"imported_from_feed\": \"フィードからインポート\",\n        \"bookmark_type_is\": \"ブックマークの種類\",\n        \"has_tag\": \"タグあり\",\n        \"is_favourited\": \"お気に入り登録されている\",\n        \"is_archived\": \"アーカイブ済み\",\n        \"and\": \"次の条件がすべて当てはまる\",\n        \"or\": \"次のいずれかが当てはまる場合\",\n        \"url_does_not_contain\": \"URL に含まれていない\",\n        \"title_contains\": \"タイトルに含む\",\n        \"title_does_not_contain\": \"タイトルに含まない\"\n      },\n      \"actions_types\": {\n        \"add_tag\": \"タグを追加\",\n        \"remove_tag\": \"タグを削除\",\n        \"add_to_list\": \"リストに追加\",\n        \"remove_from_list\": \"リストから削除\",\n        \"download_full_page_archive\": \"ページ全体のアーカイブをダウンロード\",\n        \"favourite_bookmark\": \"ブックマークをお気に入りに追加\",\n        \"archive_bookmark\": \"ブックマークをアーカイブ\"\n      },\n      \"events_types\": {\n        \"bookmark_added\": \"ブックマークが追加されたとき\",\n        \"tag_added\": \"このタグがブックマークに追加されたとき\",\n        \"tag_removed\": \"このタグがブックマークから削除された\",\n        \"added_to_list\": \"ブックマークがこのリストに追加された\",\n        \"removed_from_list\": \"ブックマークがこのリストから削除された\",\n        \"favourited\": \"ブックマークがお気に入りに追加されたとき\",\n        \"archived\": \"ブックマークがアーカイブされた\"\n      }\n    },\n    \"stats\": {\n      \"usage_statistics\": \"利用状況の統計\",\n      \"insights_description\": \"ブックマークの習慣とコレクションに関するインサイト\",\n      \"failed_to_load\": \"統計のロードに失敗しました\",\n      \"overview\": {\n        \"total_bookmarks\": \"ブックマークの総数\",\n        \"all_saved_items\": \"保存されたすべてのアイテム\",\n        \"favorites\": \"お気に入り\",\n        \"starred_bookmarks\": \"スター付きのブックマーク\",\n        \"archived\": \"アーカイブ済み\",\n        \"archived_items\": \"アーカイブされたアイテム\",\n        \"tags\": \"タグ\",\n        \"unique_tags_created\": \"一意のタグが作成されました\",\n        \"lists\": \"リスト\",\n        \"bookmark_collections\": \"ブックマークコレクション\",\n        \"highlights\": \"ハイライト\",\n        \"text_highlights\": \"テキストのハイライト\",\n        \"storage_used\": \"使用ストレージ\",\n        \"total_asset_storage\": \"アセットストレージの合計\",\n        \"this_month\": \"今月\",\n        \"bookmarks_added\": \"ブックマークが追加されました\"\n      },\n      \"bookmark_types\": {\n        \"title\": \"ブックマークの種類\",\n        \"links\": \"リンク\",\n        \"text_notes\": \"テキストメモ\",\n        \"assets\": \"アセット\"\n      },\n      \"recent_activity\": {\n        \"title\": \"最近のアクティビティ\",\n        \"this_week\": \"今週\",\n        \"this_month\": \"今月\",\n        \"this_year\": \"今年\"\n      },\n      \"top_domains\": {\n        \"title\": \"トップドメイン\",\n        \"no_domains_found\": \"ドメインが見つかりませんでした\"\n      },\n      \"most_used_tags\": {\n        \"title\": \"最も使用されたタグ\",\n        \"no_tags_found\": \"タグが見つかりません\"\n      },\n      \"activity_patterns\": {\n        \"activity_by_hour\": \"時間ごとのアクティビティ\",\n        \"activity_by_day\": \"日ごとのアクティビティ\"\n      },\n      \"storage_breakdown\": {\n        \"title\": \"ストレージの内訳\"\n      },\n      \"bookmark_sources\": {\n        \"title\": \"ソースをお気に入りに追加する\",\n        \"empty\": \"ソースデータがありません\"\n      }\n    },\n    \"subscription\": {\n      \"subscription\": \"サブスクリプション\",\n      \"manage_subscription\": \"サブスクリプションと請求情報を管理する\",\n      \"current_plan\": \"現在のプラン\",\n      \"billing_period\": \"請求期間\",\n      \"paid_plan\": \"有料プラン\",\n      \"unlock_bigger_quota\": \"より大きなクォータをアンロックしてプロジェクトを支援する\",\n      \"subscribe_now\": \"今すぐ登録\",\n      \"manage_billing\": \"請求の管理\",\n      \"subscription_canceled\": \"お客様のサブスクリプションはキャンセルされ、{{date}} に終了します。いつでも再登録できます。\",\n      \"usage_quotas\": \"使用量とクォータ\",\n      \"track_usage\": \"現在の使用状況をプランの制限と比較して追跡する\",\n      \"total_bookmarks_saved\": \"保存されたブックマークの総数\",\n      \"assets_file_storage\": \"アセットとファイルストレージ\",\n      \"unlimited_usage\": \"無制限の使用\",\n      \"quota_limit_reached\": \"クォータ制限に達しました\",\n      \"approaching_quota_limit\": \"クォータ制限に近づいています\",\n      \"loading_usage\": \"使用状況情報を読み込み中...\",\n      \"free\": \"無料\",\n      \"paid\": \"有料\"\n    },\n    \"import_sessions\": {\n      \"title\": \"インポートセッション\",\n      \"description\": \"ブックマークの一括インポートセッションを表示・管理しましょっ。セッションはブックマークをインポートするときに自動的に作られるよ。\",\n      \"load_error\": \"インポートセッションのロードに失敗しちゃった\",\n      \"no_sessions\": \"インポートセッションはまだないみたい\",\n      \"no_sessions_detail\": \"ブックマークをインポートすると、ここに自動的にインポートセッションが表示されるよ\",\n      \"created_at\": \"作成日時: {{time}}\",\n      \"progress\": \"進捗状況\",\n      \"status\": {\n        \"pending\": \"保留中\",\n        \"in_progress\": \"進行中\",\n        \"completed\": \"完了\",\n        \"failed\": \"失敗\",\n        \"processing\": \"処理中\",\n        \"staging\": \"ステージング\",\n        \"running\": \"実行中\",\n        \"paused\": \"一時停止\"\n      },\n      \"badges\": {\n        \"pending\": \"{{count}} 件が保留中です\",\n        \"processing\": \"{{count}} 件が処理中です\",\n        \"completed\": \"{{count}} 件が完了しました\",\n        \"failed\": \"{{count}} 件が失敗しました\"\n      },\n      \"imported_to\": \"インポート先:\",\n      \"view_list\": \"リスト表示\",\n      \"delete_dialog_title\": \"インポートセッションを削除する\",\n      \"delete_dialog_description\": \"「{{name}}」を削除してもいい？　これは取り消せない操作だよ。ブックマーク自体は削除されないから安心して。\",\n      \"delete_session\": \"セッションを削除\",\n      \"pause_session\": \"一時停止\",\n      \"resume_session\": \"再開\",\n      \"view_details\": \"詳細を見る\",\n      \"detail\": {\n        \"page_title\": \"インポートセッションの詳細\",\n        \"back_to_import\": \"インポートに戻る\",\n        \"filter_all\": \"すべて\",\n        \"filter_accepted\": \"承認済\",\n        \"filter_rejected\": \"拒否済\",\n        \"filter_duplicates\": \"重複\",\n        \"filter_pending\": \"保留中\",\n        \"table_title\": \"タイトル/URL\",\n        \"table_type\": \"種類\",\n        \"table_result\": \"結果\",\n        \"table_reason\": \"理由\",\n        \"table_bookmark\": \"ブックマーク\",\n        \"result_accepted\": \"承認済\",\n        \"result_rejected\": \"拒否済\",\n        \"result_skipped_duplicate\": \"重複\",\n        \"result_pending\": \"保留中\",\n        \"result_processing\": \"処理中\",\n        \"no_results\": \"このフィルターに該当する結果はありません。\",\n        \"view_bookmark\": \"ブックマークを見る\",\n        \"load_more\": \"もっと読み込む\",\n        \"no_title\": \"タイトルなし\"\n      }\n    },\n    \"backups\": {\n      \"backups\": \"バックアップ\",\n      \"page_title\": \"バックアップ\",\n      \"page_description\": \"あんたのブックマークのバックアップを自動で作成・管理するよ。バックアップは圧縮されて安全に保管されるから安心。\",\n      \"configuration\": {\n        \"title\": \"バックアップの設定\",\n        \"enable_automatic_backups\": \"自動バックアップを有効にする\",\n        \"enable_automatic_backups_description\": \"ブックマークのバックアップを自動で作成するよ\",\n        \"backup_frequency\": \"バックアップの頻度\",\n        \"backup_frequency_description\": \"バックアップをどのくらいの頻度で作成するか\",\n        \"retention_period\": \"保存期間（日数）\",\n        \"retention_period_description\": \"バックアップを削除するまで何日間保持するか\",\n        \"frequency\": {\n          \"daily\": \"毎日\",\n          \"weekly\": \"毎週\"\n        },\n        \"select_frequency\": \"頻度を選択\",\n        \"save_settings\": \"設定を保存\"\n      },\n      \"list\": {\n        \"title\": \"あんたのバックアップ\",\n        \"create_backup_now\": \"今すぐバックアップを作成\",\n        \"no_backups\": \"まだバックアップがないね。自動バックアップを有効にするか、手動で作成してくれ。\",\n        \"table\": {\n          \"created_at\": \"作成日時\",\n          \"bookmarks\": \"ブックマーク\",\n          \"size\": \"サイズ\",\n          \"status\": \"ステータス\",\n          \"actions\": \"アクション\"\n        },\n        \"status\": {\n          \"success\": \"成功\",\n          \"failed\": \"失敗\",\n          \"pending\": \"保留中\"\n        },\n        \"actions\": {\n          \"download_backup\": \"バックアップをダウンロード\",\n          \"delete_backup\": \"バックアップを削除\"\n        }\n      },\n      \"dialogs\": {\n        \"delete_backup_title\": \"バックアップを削除してもよろしいですか？\",\n        \"delete_backup_description\": \"このバックアップを削除してもよろしいですか？　この操作は取り消せません。\"\n      },\n      \"toasts\": {\n        \"backup_queued\": \"バックアップジョブがキューに追加されました。まもなく処理されます。\",\n        \"backup_deleted\": \"バックアップは削除されました！\"\n      }\n    }\n  },\n  \"common\": {\n    \"role\": \"ロール\",\n    \"roles\": {\n      \"admin\": \"管理者\",\n      \"user\": \"ユーザ\"\n    },\n    \"url\": \"URL\",\n    \"password\": \"パスワード\",\n    \"email\": \"メールアドレス\",\n    \"name\": \"Name\",\n    \"something_went_wrong\": \"問題が発生しました\",\n    \"experimental\": \"実験的\",\n    \"search\": \"検索\",\n    \"action\": \"アクション\",\n    \"tags\": \"タグ\",\n    \"note\": \"ノート\",\n    \"attachments\": \"添付ファイル\",\n    \"screenshot\": \"スクリーンショット\",\n    \"video\": \"ビデオ\",\n    \"archive\": \"アーカイブ\",\n    \"key\": \"キー\",\n    \"home\": \"ホーム\",\n    \"actions\": \"アクション\",\n    \"created_at\": \"作成日\",\n    \"type\": \"種類\",\n    \"size\": \"サイズ\",\n    \"bookmark_types\": {\n      \"title\": \"ブックマークの種類\",\n      \"link\": \"リンク\",\n      \"text\": \"テキスト\",\n      \"media\": \"メディア\"\n    },\n    \"highlights\": \"ハイライト\",\n    \"source\": \"ソース\",\n    \"updated_at\": \"更新日\",\n    \"title\": \"タイトル\",\n    \"description\": \"説明\",\n    \"summary\": \"概要\",\n    \"quota\": \"割り当て\",\n    \"bookmarks\": \"ブックマーク\",\n    \"storage\": \"ストレージ\",\n    \"pdf\": \"PDFをアーカイブしたよ\",\n    \"default\": \"既定\",\n    \"id\": \"ID\",\n    \"last_used\": \"最後に使用\"\n  },\n  \"layouts\": {\n    \"grid\": \"グリッド\",\n    \"list\": \"リスト\",\n    \"masonry\": \"石積み\",\n    \"compact\": \"コンパクト\"\n  },\n  \"lists\": {\n    \"manual_list\": \"手動リスト\",\n    \"all_lists\": \"すべてのリスト\",\n    \"favourites\": \"お気に入り\",\n    \"edit_list\": \"リストを編集\",\n    \"new_nested_list\": \"新しいネストされたリスト\",\n    \"parent_list\": \"親リスト\",\n    \"no_parent\": \"親なし\",\n    \"list_type\": \"リストの種類\",\n    \"new_list\": \"新しいリスト\",\n    \"search_query_help\": \"検索クエリ言語についてもっと学ぼう。\",\n    \"search_query\": \"検索クエリ\",\n    \"smart_list\": \"スマートリスト\",\n    \"merge_list\": \"リストをマージ\",\n    \"destination_list\": \"マージ先リスト\",\n    \"delete_after_merge\": \"マージ後に元のリストを削除\",\n    \"no_destination\": \"マージ先なし\",\n    \"description\": \"説明（任意）\",\n    \"share_list\": \"リストを共有\",\n    \"rss\": {\n      \"title\": \"RSSフィード\",\n      \"description\": \"このリストのRSSフィードを有効にする\",\n      \"feed_url\": \"RSSフィードのURL\"\n    },\n    \"public_list\": {\n      \"title\": \"公開リスト\",\n      \"description\": \"他のユーザーにこのリストの表示を許可する\",\n      \"share_link\": \"リンクを共有\"\n    },\n    \"delete_list\": {\n      \"title\": \"リストを削除\",\n      \"description\": \"リストを削除しても、そのリスト内のブックマークは削除されません。\",\n      \"delete_children\": \"子リストを（再帰的に）削除\",\n      \"delete_children_description\": \"チェックしない場合、すべての子リストがルートリストになります\"\n    },\n    \"shared\": \"共有\",\n    \"collaborators\": {\n      \"manage\": \"コラボレーターの管理\",\n      \"view\": \"コラボレーターの表示\",\n      \"collaborators\": \"コラボレーター\",\n      \"add\": \"コラボレーターの追加\",\n      \"current\": \"現在のコラボレーター\",\n      \"enter_email\": \"メールアドレスを入力\",\n      \"please_enter_email\": \"メールアドレスを入力してください\",\n      \"added_successfully\": \"コラボレーターが追加されました\",\n      \"failed_to_add\": \"コラボレーターの追加に失敗しました\",\n      \"removed\": \"コラボレーターが削除されました\",\n      \"failed_to_remove\": \"コラボレーターの削除に失敗しました\",\n      \"role_updated\": \"ロールが更新されました\",\n      \"failed_to_update_role\": \"ロールの更新に失敗しました\",\n      \"viewer\": \"閲覧者\",\n      \"editor\": \"編集者\",\n      \"owner\": \"オーナー\",\n      \"viewer_description\": \"リスト内のブックマークを表示できます\",\n      \"editor_description\": \"ブックマークを追加および削除できます\",\n      \"no_collaborators\": \"コラボレーターはまだいません。誰かを追加してコラボレーションを始めましょう！\",\n      \"no_collaborators_readonly\": \"このリストにはコラボレーターはいません。\",\n      \"people_with_access\": \"このリストにアクセスできる人\",\n      \"add_or_remove\": \"このリストにアクセスできる人を追加または削除する\",\n      \"invitation_sent\": \"招待状、送信完了！\",\n      \"invitation_revoked\": \"招待取り消し\",\n      \"failed_to_revoke\": \"招待の取り消しに失敗しました\",\n      \"pending\": \"保留中\",\n      \"revoke\": \"取り消し\",\n      \"declined\": \"拒否\"\n    },\n    \"leave_list\": {\n      \"title\": \"リストから退会する\",\n      \"confirm_message\": \"{{icon}} {{name}} から退会してもよろしいですか？\",\n      \"warning\": \"このリストのブックマークを表示またはアクセスできなくなります。必要に応じて、リストのオーナーがあなたを追加し直すことができます。\",\n      \"action\": \"リストから退会する\",\n      \"success\": \"「{{icon}} {{name}}」から退会しました。\"\n    },\n    \"invitations\": {\n      \"pending\": \"招待待ち\",\n      \"description\": \"リストのコラボレーション招待の確認と対応\",\n      \"invited_by\": \"招待元\",\n      \"accept\": \"承諾\",\n      \"decline\": \"拒否\",\n      \"accepted\": \"招待を承諾しました\",\n      \"declined\": \"招待を辞退しました\",\n      \"failed_to_accept\": \"招待の承諾に失敗しました\",\n      \"failed_to_decline\": \"招待の辞退に失敗しました\"\n    },\n    \"shared_lists\": \"共有リスト\"\n  },\n  \"options\": {\n    \"dark_mode\": \"ダークモード\",\n    \"light_mode\": \"ライトモード\",\n    \"apps_extensions\": \"アプリと拡張機能\",\n    \"documentation\": \"ドキュメンテーション\",\n    \"follow_us_on_x\": \"Xでフォローしてね\"\n  },\n  \"tags\": {\n    \"drag_and_drop_merging\": \"ドラッグ＆ドロップマージ\",\n    \"drag_and_drop_merging_info\": \"タグ同士をドラッグ＆ドロップして結合\",\n    \"unused_tags_info\": \"どのブックマークにも付いていないタグ\",\n    \"all_tags\": \"すべてのタグ\",\n    \"your_tags_info\": \"あなたが少なくとも一度は付けたタグ\",\n    \"delete_all_unused_tags\": \"未使用のタグをすべて削除\",\n    \"your_tags\": \"あなたのタグ\",\n    \"ai_tags\": \"AIタグ\",\n    \"ai_tags_info\": \"自動的に（AIによって）付与されただけのタグ\",\n    \"unused_tags\": \"未使用のタグ\",\n    \"sort_by_name\": \"名前でソート\",\n    \"create_tag\": \"タグを作成\",\n    \"create_tag_description\": \"どのブックマークにも関連付けずに、新しいタグを作成する\",\n    \"tag_name\": \"タグ名\",\n    \"enter_tag_name\": \"タグ名を入力\",\n    \"sort_by_usage\": \"使用頻度順で並び替え\",\n    \"sort_by_relevance\": \"関連度順で並び替え\",\n    \"no_custom_tags\": \"カスタムタグはまだありません\",\n    \"no_ai_tags\": \"AIタグはまだありません\",\n    \"no_unused_tags\": \"未使用のタグはありません\",\n    \"no_unused_tags_match_your_search\": \"検索に一致する未使用のタグはありません\",\n    \"no_tags_match_your_search\": \"検索に一致するタグはありません\",\n    \"search_placeholder\": \"タグを検索...\",\n    \"search_or_create_placeholder\": \"タグを検索または作成...\"\n  },\n  \"search\": {\n    \"created_on_or_after\": \"作成日以降\",\n    \"url_does_not_contain\": \"URLに含まない\",\n    \"is_not_in_any_list\": \"どのリストにも含まれていません\",\n    \"is_not_archived\": \"アーカイブされていません\",\n    \"type_is\": \"種類は\",\n    \"has_any_tag\": \"いずれかのタグを持つ\",\n    \"has_no_tags\": \"タグなし\",\n    \"is_in_any_list\": \"いずれかのリストに入っている\",\n    \"not_created_on_or_before\": \"以前に作成されていません\",\n    \"url_contains\": \"URLに含む\",\n    \"has_tag\": \"タグあり\",\n    \"does_not_have_tag\": \"タグがない\",\n    \"full_text_search\": \"全文検索\",\n    \"created_on_or_before\": \"作成日（以前）\",\n    \"is_in_list\": \"リストに入ってる？\",\n    \"is_not_in_list\": \"リストにない\",\n    \"type_is_not\": \"タイプが違う\",\n    \"and\": \"そして\",\n    \"or\": \"または\",\n    \"is_favorited\": \"お気に入り\",\n    \"is_not_favorited\": \"お気に入りではない\",\n    \"is_archived\": \"アーカイブ済み？\",\n    \"not_created_on_or_after\": \"以降に作成されなかった\",\n    \"is_not_from_feed\": \"RSSフィードからではない\",\n    \"is_from_feed\": \"RSSフィードから\",\n    \"created_within\": \"作成期間\",\n    \"created_earlier_than\": \"作成日\",\n    \"day_s\": \" ～日\",\n    \"week_s\": \" ～週間\",\n    \"month_s\": \" ～か月\",\n    \"year_s\": \" ～年\",\n    \"day_s_ago\": \" ～日前\",\n    \"week_s_ago\": \" ～週間前\",\n    \"month_s_ago\": \" ～か月前\",\n    \"year_s_ago\": \" ～年前\",\n    \"history\": \"最近の検索\",\n    \"title_contains\": \"タイトルに含む\",\n    \"title_does_not_contain\": \"タイトルに含まない\",\n    \"is_broken_link\": \"リンク切れ\",\n    \"tags\": \"タグ\",\n    \"no_suggestions\": \"サジェストはありません\",\n    \"filters\": \"フィルター\",\n    \"is_not_broken_link\": \"リンクは有効です\",\n    \"lists\": \"リスト\",\n    \"feeds\": \"フィード\",\n    \"is_from_source\": \"ソースは\",\n    \"is_not_from_source\": \"ソースじゃない\"\n  },\n  \"preview\": {\n    \"cached_content\": \"キャッシュされたコンテンツ\",\n    \"view_original\": \"オリジナルを見る\",\n    \"reader_view\": \"リーダー表示\",\n    \"tabs\": {\n      \"content\": \"コンテンツ\",\n      \"details\": \"詳細\"\n    },\n    \"archive_info\": \"アーカイブは Javascript を必要とする場合、インラインで正しく表示されないことがあります。最良の結果を得るには、<1>ダウンロードしてブラウザで開いてください</1>。\",\n    \"fetch_error_title\": \"コンテンツが利用できません\",\n    \"fetch_error_description\": \"このリンクのコンテンツを取得できませんでした。ページが保護されているか、認証が必要であるか、または一時的に利用できない可能性があります。\",\n    \"crawling_in_progress\": \"ページコンテンツを取得中…\",\n    \"continue_reading\": \"続きから再開\",\n    \"continue_reading_percent\": \"続きから再開（{{percent}}%）\",\n    \"continue_button\": \"続ける\"\n  },\n  \"editor\": {\n    \"quickly_focus\": \"⌘ + E を押すと、このフィールドにすばやくフォーカスできます\",\n    \"multiple_urls_dialog_title\": \"URLを個別のブックマークとしてインポートしますか？\",\n    \"import_as_text\": \"テキストブックマークとしてインポート\",\n    \"import_as_separate_bookmarks\": \"別のブックマークとしてインポート\",\n    \"placeholder\": \"リンクまたは画像を貼り付けたり、メモを書いたり、ここに画像をドラッグアンドドロップしたりできます…\",\n    \"text_toolbar\": {\n      \"bold\": \"太字\",\n      \"underline\": \"下線\",\n      \"strikethrough\": \"取り消し線\",\n      \"align_center\": \"中央揃え\",\n      \"align_right\": \"右揃え\",\n      \"markdown_shortcuts\": {\n        \"block_code\": {\n          \"example\": \"``` + スペース\",\n          \"label\": \"ブロックコード\"\n        },\n        \"ordered_list\": {\n          \"label\": \"順序付きリスト\",\n          \"example\": \"1. リスト項目\"\n        },\n        \"inline_code\": {\n          \"label\": \"インラインコード\",\n          \"example\": \"`コード`\"\n        },\n        \"unordered_list\": {\n          \"label\": \"順不同リスト\",\n          \"example\": \"- リスト項目\"\n        },\n        \"label\": \"マークダウンショートカット\",\n        \"heading\": {\n          \"label\": \"見出し\",\n          \"example\": \"# H1、## H2、### H3\"\n        },\n        \"bold\": {\n          \"label\": \"太字\",\n          \"example\": \"**テキスト** または CTRL+b\"\n        },\n        \"blockquote\": {\n          \"example\": \"> 引用\",\n          \"label\": \"引用\"\n        },\n        \"italic\": {\n          \"label\": \"イタリック\",\n          \"example\": \"*斜体* または _斜体_ または CTRL+i\"\n        }\n      },\n      \"code\": \"コード\",\n      \"redo\": \"やり直し\",\n      \"undo\": \"元に戻す\",\n      \"italic\": \"イタリック\",\n      \"highlight\": \"ハイライト\",\n      \"align_left\": \"左揃え\"\n    },\n    \"disabled_submissions\": \"投稿は無効です\",\n    \"multiple_urls_dialog_desc\": \"入力には複数のURLが別々の行に含まれています。それらを個別のブックマークとしてインポートしますか？\",\n    \"new_item\": \"新しいアイテム\",\n    \"placeholder_v2\": \"リンクをペーストするか、メモを書くか、画像をドロップしてくれ…\"\n  },\n  \"dialogs\": {\n    \"bookmarks\": {\n      \"delete_confirmation_title\": \"ブックマークを削除する？\",\n      \"delete_confirmation_description\": \"このブックマークを削除してもよろしいですか？\"\n    }\n  },\n  \"toasts\": {\n    \"bookmarks\": {\n      \"updated\": \"ブックマークが更新されたよ！\",\n      \"clipboard_copied\": \"リンクをクリップボードに追加したぞ！\",\n      \"full_page_archive\": \"フルページアーカイブの作成が開始されました\",\n      \"delete_from_list\": \"ブックマークがリストから削除されました\",\n      \"deleted\": \"ブックマークが削除されたよ！\",\n      \"refetch\": \"再取得をエンキューしたぞ！\",\n      \"preserve_pdf\": \"PDF保存が開始されたよ\",\n      \"update_banner\": \"バナーが更新されました！\",\n      \"uploading_banner\": \"バナーをアップロード中...\"\n    },\n    \"lists\": {\n      \"created\": \"リストが作成されました！\",\n      \"updated\": \"リストが更新されました！\",\n      \"merged\": \"リストがマージされたぞ！\",\n      \"deleted\": \"リストが削除されたぞ！\"\n    },\n    \"tags\": {\n      \"created\": \"タグが作成されました！\",\n      \"failed_to_create\": \"タグの作成に失敗しました\"\n    }\n  },\n  \"cleanups\": {\n    \"duplicate_tags\": {\n      \"title\": \"重複タグ\",\n      \"merge_all_suggestions\": \"すべての提案をマージしますか？\"\n    },\n    \"cleanups\": \"クリーンアップ\"\n  },\n  \"highlights\": {\n    \"no_highlights\": \"まだハイライトがありません。\"\n  },\n  \"banners\": {\n    \"no_bookmarks\": {\n      \"title\": \"ブックマークはまだありません\",\n      \"description\": \"気になる記事、リンク、ページを保存して、後ですぐにアクセスできるようにしよう。\"\n    }\n  },\n  \"bookmark_editor\": {\n    \"title\": \"ブックマークを編集\",\n    \"subtitle\": \"ブックマークの詳細を変更する。完了したら「保存」をクリックしてください。\",\n    \"author\": \"著者\",\n    \"publisher\": \"出版社\",\n    \"date_published\": \"公開日\",\n    \"pick_a_date\": \"日付を選択\",\n    \"save_changes\": \"変更を保存\",\n    \"extracted_content\": \"抽出されたコンテンツ\"\n  },\n  \"view_options\": {\n    \"title\": \"表示オプション\",\n    \"layout\": \"レイアウト\",\n    \"columns\": \"列\",\n    \"display_options\": \"表示オプション\",\n    \"show_note_previews\": \"注釈を表示\",\n    \"show_tags\": \"タグを表示\",\n    \"show_title\": \"タイトルを表示\",\n    \"image_options\": \"画像オプション\",\n    \"image_fit_cover\": \"カバー（塗りつぶし）\",\n    \"image_fit_contain\": \"コンテイン（フィット）\"\n  },\n  \"version\": {\n    \"new_release_available\": \"新しいリリースノートが利用可能です\",\n    \"whats_new_title\": \"v{{version}} の新機能\",\n    \"release_notes_description\": \"GitHub のリリースノートから取得した最新のアップデートです。\",\n    \"loading_release_notes\": \"リリースノートを読み込み中…\",\n    \"unable_to_load_release_notes\": \"ただ今、リリースノートを読み込めません。後でもう一度お試しください。\",\n    \"no_release_notes\": \"このバージョンに関するリリースノートは公開されていません。\",\n    \"release_notes_synced\": \"リリースノートは GitHub から同期されています。\",\n    \"view_on_github\": \"GitHub で表示\"\n  },\n  \"wrapped\": {\n    \"title\": \"あなたの {{year}} を振り返る\",\n    \"subtitle\": \"カラキープでの一年\",\n    \"banner\": {\n      \"title\": \"2025 年の振り返りができました！\",\n      \"description\": \"ブックマークで一年を振り返る\",\n      \"view_now\": \"今すぐ見る\"\n    },\n    \"button\": \"2025年を振り返る\",\n    \"loading\": \"Wrappedを読み込んでいます...\",\n    \"failed_to_load\": \"Wrappedの統計の読み込みに失敗しました\",\n    \"sections\": {\n      \"total_saves\": {\n        \"prefix\": \"保存しました\",\n        \"suffix\": \"今年のアイテム数\",\n        \"suffix_singular\": \"今年のアイテム\"\n      },\n      \"first_bookmark\": {\n        \"title\": \"旅の始まり\",\n        \"description\": \"{{year}}の最初の保存：\"\n      },\n      \"top_domains\": \"人気のサイト\",\n      \"top_tags\": \"人気のタグ\",\n      \"monthly_activity\": \"保存の記録\",\n      \"most_active_day\": \"最もアクティブだった日\",\n      \"peak_times\": {\n        \"title\": \"いつ保存したか\",\n        \"peak_hour\": \"ピーク時間\",\n        \"peak_day\": \"ピーク日\"\n      },\n      \"how_you_save\": \"保存方法\",\n      \"what_you_saved\": \"何を保存したか\",\n      \"summary\": {\n        \"favorites\": \"お気に入り\",\n        \"tags_created\": \"作成したタグ\",\n        \"highlights\": \"ハイライト\"\n      },\n      \"types\": {\n        \"links\": \"リンク\",\n        \"notes\": \"メモ\",\n        \"assets\": \"アセット\"\n      }\n    },\n    \"footer\": \"Karakeep製\",\n    \"share\": \"共有\",\n    \"download\": \"ダウンロード\",\n    \"close\": \"閉じる\",\n    \"generating\": \"作成中...\"\n  }\n}\n"
  },
  {
    "path": "apps/web/lib/i18n/locales/ko/translation.json",
    "content": "{\n  \"common\": {\n    \"url\": \"URL\",\n    \"name\": \"이름\",\n    \"email\": \"이메일\",\n    \"password\": \"비밀번호\",\n    \"screenshot\": \"스크린샷\",\n    \"video\": \"비디오\",\n    \"archive\": \"보관\",\n    \"home\": \"홈\",\n    \"bookmark_types\": {\n      \"title\": \"북마크 유형\",\n      \"link\": \"링크\",\n      \"text\": \"텍스트\",\n      \"media\": \"미디어\"\n    },\n    \"roles\": {\n      \"user\": \"사용자\",\n      \"admin\": \"관리자\"\n    },\n    \"something_went_wrong\": \"문제가 발생했습니다\",\n    \"experimental\": \"실험적\",\n    \"search\": \"검색\",\n    \"tags\": \"태그\",\n    \"note\": \"노트\",\n    \"attachments\": \"첨부 파일\",\n    \"highlights\": \"하이라이트\",\n    \"source\": \"출처\",\n    \"key\": \"키\",\n    \"action\": \"액션\",\n    \"actions\": \"액션\",\n    \"created_at\": \"생성됨\",\n    \"role\": \"역할\",\n    \"type\": \"타입\",\n    \"size\": \"크기\",\n    \"summary\": \"요약\",\n    \"updated_at\": \"업데이트된 날짜\",\n    \"title\": \"제목\",\n    \"description\": \"설명\",\n    \"quota\": \"할당량\",\n    \"bookmarks\": \"북마크\",\n    \"storage\": \"저장 공간\",\n    \"pdf\": \"보관된 PDF\",\n    \"default\": \"기본값\",\n    \"id\": \"ID\",\n    \"last_used\": \"마지막으로 사용됨\"\n  },\n  \"layouts\": {\n    \"list\": \"목록\",\n    \"masonry\": \"벽돌집\",\n    \"grid\": \"그리드\",\n    \"compact\": \"축소\"\n  },\n  \"actions\": {\n    \"change_layout\": \"레이아웃 변경\",\n    \"archive\": \"보관\",\n    \"favorite\": \"즐겨찾기\",\n    \"unfavorite\": \"즐겨찾기 해제\",\n    \"delete\": \"삭제\",\n    \"refresh\": \"새로 고침\",\n    \"add_to_list\": \"목록에 추가\",\n    \"unarchive\": \"보관해제\",\n    \"recrawl\": \"다시 가져오기\",\n    \"download_full_page_archive\": \"보관된 전체 페이지 다운로드\",\n    \"edit_tags\": \"태그 수정\",\n    \"select_all\": \"전체 선택\",\n    \"unselect_all\": \"전체 선택해제\",\n    \"copy_link\": \"링크 복사\",\n    \"close_bulk_edit\": \"다건 수정 닫기\",\n    \"bulk_edit\": \"다건 수정\",\n    \"manage_lists\": \"목록 관리\",\n    \"remove_from_list\": \"목록에서 제거\",\n    \"save\": \"저장\",\n    \"add\": \"추가\",\n    \"edit\": \"수정\",\n    \"create\": \"생성\",\n    \"fetch_now\": \"지금 가져오기\",\n    \"summarize_with_ai\": \"AI로 요약하기\",\n    \"edit_title\": \"제목 수정\",\n    \"sign_out\": \"로그아웃\",\n    \"close\": \"닫기\",\n    \"merge\": \"병합\",\n    \"cancel\": \"취소\",\n    \"apply_all\": \"모두 적용\",\n    \"ignore\": \"무시\",\n    \"sort\": {\n      \"title\": \"정렬\",\n      \"newest_first\": \"최근순\",\n      \"oldest_first\": \"오래된순\",\n      \"relevant_first\": \"관련성순\"\n    },\n    \"open_editor\": \"편집기 열기\",\n    \"toggle_show_archived\": \"보관된 항목 표시\",\n    \"confirm\": \"확인\",\n    \"regenerate\": \"다시 생성\",\n    \"load_more\": \"더 불러오기\",\n    \"edit_notes\": \"노트 편집\",\n    \"preserve_as_pdf\": \"PDF로 보존\",\n    \"offline_copies\": \"오프라인 사본\",\n    \"preserve_offline_archive\": \"오프라인 아카이브 보존\",\n    \"download_full_page_archive_file\": \"아카이브 파일 다운로드\",\n    \"download_pdf_file\": \"PDF 파일 다운로드\",\n    \"remove\": \"제거\",\n    \"more\": \"더 보기\",\n    \"replace_banner\": \"배너 교체\",\n    \"add_banner\": \"배너 추가\",\n    \"download\": \"다운로드\"\n  },\n  \"tags\": {\n    \"unused_tags\": \"사용되지 않은 태그\",\n    \"unused_tags_info\": \"어떤 북마크에도 사용되지 않은 태그\",\n    \"delete_all_unused_tags\": \"사용되지 않은 태그 모두 삭제\",\n    \"drag_and_drop_merging\": \"드래그 앤 드랍으로 병합하기\",\n    \"drag_and_drop_merging_info\": \"태그를 드래그 앤 드랍해서 다른 태그에 병합합니다\",\n    \"sort_by_name\": \"이름으로 정렬\",\n    \"all_tags\": \"모든 태그\",\n    \"your_tags\": \"내 태그\",\n    \"your_tags_info\": \"한번이라도 사용된 태그\",\n    \"ai_tags\": \"모든 태그\",\n    \"ai_tags_info\": \"한번이라도 사용된 태그(AI에 의해)\",\n    \"create_tag\": \"태그 만들기\",\n    \"create_tag_description\": \"새로운 태그를 어떤 북마크에도 연결하지 않고 생성합니다.\",\n    \"tag_name\": \"태그 이름\",\n    \"enter_tag_name\": \"태그 이름을 입력하세요\",\n    \"sort_by_usage\": \"사용 빈도순으로 정렬\",\n    \"sort_by_relevance\": \"관련성순으로 정렬\",\n    \"no_custom_tags\": \"아직 지정 태그가 없어\",\n    \"no_ai_tags\": \"아직 AI 태그가 없어\",\n    \"no_unused_tags\": \"사용하지 않는 태그가 없습니다\",\n    \"no_unused_tags_match_your_search\": \"검색어와 일치하는 사용하지 않는 태그가 없습니다\",\n    \"no_tags_match_your_search\": \"검색어와 일치하는 태그가 없습니다\",\n    \"search_placeholder\": \"태그 검색...\",\n    \"search_or_create_placeholder\": \"태그 검색 또는 생성...\"\n  },\n  \"search\": {\n    \"is_favorited\": \"즐겨찾기 추가됨\",\n    \"is_not_favorited\": \"즐겨찾기에 추가되지 않음\",\n    \"is_archived\": \"보관됨\",\n    \"is_not_archived\": \"보관되지 않음\",\n    \"has_any_tag\": \"태그를 가짐\",\n    \"has_no_tags\": \"태그를 가지지 않음\",\n    \"is_in_any_list\": \"목록에 포함됨\",\n    \"is_not_in_any_list\": \"목록에 포함되지 않음\",\n    \"created_on_or_after\": \"이후 생성됨\",\n    \"created_on_or_before\": \"이후에 생성됨\",\n    \"not_created_on_or_before\": \"이후에 생성되지 않음\",\n    \"url_contains\": \"포함된 URL\",\n    \"url_does_not_contain\": \"URL 포함하지 않음\",\n    \"is_in_list\": \"목록에 포함된\",\n    \"has_tag\": \"태그 가짐\",\n    \"does_not_have_tag\": \"태그를 가지지 않음\",\n    \"type_is\": \"종류는\",\n    \"type_is_not\": \"종류가 아닌\",\n    \"and\": \"그리고\",\n    \"not_created_on_or_after\": \"이후에 생성되지 않음\",\n    \"is_not_in_list\": \"목록에 포함되지 않음\",\n    \"full_text_search\": \"전문 검색\",\n    \"or\": \"또는\",\n    \"is_from_feed\": \"RSS 피드에서 가져옴\",\n    \"is_not_from_feed\": \"RSS 피드에서 가져오지 않음\",\n    \"created_within\": \"다음 기간 내에 생성됨\",\n    \"week_s\": \" 주\",\n    \"year_s\": \" 년\",\n    \"created_earlier_than\": \"다음보다 더 오래 전에 생성됨\",\n    \"day_s\": \" 일\",\n    \"month_s\": \" 개월\",\n    \"day_s_ago\": \" 일 전\",\n    \"week_s_ago\": \" 주 전\",\n    \"month_s_ago\": \" 개월 전\",\n    \"year_s_ago\": \" 년 전\",\n    \"history\": \"최근 검색어\",\n    \"title_contains\": \"제목에 다음 내용이 포함됨\",\n    \"title_does_not_contain\": \"제목에 다음 내용이 포함되지 않음\",\n    \"is_broken_link\": \"깨진 링크 있음\",\n    \"tags\": \"태그\",\n    \"no_suggestions\": \"추천 항목 없음\",\n    \"filters\": \"필터\",\n    \"is_not_broken_link\": \"작동하는 링크 있음\",\n    \"lists\": \"목록\",\n    \"feeds\": \"피드\",\n    \"is_from_source\": \"출처:\",\n    \"is_not_from_source\": \"출처가 아님\"\n  },\n  \"preview\": {\n    \"view_original\": \"원본 보기\",\n    \"cached_content\": \"캐싱된 컨텐츠\",\n    \"reader_view\": \"리더 보기\",\n    \"tabs\": {\n      \"content\": \"콘텐츠\",\n      \"details\": \"세부 정보\"\n    },\n    \"archive_info\": \"보관 파일은 Javascript가 필요한 경우 인라인으로 올바르게 렌더링되지 않을 수 있습니다. 최상의 결과를 얻으려면 <1>다운로드하여 브라우저에서 여세요</1>.\",\n    \"fetch_error_title\": \"콘텐츠를 사용할 수 없음\",\n    \"fetch_error_description\": \"이 링크의 콘텐츠를 가져올 수 없습니다. 페이지가 보호되어 있거나 인증이 필요하거나 일시적으로 사용하지 못할 수 있습니다.\",\n    \"crawling_in_progress\": \"페이지 콘텐츠 가져오는 중…\",\n    \"continue_reading\": \"이어서 하셈\",\n    \"continue_reading_percent\": \"이어서 하셈 ({{percent}}%)\",\n    \"continue_button\": \"계속\"\n  },\n  \"editor\": {\n    \"quickly_focus\": \"⌘ + E를 누르면 이 필드에 초점이 옮겨집니다\",\n    \"multiple_urls_dialog_title\": \"URL을 분리된 북마크로 가져올까요?\",\n    \"multiple_urls_dialog_desc\": \"입력에 별도의 줄에 여러 URL이 포함되어 있습니다. 별도의 북마크로 가져오시겠습니까?\",\n    \"import_as_text\": \"텍스트 북마크로 가져오기\",\n    \"import_as_separate_bookmarks\": \"분리된 북마크로 가져오기\",\n    \"placeholder\": \"링크나 이미지를 붙여넣거나, 메모를 작성하거나, 이미지를 끌어다 놓으세요…\",\n    \"new_item\": \"새로운 아이템\",\n    \"disabled_submissions\": \"저장이 비활성화 됨\",\n    \"text_toolbar\": {\n      \"undo\": \"되돌리기\",\n      \"align_left\": \"왼쪽 정렬\",\n      \"align_center\": \"가운데 정렬\",\n      \"align_right\": \"오른쪽 정렬\",\n      \"markdown_shortcuts\": {\n        \"label\": \"마크다운 단축키\",\n        \"heading\": {\n          \"label\": \"들여쓰기\",\n          \"example\": \"# H1, ## H2, ### H3\"\n        },\n        \"bold\": {\n          \"label\": \"굵게\",\n          \"example\": \"**text** 또는 CTRL+b\"\n        },\n        \"italic\": {\n          \"label\": \"기울이기\",\n          \"example\": \"*Italic* 또는 _Italic_ 또는 CTRL+i\"\n        },\n        \"blockquote\": {\n          \"label\": \"따옴표\",\n          \"example\": \"> 인용구\"\n        },\n        \"ordered_list\": {\n          \"label\": \"정렬된 목록\",\n          \"example\": \"1. 목록 아이템\"\n        },\n        \"unordered_list\": {\n          \"label\": \"순서 없는 목록\",\n          \"example\": \"- 목록 아이템\"\n        },\n        \"inline_code\": {\n          \"label\": \"인라인 코드\",\n          \"example\": \"`Code`\"\n        },\n        \"block_code\": {\n          \"label\": \"코드 블럭\",\n          \"example\": \"``` + 스페이스 키\"\n        }\n      },\n      \"redo\": \"다시하기\",\n      \"italic\": \"이탤릭\",\n      \"underline\": \"밑줄\",\n      \"strikethrough\": \"취소선\",\n      \"code\": \"코드\",\n      \"highlight\": \"강조하기\",\n      \"bold\": \"굵게\"\n    },\n    \"placeholder_v2\": \"링크를 붙여넣거나, 메모를 작성하거나, 이미지를 드롭하세요…\"\n  },\n  \"dialogs\": {\n    \"bookmarks\": {\n      \"delete_confirmation_description\": \"정말로 이 북마크를 삭제할까요?\",\n      \"delete_confirmation_title\": \"북마크를 삭제할까요?\"\n    }\n  },\n  \"toasts\": {\n    \"bookmarks\": {\n      \"updated\": \"북마크가 갱신 되었습니다!\",\n      \"deleted\": \"북마크가 삭제되었습니다!\",\n      \"refetch\": \"다시 가져오기가 큐에 추가 되었습니다!\",\n      \"full_page_archive\": \"전체 페이지 보관 생성이 요청되었습니다\",\n      \"delete_from_list\": \"북마크를 목록에서 삭제했습니다\",\n      \"clipboard_copied\": \"링크를 클립보드에 복사했습니다!\",\n      \"preserve_pdf\": \"PDF 보존이 시작되었습니다\",\n      \"update_banner\": \"배너가 업데이트되었습니다!\",\n      \"uploading_banner\": \"배너 업로드 중...\"\n    },\n    \"lists\": {\n      \"created\": \"목록이 생성 되었습니다!\",\n      \"updated\": \"목록이 갱신 되었습니다!\",\n      \"merged\": \"목록이 병합되었습니다!\",\n      \"deleted\": \"목록이 삭제되었습니다!\"\n    },\n    \"tags\": {\n      \"created\": \"태그가 생성되었어요!\",\n      \"failed_to_create\": \"태그를 만드는 데 실패했습니다\"\n    }\n  },\n  \"cleanups\": {\n    \"cleanups\": \"정리\",\n    \"duplicate_tags\": {\n      \"title\": \"태그 중복\",\n      \"merge_all_suggestions\": \"제안을 병합 하시겠습니까?\"\n    }\n  },\n  \"settings\": {\n    \"api_keys\": {\n      \"key_success\": \"키가 성공적으로 생성되었습니다\",\n      \"key_success_please_copy\": \"키를 복사해서 안전한 곳에 보관해 주세요. 대화 상자를 닫으면 다시 볼 수 없습니다.\",\n      \"api_keys\": \"API 키\",\n      \"new_api_key\": \"새로운 API 키\",\n      \"new_api_key_desc\": \"API 키에 고유한 이름을 주세요\",\n      \"regenerate_api_key\": \"API 키 다시 생성\",\n      \"key_regenerated\": \"키가 성공적으로 다시 생성되었습니다\",\n      \"key_regenerated_please_copy\": \"새로운 키를 복사해서 안전한 곳에 보관해 주세요. 예전 키는 폐기되었으므로 더 이상 작동하지 않습니다.\",\n      \"regenerate_warning\": \"API 키 \\\"{{name}}\\\"을(를) 다시 생성하시겠습니까?\",\n      \"regenerate_confirmation\": \"현재 키를 폐기하고 새로운 키를 생성합니다. 현재 키를 사용하는 모든 애플리케이션은 작동을 멈추게 됩니다.\"\n    },\n    \"webhooks\": {\n      \"events\": {\n        \"title\": \"이벤트\",\n        \"crawled\": \"가져옴\",\n        \"created\": \"생성됨\",\n        \"edited\": \"수정됨\"\n      },\n      \"edit_webhook\": \"웹훅 수정\",\n      \"webhooks\": \"웹훅\",\n      \"description\": \"북마크가 생성, 변경 또는 크롤링될 때 웹훅을 사용하여 작업을 트리거할 수 있습니다.\",\n      \"auth_token\": \"인증 토큰\",\n      \"add_auth_token\": \"인증 토큰 추가\",\n      \"edit_auth_token\": \"인증 토큰 수정\",\n      \"create_webhook\": \"웹훅 생성\",\n      \"delete_webhook\": \"웹훅 삭제\",\n      \"delete_webhook_confirmation\": \"이 웹훅을 삭제하시겠습니까?\",\n      \"webhook_url\": \"웹훅 URL\"\n    },\n    \"info\": {\n      \"change_password\": \"암호 변경\",\n      \"user_info\": \"사용자 정보\",\n      \"basic_details\": \"기본 정보\",\n      \"current_password\": \"현재 암호\",\n      \"new_password\": \"새로운 암호\",\n      \"confirm_new_password\": \"새로운 암호 확인\",\n      \"options\": \"옵션\",\n      \"interface_lang\": \"인터페이스 언어\",\n      \"user_settings\": {\n        \"user_settings_updated\": \"사용자 설정이 업데이트되었습니다!\",\n        \"bookmark_click_action\": {\n          \"title\": \"북마크 클릭 액션\",\n          \"open_external_url\": \"원본 URL 열기\",\n          \"open_bookmark_details\": \"북마크 세부 정보 열기\"\n        },\n        \"archive_display_behaviour\": {\n          \"title\": \"보관된 북마크\",\n          \"show\": \"보관된 북마크를 태그 및 목록에 표시\",\n          \"hide\": \"보관된 북마크를 태그 및 목록에서 숨기기\"\n        }\n      },\n      \"reader_settings\": {\n        \"local_overrides_title\": \"장치별 설정 활성화됨\",\n        \"using_default\": \"클라이언트 기본값 사용\",\n        \"clear_override_hint\": \"전역 설정을 사용하려면 기기 재정의를 지우세요 ({{value}})\",\n        \"font_size\": \"글꼴 크기\",\n        \"font_family\": \"글꼴\",\n        \"preview_inline\": \"(미리보기)\",\n        \"tooltip_preview\": \"저장되지 않은 미리 보기 변경 사항\",\n        \"save_to_all_devices\": \"모든 기기\",\n        \"tooltip_local\": \"기기 설정이 전역 설정과 다름\",\n        \"reset_preview\": \"미리 보기 초기화\",\n        \"mono\": \"고정폭\",\n        \"line_height\": \"줄 높이\",\n        \"tooltip_default\": \"읽기 설정\",\n        \"title\": \"글 뷰어 설정\",\n        \"serif\": \"세리프\",\n        \"preview\": \"미리 보기\",\n        \"not_set\": \"설정 안 됨\",\n        \"clear_local_overrides\": \"장치 설정 삭제\",\n        \"preview_text\": \"The quick brown fox jumps over the lazy dog. 글 뷰어 텍스트는 다음과 같이 표시됩니다.\",\n        \"local_overrides_cleared\": \"장치별 설정이 삭제됨\",\n        \"local_overrides_description\": \"이 장치에는 글로벌 기본값과 다른 글 뷰어 설정이 있습니다.\",\n        \"clear_defaults\": \"모든 기본값 삭제\",\n        \"description\": \"글 뷰어의 기본 텍스트 설정을 구성합니다. 이 설정은 모든 장치에서 동기화됩니다.\",\n        \"defaults_cleared\": \"글 뷰어 기본값이 삭제됨\",\n        \"save_hint\": \"이 기기 설정만 저장하거나 모든 기기에서 동기화\",\n        \"save_as_default\": \"기본값으로 저장\",\n        \"save_to_device\": \"이 기기\",\n        \"sans\": \"산세리프\",\n        \"tooltip_preview_and_local\": \"저장되지 않은 미리 보기 변경 사항, 기기 설정이 전역 설정과 다름\",\n        \"adjust_hint\": \"위에 설정을 조정하여 변경 사항 미리 보기\"\n      },\n      \"avatar\": {\n        \"upload\": \"아바타 업로드\",\n        \"change\": \"아바타 변경\",\n        \"remove_confirm_title\": \"아바타를 삭제하겠습니까?\",\n        \"updated\": \"아바타 업데이트 완료\",\n        \"removed\": \"아바타 삭제 완료\",\n        \"description\": \"아바타로 사용할 정사각형 이미지를 업로드하세요.\",\n        \"remove_confirm_description\": \"이렇게 하면 현재 프로필 사진이 삭제됩니다.\",\n        \"title\": \"프로필 사진\",\n        \"remove\": \"아바타 삭제\"\n      }\n    },\n    \"ai\": {\n      \"ai_settings\": \"AI 설정\",\n      \"tagging_rule_description\": \"여기에 추가하는 프롬프트는 태그 생성 중에 모델에 규칙으로 포함됩니다. 프롬프트 미리보기 섹션에서 최종 프롬프트를 볼 수 있습니다.\",\n      \"tagging_rules\": \"태깅 규칙\",\n      \"prompt_preview\": \"프롬프트 미리보기\",\n      \"text_prompt\": \"텍스트 프롬프트\",\n      \"images_prompt\": \"이미지 프롬프트\",\n      \"summarization_prompt\": \"요약 프롬프트\",\n      \"all_tagging\": \"모든 태깅\",\n      \"text_tagging\": \"텍스트 태깅\",\n      \"image_tagging\": \"이미지 태깅\",\n      \"summarization\": \"요약\",\n      \"tag_style\": \"태그 스타일\",\n      \"auto_summarization_description\": \"AI를 사용하여 책갈피에 대한 요약을 자동으로 생성합니다.\",\n      \"auto_tagging\": \"자동 태그 지정\",\n      \"titlecase_spaces\": \"공백을 넣은 제목 케이스\",\n      \"lowercase_underscores\": \"밑줄을 넣은 소문자\",\n      \"inference_language\": \"추론 언어\",\n      \"titlecase_hyphens\": \"하이픈을 넣은 제목 케이스\",\n      \"lowercase_hyphens\": \"하이픈을 넣은 소문자\",\n      \"lowercase_spaces\": \"공백을 넣은 소문자\",\n      \"inference_language_description\": \"AI가 생성한 태그 및 요약에 사용할 언어를 선택합니다.\",\n      \"tag_style_description\": \"자동 생성 태그 형식을 선택하세요.\",\n      \"auto_tagging_description\": \"AI를 사용하여 책갈피에 대한 태그를 자동으로 생성합니다.\",\n      \"camelCase\": \"camelCase\",\n      \"auto_summarization\": \"자동 요약\",\n      \"no_preference\": \"기본 설정 없음\",\n      \"curated_tags\": \"선별된 태그\",\n      \"curated_tags_description\": \"선택적으로 AI 태그 지정을 이 목록의 태그만 사용하도록 제한합니다. 태그를 선택하지 않으면 AI가 자유롭게 태그를 생성합니다.\",\n      \"curated_tags_updated\": \"선별된 태그가 성공적으로 업데이트되었습니다!\",\n      \"curated_tags_update_failed\": \"선별된 태그 업데이트에 실패했습니다\"\n    },\n    \"feeds\": {\n      \"add_a_subscription\": \"구독 추가\",\n      \"rss_subscriptions\": \"RSS 구독\",\n      \"feed_enabled\": \"RSS 피드 활성화됨\",\n      \"feed_disabled\": \"RSS 피드 비활성화됨\"\n    },\n    \"import\": {\n      \"import_bookmarks_from_linkwarden_export\": \"Linkwarden 내보내기에서 북마크 가져오기\",\n      \"import_export\": \"가져오기 / 내보내기\",\n      \"import_export_bookmarks\": \"북마크 가져오기 / 내보내기\",\n      \"import_bookmarks_from_html_file\": \"HTML 파일에서 북마크 가져오기\",\n      \"import_bookmarks_from_pocket_export\": \"Pocket 내보내기에서 북마크 가져오기\",\n      \"import_bookmarks_from_matter_export\": \"Matter 내보내기에서 북마크 가져오기\",\n      \"import_bookmarks_from_omnivore_export\": \"Omnivore 내보내기에서 북마크 가져오기\",\n      \"import_bookmarks_from_karakeep_export\": \"Karakeep 내보내기에서 북마크 가져오기\",\n      \"export_links_and_notes\": \"링크와 주석 내보내기\",\n      \"imported_bookmarks\": \"가져온 북마크\",\n      \"import_bookmarks_from_tab_session_manager_export\": \"탭 세션 관리자에서 북마크 가져오기\",\n      \"import_bookmarks_from_mymind_export\": \"마인드 내보내기에서 북마크 가져오기\",\n      \"import_bookmarks_from_instapaper_export\": \"Instapaper 내보내기에서 북마크 가져오기\"\n    },\n    \"back_to_app\": \"앱으로 돌아가기\",\n    \"user_settings\": \"사용자 설정\",\n    \"broken_links\": {\n      \"broken_links\": \"손상된 링크\",\n      \"last_crawled_at\": \"마지막 가져온 시간\",\n      \"crawling_status\": \"가져오기 상태\",\n      \"crawling_failed\": \"가져오기 실패함\"\n    },\n    \"manage_assets\": {\n      \"no_assets\": \"아직 자산이 없습니다.\",\n      \"asset_type\": \"자산 종류\",\n      \"asset_link\": \"자산 링크\",\n      \"delete_asset\": \"자산 삭제\",\n      \"delete_asset_confirmation\": \"정말 이 자산을 삭제 하시겠습니까?\",\n      \"manage_assets\": \"자산 관리\",\n      \"bookmark_link\": \"북마크 링크\"\n    },\n    \"rules\": {\n      \"rules\": \"규칙 엔진\",\n      \"rule_name\": \"규칙 이름\",\n      \"description\": \"규칙을 사용해서 이벤트가 발생할 때 동작을 트리거할 수 있습니다.\",\n      \"ceate_rule\": \"규칙 만들기\",\n      \"edit_rule\": \"규칙 편집\",\n      \"actions_types\": {\n        \"favourite_bookmark\": \"즐겨찾는 북마크\",\n        \"archive_bookmark\": \"북마크 보관\",\n        \"add_tag\": \"태그 추가\",\n        \"remove_tag\": \"태그 제거\",\n        \"add_to_list\": \"목록에 추가\",\n        \"remove_from_list\": \"목록에서 제거\",\n        \"download_full_page_archive\": \"전체 페이지 아카이브 다운로드\"\n      },\n      \"events_types\": {\n        \"bookmark_added\": \"북마크가 추가됐습니다\",\n        \"tag_added\": \"이 태그가 북마크에 추가됐습니다\",\n        \"favourited\": \"북마크가 즐겨찾기 됐습니다\",\n        \"tag_removed\": \"이 태그가 북마크에서 제거됐습니다\",\n        \"added_to_list\": \"북마크가 이 목록에 추가됐습니다\",\n        \"archived\": \"북마크가 보관 처리됐습니다\",\n        \"removed_from_list\": \"북마크가 이 목록에서 제거됐습니다\"\n      },\n      \"save_rule\": \"규칙 저장\",\n      \"delete_rule\": \"규칙 삭제\",\n      \"delete_rule_confirmation\": \"이 규칙을 삭제하시겠어요?\",\n      \"enter_rule_name\": \"규칙 이름을 입력하세요\",\n      \"whenever\": \"언제든지...\",\n      \"if\": \"만약에...\",\n      \"describe_what_this_rule_does\": \"이 규칙이 하는 일을 설명해 주세요\",\n      \"rule_has_been_created\": \"규칙이 생성됐습니다!\",\n      \"rule_has_been_updated\": \"규칙이 업데이트됐습니다!\",\n      \"rule_has_been_deleted\": \"규칙이 삭제됐습니다!\",\n      \"no_rules_created_yet\": \"아직 규칙이 없습니다\",\n      \"create_your_first_rule\": \"첫 번째 규칙을 만들어 워크플로를 자동화해 보세요\",\n      \"conditions_types\": {\n        \"always\": \"항상\",\n        \"url_contains\": \"URL에 다음 내용이 포함됨\",\n        \"imported_from_feed\": \"피드에서 가져옴\",\n        \"bookmark_type_is\": \"북마크 유형은\",\n        \"has_tag\": \"태그 있음\",\n        \"is_favourited\": \"즐겨찾기 여부\",\n        \"is_archived\": \"보관됨\",\n        \"and\": \"다음 조건이 모두 참입니다\",\n        \"or\": \"다음 중 하나라도 참이면\",\n        \"url_does_not_contain\": \"URL에 다음이 포함되지 않음\",\n        \"title_contains\": \"제목에 다음 내용이 포함됨\",\n        \"title_does_not_contain\": \"제목에 다음 내용이 포함되지 않음\"\n      }\n    },\n    \"stats\": {\n      \"usage_statistics\": \"사용 통계\",\n      \"insights_description\": \"북마크 습관 및 컬렉션에 대한 통찰력\",\n      \"failed_to_load\": \"통계 로드 실패\",\n      \"overview\": {\n        \"total_bookmarks\": \"총 북마크 수\",\n        \"all_saved_items\": \"저장된 모든 항목\",\n        \"favorites\": \"즐겨찾기\",\n        \"starred_bookmarks\": \"별표 표시된 북마크\",\n        \"archived\": \"보관됨\",\n        \"archived_items\": \"보관된 항목\",\n        \"tags\": \"태그\",\n        \"unique_tags_created\": \"고유 태그 생성됨\",\n        \"lists\": \"목록\",\n        \"bookmark_collections\": \"북마크 컬렉션\",\n        \"highlights\": \"하이라이트\",\n        \"text_highlights\": \"텍스트 하이라이트\",\n        \"storage_used\": \"사용된 저장 공간\",\n        \"total_asset_storage\": \"총 자산 저장 공간\",\n        \"this_month\": \"이번 달\",\n        \"bookmarks_added\": \"북마크 추가됨\"\n      },\n      \"bookmark_types\": {\n        \"title\": \"북마크 유형\",\n        \"links\": \"링크\",\n        \"text_notes\": \"텍스트 노트\",\n        \"assets\": \"자산\"\n      },\n      \"recent_activity\": {\n        \"title\": \"최근 활동\",\n        \"this_week\": \"이번 주\",\n        \"this_month\": \"이번 달\",\n        \"this_year\": \"올해\"\n      },\n      \"top_domains\": {\n        \"title\": \"인기 도메인\",\n        \"no_domains_found\": \"도메인 없음\"\n      },\n      \"most_used_tags\": {\n        \"title\": \"가장 많이 사용된 태그\",\n        \"no_tags_found\": \"태그를 찾을 수 없음\"\n      },\n      \"activity_patterns\": {\n        \"activity_by_hour\": \"시간별 활동\",\n        \"activity_by_day\": \"일별 활동\"\n      },\n      \"storage_breakdown\": {\n        \"title\": \"저장 공간 분석\"\n      },\n      \"bookmark_sources\": {\n        \"title\": \"소스 위치\",\n        \"empty\": \"사용 가능한 원본 데이터 없음\"\n      }\n    },\n    \"subscription\": {\n      \"subscription\": \"구독\",\n      \"manage_subscription\": \"구독 및 결제 정보 관리\",\n      \"current_plan\": \"현재 플랜\",\n      \"billing_period\": \"결제 기간\",\n      \"paid_plan\": \"유료 플랜\",\n      \"unlock_bigger_quota\": \"더 큰 할당량을 확보하고 프로젝트를 지원하세요\",\n      \"subscribe_now\": \"지금 구독하기\",\n      \"manage_billing\": \"결제 관리\",\n      \"subscription_canceled\": \"구독이 취소되었으며 {{date}}에 종료됩니다. 언제든지 다시 구독할 수 있습니다.\",\n      \"usage_quotas\": \"사용량 및 할당량\",\n      \"track_usage\": \"현재 사용량을 플랜 제한과 비교하여 추적하세요\",\n      \"total_bookmarks_saved\": \"총 저장된 북마크 수\",\n      \"assets_file_storage\": \"에셋 및 파일 저장소\",\n      \"unlimited_usage\": \"무제한 사용\",\n      \"quota_limit_reached\": \"할당량 제한에 도달했습니다\",\n      \"approaching_quota_limit\": \"할당량 제한에 거의 도달했습니다\",\n      \"loading_usage\": \"사용량 정보 로딩 중...\",\n      \"free\": \"무료\",\n      \"paid\": \"유료\"\n    },\n    \"import_sessions\": {\n      \"title\": \"세션 가져오기\",\n      \"description\": \"대량 가져오기 세션을 보고 관리하세요. 세션은 책갈피를 가져올 때 자동으로 생성됩니다.\",\n      \"load_error\": \"가져오기 세션을 불러오지 못했습니다\",\n      \"no_sessions\": \"아직 가져오기 세션이 없습니다\",\n      \"no_sessions_detail\": \"책갈피를 가져오면 가져오기 세션이 자동으로 여기에 표시됩니다\",\n      \"created_at\": \"{{time}}에 생성됨\",\n      \"progress\": \"진행률\",\n      \"status\": {\n        \"pending\": \"대기 중\",\n        \"in_progress\": \"진행 중\",\n        \"completed\": \"완료됨\",\n        \"failed\": \"실패함\",\n        \"processing\": \"처리 중\",\n        \"staging\": \"준비 중\",\n        \"running\": \"실행 중\",\n        \"paused\": \"일시 중단됨\"\n      },\n      \"badges\": {\n        \"pending\": \"{{count}} 대기 중\",\n        \"processing\": \"{{count}} 처리 중\",\n        \"completed\": \"{{count}} 완료됨\",\n        \"failed\": \"{{count}} 실패함\"\n      },\n      \"imported_to\": \"가져온 위치:\",\n      \"view_list\": \"목록 보기\",\n      \"delete_dialog_title\": \"가져오기 세션 삭제\",\n      \"delete_dialog_description\": \"정말 \\\"{{name}}\\\"을(를) 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다. 책갈피 자체는 삭제되지 않습니다.\",\n      \"delete_session\": \"세션 삭제\",\n      \"pause_session\": \"일시 중단\",\n      \"resume_session\": \"재개\",\n      \"view_details\": \"세부 정보 보기\",\n      \"detail\": {\n        \"page_title\": \"세션 가져오기 정보\",\n        \"back_to_import\": \"가져오기로 돌아가기\",\n        \"filter_all\": \"전부\",\n        \"filter_accepted\": \"수락됨\",\n        \"filter_rejected\": \"거부됨\",\n        \"filter_duplicates\": \"중복\",\n        \"filter_pending\": \"보류 중\",\n        \"table_title\": \"제목/URL\",\n        \"table_type\": \"종류\",\n        \"table_result\": \"결과\",\n        \"table_reason\": \"이유\",\n        \"table_bookmark\": \"북마크\",\n        \"result_accepted\": \"수락됨\",\n        \"result_rejected\": \"거부됨\",\n        \"result_skipped_duplicate\": \"중복\",\n        \"result_pending\": \"보류 중\",\n        \"result_processing\": \"처리 중\",\n        \"no_results\": \"이 필터에 대한 결과가 없습니다.\",\n        \"view_bookmark\": \"북마크 보기\",\n        \"load_more\": \"더 불러오기\",\n        \"no_title\": \"제목 없음\"\n      }\n    },\n    \"backups\": {\n      \"backups\": \"백업\",\n      \"page_title\": \"백업\",\n      \"page_description\": \"즐겨찾기를 자동으로 백업하고 관리하세요. 백업 파일은 압축되어 안전하게 보관됩니다.\",\n      \"configuration\": {\n        \"title\": \"백업 구성\",\n        \"enable_automatic_backups\": \"자동 백업 사용\",\n        \"enable_automatic_backups_description\": \"즐겨찾기를 자동으로 백업합니다\",\n        \"backup_frequency\": \"백업 빈도\",\n        \"backup_frequency_description\": \"얼마나 자주 백업을 생성할까요?\",\n        \"retention_period\": \"보존 기간(일)\",\n        \"retention_period_description\": \"백업을 삭제하기 전 며칠 동안 보관할까요?\",\n        \"frequency\": {\n          \"daily\": \"매일\",\n          \"weekly\": \"매주\"\n        },\n        \"select_frequency\": \"빈도 선택\",\n        \"save_settings\": \"설정 저장\"\n      },\n      \"list\": {\n        \"title\": \"내 백업\",\n        \"create_backup_now\": \"지금 백업 만들기\",\n        \"no_backups\": \"아직 백업이 없어요. 자동 백업을 활성화하거나 수동으로 만드세요.\",\n        \"table\": {\n          \"created_at\": \"생성 날짜\",\n          \"bookmarks\": \"즐겨찾기\",\n          \"size\": \"크기\",\n          \"status\": \"상태\",\n          \"actions\": \"작업\"\n        },\n        \"status\": {\n          \"success\": \"성공\",\n          \"failed\": \"실패\",\n          \"pending\": \"대기 중\"\n        },\n        \"actions\": {\n          \"download_backup\": \"백업 내려받기\",\n          \"delete_backup\": \"백업 삭제\"\n        }\n      },\n      \"dialogs\": {\n        \"delete_backup_title\": \"백업 삭제할까요?\",\n        \"delete_backup_description\": \"이 백업을 삭제하시겠습니까? 이 작업은 실행 취소할 수 없습니다.\"\n      },\n      \"toasts\": {\n        \"backup_queued\": \"백업 작업이 대기열에 추가되었습니다! 곧 처리될 겁니다.\",\n        \"backup_deleted\": \"백업이 삭제되었습니다!\"\n      }\n    }\n  },\n  \"admin\": {\n    \"users_list\": {\n      \"users_list\": \"사용자 목록\",\n      \"create_user\": \"사용자 생성\",\n      \"change_role\": \"역할 변경\",\n      \"reset_password\": \"암호 초기화\",\n      \"delete_user\": \"사용자 삭제\",\n      \"num_bookmarks\": \"북마크 갯수\",\n      \"asset_sizes\": \"애셋 크기\",\n      \"local_user\": \"로컬 사용자\",\n      \"confirm_password\": \"암호 확인\",\n      \"delete_user_confirm_description\": \"진짜 \\\"{{name}}\\\" 유저를 삭제하시겠어요?\",\n      \"unlimited\": \"무제한\"\n    },\n    \"admin_settings\": \"관리자 설정\",\n    \"server_stats\": {\n      \"server_stats\": \"서버 상태\",\n      \"total_users\": \"총 사용자\",\n      \"total_bookmarks\": \"총 북마크\",\n      \"server_version\": \"서버 버전\"\n    },\n    \"background_jobs\": {\n      \"background_jobs\": \"백그라운드 작업\",\n      \"crawler_jobs\": \"가져오기 작업\",\n      \"indexing_jobs\": \"인덱싱 작업\",\n      \"inference_jobs\": \"추론 작업\",\n      \"tidy_assets_jobs\": \"정리 작업\",\n      \"job\": \"작업\",\n      \"queued\": \"큐에 추가됨\",\n      \"pending\": \"대기중\",\n      \"failed\": \"실패함\",\n      \"asset_preprocessing_jobs\": \"에셋 전처리 작업\",\n      \"feed_jobs\": \"RSS 피드 작업\",\n      \"video_jobs\": \"비디오 다운로드 작업\",\n      \"webhook_jobs\": \"웹훅 작업\",\n      \"monitor_and_manage\": \"백그라운드 작업 대기열 및 시스템 처리 작업 모니터링 및 관리\",\n      \"jobs\": {\n        \"crawler\": {\n          \"title\": \"크롤러 작업\",\n          \"description\": \"URL에서 웹 크롤링 및 콘텐츠 추출\"\n        },\n        \"inference\": {\n          \"title\": \"추론 작업\",\n          \"description\": \"AI 기반 콘텐츠 태깅 및 요약\"\n        },\n        \"indexing\": {\n          \"title\": \"색인 작업\",\n          \"description\": \"검색 색인 업데이트\"\n        },\n        \"asset_preprocessing\": {\n          \"title\": \"자산 전처리 작업\",\n          \"description\": \"이미지 및 문서 전처리 (스크린샷, 텍스트 추출 등)\"\n        },\n        \"tidy_assets\": {\n          \"title\": \"자산 정리 작업\",\n          \"description\": \"자산 정리 및 저장소 최적화\"\n        },\n        \"video\": {\n          \"title\": \"비디오 다운로드 작업\",\n          \"description\": \"비디오 추출 및 다운로드\"\n        },\n        \"webhook\": {\n          \"title\": \"Webhook 작업\",\n          \"description\": \"외부 웹후크 알림\"\n        },\n        \"feed\": {\n          \"title\": \"RSS 피드 작업\",\n          \"description\": \"RSS 피드 처리 및 콘텐츠 업데이트\"\n        },\n        \"admin_maintenance\": {\n          \"title\": \"관리자 유지 관리 작업\",\n          \"description\": \"관리 정리 및 자산 유지 관리\"\n        }\n      },\n      \"active\": \"활성\",\n      \"available_actions\": \"사용 가능한 액션\",\n      \"status\": {\n        \"title\": \"작업 상태 이해\",\n        \"queued\": {\n          \"title\": \"대기 중\",\n          \"description\": \"처리 대기 중인 작업들입니다. 리소스가 사용 가능해지면 자동으로 시작됩니다.\"\n        },\n        \"unprocessed\": {\n          \"title\": \"미처리됨\",\n          \"description\": \"처리되지 않은 북마크입니다. 대부분 이미 처리 대기열에 추가되었을 가능성이 높지만, 그렇지 않은 경우 수동으로 다시 대기열에 추가해야 할 수도 있습니다.\"\n        },\n        \"failed\": {\n          \"title\": \"실패함\",\n          \"description\": \"처리 중 오류가 발생한 북마크입니다. 수동으로 확인해야 할 수 있습니다.\"\n        }\n      },\n      \"actions\": {\n        \"recrawl_failed_links_only\": \"실패한 링크만 다시 크롤링\",\n        \"recrawl_all_links\": \"모든 링크 다시 크롤링\",\n        \"without_inference\": \"추론 없이\",\n        \"regenerate_ai_tags_for_failed_bookmarks_only\": \"실패한 북마크만 AI 태그 다시 생성\",\n        \"regenerate_ai_tags_for_all_bookmarks\": \"모든 북마크 AI 태그 다시 생성\",\n        \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"실패한 북마크만 AI 요약 다시 생성\",\n        \"regenerate_ai_summaries_for_all_bookmarks\": \"모든 북마크 AI 요약 다시 생성\",\n        \"reindex_all_bookmarks\": \"모든 북마크 다시 색인하기\",\n        \"clean_assets\": \"연결되지 않은 에셋 정리 및 메타데이터 재동기화\",\n        \"reprocess_assets_fix_mode\": \"미처리된 에셋 다시 처리\",\n        \"migrate_large_link_html_content\": \"큰 인라인 HTML 콘텐츠를 에셋으로 옮기기\",\n        \"recrawl_pending_links_only\": \"보류 중인 링크만 다시 크롤링\",\n        \"regenerate_ai_tags_for_pending_bookmarks_only\": \"보류 중인 북마크에 대해서만 AI 태그 다시 생성\",\n        \"regenerate_ai_summaries_for_pending_bookmarks_only\": \"보류 중인 북마크에 대해서만 AI 요약 다시 생성\"\n      }\n    },\n    \"actions\": {\n      \"recrawl_failed_links_only\": \"실패한 링크만 다시 가져오기\",\n      \"recrawl_all_links\": \"모든 링크 다시 가져오기\",\n      \"without_inference\": \"추론하지 않기\",\n      \"regenerate_ai_tags_for_failed_bookmarks_only\": \"실패한 북마크만 AI 태그 재생성\",\n      \"regenerate_ai_tags_for_all_bookmarks\": \"모든 북마크 AI 태그 재생성\",\n      \"reindex_all_bookmarks\": \"모든 북마크 재 인덱싱\",\n      \"compact_assets\": \"애셋 압축\",\n      \"reprocess_assets_fix_mode\": \"재작업된 자산(수정 모드)\",\n      \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"실패한 북마크에 대해서만 AI 요약 다시 생성\",\n      \"regenerate_ai_summaries_for_all_bookmarks\": \"모든 북마크에 대해 AI 요약 다시 생성\"\n    },\n    \"service_connections\": {\n      \"title\": \"서비스 연결\",\n      \"description\": \"외부 시스템 종속성의 상태 및 연결 상태를 모니터링합니다.\",\n      \"search_engine\": \"검색 엔진\",\n      \"browser\": \"브라우저\",\n      \"queue_system\": \"대기열 시스템\",\n      \"status\": {\n        \"not_configured\": \"구성되지 않음\",\n        \"connected\": \"연결됨\",\n        \"disconnected\": \"연결 끊김\"\n      }\n    },\n    \"admin_tools\": {\n      \"admin_tools\": \"관리자 도구\",\n      \"bookmark_debugger\": \"북마크 디버거\",\n      \"bookmark_id\": \"북마크 ID\",\n      \"bookmark_id_placeholder\": \"북마크 ID 입력\",\n      \"lookup\": \"찾아보기\",\n      \"debug_info\": \"디버그 정보\",\n      \"basic_info\": \"기본 정보\",\n      \"status\": \"상태\",\n      \"content\": \"콘텐츠\",\n      \"html_preview\": \"HTML 미리 보기 (처음 1000자)\",\n      \"summary\": \"요약\",\n      \"url\": \"URL\",\n      \"source_url\": \"소스 URL\",\n      \"asset_type\": \"자산 유형\",\n      \"file_name\": \"파일 이름\",\n      \"owner_user_id\": \"소유자 사용자 ID\",\n      \"tagging_status\": \"태깅 상태\",\n      \"summarization_status\": \"요약 상태\",\n      \"crawl_status\": \"크롤링 상태\",\n      \"crawl_status_code\": \"HTTP 상태 코드\",\n      \"crawled_at\": \"크롤링된 시간\",\n      \"recrawl\": \"다시 크롤링\",\n      \"reindex\": \"다시 색인\",\n      \"retag\": \"다시 태그하기\",\n      \"resummarize\": \"다시 요약하기\",\n      \"bookmark_not_found\": \"북마크를 찾을 수 없음\",\n      \"action_success\": \"작업이 성공적으로 완료됨\",\n      \"action_failed\": \"작업 실패\",\n      \"recrawl_queued\": \"다시 크롤링 작업이 대기열에 추가됨\",\n      \"reindex_queued\": \"다시 색인 작업이 대기열에 추가됨\",\n      \"retag_queued\": \"다시 태그 작업이 대기열에 추가됨\",\n      \"resummarize_queued\": \"다시 요약 작업이 대기열에 추가됨\",\n      \"view\": \"보기\",\n      \"fetch_error\": \"북마크 가져오기 오류\"\n    }\n  },\n  \"lists\": {\n    \"smart_list\": \"스마트 목록\",\n    \"search_query_help\": \"검색 질의어에 대해 더 알아보기.\",\n    \"all_lists\": \"모든 목록\",\n    \"favourites\": \"즐겨찾기\",\n    \"new_list\": \"새로운 목록\",\n    \"edit_list\": \"목록 수정\",\n    \"new_nested_list\": \"새로운 하위 목록\",\n    \"parent_list\": \"상위 목록\",\n    \"no_parent\": \"상위 목록 없음\",\n    \"list_type\": \"목록 종류\",\n    \"manual_list\": \"수동 목록\",\n    \"search_query\": \"검색 쿼리\",\n    \"merge_list\": \"목록 병합\",\n    \"destination_list\": \"대상 목록\",\n    \"delete_after_merge\": \"병합 후 원본 목록 삭제\",\n    \"no_destination\": \"대상 없음\",\n    \"description\": \"설명 (선택 사항)\",\n    \"rss\": {\n      \"title\": \"RSS 피드\",\n      \"description\": \"이 목록에 대한 RSS 피드 활성화\",\n      \"feed_url\": \"RSS 피드 URL\"\n    },\n    \"public_list\": {\n      \"description\": \"다른 사람이 이 목록을 볼 수 있도록 허용\",\n      \"share_link\": \"링크 공유\",\n      \"title\": \"공개 목록\"\n    },\n    \"share_list\": \"목록 공유\",\n    \"delete_list\": {\n      \"title\": \"목록 삭제\",\n      \"description\": \"목록을 삭제해도 해당 목록에 있는 북마크는 삭제되지 않아.\",\n      \"delete_children\": \"자식 목록 삭제 (재귀적으로)\",\n      \"delete_children_description\": \"선택하지 않으면 모든 직계 자식 목록이 루트 목록이 됨\"\n    },\n    \"shared\": \"공유됨\",\n    \"collaborators\": {\n      \"manage\": \"협력자 관리\",\n      \"view\": \"협력자 보기\",\n      \"collaborators\": \"협력자\",\n      \"add\": \"협력자 추가\",\n      \"current\": \"현재 협력자\",\n      \"enter_email\": \"이메일 주소 입력\",\n      \"please_enter_email\": \"이메일 주소를 입력해 주세요\",\n      \"added_successfully\": \"협력자가 성공적으로 추가되었습니다\",\n      \"failed_to_add\": \"협력자 추가에 실패했습니다\",\n      \"removed\": \"협력자가 제거되었습니다\",\n      \"failed_to_remove\": \"협력자 제거에 실패했습니다\",\n      \"role_updated\": \"역할이 업데이트되었습니다\",\n      \"failed_to_update_role\": \"역할 업데이트에 실패했습니다\",\n      \"viewer\": \"뷰어\",\n      \"editor\": \"편집자\",\n      \"owner\": \"소유자\",\n      \"viewer_description\": \"목록에서 북마크를 볼 수 있음\",\n      \"editor_description\": \"북마크를 추가 및 제거할 수 있음\",\n      \"no_collaborators\": \"아직 협력자가 없습니다. 협업을 시작하려면 누군가를 추가하세요!\",\n      \"no_collaborators_readonly\": \"요 리스트에는 협업자가 없어.\",\n      \"people_with_access\": \"요 리스트에 접근할 수 있는 사람들\",\n      \"add_or_remove\": \"요 리스트에 접근할 수 있는 사람들을 추가/삭제해 줘\",\n      \"invitation_sent\": \"초대장 전송 완료\",\n      \"invitation_revoked\": \"초대 취소됨\",\n      \"failed_to_revoke\": \"초대 취소 실패\",\n      \"pending\": \"대기 중\",\n      \"revoke\": \"취소\",\n      \"declined\": \"거절됨\"\n    },\n    \"leave_list\": {\n      \"title\": \"리스트에서 나가기\",\n      \"confirm_message\": \"진짜 {{icon}} {{name}}에서 나가려고?\",\n      \"warning\": \"이제 이 리스트의 북마크를 보거나 접근할 수 없어. 리스트 주인이 필요하면 다시 추가해 줄 수 있어.\",\n      \"action\": \"리스트에서 나가기\",\n      \"success\": \"\\\"{icon}} {{name}}\\\"에서 나왔어.\"\n    },\n    \"invitations\": {\n      \"pending\": \"초대 대기 중\",\n      \"description\": \"목록 협업 초대를 검토하고 응답하기\",\n      \"invited_by\": \"초대한 사람\",\n      \"accept\": \"수락\",\n      \"decline\": \"거절\",\n      \"accepted\": \"초대 수락됨\",\n      \"declined\": \"초대 거절됨\",\n      \"failed_to_accept\": \"초대 수락 실패\",\n      \"failed_to_decline\": \"초대 거절 실패\"\n    },\n    \"shared_lists\": \"공유 목록\"\n  },\n  \"options\": {\n    \"light_mode\": \"라이트 모드\",\n    \"dark_mode\": \"다크 모드\",\n    \"apps_extensions\": \"앱 & 확장 기능\",\n    \"documentation\": \"문서\",\n    \"follow_us_on_x\": \"X에서 팔로우하세요\"\n  },\n  \"highlights\": {\n    \"no_highlights\": \"강조된 부분이 없습니다.\"\n  },\n  \"banners\": {\n    \"no_bookmarks\": {\n      \"title\": \"아직 북마크가 없어요\",\n      \"description\": \"나중에 빠르게 액세스할 수 있도록 흥미로운 기사, 링크 및 페이지를 저장하세요.\"\n    }\n  },\n  \"bookmark_editor\": {\n    \"title\": \"북마크 편집\",\n    \"subtitle\": \"북마크 세부 정보를 변경하세요. 완료되면 저장을 클릭하세요.\",\n    \"author\": \"작성자\",\n    \"publisher\": \"게시자\",\n    \"date_published\": \"게시 날짜\",\n    \"pick_a_date\": \"날짜 선택\",\n    \"save_changes\": \"변경 사항 저장\",\n    \"extracted_content\": \"추출된 콘텐츠\"\n  },\n  \"view_options\": {\n    \"title\": \"옵션 보기\",\n    \"layout\": \"레이아웃\",\n    \"columns\": \"열\",\n    \"display_options\": \"표시 옵션\",\n    \"show_note_previews\": \"노트 표시\",\n    \"show_tags\": \"태그 표시\",\n    \"show_title\": \"제목 표시\",\n    \"image_options\": \"이미지 옵션\",\n    \"image_fit_cover\": \"채우기 (맞춤)\",\n    \"image_fit_contain\": \"맞춤 (맞춤)\"\n  },\n  \"version\": {\n    \"new_release_available\": \"새로운 릴리스 정보를 사용할 수 있습니다.\",\n    \"whats_new_title\": \"v{{version}}의 새로운 기능\",\n    \"release_notes_description\": \"GitHub 릴리스 정보에서 가져온 최신 업데이트입니다.\",\n    \"loading_release_notes\": \"릴리스 정보 로딩 중…\",\n    \"unable_to_load_release_notes\": \"지금은 릴리스 정보를 로드할 수 없습니다. 나중에 다시 시도해 주세요.\",\n    \"no_release_notes\": \"이 버전에 대한 릴리스 정보가 게시되지 않았습니다.\",\n    \"release_notes_synced\": \"릴리스 정보는 GitHub에서 동기화됩니다.\",\n    \"view_on_github\": \"GitHub에서 보기\"\n  },\n  \"wrapped\": {\n    \"title\": \"나만의 {{year}} 랩\",\n    \"subtitle\": \"카라킵에서 보낸 1년\",\n    \"banner\": {\n      \"title\": \"나만의 2025 랩이 준비되었습니다!\",\n      \"description\": \"나만의 책갈피 한 해 보기\",\n      \"view_now\": \"지금 보기\"\n    },\n    \"button\": \"2025 랩\",\n    \"loading\": \"나만의 랩 로드 중...\",\n    \"failed_to_load\": \"랩 통계 로드 실패\",\n    \"sections\": {\n      \"total_saves\": {\n        \"prefix\": \"저장한 항목\",\n        \"suffix\": \"올해의 아이템\",\n        \"suffix_singular\": \"올해의 아이템\"\n      },\n      \"first_bookmark\": {\n        \"title\": \"여정이 시작되었습니다\",\n        \"description\": \"{{year}}년 첫 번째 저장:\"\n      },\n      \"top_domains\": \"인기 사이트\",\n      \"top_tags\": \"인기 태그\",\n      \"monthly_activity\": \"올해의 저장\",\n      \"most_active_day\": \"가장 활동적인 날\",\n      \"peak_times\": {\n        \"title\": \"저장 시간\",\n        \"peak_hour\": \"최고 시간\",\n        \"peak_day\": \"최고의 날\"\n      },\n      \"how_you_save\": \"저장 방법\",\n      \"what_you_saved\": \"저장한 항목\",\n      \"summary\": {\n        \"favorites\": \"즐겨찾기\",\n        \"tags_created\": \"생성된 태그\",\n        \"highlights\": \"하이라이트\"\n      },\n      \"types\": {\n        \"links\": \"링크\",\n        \"notes\": \"노트\",\n        \"assets\": \"자산\"\n      }\n    },\n    \"footer\": \"Karakeep으로 만들었습니다\",\n    \"share\": \"공유\",\n    \"download\": \"다운로드\",\n    \"close\": \"닫기\",\n    \"generating\": \"생성 중...\"\n  }\n}\n"
  },
  {
    "path": "apps/web/lib/i18n/locales/nb_NO/translation.json",
    "content": "{\n  \"common\": {\n    \"home\": \"Hjem\",\n    \"name\": \"Navn\",\n    \"roles\": {\n      \"user\": \"Bruker\",\n      \"admin\": \"Administrator\"\n    },\n    \"something_went_wrong\": \"Noe gikk galt\",\n    \"email\": \"E-post\",\n    \"search\": \"Søk\",\n    \"password\": \"Passord\",\n    \"url\": \"URL\",\n    \"action\": \"Handling\",\n    \"actions\": \"Handlinger\",\n    \"created_at\": \"Opprettet\",\n    \"key\": \"Nøkkel\",\n    \"role\": \"Rolle\",\n    \"experimental\": \"Eksperimentell\",\n    \"bookmark_types\": {\n      \"media\": \"Media\",\n      \"title\": \"Bokmerketype\",\n      \"link\": \"Lenke\",\n      \"text\": \"Tekst\"\n    },\n    \"type\": \"Type\",\n    \"size\": \"Størrelse\",\n    \"tags\": \"Merker\",\n    \"note\": \"Merknad\",\n    \"attachments\": \"Vedlegg\",\n    \"highlights\": \"Høydepunkter\",\n    \"source\": \"Kilde\",\n    \"screenshot\": \"Skjermbilde\",\n    \"video\": \"Video\",\n    \"archive\": \"Arkiv\",\n    \"updated_at\": \"Oppdatert\",\n    \"description\": \"Beskrivelse\",\n    \"summary\": \"Sammendrag\",\n    \"title\": \"Tittel\",\n    \"quota\": \"Kvote\",\n    \"bookmarks\": \"Bokmerker\",\n    \"storage\": \"Lagring\",\n    \"pdf\": \"Arkivert PDF\",\n    \"default\": \"Standard\",\n    \"id\": \"ID\",\n    \"last_used\": \"Sist brukt\"\n  },\n  \"admin\": {\n    \"users_list\": {\n      \"delete_user\": \"Slett bruker\",\n      \"num_bookmarks\": \"Antall bokmerker\",\n      \"create_user\": \"Opprett bruker\",\n      \"change_role\": \"Endre rolle\",\n      \"reset_password\": \"Tilbakestill passord\",\n      \"asset_sizes\": \"Elementstørrelser\",\n      \"confirm_password\": \"Bekreft passord\",\n      \"users_list\": \"Brukerliste\",\n      \"local_user\": \"Lokal bruker\",\n      \"delete_user_confirm_description\": \"Er du sikker på at du vil slette brukeren «{{name}}»?\",\n      \"unlimited\": \"Ubegrenset\"\n    },\n    \"server_stats\": {\n      \"server_version\": \"Serverversjon\",\n      \"server_stats\": \"Serverstatistikk\",\n      \"total_users\": \"Totalt antall brukere\",\n      \"total_bookmarks\": \"Totalt antall bokmerker\"\n    },\n    \"background_jobs\": {\n      \"background_jobs\": \"Bakgrunnsjobber\",\n      \"asset_preprocessing_jobs\": \"Jobber for forbehandling av aktiva\",\n      \"video_jobs\": \"Nedlastingsjobber for video\",\n      \"webhook_jobs\": \"Webhook-jobber\",\n      \"feed_jobs\": \"RSS-feed-jobber\",\n      \"pending\": \"Venter\",\n      \"failed\": \"Mislyktes\",\n      \"crawler_jobs\": \"Crawler-jobber\",\n      \"indexing_jobs\": \"Indekseringsjobber\",\n      \"inference_jobs\": \"Inferensjobber\",\n      \"tidy_assets_jobs\": \"Rydd opp i ressursjobber\",\n      \"job\": \"Jobb\",\n      \"queued\": \"I kø\",\n      \"jobs\": {\n        \"crawler\": {\n          \"title\": \"Crawler-jobber\",\n          \"description\": \"Web-crawling og innholdsekstrahering fra URL-er\"\n        },\n        \"inference\": {\n          \"title\": \"Inferens-jobber\",\n          \"description\": \"AI-drevet merking og oppsummering av innhold\"\n        },\n        \"indexing\": {\n          \"title\": \"Indekseringsjobber\",\n          \"description\": \"Søkeindeksoppdateringer\"\n        },\n        \"asset_preprocessing\": {\n          \"title\": \"Jobber for forhåndsbehandling av ressurser\",\n          \"description\": \"Forhåndsbehandling av bilder og dokumenter (skjermbilder, tekstuttrekk osv.)\"\n        },\n        \"tidy_assets\": {\n          \"title\": \"Ryddige ressursjobber\",\n          \"description\": \"Opprydding av ressurser og lagringsoptimalisering\"\n        },\n        \"video\": {\n          \"title\": \"Nedlastingsjobber for video\",\n          \"description\": \"Videoekstrahering og nedlasting\"\n        },\n        \"webhook\": {\n          \"title\": \"Webhook-jobber\",\n          \"description\": \"Eksterne webhook-varsler\"\n        },\n        \"feed\": {\n          \"title\": \"RSS-feed-jobber\",\n          \"description\": \"RSS-feed-behandling og innholdsoppdateringer\"\n        },\n        \"admin_maintenance\": {\n          \"title\": \"Admin-vedlikeholdsjobber\",\n          \"description\": \"Administrativ opprydding og ressursvedlikehold\"\n        }\n      },\n      \"monitor_and_manage\": \"Overvåk og administrer bakgrunnsjobbkøer og systembehandlingsoppgaver\",\n      \"active\": \"Aktiv\",\n      \"available_actions\": \"Tilgjengelige handlinger\",\n      \"status\": {\n        \"title\": \"Forstå jobbtilstander\",\n        \"queued\": {\n          \"title\": \"I kø\",\n          \"description\": \"Jobber som venter på tur for å bli behandlet. De starter automatisk når ressurser er tilgjengelige.\"\n        },\n        \"unprocessed\": {\n          \"title\": \"Ubehandlet\",\n          \"description\": \"Bokmerker som ikke er behandlet ennå. De er mest sannsynlig allerede satt i kø for behandling, hvis ikke må du kanskje sette dem i kø manuelt på nytt.\"\n        },\n        \"failed\": {\n          \"title\": \"Mislyktes\",\n          \"description\": \"Bokmerker som støtte på feil under behandling. Disse kan trenge manuell oppmerksomhet.\"\n        }\n      },\n      \"actions\": {\n        \"recrawl_failed_links_only\": \"Gjennomgå bare mislykkede lenker på nytt\",\n        \"recrawl_all_links\": \"Gjennomgå alle lenker på nytt\",\n        \"without_inference\": \"Uten Inferences\",\n        \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Gjenskap AI-tagger bare for mislykkede bokmerker\",\n        \"regenerate_ai_tags_for_all_bookmarks\": \"Gjenskap AI-tagger for alle bokmerker\",\n        \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Gjenskap AI-sammendrag bare for mislykkede bokmerker\",\n        \"regenerate_ai_summaries_for_all_bookmarks\": \"Gjenskap AI-sammendrag for alle bokmerker\",\n        \"reindex_all_bookmarks\": \"Reindekser alle bokmerker\",\n        \"clean_assets\": \"Fjern løse filer og synkroniser metadata på nytt\",\n        \"reprocess_assets_fix_mode\": \"Behandle aktiva som ikke er behandlet, på nytt\",\n        \"migrate_large_link_html_content\": \"Flytt stort HTML-innhold på nettet til aktiva\",\n        \"recrawl_pending_links_only\": \"Bare skann ventende lenker på nytt\",\n        \"regenerate_ai_tags_for_pending_bookmarks_only\": \"Bare gjenopprett AI-tagger for ventende bokmerker\",\n        \"regenerate_ai_summaries_for_pending_bookmarks_only\": \"Bare gjenopprett AI-sammendrag for ventende bokmerker\"\n      }\n    },\n    \"actions\": {\n      \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Regenerer AI-tagger bare for mislykkede bokmerker\",\n      \"recrawl_failed_links_only\": \"Bare recrawl mislykkede lenker\",\n      \"recrawl_all_links\": \"Gjennomsøk alle lenker på nytt\",\n      \"without_inference\": \"Uten inferens\",\n      \"regenerate_ai_tags_for_all_bookmarks\": \"Gjenskap AI-tagger for alle bokmerker\",\n      \"reindex_all_bookmarks\": \"Reindekser alle bokmerker\",\n      \"compact_assets\": \"Komprimer ressurser\",\n      \"reprocess_assets_fix_mode\": \"Behandle aktiva på nytt (Fiks-modus)\",\n      \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Gjenopprett AI-sammendrag kun for mislykkede bokmerker\",\n      \"regenerate_ai_summaries_for_all_bookmarks\": \"Gjenopprett AI-sammendrag for alle bokmerker\"\n    },\n    \"admin_settings\": \"Admin-innstillinger\",\n    \"service_connections\": {\n      \"title\": \"Tilkoblinger for tjenester\",\n      \"description\": \"Overvåk helsen og tilkoblingen til eksterne systemavhengigheter\",\n      \"search_engine\": \"Søkemotor\",\n      \"browser\": \"Nettleser\",\n      \"queue_system\": \"Køsystem\",\n      \"status\": {\n        \"not_configured\": \"Ikke konfigurert\",\n        \"connected\": \"Tilkoblet\",\n        \"disconnected\": \"Frakoblet\"\n      }\n    },\n    \"admin_tools\": {\n      \"admin_tools\": \"Admin-verktøy\",\n      \"bookmark_debugger\": \"Feilsøking av bokmerker\",\n      \"bookmark_id\": \"Bokmerke-ID\",\n      \"bookmark_id_placeholder\": \"Skriv inn bokmerke-ID\",\n      \"lookup\": \"Slå opp\",\n      \"debug_info\": \"Feilsøkings-informasjon\",\n      \"basic_info\": \"Grunnleggende informasjon\",\n      \"status\": \"Status\",\n      \"content\": \"Innhold\",\n      \"html_preview\": \"HTML-forhåndsvisning (første 1000 tegn)\",\n      \"summary\": \"Sammendrag\",\n      \"url\": \"URL\",\n      \"source_url\": \"Kilde-URL\",\n      \"asset_type\": \"Ressurstype\",\n      \"file_name\": \"Filnavn\",\n      \"owner_user_id\": \"Eier-bruker-ID\",\n      \"tagging_status\": \"Merkestatus\",\n      \"summarization_status\": \"Oppsummeringsstatus\",\n      \"crawl_status\": \"Gjennomsøkingsstatus\",\n      \"crawl_status_code\": \"HTTP-statuskode\",\n      \"crawled_at\": \"Crawlet på\",\n      \"recrawl\": \"Recrawl\",\n      \"reindex\": \"Reindekser\",\n      \"retag\": \"Retagg\",\n      \"resummarize\": \"Oppsummer på nytt\",\n      \"bookmark_not_found\": \"Fant ikke bokmerke\",\n      \"action_success\": \"Handling fullført\",\n      \"action_failed\": \"Handling feilet\",\n      \"recrawl_queued\": \"Recrawl-jobb er lagt i kø\",\n      \"reindex_queued\": \"Reindekseringsjobb er lagt i kø\",\n      \"retag_queued\": \"Retaggingsjobb er lagt i kø\",\n      \"resummarize_queued\": \"Oppsummeringsjobb er lagt i kø\",\n      \"view\": \"Visning\",\n      \"fetch_error\": \"Feil ved henting av bokmerke\"\n    }\n  },\n  \"actions\": {\n    \"unselect_all\": \"Fjern alle valg\",\n    \"remove_from_list\": \"Fjern fra liste\",\n    \"sort\": {\n      \"title\": \"Sorter\",\n      \"newest_first\": \"Nyeste først\",\n      \"oldest_first\": \"Eldste først\",\n      \"relevant_first\": \"Mest relevant først\"\n    },\n    \"cancel\": \"Avbryt\",\n    \"change_layout\": \"Endre layout\",\n    \"archive\": \"Arkiv\",\n    \"unarchive\": \"Av-arkiver\",\n    \"unfavorite\": \"Fjern fra favoritter\",\n    \"delete\": \"Slett\",\n    \"refresh\": \"Oppdater\",\n    \"recrawl\": \"Gjennomsøk på nytt\",\n    \"download_full_page_archive\": \"Last ned fullsidearkiv\",\n    \"edit_tags\": \"Rediger tagger\",\n    \"add_to_list\": \"Legg til i liste\",\n    \"select_all\": \"Velg alle\",\n    \"copy_link\": \"Kopier lenke\",\n    \"close_bulk_edit\": \"Lukk masseendring\",\n    \"bulk_edit\": \"Masseendring\",\n    \"manage_lists\": \"Administrer lister\",\n    \"save\": \"Lagre\",\n    \"add\": \"Legg til\",\n    \"edit\": \"Rediger\",\n    \"create\": \"Opprett\",\n    \"fetch_now\": \"Hent nå\",\n    \"summarize_with_ai\": \"Oppsummer med AI\",\n    \"edit_title\": \"Rediger tittel\",\n    \"sign_out\": \"Logg ut\",\n    \"close\": \"Lukk\",\n    \"merge\": \"Slå sammen\",\n    \"apply_all\": \"Bruk alle\",\n    \"ignore\": \"Ignorer\",\n    \"favorite\": \"Favoritt\",\n    \"open_editor\": \"Åpne redigeringsprogram\",\n    \"toggle_show_archived\": \"Vis arkiverte\",\n    \"confirm\": \"Bekreft\",\n    \"regenerate\": \"Regenerer\",\n    \"load_more\": \"Last inn mer\",\n    \"edit_notes\": \"Rediger notater\",\n    \"preserve_as_pdf\": \"Bevar som PDF\",\n    \"offline_copies\": \"Offline kopier\",\n    \"preserve_offline_archive\": \"Bevar offline-arkiv\",\n    \"download_full_page_archive_file\": \"Last ned arkivfil\",\n    \"download_pdf_file\": \"Last ned PDF-fil\",\n    \"remove\": \"Fjern\",\n    \"more\": \"Mer\",\n    \"replace_banner\": \"Erstatt banner\",\n    \"add_banner\": \"Legg til banner\",\n    \"download\": \"Last ned\"\n  },\n  \"settings\": {\n    \"info\": {\n      \"basic_details\": \"Grunnleggende detaljer\",\n      \"interface_lang\": \"Språk for brukergrensesnitt\",\n      \"user_info\": \"Brukerinfo\",\n      \"change_password\": \"Endre passord\",\n      \"confirm_new_password\": \"Bekreft nytt passord\",\n      \"options\": \"Alternativer\",\n      \"current_password\": \"Nåværende passord\",\n      \"new_password\": \"Nytt passord\",\n      \"user_settings\": {\n        \"user_settings_updated\": \"Brukerinnstillingene er oppdatert!\",\n        \"bookmark_click_action\": {\n          \"title\": \"Handling ved klikk på bokmerke\",\n          \"open_external_url\": \"Åpne original URL\",\n          \"open_bookmark_details\": \"Åpne bokmerkedetaljer\"\n        },\n        \"archive_display_behaviour\": {\n          \"title\": \"Arkiverte bokmerker\",\n          \"show\": \"Vis arkiverte bokmerker i tagger og lister\",\n          \"hide\": \"Skjul arkiverte bokmerker i tagger og lister\"\n        }\n      },\n      \"reader_settings\": {\n        \"local_overrides_title\": \"Enhetsspesifikke innstillinger er aktive\",\n        \"using_default\": \"Bruker klientstandard\",\n        \"clear_override_hint\": \"Fjern overstyring av enhet for å bruke global innstilling ({{value}})\",\n        \"font_size\": \"Skriftstørrelse\",\n        \"font_family\": \"Skrifttype\",\n        \"preview_inline\": \"(forhåndsvisning)\",\n        \"tooltip_preview\": \"Ulagrede forhåndsvisningsendringer\",\n        \"save_to_all_devices\": \"Alle enheter\",\n        \"tooltip_local\": \"Enhetsinnstillingene er forskjellige fra de globale\",\n        \"reset_preview\": \"Tilbakestill forhåndsvisning\",\n        \"mono\": \"Monospace\",\n        \"line_height\": \"Linjehøyde\",\n        \"tooltip_default\": \"Leseinnstillinger\",\n        \"title\": \"Leserinnstillinger\",\n        \"serif\": \"Serif\",\n        \"preview\": \"Forhåndsvisning\",\n        \"not_set\": \"Ikke angitt\",\n        \"clear_local_overrides\": \"Fjern enhetsinnstillinger\",\n        \"preview_text\": \"Den rappe, brune reven hopper over den late hunden. Slik vil teksten i leservisningen din se ut.\",\n        \"local_overrides_cleared\": \"Enhetsspesifikke innstillinger er fjernet\",\n        \"local_overrides_description\": \"Denne enheten har leserinnstillinger som er forskjellige fra dine globale standardinnstillinger:\",\n        \"clear_defaults\": \"Fjern alle standarder\",\n        \"description\": \"Konfigurer standard tekstinnstillinger for leservisningen. Disse innstillingene synkroniseres på tvers av alle enhetene dine.\",\n        \"defaults_cleared\": \"Leserstandarder er fjernet\",\n        \"save_hint\": \"Lagre innstillinger bare for denne enheten eller synkroniser på tvers av alle enheter\",\n        \"save_as_default\": \"Lagre som standard\",\n        \"save_to_device\": \"Denne enheten\",\n        \"sans\": \"Sans Serif\",\n        \"tooltip_preview_and_local\": \"Ulagrede forhåndsvisningsendringer; enhetsinnstillingene er forskjellige fra de globale\",\n        \"adjust_hint\": \"Juster innstillingene ovenfor for å forhåndsvise endringer\"\n      },\n      \"avatar\": {\n        \"upload\": \"Last opp avatar\",\n        \"change\": \"Endre avatar\",\n        \"remove_confirm_title\": \"Fjerne avatar?\",\n        \"updated\": \"Avatar oppdatert\",\n        \"removed\": \"Avatar fjernet\",\n        \"description\": \"Last opp et kvadratisk bilde som avatar.\",\n        \"remove_confirm_description\": \"Dette vil fjerne ditt nåværende profilbilde.\",\n        \"title\": \"Profilbilde\",\n        \"remove\": \"Fjern avatar\"\n      }\n    },\n    \"ai\": {\n      \"tagging_rules\": \"Regler for merking\",\n      \"summarization_prompt\": \"Oppsummeringsprompt\",\n      \"ai_settings\": \"AI-innstillinger\",\n      \"tagging_rule_description\": \"Meldinger du legger til her vil inkluderes som regler for modellen under taggenerering. Du kan se de endelige meldingene i forhåndsvisningsseksjonen.\",\n      \"prompt_preview\": \"Forhåndsvisning av ledetekst\",\n      \"text_prompt\": \"Tekstledetekst\",\n      \"all_tagging\": \"All merking\",\n      \"text_tagging\": \"Teksttagging\",\n      \"image_tagging\": \"Bilde-tagging\",\n      \"summarization\": \"Oppsummering\",\n      \"images_prompt\": \"Bildeledetekst\",\n      \"tag_style\": \"Stil for merkelapper\",\n      \"auto_summarization_description\": \"Generer automatisk sammendrag for bokmerkene dine ved hjelp av AI.\",\n      \"auto_tagging\": \"Automatisk merking\",\n      \"titlecase_spaces\": \"Tittel-case med mellomrom\",\n      \"lowercase_underscores\": \"Små bokstaver med understreker\",\n      \"inference_language\": \"Språk for inferens\",\n      \"titlecase_hyphens\": \"Tittel-case med bindestreker\",\n      \"lowercase_hyphens\": \"Små bokstaver med bindestreker\",\n      \"lowercase_spaces\": \"Små bokstaver med mellomrom\",\n      \"inference_language_description\": \"Velg språk for AI-genererte merkelapper og sammendrag.\",\n      \"tag_style_description\": \"Velg hvordan de automatisk genererte merkelappene dine skal formateres.\",\n      \"auto_tagging_description\": \"Generer automatisk tagger for bokmerkene dine ved hjelp av AI.\",\n      \"camelCase\": \"camelCase\",\n      \"auto_summarization\": \"Automatisk oppsummering\",\n      \"no_preference\": \"Ingen preferanse\",\n      \"curated_tags\": \"Kurerte tagger\",\n      \"curated_tags_description\": \"Du kan eventuelt begrense AI-taggingen til å bare bruke tagger fra denne listen. Når ingen tagger er valgt vil AI-en generere tagger fritt.\",\n      \"curated_tags_updated\": \"Kurerte tagger oppdatert!\",\n      \"curated_tags_update_failed\": \"Klarte ikke å oppdatere kurerte tagger\"\n    },\n    \"import\": {\n      \"import_bookmarks_from_omnivore_export\": \"Importer bokmerker fra Omnivore-eksport\",\n      \"import_export\": \"Importer/eksporter\",\n      \"import_export_bookmarks\": \"Importer / eksporter bokmerker\",\n      \"import_bookmarks_from_html_file\": \"Importer bokmerker fra HTML-fil\",\n      \"import_bookmarks_from_pocket_export\": \"Importer bokmerker fra Pocket-eksport\",\n      \"import_bookmarks_from_matter_export\": \"Importer bokmerker fra Matter-eksport\",\n      \"import_bookmarks_from_linkwarden_export\": \"Importer bokmerker fra Linkwarden-eksport\",\n      \"import_bookmarks_from_karakeep_export\": \"Importer bokmerker fra Karakeepp-eksport\",\n      \"export_links_and_notes\": \"Eksporter lenker og notater\",\n      \"imported_bookmarks\": \"Importerte bokmerker\",\n      \"import_bookmarks_from_tab_session_manager_export\": \"Importer bokmerker fra Tab Session Manager\",\n      \"import_bookmarks_from_mymind_export\": \"Importer bokmerker fra mymind-eksporten min\",\n      \"import_bookmarks_from_instapaper_export\": \"Importer bokmerker fra Instapaper-eksport\"\n    },\n    \"api_keys\": {\n      \"new_api_key\": \"Ny API-nøkkel\",\n      \"key_success\": \"Nøkkelen ble opprettet\",\n      \"new_api_key_desc\": \"Gi API-nøkkelen din et unikt navn\",\n      \"api_keys\": \"API-nøkler\",\n      \"key_success_please_copy\": \"Vennligst kopier nøkkelen og oppbevar den et trygt sted. Når du lukker dialogen, vil du ikke ha tilgang til den igjen.\",\n      \"regenerate_api_key\": \"Regenerer API-nøkkel\",\n      \"key_regenerated\": \"Nøkkelen ble regenerert\",\n      \"key_regenerated_please_copy\": \"Kopier den nye nøkkelen og oppbevar den på et trygt sted. Den gamle nøkkelen er trukket tilbake og vil ikke lenger fungere.\",\n      \"regenerate_warning\": \"Er du sikker på at du vil regenerere API-nøkkelen \\\"{{name}}\\\"?\",\n      \"regenerate_confirmation\": \"Dette vil tilbakekalle den nåværende nøkkelen og generere en ny. Alle applikasjoner som bruker den nåværende nøkkelen, slutter å virke.\"\n    },\n    \"webhooks\": {\n      \"edit_webhook\": \"Rediger Webhook\",\n      \"events\": {\n        \"title\": \"Hendelser\",\n        \"crawled\": \"Crawlet\",\n        \"created\": \"Opprettet\",\n        \"edited\": \"Redigert\"\n      },\n      \"webhooks\": \"Webhooks\",\n      \"description\": \"Du kan bruke webhooks til å trigge handlinger når bokmerker opprettes, endres eller crawles.\",\n      \"auth_token\": \"Autentiseringstoken\",\n      \"add_auth_token\": \"Legg til autentiseringstoken\",\n      \"edit_auth_token\": \"Rediger autentiseringstoken\",\n      \"create_webhook\": \"Lag webhook\",\n      \"delete_webhook\": \"Slett Webhook\",\n      \"delete_webhook_confirmation\": \"Er du sikker på at du vil slette denne webhooken?\",\n      \"webhook_url\": \"Webhook-URL\"\n    },\n    \"back_to_app\": \"Tilbake til appen\",\n    \"user_settings\": \"Brukerinnstillinger\",\n    \"feeds\": {\n      \"rss_subscriptions\": \"RSS-abonnementer\",\n      \"add_a_subscription\": \"Legg til et abonnement\",\n      \"feed_enabled\": \"RSS-strøm aktivert\",\n      \"feed_disabled\": \"RSS-strøm deaktivert\"\n    },\n    \"broken_links\": {\n      \"broken_links\": \"Ødelagte lenker\",\n      \"last_crawled_at\": \"Sist crawlet på\",\n      \"crawling_status\": \"Gjennomsøkingsstatus\",\n      \"crawling_failed\": \"Kryping feilet\"\n    },\n    \"manage_assets\": {\n      \"bookmark_link\": \"Bokmerke-lenke\",\n      \"asset_link\": \"Ressurslenke\",\n      \"manage_assets\": \"Administrer ressurser\",\n      \"no_assets\": \"Du har ingen elementer ennå.\",\n      \"asset_type\": \"Ressurstype\",\n      \"delete_asset\": \"Slett element\",\n      \"delete_asset_confirmation\": \"Er du sikker på at du vil slette denne ressursen?\"\n    },\n    \"rules\": {\n      \"rule_has_been_updated\": \"Regelen er oppdatert!\",\n      \"rule_has_been_deleted\": \"Regelen er slettet!\",\n      \"no_rules_created_yet\": \"Ingen regler er opprettet ennå\",\n      \"conditions_types\": {\n        \"always\": \"Alltid\",\n        \"url_contains\": \"URL inneholder\",\n        \"imported_from_feed\": \"Importert fra feed\",\n        \"is_favourited\": \"Er favorittmerket\",\n        \"is_archived\": \"Er arkivert\",\n        \"and\": \"Alle de følgende er sanne\",\n        \"or\": \"Noe av det følgende er sant\",\n        \"bookmark_type_is\": \"Bokmerketype er\",\n        \"has_tag\": \"Har tagg\",\n        \"url_does_not_contain\": \"URL-en inneholder ikke\",\n        \"title_contains\": \"Tittelen inneholder\",\n        \"title_does_not_contain\": \"Tittelen inneholder ikke\"\n      },\n      \"if\": \"Hvis ...\",\n      \"enter_rule_name\": \"Skriv inn regelnavn\",\n      \"describe_what_this_rule_does\": \"Beskriv hva denne regelen gjør\",\n      \"rule_has_been_created\": \"Regelen er opprettet!\",\n      \"rules\": \"Regelmotor\",\n      \"rule_name\": \"Regelnavn\",\n      \"description\": \"Du kan bruke regler til å utløse handlinger når en hendelse fyres av.\",\n      \"ceate_rule\": \"Lag regel\",\n      \"edit_rule\": \"Rediger regel\",\n      \"save_rule\": \"Lagre regel\",\n      \"delete_rule\": \"Slett regel\",\n      \"delete_rule_confirmation\": \"Er du sikker på at du vil slette denne regelen?\",\n      \"whenever\": \"Når som helst ...\",\n      \"create_your_first_rule\": \"Lag din første regel for å automatisere arbeidsflyten din\",\n      \"actions_types\": {\n        \"add_tag\": \"Legg til tagg\",\n        \"download_full_page_archive\": \"Last ned full sidearkiv\",\n        \"favourite_bookmark\": \"Favorittbokmerke\",\n        \"archive_bookmark\": \"Arkiver bokmerke\",\n        \"remove_tag\": \"Fjern tagg\",\n        \"add_to_list\": \"Legg til i liste\",\n        \"remove_from_list\": \"Fjern fra liste\"\n      },\n      \"events_types\": {\n        \"bookmark_added\": \"Et bokmerke er lagt til\",\n        \"tag_added\": \"Denne taggen er lagt til et bokmerke\",\n        \"tag_removed\": \"Denne taggen er fjernet fra et bokmerke\",\n        \"added_to_list\": \"Et bokmerke er lagt til i denne lista\",\n        \"removed_from_list\": \"Et bokmerke er fjernet fra denne lista\",\n        \"favourited\": \"Et bokmerke er favorittmerket\",\n        \"archived\": \"Et bokmerke er arkivert\"\n      }\n    },\n    \"stats\": {\n      \"usage_statistics\": \"Bruksstatistikk\",\n      \"insights_description\": \"Innsikt i bokmerkingsvanene og samlingen din\",\n      \"failed_to_load\": \"Kunne ikke laste statistikk\",\n      \"overview\": {\n        \"total_bookmarks\": \"Totalt antall bokmerker\",\n        \"all_saved_items\": \"Alle lagrede elementer\",\n        \"favorites\": \"Favoritter\",\n        \"starred_bookmarks\": \"Stjernemerkede bokmerker\",\n        \"archived\": \"Arkivert\",\n        \"archived_items\": \"Arkiverte elementer\",\n        \"tags\": \"Merker\",\n        \"unique_tags_created\": \"Unike tagger opprettet\",\n        \"lists\": \"Lister\",\n        \"bookmark_collections\": \"Bokmerkesamlinger\",\n        \"highlights\": \"Uthevinger\",\n        \"text_highlights\": \"Tekstuthevinger\",\n        \"storage_used\": \"Lagringsplass brukt\",\n        \"total_asset_storage\": \"Total lagringsplass for ressurser\",\n        \"this_month\": \"Denne måneden\",\n        \"bookmarks_added\": \"Bokmerker lagt til\"\n      },\n      \"bookmark_types\": {\n        \"title\": \"Bokmerketyper\",\n        \"links\": \"Lenker\",\n        \"text_notes\": \"Tekstnotater\",\n        \"assets\": \"Ressurser\"\n      },\n      \"recent_activity\": {\n        \"title\": \"Nylig aktivitet\",\n        \"this_week\": \"Denne uken\",\n        \"this_month\": \"Denne måneden\",\n        \"this_year\": \"Dette året\"\n      },\n      \"top_domains\": {\n        \"title\": \"Toppdomener\",\n        \"no_domains_found\": \"Ingen domener funnet\"\n      },\n      \"most_used_tags\": {\n        \"title\": \"Mest brukte tagger\",\n        \"no_tags_found\": \"Fant ingen merker\"\n      },\n      \"activity_patterns\": {\n        \"activity_by_hour\": \"Aktivitet per time\",\n        \"activity_by_day\": \"Aktivitet per dag\"\n      },\n      \"storage_breakdown\": {\n        \"title\": \"Lagringsoversikt\"\n      },\n      \"bookmark_sources\": {\n        \"title\": \"Bokmerk kilder\",\n        \"empty\": \"Ingen kildedata tilgjengelig\"\n      }\n    },\n    \"subscription\": {\n      \"subscription\": \"Abonnement\",\n      \"manage_subscription\": \"Administrer abonnementet og faktureringsinformasjonen din\",\n      \"current_plan\": \"Gjeldende abonnement\",\n      \"billing_period\": \"Faktureringsperiode\",\n      \"paid_plan\": \"Betalt abonnement\",\n      \"unlock_bigger_quota\": \"Lås opp større kvote og støtt prosjektet\",\n      \"subscribe_now\": \"Abonner nå\",\n      \"manage_billing\": \"Administrer fakturering\",\n      \"subscription_canceled\": \"Abonnementet ditt er kansellert og avsluttes {{date}}. Du kan abonnere på nytt når som helst.\",\n      \"usage_quotas\": \"Bruk og kvoter\",\n      \"track_usage\": \"Følg med på bruken din opp mot planens grenser\",\n      \"total_bookmarks_saved\": \"Totalt antall bokmerker lagret\",\n      \"assets_file_storage\": \"Lagring av filer og ressurser\",\n      \"unlimited_usage\": \"Ubegrenset bruk\",\n      \"quota_limit_reached\": \"Kvotegrensen er nådd\",\n      \"approaching_quota_limit\": \"Nærmer seg kvotegrensen\",\n      \"loading_usage\": \"Laster inn bruksinformasjon …\",\n      \"free\": \"Gratis\",\n      \"paid\": \"Betalt\"\n    },\n    \"import_sessions\": {\n      \"title\": \"Importer sesjoner\",\n      \"description\": \"Se og administrer bulkimport-sesjonene dine. Sesjoner opprettes automatisk når du importerer bokmerker.\",\n      \"load_error\": \"Kunne ikke laste inn importsesjoner\",\n      \"no_sessions\": \"Ingen importsesjoner enda\",\n      \"no_sessions_detail\": \"Importsesjoner vises her automatisk når du importerer bokmerker\",\n      \"created_at\": \"Opprettet {{time}}\",\n      \"progress\": \"Fremdrift\",\n      \"status\": {\n        \"pending\": \"Venter\",\n        \"in_progress\": \"Pågår\",\n        \"completed\": \"Fullført\",\n        \"failed\": \"Mislyktes\",\n        \"processing\": \"Behandler\",\n        \"staging\": \"Staging\",\n        \"running\": \"Kjører\",\n        \"paused\": \"Pause\"\n      },\n      \"badges\": {\n        \"pending\": \"{{count}} venter\",\n        \"processing\": \"{{count}} behandler\",\n        \"completed\": \"{{count}} fullført\",\n        \"failed\": \"{{count}} feilet\"\n      },\n      \"imported_to\": \"Importert til:\",\n      \"view_list\": \"Vis liste\",\n      \"delete_dialog_title\": \"Slett importsesjon\",\n      \"delete_dialog_description\": \"Er du sikker på at du vil slette «{{name}}»? Denne handlingen kan ikke angres. Selve bokmerkene blir ikke slettet.\",\n      \"delete_session\": \"Slett økt\",\n      \"pause_session\": \"Pause\",\n      \"resume_session\": \"Gjenoppta\",\n      \"view_details\": \"Vis detaljer\",\n      \"detail\": {\n        \"page_title\": \"Importer sesjonsdetaljer\",\n        \"back_to_import\": \"Tilbake til import\",\n        \"filter_all\": \"Alle\",\n        \"filter_accepted\": \"Akseptert\",\n        \"filter_rejected\": \"Avvist\",\n        \"filter_duplicates\": \"Duplikater\",\n        \"filter_pending\": \"Venter\",\n        \"table_title\": \"Tittel / URL\",\n        \"table_type\": \"Type\",\n        \"table_result\": \"Resultat\",\n        \"table_reason\": \"Årsak\",\n        \"table_bookmark\": \"Bokmerke\",\n        \"result_accepted\": \"Akseptert\",\n        \"result_rejected\": \"Avvist\",\n        \"result_skipped_duplicate\": \"Duplikat\",\n        \"result_pending\": \"Venter\",\n        \"result_processing\": \"Behandler\",\n        \"no_results\": \"Fant ingen resultater for dette filteret.\",\n        \"view_bookmark\": \"Vis bokmerke\",\n        \"load_more\": \"Last mer\",\n        \"no_title\": \"Ingen tittel\"\n      }\n    },\n    \"backups\": {\n      \"backups\": \"Sikkerhetskopieringer\",\n      \"page_title\": \"Sikkerhetskopieringer\",\n      \"page_description\": \"Lag og administrer sikkerhetskopieringer av bokmerkene dine automatisk. Sikkerhetskopieringene komprimeres og lagres sikkert.\",\n      \"configuration\": {\n        \"title\": \"Konfigurasjon av sikkerhetskopiering\",\n        \"enable_automatic_backups\": \"Aktiver automatiske sikkerhetskopieringer\",\n        \"enable_automatic_backups_description\": \"Lag sikkerhetskopieringer av bokmerkene dine automatisk\",\n        \"backup_frequency\": \"Frekvens for sikkerhetskopiering\",\n        \"backup_frequency_description\": \"Hvor ofte sikkerhetskopieringer skal opprettes\",\n        \"retention_period\": \"Oppbevaringsperiode (dager)\",\n        \"retention_period_description\": \"Hvor mange dager sikkerhetskopieringer skal beholdes før de slettes\",\n        \"frequency\": {\n          \"daily\": \"Daglig\",\n          \"weekly\": \"Ukentlig\"\n        },\n        \"select_frequency\": \"Velg frekvens\",\n        \"save_settings\": \"Lagre innstillinger\"\n      },\n      \"list\": {\n        \"title\": \"Dine sikkerhetskopieringer\",\n        \"create_backup_now\": \"Lag sikkerhetskopiering nå\",\n        \"no_backups\": \"Du har ingen sikkerhetskopieringer ennå. Aktiver automatiske sikkerhetskopieringer eller lag en manuelt.\",\n        \"table\": {\n          \"created_at\": \"Opprettet\",\n          \"bookmarks\": \"Bokmerker\",\n          \"size\": \"Størrelse\",\n          \"status\": \"Status\",\n          \"actions\": \"Handlinger\"\n        },\n        \"status\": {\n          \"success\": \"Vellykket\",\n          \"failed\": \"Mislyktes\",\n          \"pending\": \"Venter\"\n        },\n        \"actions\": {\n          \"download_backup\": \"Last ned sikkerhetskopi\",\n          \"delete_backup\": \"Slett sikkerhetskopi\"\n        }\n      },\n      \"dialogs\": {\n        \"delete_backup_title\": \"Slett sikkerhetskopi?\",\n        \"delete_backup_description\": \"Er du sikker på at du vil slette denne sikkerhetskopien? Dette kan ikke angres.\"\n      },\n      \"toasts\": {\n        \"backup_queued\": \"Sikkerhetskopieringsjobben er satt i kø! Den behandles snart.\",\n        \"backup_deleted\": \"Sikkerhetskopien er slettet!\"\n      }\n    }\n  },\n  \"options\": {\n    \"dark_mode\": \"Mørk modus\",\n    \"light_mode\": \"Lys modus\",\n    \"apps_extensions\": \"Apper og utvidelser\",\n    \"documentation\": \"Dokumentasjon\",\n    \"follow_us_on_x\": \"Følg oss på X\"\n  },\n  \"lists\": {\n    \"all_lists\": \"Alle lister\",\n    \"no_parent\": \"Ingen overordnet\",\n    \"list_type\": \"Listetype\",\n    \"search_query_help\": \"Lær mer om søkespråket.\",\n    \"favourites\": \"Favoritter\",\n    \"new_list\": \"Ny liste\",\n    \"edit_list\": \"Rediger liste\",\n    \"new_nested_list\": \"Ny nestet liste\",\n    \"search_query\": \"Søkespørring\",\n    \"parent_list\": \"Overordnet liste\",\n    \"manual_list\": \"Manuell liste\",\n    \"smart_list\": \"Smartliste\",\n    \"merge_list\": \"Slå sammen liste\",\n    \"destination_list\": \"Destinasjonsliste\",\n    \"delete_after_merge\": \"Slett opprinnelig liste etter sammenslåing\",\n    \"no_destination\": \"Ingen destinasjon\",\n    \"description\": \"Beskrivelse (valgfritt)\",\n    \"rss\": {\n      \"description\": \"Aktiver en RSS-feed for denne lista\",\n      \"feed_url\": \"RSS-feed URL\",\n      \"title\": \"RSS-feed\"\n    },\n    \"public_list\": {\n      \"title\": \"Offentlig liste\",\n      \"share_link\": \"Del lenke\",\n      \"description\": \"Tillat andre å se denne lista\"\n    },\n    \"share_list\": \"Del liste\",\n    \"delete_list\": {\n      \"title\": \"Slett liste\",\n      \"description\": \"Hvis du sletter ei liste, slettes ikke bokmerkene i lista.\",\n      \"delete_children\": \"Slett underlister (rekursivt)\",\n      \"delete_children_description\": \"Hvis dette ikke er haket av, vil alle direkte underlister bli topplister\"\n    },\n    \"shared\": \"Delt\",\n    \"collaborators\": {\n      \"manage\": \"Administrer samarbeidspartnere\",\n      \"view\": \"Vis samarbeidspartnere\",\n      \"collaborators\": \"Samarbeidspartnere\",\n      \"add\": \"Legg til samarbeidspartner\",\n      \"current\": \"Nåværende samarbeidspartnere\",\n      \"enter_email\": \"Skriv inn e-postadresse\",\n      \"please_enter_email\": \"Vennligst skriv inn en e-postadresse\",\n      \"added_successfully\": \"Samarbeidspartner lagt til\",\n      \"failed_to_add\": \"Kunne ikke legge til samarbeidspartner\",\n      \"removed\": \"Samarbeidspartner fjernet\",\n      \"failed_to_remove\": \"Kunne ikke fjerne samarbeidspartner\",\n      \"role_updated\": \"Rolle oppdatert\",\n      \"failed_to_update_role\": \"Kunne ikke oppdatere rolle\",\n      \"viewer\": \"Visningsrettigheter\",\n      \"editor\": \"Redigeringsrettigheter\",\n      \"owner\": \"Eier\",\n      \"viewer_description\": \"Kan se bokmerker i listen\",\n      \"editor_description\": \"Kan legge til og fjerne bokmerker\",\n      \"no_collaborators\": \"Ingen samarbeidspartnere ennå. Legg til noen for å starte samarbeidet!\",\n      \"no_collaborators_readonly\": \"Ingen samarbeidspartnere for denne lista.\",\n      \"people_with_access\": \"Folk som har tilgang til denne lista\",\n      \"add_or_remove\": \"Legg til eller fjern folk som har tilgang til denne lista\",\n      \"invitation_sent\": \"Invitasjonen er sendt!\",\n      \"invitation_revoked\": \"Invitasjonen er tilbakekalt\",\n      \"failed_to_revoke\": \"Kunne ikke tilbakekalle inviasjonen\",\n      \"pending\": \"Venter\",\n      \"revoke\": \"Tilbakekall\",\n      \"declined\": \"Avslått\"\n    },\n    \"leave_list\": {\n      \"title\": \"Forlat liste\",\n      \"confirm_message\": \"Er du sikker på at du vil forlate {{icon}} {{name}}?\",\n      \"warning\": \"Du vil ikke lenger kunne se eller få tilgang til bokmerker i denne lista. Listeeieren kan legge deg tilbake hvis nødvendig.\",\n      \"action\": \"Forlat liste\",\n      \"success\": \"Du har forlatt \\\"{{icon}} {{name}}\\\"\"\n    },\n    \"invitations\": {\n      \"pending\": \"Ventende invitasjoner\",\n      \"description\": \"Se gjennom og svar på invitasjoner for listesamarbeid\",\n      \"invited_by\": \"Invitert av\",\n      \"accept\": \"Aksepter\",\n      \"decline\": \"Avslå\",\n      \"accepted\": \"Invitasjonen er akseptert\",\n      \"declined\": \"Invitasjonen er avslått\",\n      \"failed_to_accept\": \"Kunne ikke akseptere invitasjonen\",\n      \"failed_to_decline\": \"Kunne ikke avslå invitasjonen\"\n    },\n    \"shared_lists\": \"Delte lister\"\n  },\n  \"tags\": {\n    \"all_tags\": \"Alle tagger\",\n    \"unused_tags\": \"Ubrukte tag-er\",\n    \"sort_by_name\": \"Sorter etter navn\",\n    \"ai_tags\": \"AI-tagger\",\n    \"your_tags\": \"Dine tagger\",\n    \"your_tags_info\": \"Tags som ble lagt til minst én gang av deg\",\n    \"ai_tags_info\": \"Tags som bare ble lagt til automatisk (av AI)\",\n    \"drag_and_drop_merging\": \"Sammenslåing med dra og slipp\",\n    \"drag_and_drop_merging_info\": \"Dra og slipp tagger oppå hverandre for å slå dem sammen\",\n    \"unused_tags_info\": \"Tag-er som ikke er knyttet til noen bokmerker\",\n    \"delete_all_unused_tags\": \"Slett alle ubrukte tagger\",\n    \"create_tag\": \"Lag tagg\",\n    \"create_tag_description\": \"Lag en ny tagg uten å feste den til noe bokmerke\",\n    \"tag_name\": \"Taggnavn\",\n    \"enter_tag_name\": \"Skriv inn taggnavn\",\n    \"sort_by_usage\": \"Sorter etter bruk\",\n    \"sort_by_relevance\": \"Sorter etter relevans\",\n    \"no_custom_tags\": \"Ingen egendefinerte tagger ennå\",\n    \"no_ai_tags\": \"Ingen AI-tagger ennå\",\n    \"no_unused_tags\": \"Du har ingen ubrukte tagger\",\n    \"no_unused_tags_match_your_search\": \"Ingen ubrukte tagger passer søket ditt\",\n    \"no_tags_match_your_search\": \"Ingen tagger samsvarer med søket ditt\",\n    \"search_placeholder\": \"Søk etter tagger …\",\n    \"search_or_create_placeholder\": \"Søk eller lag tagger …\"\n  },\n  \"search\": {\n    \"is_not_favorited\": \"Er ikke favorittmerket\",\n    \"is_favorited\": \"Er favorittmerket\",\n    \"not_created_on_or_after\": \"Ikke opprettet på eller etter\",\n    \"url_does_not_contain\": \"URL-en inneholder ikke\",\n    \"has_tag\": \"Har tag\",\n    \"type_is\": \"Type er\",\n    \"and\": \"Og\",\n    \"is_archived\": \"Er arkivert\",\n    \"is_in_any_list\": \"Er i en liste\",\n    \"is_not_in_any_list\": \"Er ikke i noen liste\",\n    \"created_on_or_after\": \"Opprettet på eller etter\",\n    \"is_in_list\": \"Er i liste\",\n    \"is_not_in_list\": \"Er ikke i liste\",\n    \"does_not_have_tag\": \"Har ikke tag\",\n    \"is_not_archived\": \"Er ikke arkivert\",\n    \"has_any_tag\": \"Har en tagg\",\n    \"has_no_tags\": \"Har ingen tag\",\n    \"created_on_or_before\": \"Opprettet på eller før\",\n    \"not_created_on_or_before\": \"Ikke opprettet på eller før\",\n    \"url_contains\": \"URL inneholder\",\n    \"full_text_search\": \"Fulltekstsøk\",\n    \"type_is_not\": \"Type er ikke\",\n    \"or\": \"Eller\",\n    \"is_from_feed\": \"Er fra RSS-feed\",\n    \"is_not_from_feed\": \"Er ikke fra RSS-feed\",\n    \"created_within\": \"Opprettet innen\",\n    \"created_earlier_than\": \"Opprettet tidligere enn\",\n    \"day_s\": \" Dager\",\n    \"week_s\": \" Uker\",\n    \"month_s\": \" Måneder\",\n    \"year_s\": \" År\",\n    \"day_s_ago\": \" Dager siden\",\n    \"week_s_ago\": \" Uker siden\",\n    \"month_s_ago\": \" Måneder siden\",\n    \"year_s_ago\": \" År siden\",\n    \"history\": \"Nylige søk\",\n    \"title_contains\": \"Tittel inneholder\",\n    \"title_does_not_contain\": \"Tittel inneholder ikke\",\n    \"is_broken_link\": \"Har ødelagt lenke\",\n    \"tags\": \"Merker\",\n    \"no_suggestions\": \"Ingen forslag\",\n    \"filters\": \"Filtere\",\n    \"is_not_broken_link\": \"Har fungerende lenke\",\n    \"lists\": \"Lister\",\n    \"feeds\": \"Feeder\",\n    \"is_from_source\": \"Kilden er\",\n    \"is_not_from_source\": \"Kilden er ikke\"\n  },\n  \"editor\": {\n    \"text_toolbar\": {\n      \"markdown_shortcuts\": {\n        \"italic\": {\n          \"example\": \"*Kursiv* eller _Kursiv_ eller CTRL+i\",\n          \"label\": \"Kursiv\"\n        },\n        \"block_code\": {\n          \"label\": \"Blokker kode\",\n          \"example\": \"``` + mellomrom\"\n        },\n        \"heading\": {\n          \"example\": \"# H1, ## H2, ### H3\",\n          \"label\": \"Overskrift\"\n        },\n        \"bold\": {\n          \"label\": \"Fet\",\n          \"example\": \"**tekst** eller CTRL+b\"\n        },\n        \"ordered_list\": {\n          \"label\": \"Ordnet liste\",\n          \"example\": \"1. Listeelement\"\n        },\n        \"inline_code\": {\n          \"label\": \"Innebygd kode\",\n          \"example\": \"`Kode`\"\n        },\n        \"label\": \"Markdown-snarveier\",\n        \"blockquote\": {\n          \"label\": \"Blokksitat\",\n          \"example\": \"> Blokk sitat\"\n        },\n        \"unordered_list\": {\n          \"label\": \"Uordnet liste\",\n          \"example\": \"- Listeelement\"\n        }\n      },\n      \"undo\": \"Angre\",\n      \"italic\": \"Kursiv\",\n      \"code\": \"Kode\",\n      \"highlight\": \"Uthev\",\n      \"align_left\": \"Venstrejuster\",\n      \"redo\": \"Gjør om\",\n      \"bold\": \"Fet\",\n      \"underline\": \"Understreking\",\n      \"strikethrough\": \"Gjennomstreking\",\n      \"align_center\": \"Midtstill\",\n      \"align_right\": \"Juster til høyre\"\n    },\n    \"import_as_text\": \"Importer som tekstbokmerke\",\n    \"quickly_focus\": \"Du kan raskt fokusere på dette feltet ved å trykke ⌘ + E\",\n    \"multiple_urls_dialog_title\": \"Vil du importere URL-er som separate bokmerker?\",\n    \"multiple_urls_dialog_desc\": \"Inputen inneholder flere URL-er på separate linjer. Vil du importere dem som separate bokmerker?\",\n    \"import_as_separate_bookmarks\": \"Importer som separate bokmerker\",\n    \"placeholder\": \"Lim inn en lenke eller et bilde, skriv et notat eller dra og slipp et bilde her…\",\n    \"new_item\": \"NYTT ELEMENT\",\n    \"disabled_submissions\": \"Innsendinger er deaktivert\",\n    \"placeholder_v2\": \"Lim inn en lenke, skriv et notat eller slipp et bilde…\"\n  },\n  \"toasts\": {\n    \"bookmarks\": {\n      \"full_page_archive\": \"Full Page Archive-opprettelse er trigga\",\n      \"refetch\": \"Ny henting er satt i kø!\",\n      \"delete_from_list\": \"Bokmerket er sletta fra lista\",\n      \"clipboard_copied\": \"Lenken er lagt til utklippstavlen din!\",\n      \"updated\": \"Bokmerket er oppdatert!\",\n      \"deleted\": \"Bokmerket er slettet!\",\n      \"preserve_pdf\": \"PDF-bevaring er trigget\",\n      \"update_banner\": \"Banneret er oppdatert!\",\n      \"uploading_banner\": \"Laster opp banner...\"\n    },\n    \"lists\": {\n      \"created\": \"Liste er opprettet!\",\n      \"updated\": \"Lista er oppdatert!\",\n      \"merged\": \"Liste er slått sammen!\",\n      \"deleted\": \"Liste er slettet!\"\n    },\n    \"tags\": {\n      \"created\": \"Taggen er oppretta!\",\n      \"failed_to_create\": \"Klarte ikke å opprette taggen\"\n    }\n  },\n  \"cleanups\": {\n    \"cleanups\": \"Opprydninger\",\n    \"duplicate_tags\": {\n      \"title\": \"Duplikate tagger\",\n      \"merge_all_suggestions\": \"Slå sammen alle forslag?\"\n    }\n  },\n  \"layouts\": {\n    \"masonry\": \"Murverk\",\n    \"list\": \"Liste\",\n    \"compact\": \"Kompakt\",\n    \"grid\": \"Rutenett\"\n  },\n  \"highlights\": {\n    \"no_highlights\": \"Du har ingen uthevinger ennå.\"\n  },\n  \"preview\": {\n    \"cached_content\": \"Cachet innhold\",\n    \"view_original\": \"Vis original\",\n    \"reader_view\": \"Lesevisning\",\n    \"tabs\": {\n      \"content\": \"Innhold\",\n      \"details\": \"Detaljer\"\n    },\n    \"archive_info\": \"Det kan hende at arkiver ikke gjengis riktig direkte hvis de krever Javascript. For best resultat, <1>last ned og åpne i nettleseren din</1>.\",\n    \"fetch_error_title\": \"Innhold utilgjengelig\",\n    \"fetch_error_description\": \"Vi kunne ikke hente innholdet for denne lenken. Siden kan være beskyttet, kreve autentisering eller være midlertidig utilgjengelig.\",\n    \"crawling_in_progress\": \"Henter sideinnhold …\",\n    \"continue_reading\": \"Fortsett der du slapp\",\n    \"continue_reading_percent\": \"Fortsett der du slapp ({{percent}} %)\",\n    \"continue_button\": \"Fortsett\"\n  },\n  \"dialogs\": {\n    \"bookmarks\": {\n      \"delete_confirmation_title\": \"Slette bokmerke?\",\n      \"delete_confirmation_description\": \"Er du sikker på at du vil slette dette bokmerket?\"\n    }\n  },\n  \"banners\": {\n    \"no_bookmarks\": {\n      \"title\": \"Ingen bokmerker ennå\",\n      \"description\": \"Lagre interessante artikler, lenker og sider for å få rask tilgang til dem senere.\"\n    }\n  },\n  \"bookmark_editor\": {\n    \"title\": \"Rediger bokmerke\",\n    \"subtitle\": \"Gjør endringer i bokmerkedetaljene. Klikk på lagre når du er ferdig.\",\n    \"author\": \"Forfatter\",\n    \"publisher\": \"Utgiver\",\n    \"date_published\": \"Publiseringsdato\",\n    \"pick_a_date\": \"Velg en dato\",\n    \"save_changes\": \"Lagre endringer\",\n    \"extracted_content\": \"Ekstrahert innhold\"\n  },\n  \"view_options\": {\n    \"title\": \"Visningsalternativer\",\n    \"layout\": \"Oppsett\",\n    \"columns\": \"Kolonner\",\n    \"display_options\": \"Visningsinnstillinger\",\n    \"show_note_previews\": \"Vis notater\",\n    \"show_tags\": \"Vis tagger\",\n    \"show_title\": \"Vis tittel\",\n    \"image_options\": \"Bildeinnstillinger\",\n    \"image_fit_cover\": \"Dekk (Fyll)\",\n    \"image_fit_contain\": \"Innehold (Tilpass)\"\n  },\n  \"version\": {\n    \"new_release_available\": \"Nye versjonsmerknader tilgjengelig\",\n    \"whats_new_title\": \"Hva er nytt i v{{version}}\",\n    \"release_notes_description\": \"Her er de nyeste oppdateringene hentet fra GitHub-versjonsmerknadene.\",\n    \"loading_release_notes\": \"Laster inn versjonsmerknader…\",\n    \"unable_to_load_release_notes\": \"Kunne ikke laste inn versjonsmerknader akkurat nå. Prøv igjen senere.\",\n    \"no_release_notes\": \"Ingen versjonsmerknader ble publisert for denne versjonen.\",\n    \"release_notes_synced\": \"Versjonsmerknadene er synkronisert fra GitHub.\",\n    \"view_on_github\": \"Vis på GitHub\"\n  },\n  \"wrapped\": {\n    \"title\": \"Din samlede oppsummering for {{year}}\",\n    \"subtitle\": \"Et år i Karakeep\",\n    \"banner\": {\n      \"title\": \"Oppsummeringen din for 2025 er klar!\",\n      \"description\": \"Se året ditt i bokmerker\",\n      \"view_now\": \"Vis nå\"\n    },\n    \"button\": \"Oppsummering for 2025\",\n    \"loading\": \"Laster inn oppsummeringen din...\",\n    \"failed_to_load\": \"Kunne ikke laste inn oppsummeringsstatistikken din\",\n    \"sections\": {\n      \"total_saves\": {\n        \"prefix\": \"Du lagret\",\n        \"suffix\": \"ting i år\",\n        \"suffix_singular\": \"ting i år\"\n      },\n      \"first_bookmark\": {\n        \"title\": \"Reisen din har begynt\",\n        \"description\": \"Første lagring i {{year}}:\"\n      },\n      \"top_domains\": \"Dine mest populære nettsteder\",\n      \"top_tags\": \"Dine mest populære emneknagger\",\n      \"monthly_activity\": \"Ditt år i lagringer\",\n      \"most_active_day\": \"Din mest aktive dag\",\n      \"peak_times\": {\n        \"title\": \"Når du lagrer\",\n        \"peak_hour\": \"Høysesong\",\n        \"peak_day\": \"Toppdag\"\n      },\n      \"how_you_save\": \"Hvordan du lagrer\",\n      \"what_you_saved\": \"Hva du har lagret\",\n      \"summary\": {\n        \"favorites\": \"Favoritter\",\n        \"tags_created\": \"Opprettede emneknagger\",\n        \"highlights\": \"Høydepunkter\"\n      },\n      \"types\": {\n        \"links\": \"Lenker\",\n        \"notes\": \"Notater\",\n        \"assets\": \"Ressurser\"\n      }\n    },\n    \"footer\": \"Laget med Karakeep\",\n    \"share\": \"Del\",\n    \"download\": \"Last ned\",\n    \"close\": \"Lukk\",\n    \"generating\": \"Genererer…\"\n  }\n}\n"
  },
  {
    "path": "apps/web/lib/i18n/locales/nl/translation.json",
    "content": "{\n  \"common\": {\n    \"created_at\": \"Gemaakt op\",\n    \"archive\": \"Archiveer\",\n    \"url\": \"URL\",\n    \"name\": \"Naam\",\n    \"email\": \"E-mail\",\n    \"actions\": \"Acties\",\n    \"role\": \"Rol\",\n    \"roles\": {\n      \"user\": \"Gebruiker\",\n      \"admin\": \"Admin\"\n    },\n    \"something_went_wrong\": \"Er is iets fout gegaan\",\n    \"experimental\": \"Experimenteel\",\n    \"search\": \"Zoeken\",\n    \"tags\": \"Labels\",\n    \"note\": \"Notitie\",\n    \"attachments\": \"Bijlagen\",\n    \"screenshot\": \"Schermopname\",\n    \"video\": \"Video\",\n    \"home\": \"Home\",\n    \"password\": \"Wachtwoord\",\n    \"action\": \"Actie\",\n    \"key\": \"Sleutel\",\n    \"highlights\": \"Hoogtepunten\",\n    \"size\": \"Grootte\",\n    \"bookmark_types\": {\n      \"title\": \"Bladwijzertype\",\n      \"link\": \"Link\",\n      \"text\": \"Tekst\",\n      \"media\": \"Media\"\n    },\n    \"type\": \"Type\",\n    \"source\": \"Bron\",\n    \"updated_at\": \"Bijgewerkt op\",\n    \"title\": \"Titel\",\n    \"description\": \"Beschrijving\",\n    \"summary\": \"Samenvatting\",\n    \"quota\": \"Quota\",\n    \"bookmarks\": \"Bladwijzers\",\n    \"storage\": \"Opslag\",\n    \"pdf\": \"Gearchiveerde PDF\",\n    \"default\": \"Standaard\",\n    \"id\": \"ID\",\n    \"last_used\": \"Laatst gebruikt\"\n  },\n  \"layouts\": {\n    \"list\": \"Lijst\",\n    \"masonry\": \"Metselwerk\",\n    \"grid\": \"Rooster\",\n    \"compact\": \"Compact\"\n  },\n  \"actions\": {\n    \"close_bulk_edit\": \"Bulk bewerking sluiten\",\n    \"delete\": \"Verwijderen\",\n    \"change_layout\": \"Wijzig Layout\",\n    \"archive\": \"Archiveer\",\n    \"unarchive\": \"Verwijder uit archief\",\n    \"unfavorite\": \"Verwijder uit favorieten\",\n    \"refresh\": \"Vernieuwen\",\n    \"download_full_page_archive\": \"Download volledig pagina archief\",\n    \"edit_tags\": \"Wijzig Tags\",\n    \"add_to_list\": \"Toevoegen aan lijst\",\n    \"select_all\": \"Selecteer alles\",\n    \"unselect_all\": \"Deselecteer alles\",\n    \"bulk_edit\": \"Bulk bewerking\",\n    \"remove_from_list\": \"Verwijder uit lijst\",\n    \"add\": \"Toevoegen\",\n    \"edit\": \"Wijzigen\",\n    \"create\": \"Creëer\",\n    \"fetch_now\": \"Nu ophalen\",\n    \"summarize_with_ai\": \"Samenvatten met AI\",\n    \"manage_lists\": \"Lijsten beheren\",\n    \"favorite\": \"Favoriet\",\n    \"copy_link\": \"Kopieër link\",\n    \"save\": \"Bewaar\",\n    \"recrawl\": \"Opnieuw doorzoeken\",\n    \"apply_all\": \"Alles Toepassen\",\n    \"edit_title\": \"Wijzig Titel\",\n    \"sign_out\": \"Afmelden\",\n    \"close\": \"Sluiten\",\n    \"merge\": \"Samenvoegen\",\n    \"cancel\": \"Annuleer\",\n    \"ignore\": \"Negeren\",\n    \"sort\": {\n      \"title\": \"Sorteren\",\n      \"newest_first\": \"Nieuwste eerst\",\n      \"oldest_first\": \"Oudste eerst\",\n      \"relevant_first\": \"Meest relevant eerst\"\n    },\n    \"open_editor\": \"Editor openen\",\n    \"toggle_show_archived\": \"Gearchiveerde items weergeven\",\n    \"confirm\": \"Bevestigen\",\n    \"regenerate\": \"Opnieuw genereren\",\n    \"load_more\": \"Laad meer\",\n    \"edit_notes\": \"Notities bewerken\",\n    \"preserve_as_pdf\": \"Opslaan als PDF\",\n    \"offline_copies\": \"Offline kopieën\",\n    \"remove\": \"Verwijderen\",\n    \"more\": \"Meer\",\n    \"replace_banner\": \"Banner vervangen\",\n    \"add_banner\": \"Banner toevoegen\",\n    \"preserve_offline_archive\": \"Offline archief behouden\",\n    \"download_full_page_archive_file\": \"Archiefbestand downloaden\",\n    \"download_pdf_file\": \"PDF-bestand downloaden\",\n    \"download\": \"Downloaden\"\n  },\n  \"settings\": {\n    \"ai\": {\n      \"ai_settings\": \"AI Instellingen\",\n      \"tagging_rules\": \"Labels Regels\",\n      \"text_prompt\": \"Tekstprompt\",\n      \"prompt_preview\": \"Prompt voorbeeld\",\n      \"images_prompt\": \"Afbeelding prompt\",\n      \"summarization\": \"Samenvatting\",\n      \"summarization_prompt\": \"Prompt voor samenvatting\",\n      \"all_tagging\": \"Alle tags\",\n      \"text_tagging\": \"Tekst taggen\",\n      \"image_tagging\": \"Afbeeldingen taggen\",\n      \"tagging_rule_description\": \"Prompts die je hier toevoegt, worden opgenomen als regels voor het model tijdens het genereren van tags. Je kunt de uiteindelijke prompts bekijken in het promptvoorbeeldgedeelte.\",\n      \"tag_style\": \"Tagstijl\",\n      \"auto_summarization_description\": \"Genereer automatisch samenvattingen voor je bladwijzers met behulp van AI.\",\n      \"auto_tagging\": \"Automatisch labelen\",\n      \"titlecase_spaces\": \"Hoofdletters met spaties\",\n      \"lowercase_underscores\": \"Kleine letters met underscores\",\n      \"inference_language\": \"Inferentietalen\",\n      \"titlecase_hyphens\": \"Hoofdletters met koppeltekens\",\n      \"lowercase_hyphens\": \"Kleine letters met koppeltekens\",\n      \"lowercase_spaces\": \"Kleine letters met spaties\",\n      \"inference_language_description\": \"Kies taal voor door AI gegenereerde tags en samenvattingen.\",\n      \"tag_style_description\": \"Kies hoe je automatisch gegenereerde tags moeten worden opgemaakt.\",\n      \"auto_tagging_description\": \"Genereer automatisch tags voor je bladwijzers met behulp van AI.\",\n      \"camelCase\": \"camelCase\",\n      \"auto_summarization\": \"Automatische samenvatting\",\n      \"no_preference\": \"Geen voorkeur\",\n      \"curated_tags\": \"Samengestelde tags\",\n      \"curated_tags_description\": \"Beperk optioneel de AI-tagging om alleen tags uit deze lijst te gebruiken. Als er geen tags zijn geselecteerd, genereert de AI tags vrijelijk.\",\n      \"curated_tags_updated\": \"Samengestelde tags succesvol bijgewerkt!\",\n      \"curated_tags_update_failed\": \"Kon de samengestelde tags niet bijwerken\"\n    },\n    \"import\": {\n      \"import_export\": \"Importeren / Exporteren\",\n      \"imported_bookmarks\": \"Geïmporteerde Bladwijzers\",\n      \"import_export_bookmarks\": \"Importeer / Exporteer Bladwijzers\",\n      \"import_bookmarks_from_html_file\": \"Importeer Bladwijzers van HTML bestand\",\n      \"import_bookmarks_from_pocket_export\": \"Importeer Bladwijzers van Pocket export\",\n      \"import_bookmarks_from_matter_export\": \"Importeer Bladwijzers van Matter export\",\n      \"import_bookmarks_from_omnivore_export\": \"Bladwijzers importeren uit Omnivore export\",\n      \"import_bookmarks_from_linkwarden_export\": \"Bladwijzers importeren uit Linkwarden-export\",\n      \"import_bookmarks_from_karakeep_export\": \"Bladwijzers importeren uit Karakeep-export\",\n      \"export_links_and_notes\": \"Links en notities exporteren\",\n      \"import_bookmarks_from_tab_session_manager_export\": \"Bladwijzers importeren uit Tab Session Manager\",\n      \"import_bookmarks_from_mymind_export\": \"Bladwijzers importeren uit mijnmind-export\",\n      \"import_bookmarks_from_instapaper_export\": \"Bladwijzers importeren uit Instapaper-export\"\n    },\n    \"broken_links\": {\n      \"broken_links\": \"Gebroken Links\",\n      \"last_crawled_at\": \"Laatste Crawl Op\",\n      \"crawling_status\": \"Crawl Status\",\n      \"crawling_failed\": \"Crawl Mislukt\"\n    },\n    \"api_keys\": {\n      \"key_success_please_copy\": \"Kopieer de sleutel en bewaar het op een veilige plaats. Wanneer je de dialoog sluit, zal je deze niet meer opnieuw kunnen raadplegen.\",\n      \"key_success\": \"Sleutel werd succesvol aangemaakt\",\n      \"api_keys\": \"API Sleutels\",\n      \"new_api_key\": \"Nieuwe API Sleutel\",\n      \"new_api_key_desc\": \"Geef je API sleutel een unieke naam\",\n      \"regenerate_api_key\": \"API-sleutel opnieuw genereren\",\n      \"key_regenerated\": \"Sleutel is succesvol opnieuw gegenereerd\",\n      \"key_regenerated_please_copy\": \"Kopieer de nieuwe sleutel en bewaar hem op een veilige plaats. De oude sleutel is ingetrokken en werkt niet meer.\",\n      \"regenerate_warning\": \"Weet je zeker dat je de API-sleutel '{{name}}' opnieuw wilt genereren?\",\n      \"regenerate_confirmation\": \"Hiermee wordt de huidige sleutel ingetrokken en een nieuwe gegenereerd. Alle applicaties die de huidige sleutel gebruiken, werken niet meer.\"\n    },\n    \"info\": {\n      \"user_info\": \"Gebruikersinfo\",\n      \"basic_details\": \"Algemene Details\",\n      \"change_password\": \"Wijzig Wachtwoord\",\n      \"current_password\": \"Huidige Wachtwoord\",\n      \"new_password\": \"Nieuwe Wachtwoord\",\n      \"options\": \"Opties\",\n      \"confirm_new_password\": \"Confirmeer Nieuwe Wachtwoord\",\n      \"interface_lang\": \"Interface Taal\",\n      \"user_settings\": {\n        \"user_settings_updated\": \"Gebruikersinstellingen zijn bijgewerkt!\",\n        \"bookmark_click_action\": {\n          \"title\": \"Actie bij klikken op bladwijzer\",\n          \"open_external_url\": \"Originele URL openen\",\n          \"open_bookmark_details\": \"Bladwijzerdetails openen\"\n        },\n        \"archive_display_behaviour\": {\n          \"title\": \"Gearchiveerde bladwijzers\",\n          \"show\": \"Gearchiveerde bladwijzers weergeven in tags en lijsten\",\n          \"hide\": \"Gearchiveerde bladwijzers verbergen in tags en lijsten\"\n        }\n      },\n      \"reader_settings\": {\n        \"local_overrides_title\": \"Apparaatspecifieke instellingen actief\",\n        \"using_default\": \"Standaardinstelling van de client gebruiken\",\n        \"clear_override_hint\": \"Apparaatoverschrijving wissen om algemene instelling te gebruiken ({{value}})\",\n        \"font_size\": \"Lettergrootte\",\n        \"font_family\": \"Lettertypefamilie\",\n        \"preview_inline\": \"(voorbeeld)\",\n        \"tooltip_preview\": \"Niet-opgeslagen voorbeeldwijzigingen\",\n        \"save_to_all_devices\": \"Alle apparaten\",\n        \"tooltip_local\": \"Apparaatinstellingen verschillen van algemene instellingen\",\n        \"reset_preview\": \"Voorbeeld resetten\",\n        \"mono\": \"Monospace\",\n        \"line_height\": \"Regelhoogte\",\n        \"tooltip_default\": \"Leesinstellingen\",\n        \"title\": \"Lezerinstellingen\",\n        \"serif\": \"Serif\",\n        \"preview\": \"Voorbeeld\",\n        \"not_set\": \"Niet ingesteld\",\n        \"clear_local_overrides\": \"Apparaatinstellingen wissen\",\n        \"preview_text\": \"The quick brown fox jumps over the lazy dog. Zo ziet de tekst in je lezerweergave eruit.\",\n        \"local_overrides_cleared\": \"Apparaatspecifieke instellingen zijn gewist\",\n        \"local_overrides_description\": \"Dit apparaat heeft lezerinstellingen die afwijken van je globale standaardwaarden:\",\n        \"clear_defaults\": \"Alle standaarden wissen\",\n        \"description\": \"Configureer de standaard tekstinstellingen voor de lezerweergave. Deze instellingen worden gesynchroniseerd op al je apparaten.\",\n        \"defaults_cleared\": \"Standaardwaarden van de lezer zijn gewist\",\n        \"save_hint\": \"Instellingen opslaan alleen voor dit apparaat of synchroniseren op alle apparaten\",\n        \"save_as_default\": \"Opslaan als standaard\",\n        \"save_to_device\": \"Dit apparaat\",\n        \"sans\": \"Sans Serif\",\n        \"tooltip_preview_and_local\": \"Niet-opgeslagen voorbeeldwijzigingen; apparaatinstellingen verschillen van algemene instellingen\",\n        \"adjust_hint\": \"Pas de bovenstaande instellingen aan om een voorbeeld van de wijzigingen te bekijken\"\n      },\n      \"avatar\": {\n        \"upload\": \"Avatar uploaden\",\n        \"change\": \"Avatar wijzigen\",\n        \"remove_confirm_title\": \"Avatar verwijderen?\",\n        \"updated\": \"Avatar bijgewerkt\",\n        \"removed\": \"Avatar verwijderd\",\n        \"description\": \"Upload een vierkante afbeelding om als je avatar te gebruiken.\",\n        \"remove_confirm_description\": \"Hiermee verwijder je je huidige profielfoto.\",\n        \"title\": \"Profielfoto\",\n        \"remove\": \"Avatar verwijderen\"\n      }\n    },\n    \"back_to_app\": \"Terug Naar App\",\n    \"user_settings\": \"Gebruikersinstellingen\",\n    \"feeds\": {\n      \"rss_subscriptions\": \"RSS Abonnementen\",\n      \"add_a_subscription\": \"Abonnement Toevoegen\",\n      \"feed_enabled\": \"RSS-feed ingeschakeld\",\n      \"feed_disabled\": \"RSS-feed uitgeschakeld\"\n    },\n    \"manage_assets\": {\n      \"manage_assets\": \"Activa beheren\",\n      \"no_assets\": \"Je hebt nog geen assets.\",\n      \"asset_type\": \"Type asset\",\n      \"delete_asset_confirmation\": \"Weet je zeker dat je dit item wilt verwijderen?\",\n      \"bookmark_link\": \"Bladwijzerlink\",\n      \"asset_link\": \"Asset-link\",\n      \"delete_asset\": \"Asset verwijderen\"\n    },\n    \"webhooks\": {\n      \"webhooks\": \"Webhooks\",\n      \"description\": \"Je kunt webhooks gebruiken om acties te activeren wanneer bladwijzers worden gemaakt, gewijzigd of gecrawld.\",\n      \"events\": {\n        \"title\": \"Evenementen\",\n        \"crawled\": \"Gecrawld\",\n        \"created\": \"Aangemaakt\",\n        \"edited\": \"Bewerkt\"\n      },\n      \"auth_token\": \"Auth-token\",\n      \"add_auth_token\": \"Auth-token toevoegen\",\n      \"edit_auth_token\": \"Authenticatietoken bewerken\",\n      \"create_webhook\": \"Webhook maken\",\n      \"delete_webhook\": \"Webhook verwijderen\",\n      \"delete_webhook_confirmation\": \"Weet je zeker dat je deze webhook wilt verwijderen?\",\n      \"edit_webhook\": \"Webhook bewerken\",\n      \"webhook_url\": \"Webhook URL\"\n    },\n    \"rules\": {\n      \"rules\": \"Regelengine\",\n      \"rule_name\": \"Regelnaam\",\n      \"description\": \"Je kunt regels gebruiken om acties te activeren wanneer een gebeurtenis wordt geactiveerd.\",\n      \"ceate_rule\": \"Regel aanmaken\",\n      \"edit_rule\": \"Regel bewerken\",\n      \"save_rule\": \"Regel opslaan\",\n      \"delete_rule\": \"Regel verwijderen\",\n      \"delete_rule_confirmation\": \"Weet je zeker dat je deze regel wilt verwijderen?\",\n      \"whenever\": \"Wanneer ...\",\n      \"if\": \"Indien ...\",\n      \"enter_rule_name\": \"Voer regelnaam in\",\n      \"describe_what_this_rule_does\": \"Beschrijf wat deze regel doet\",\n      \"rule_has_been_created\": \"Regel is aangemaakt!\",\n      \"rule_has_been_updated\": \"Regel is bijgewerkt!\",\n      \"rule_has_been_deleted\": \"Regel is verwijderd!\",\n      \"no_rules_created_yet\": \"Nog geen regels aangemaakt\",\n      \"create_your_first_rule\": \"Maak je eerste regel om je workflow te automatiseren\",\n      \"conditions_types\": {\n        \"always\": \"Altijd\",\n        \"url_contains\": \"URL bevat\",\n        \"imported_from_feed\": \"Geïmporteerd uit feed\",\n        \"bookmark_type_is\": \"Bladwijzertype is\",\n        \"has_tag\": \"Heeft tag\",\n        \"is_favourited\": \"Is favoriet\",\n        \"is_archived\": \"Is Gearchiveerd\",\n        \"and\": \"Al het volgende is waar\",\n        \"or\": \"Als een van de volgende waar is\",\n        \"url_does_not_contain\": \"URL bevat niet\",\n        \"title_contains\": \"Titel bevat\",\n        \"title_does_not_contain\": \"Titel bevat niet\"\n      },\n      \"actions_types\": {\n        \"add_tag\": \"Tag toevoegen\",\n        \"remove_tag\": \"Tag verwijderen\",\n        \"add_to_list\": \"Toevoegen aan lijst\",\n        \"remove_from_list\": \"Verwijderen uit lijst\",\n        \"download_full_page_archive\": \"Volledige pagina-archief downloaden\",\n        \"favourite_bookmark\": \"Favoriete bladwijzer\",\n        \"archive_bookmark\": \"Bladwijzer archiveren\"\n      },\n      \"events_types\": {\n        \"bookmark_added\": \"Er is een bladwijzer toegevoegd\",\n        \"tag_added\": \"Deze tag is toegevoegd aan een bladwijzer\",\n        \"tag_removed\": \"Deze tag is verwijderd van een bladwijzer\",\n        \"added_to_list\": \"Een bladwijzer is toegevoegd aan deze lijst\",\n        \"removed_from_list\": \"Een bladwijzer is verwijderd uit deze lijst\",\n        \"favourited\": \"Een bladwijzer is favoriet\",\n        \"archived\": \"Een bladwijzer is gearchiveerd\"\n      }\n    },\n    \"stats\": {\n      \"usage_statistics\": \"Gebruiksstatistieken\",\n      \"insights_description\": \"Inzichten in je bladwijzergewoonten en -verzameling\",\n      \"failed_to_load\": \"Statistieken konden niet worden geladen\",\n      \"overview\": {\n        \"total_bookmarks\": \"Totaal aantal bladwijzers\",\n        \"all_saved_items\": \"Alle opgeslagen items\",\n        \"favorites\": \"Favorieten\",\n        \"starred_bookmarks\": \"Bladwijzers met ster\",\n        \"archived\": \"Gearchiveerd\",\n        \"archived_items\": \"Gearchiveerde items\",\n        \"tags\": \"Tags\",\n        \"unique_tags_created\": \"Unieke tags aangemaakt\",\n        \"lists\": \"Lijsten\",\n        \"bookmark_collections\": \"Bladwijzerverzamelingen\",\n        \"highlights\": \"Markeringen\",\n        \"text_highlights\": \"Tekstmarkeringen\",\n        \"storage_used\": \"Gebruikte opslag\",\n        \"total_asset_storage\": \"Totale opslag van activa\",\n        \"this_month\": \"Deze maand\",\n        \"bookmarks_added\": \"Bladwijzers toegevoegd\"\n      },\n      \"bookmark_types\": {\n        \"title\": \"Bladwijzertypen\",\n        \"links\": \"Links\",\n        \"text_notes\": \"Tekstnotities\",\n        \"assets\": \"Activa\"\n      },\n      \"recent_activity\": {\n        \"title\": \"Recente activiteit\",\n        \"this_week\": \"Deze week\",\n        \"this_month\": \"Deze maand\",\n        \"this_year\": \"Dit jaar\"\n      },\n      \"top_domains\": {\n        \"title\": \"Topdomeinen\",\n        \"no_domains_found\": \"Geen domeinen gevonden\"\n      },\n      \"most_used_tags\": {\n        \"title\": \"Meest gebruikte tags\",\n        \"no_tags_found\": \"Geen tags gevonden\"\n      },\n      \"activity_patterns\": {\n        \"activity_by_hour\": \"Activiteit per uur\",\n        \"activity_by_day\": \"Activiteit per dag\"\n      },\n      \"storage_breakdown\": {\n        \"title\": \"Opslagoverzicht\"\n      },\n      \"bookmark_sources\": {\n        \"title\": \"Bookmarkbronnen\",\n        \"empty\": \"Geen brongegevens beschikbaar\"\n      }\n    },\n    \"subscription\": {\n      \"subscription\": \"Abonnement\",\n      \"manage_subscription\": \"Beheer je abonnement en factuurgegevens\",\n      \"current_plan\": \"Huidig abonnement\",\n      \"billing_period\": \"Factureringsperiode\",\n      \"paid_plan\": \"Betaald abonnement\",\n      \"unlock_bigger_quota\": \"Ontgrendel een groter quotum en steun het project\",\n      \"subscribe_now\": \"Abonneer nu\",\n      \"manage_billing\": \"Facturering beheren\",\n      \"subscription_canceled\": \"Je abonnement is geannuleerd en eindigt op {{date}}. Je kunt je op elk moment opnieuw abonneren.\",\n      \"usage_quotas\": \"Gebruik & Quota's\",\n      \"track_usage\": \"Volg je huidige gebruik ten opzichte van je planlimieten\",\n      \"total_bookmarks_saved\": \"Totaal aantal opgeslagen bladwijzers\",\n      \"assets_file_storage\": \"Opslag van activa en bestanden\",\n      \"unlimited_usage\": \"Onbeperkt gebruik\",\n      \"quota_limit_reached\": \"Quotumlimiet bereikt\",\n      \"approaching_quota_limit\": \"Nadert quotumlimiet\",\n      \"loading_usage\": \"Gebruiksinformatie laden...\",\n      \"free\": \"Gratis\",\n      \"paid\": \"Betaald\"\n    },\n    \"import_sessions\": {\n      \"title\": \"Sessies importeren\",\n      \"description\": \"Bekijk en beheer je bulk import sessies. Sessies worden automatisch aangemaakt wanneer je bladwijzers importeert.\",\n      \"load_error\": \"Importeren van sessies mislukt\",\n      \"no_sessions\": \"Nog geen import sessies\",\n      \"no_sessions_detail\": \"Import sessies verschijnen hier automatisch wanneer je bladwijzers importeert.\",\n      \"created_at\": \"Aangemaakt {{time}}\",\n      \"progress\": \"Voortgang\",\n      \"status\": {\n        \"pending\": \"In behandeling\",\n        \"in_progress\": \"Bezig\",\n        \"completed\": \"Voltooid\",\n        \"failed\": \"Mislukt\",\n        \"processing\": \"Verwerken\",\n        \"staging\": \"Voorbereiding\",\n        \"running\": \"Actief\",\n        \"paused\": \"Gepauzeerd\"\n      },\n      \"badges\": {\n        \"pending\": \"{{count}} in behandeling\",\n        \"processing\": \"{{count}} bezig\",\n        \"completed\": \"{{count}} voltooid\",\n        \"failed\": \"{{count}} mislukt\"\n      },\n      \"imported_to\": \"Geïmporteerd naar:\",\n      \"view_list\": \"Lijst bekijken\",\n      \"delete_dialog_title\": \"Importsessie verwijderen\",\n      \"delete_dialog_description\": \"Weet je zeker dat je “{{name}}” wilt verwijderen? Dit kan niet ongedaan worden gemaakt. De bladwijzers zelf worden niet verwijderd.\",\n      \"delete_session\": \"Sessie verwijderen\",\n      \"pause_session\": \"Pauzeren\",\n      \"resume_session\": \"Hervatten\",\n      \"view_details\": \"Details bekijken\",\n      \"detail\": {\n        \"page_title\": \"Details importeer sessie\",\n        \"back_to_import\": \"Terug naar importeren\",\n        \"filter_all\": \"Alles\",\n        \"filter_accepted\": \"Geaccepteerd\",\n        \"filter_rejected\": \"Afgewezen\",\n        \"filter_duplicates\": \"Duplicaten\",\n        \"filter_pending\": \"In behandeling\",\n        \"table_title\": \"Titel / URL\",\n        \"table_type\": \"Type\",\n        \"table_result\": \"Resultaat\",\n        \"table_reason\": \"Reden\",\n        \"table_bookmark\": \"Bladwijzer\",\n        \"result_accepted\": \"Geaccepteerd\",\n        \"result_rejected\": \"Afgewezen\",\n        \"result_skipped_duplicate\": \"Duplicaat\",\n        \"result_pending\": \"In behandeling\",\n        \"result_processing\": \"Verwerken\",\n        \"no_results\": \"Geen resultaten gevonden voor dit filter.\",\n        \"view_bookmark\": \"Bladwijzer bekijken\",\n        \"load_more\": \"Meer laden\",\n        \"no_title\": \"Geen titel\"\n      }\n    },\n    \"backups\": {\n      \"backups\": \"Back-ups\",\n      \"page_title\": \"Back-ups\",\n      \"page_description\": \"Maak automatisch back-ups van je bladwijzers en beheer ze. Back-ups worden gecomprimeerd en veilig opgeslagen.\",\n      \"configuration\": {\n        \"title\": \"Back-up configuratie\",\n        \"enable_automatic_backups\": \"Automatische back-ups inschakelen\",\n        \"enable_automatic_backups_description\": \"Maak automatisch back-ups van je bladwijzers\",\n        \"backup_frequency\": \"Back-up frequentie\",\n        \"backup_frequency_description\": \"Hoe vaak back-ups moeten worden gemaakt\",\n        \"retention_period\": \"Bewaarperiode (dagen)\",\n        \"retention_period_description\": \"Hoeveel dagen back-ups moeten worden bewaard voordat ze worden verwijderd\",\n        \"frequency\": {\n          \"daily\": \"Dagelijks\",\n          \"weekly\": \"Wekelijks\"\n        },\n        \"select_frequency\": \"Selecteer frequentie\",\n        \"save_settings\": \"Instellingen opslaan\"\n      },\n      \"list\": {\n        \"title\": \"Jouw back-ups\",\n        \"create_backup_now\": \"Nu een back-up maken\",\n        \"no_backups\": \"Je hebt nog geen back-ups. Schakel automatische back-ups in of maak er handmatig een.\",\n        \"table\": {\n          \"created_at\": \"Gemaakt op\",\n          \"bookmarks\": \"Bladwijzers\",\n          \"size\": \"Grootte\",\n          \"status\": \"Status\",\n          \"actions\": \"Acties\"\n        },\n        \"status\": {\n          \"success\": \"Gelukt\",\n          \"failed\": \"Mislukt\",\n          \"pending\": \"In behandeling\"\n        },\n        \"actions\": {\n          \"download_backup\": \"Backup downloaden\",\n          \"delete_backup\": \"Back-up verwijderen\"\n        }\n      },\n      \"dialogs\": {\n        \"delete_backup_title\": \"Back-up verwijderen?\",\n        \"delete_backup_description\": \"Weet je zeker dat je deze back-up wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.\"\n      },\n      \"toasts\": {\n        \"backup_queued\": \"Back-uptaak is in de wachtrij geplaatst! Het wordt binnenkort verwerkt.\",\n        \"backup_deleted\": \"Back-up is verwijderd!\"\n      }\n    }\n  },\n  \"admin\": {\n    \"server_stats\": {\n      \"server_stats\": \"Server Statistieken\",\n      \"total_users\": \"Totaal Gebruikers\",\n      \"total_bookmarks\": \"Totaal Bladwijzers\",\n      \"server_version\": \"Server Versie\"\n    },\n    \"background_jobs\": {\n      \"failed\": \"Mislukt\",\n      \"crawler_jobs\": \"Crawl Taken\",\n      \"background_jobs\": \"Achtergrondtaken\",\n      \"indexing_jobs\": \"Indexeer Taken\",\n      \"inference_jobs\": \"Inferentie Taken\",\n      \"pending\": \"In behandeling\",\n      \"tidy_assets_jobs\": \"Tidy Assets-taken\",\n      \"asset_preprocessing_jobs\": \"Asset pre-processing taken\",\n      \"queued\": \"In de wachtrij\",\n      \"job\": \"Taak\",\n      \"video_jobs\": \"Taken voor het downloaden van video's\",\n      \"webhook_jobs\": \"Webhook-taken\",\n      \"feed_jobs\": \"RSS-feedtaken\",\n      \"jobs\": {\n        \"crawler\": {\n          \"title\": \"Crawler Jobs\",\n          \"description\": \"Webcrawling en contentextractie van URL's\"\n        },\n        \"inference\": {\n          \"title\": \"Inference Jobs\",\n          \"description\": \"AI-gestuurde tagging en samenvatting van content\"\n        },\n        \"indexing\": {\n          \"title\": \"Indexing Jobs\",\n          \"description\": \"Zoekindexupdates\"\n        },\n        \"asset_preprocessing\": {\n          \"title\": \"Asset Preprocessing Jobs\",\n          \"description\": \"Voorbewerking van afbeeldingen en documenten (screenshots, tekstuitlezing, enz.)\"\n        },\n        \"tidy_assets\": {\n          \"title\": \"Tidy Assets Jobs\",\n          \"description\": \"Asset opschonen en opslagoptimalisatie\"\n        },\n        \"video\": {\n          \"title\": \"Video Download Jobs\",\n          \"description\": \"Video-extractie en download\"\n        },\n        \"webhook\": {\n          \"title\": \"Webhook Jobs\",\n          \"description\": \"Externe webhooknotificaties\"\n        },\n        \"feed\": {\n          \"title\": \"RSS Feed Jobs\",\n          \"description\": \"RSS-feedverwerking en contentupdates\"\n        },\n        \"admin_maintenance\": {\n          \"title\": \"Admin onderhoudstaken\",\n          \"description\": \"Administratieve opschoon en asset onderhoud\"\n        }\n      },\n      \"monitor_and_manage\": \"Monitor en beheer achtergrondtaakwachtrijen en systeemverwerkingstaken\",\n      \"active\": \"Actief\",\n      \"available_actions\": \"Beschikbare acties\",\n      \"status\": {\n        \"title\": \"Inzicht in taakstatussen\",\n        \"queued\": {\n          \"title\": \"In de wachtrij\",\n          \"description\": \"Taken die in de wachtrij staan om verwerkt te worden. Ze starten automatisch wanneer er resources beschikbaar zijn.\"\n        },\n        \"unprocessed\": {\n          \"title\": \"Niet verwerkt\",\n          \"description\": \"Bladwijzers die nog niet zijn verwerkt. Ze staan hoogstwaarschijnlijk al in de wachtrij voor verwerking, zo niet, dan moet je ze mogelijk handmatig opnieuw in de wachtrij plaatsen.\"\n        },\n        \"failed\": {\n          \"title\": \"Mislukt\",\n          \"description\": \"Bladwijzers die fouten hebben ondervonden tijdens de verwerking. Deze hebben mogelijk handmatige aandacht nodig.\"\n        }\n      },\n      \"actions\": {\n        \"recrawl_failed_links_only\": \"Alleen mislukte links opnieuw crawlen\",\n        \"recrawl_all_links\": \"Alle links opnieuw crawlen\",\n        \"without_inference\": \"Zonder gevolgtrekking\",\n        \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Alleen AI-tags voor mislukte bladwijzers opnieuw genereren\",\n        \"regenerate_ai_tags_for_all_bookmarks\": \"AI-tags voor alle bladwijzers opnieuw genereren\",\n        \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Alleen AI-samenvattingen voor mislukte bladwijzers opnieuw genereren\",\n        \"regenerate_ai_summaries_for_all_bookmarks\": \"AI-samenvattingen voor alle bladwijzers opnieuw genereren\",\n        \"reindex_all_bookmarks\": \"Alle bladwijzers opnieuw indexeren\",\n        \"clean_assets\": \"Zwerfbestanden opschonen en metadata opnieuw synchroniseren\",\n        \"reprocess_assets_fix_mode\": \"Niet-verwerkte bestanden opnieuw verwerken\",\n        \"migrate_large_link_html_content\": \"Verplaats grote inline HTML-content naar assets.\",\n        \"recrawl_pending_links_only\": \"Alleen in behandeling zijnde links opnieuw crawlen\",\n        \"regenerate_ai_tags_for_pending_bookmarks_only\": \"AI-tags opnieuw genereren, maar dan alleen voor bladwijzers die in behandeling zijn\",\n        \"regenerate_ai_summaries_for_pending_bookmarks_only\": \"AI-samenvattingen opnieuw genereren, maar dan alleen voor bladwijzers die in behandeling zijn\"\n      }\n    },\n    \"users_list\": {\n      \"users_list\": \"Gebruikerslijst\",\n      \"delete_user\": \"Verwijder Gebruiker\",\n      \"num_bookmarks\": \"Aantal Bladwijzers\",\n      \"local_user\": \"Lokale Gebruiker\",\n      \"reset_password\": \"Reset Wachtwoord\",\n      \"create_user\": \"Maak Gebruiker Aan\",\n      \"change_role\": \"Wijzig Rol\",\n      \"confirm_password\": \"Wachtwoord bevestigen\",\n      \"asset_sizes\": \"Assetformaten\",\n      \"delete_user_confirm_description\": \"Weet je zeker dat je gebruiker \\\"{{name}}\\\" wilt verwijderen?\",\n      \"unlimited\": \"Onbeperkt\"\n    },\n    \"admin_settings\": \"Admin Instellingen\",\n    \"actions\": {\n      \"without_inference\": \"Zonder gevolgtrekking\",\n      \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Alleen AI-tags opnieuw genereren voor mislukte bladwijzers\",\n      \"regenerate_ai_tags_for_all_bookmarks\": \"AI-tags voor alle bladwijzers opnieuw genereren\",\n      \"compact_assets\": \"Compacte assets\",\n      \"reprocess_assets_fix_mode\": \"Activa opnieuw verwerken (Fix-modus)\",\n      \"recrawl_failed_links_only\": \"Alleen mislukte links opnieuw crawlen\",\n      \"recrawl_all_links\": \"Alle links opnieuw crawlen\",\n      \"reindex_all_bookmarks\": \"Alle bladwijzers opnieuw indexeren\",\n      \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"AI-samenvattingen opnieuw genereren, alleen voor mislukte bladwijzers\",\n      \"regenerate_ai_summaries_for_all_bookmarks\": \"AI-samenvattingen opnieuw genereren voor alle bladwijzers\"\n    },\n    \"service_connections\": {\n      \"title\": \"Serviceverbindingen\",\n      \"description\": \"Bewaak de gezondheid en connectiviteit van externe systeemafhankelijkheden\",\n      \"search_engine\": \"Zoekmachine\",\n      \"browser\": \"Browser\",\n      \"queue_system\": \"Wachtsysteem\",\n      \"status\": {\n        \"not_configured\": \"Niet geconfigureerd\",\n        \"connected\": \"Verbonden\",\n        \"disconnected\": \"Niet verbonden\"\n      }\n    },\n    \"admin_tools\": {\n      \"admin_tools\": \"Beheertools\",\n      \"bookmark_debugger\": \"Bladwijzer-foutopsporing\",\n      \"bookmark_id\": \"Bladwijzer-ID\",\n      \"bookmark_id_placeholder\": \"Voer bladwijzer-ID in\",\n      \"lookup\": \"Opzoeken\",\n      \"debug_info\": \"Foutopsporingsinformatie\",\n      \"basic_info\": \"Basis informatie\",\n      \"status\": \"Status\",\n      \"content\": \"Inhoud\",\n      \"html_preview\": \"HTML-voorbeeld (eerste 1000 tekens)\",\n      \"summary\": \"Samenvatting\",\n      \"url\": \"URL\",\n      \"source_url\": \"Bron-URL\",\n      \"asset_type\": \"Type activa\",\n      \"file_name\": \"Bestandsnaam\",\n      \"owner_user_id\": \"Gebruikers-ID van eigenaar\",\n      \"tagging_status\": \"Taggingstatus\",\n      \"summarization_status\": \"Samenvattingsstatus\",\n      \"crawl_status\": \"Crawlingstatus\",\n      \"crawl_status_code\": \"HTTP-statuscode\",\n      \"crawled_at\": \"Gecrawld op\",\n      \"recrawl\": \"Opnieuw crawlen\",\n      \"reindex\": \"Opnieuw indexeren\",\n      \"retag\": \"Opnieuw taggen\",\n      \"resummarize\": \"Opnieuw samenvatten\",\n      \"bookmark_not_found\": \"Bladwijzer niet gevonden\",\n      \"action_success\": \"Actie succesvol voltooid\",\n      \"action_failed\": \"Actie mislukt\",\n      \"recrawl_queued\": \"Opdracht voor opnieuw crawlen is in de wachtrij geplaatst\",\n      \"reindex_queued\": \"Opdracht voor opnieuw indexeren is in de wachtrij geplaatst\",\n      \"retag_queued\": \"Opdracht voor opnieuw taggen is in de wachtrij geplaatst\",\n      \"resummarize_queued\": \"Opdracht voor opnieuw samenvatten is in de wachtrij geplaatst\",\n      \"view\": \"Bekijken\",\n      \"fetch_error\": \"Fout bij ophalen bladwijzer\"\n    }\n  },\n  \"tags\": {\n    \"ai_tags\": \"AI Labels\",\n    \"sort_by_name\": \"Sorteer op naam\",\n    \"your_tags_info\": \"Labels die ten minste eenmaal toegekend zijn door jou\",\n    \"ai_tags_info\": \"Labels die automatisch toegekend zijn (door AI)\",\n    \"all_tags\": \"Alle Labels\",\n    \"your_tags\": \"Jouw Labels\",\n    \"drag_and_drop_merging\": \"Drag & Drop Samenvoegen\",\n    \"unused_tags\": \"Ongebruikte Labels\",\n    \"drag_and_drop_merging_info\": \"Sleep labels naar elkaar toe om ze samen te voegen\",\n    \"unused_tags_info\": \"Labels die niet toegekend zijn aan een bladwijzer\",\n    \"delete_all_unused_tags\": \"Verwijder Alle Ongebruikte Labels\",\n    \"create_tag\": \"Tag aanmaken\",\n    \"create_tag_description\": \"Maak een nieuwe tag aan zonder deze aan een bladwijzer te koppelen\",\n    \"tag_name\": \"Tagnaam\",\n    \"enter_tag_name\": \"Voer de tagnaam in\",\n    \"sort_by_usage\": \"Sorteer op gebruik\",\n    \"sort_by_relevance\": \"Sorteer op relevantie\",\n    \"no_custom_tags\": \"Nog geen aangepaste tags\",\n    \"no_ai_tags\": \"Nog geen AI-tags\",\n    \"no_unused_tags\": \"Je hebt geen ongebruikte tags\",\n    \"no_unused_tags_match_your_search\": \"Geen ongebruikte tags komen overeen met je zoekopdracht\",\n    \"no_tags_match_your_search\": \"Geen tags komen overeen met je zoekopdracht\",\n    \"search_placeholder\": \"Tags zoeken...\",\n    \"search_or_create_placeholder\": \"Tags zoeken of maken...\"\n  },\n  \"preview\": {\n    \"view_original\": \"Toon origineel\",\n    \"cached_content\": \"Inhoud in cache\",\n    \"reader_view\": \"Lezersweergave\",\n    \"tabs\": {\n      \"content\": \"Inhoud\",\n      \"details\": \"Details\"\n    },\n    \"archive_info\": \"Archieven worden mogelijk niet correct inline weergegeven als ze Javascript vereisen. Voor de beste resultaten kun je het <1>downloaden en openen in je browser</1>.\",\n    \"fetch_error_title\": \"Inhoud niet beschikbaar\",\n    \"fetch_error_description\": \"We konden de inhoud voor deze link niet ophalen. De pagina is mogelijk beveiligd, vereist authenticatie of is tijdelijk niet beschikbaar.\",\n    \"crawling_in_progress\": \"Pagina-inhoud ophalen…\",\n    \"continue_reading\": \"Ga verder waar je was gebleven\",\n    \"continue_reading_percent\": \"Ga verder waar je was gebleven ({{percent}}%)\",\n    \"continue_button\": \"Ga door\"\n  },\n  \"editor\": {\n    \"text_toolbar\": {\n      \"bold\": \"Vetgedrukt\",\n      \"italic\": \"Cursief\",\n      \"underline\": \"Onderstrepen\",\n      \"markdown_shortcuts\": {\n        \"label\": \"Markdown sneltoetsen\",\n        \"italic\": {\n          \"label\": \"Cursief\",\n          \"example\": \"*Cursief* of _Cursief_ of CTRL+i\"\n        },\n        \"ordered_list\": {\n          \"label\": \"Geordende lijst\",\n          \"example\": \"1. Opsommingsteken\"\n        },\n        \"unordered_list\": {\n          \"label\": \"Ongeordende lijst\",\n          \"example\": \"- Lijstitem\"\n        },\n        \"inline_code\": {\n          \"example\": \"`Code`\",\n          \"label\": \"Inline code\"\n        },\n        \"block_code\": {\n          \"label\": \"Blokcode\",\n          \"example\": \"``` + spatie\"\n        },\n        \"heading\": {\n          \"example\": \"# H1, ## H2, ### H3\",\n          \"label\": \"Kop\"\n        },\n        \"blockquote\": {\n          \"label\": \"Blokcitaat\",\n          \"example\": \"> Citaat\"\n        },\n        \"bold\": {\n          \"example\": \"**tekst** of CTRL+b\",\n          \"label\": \"Vet\"\n        }\n      },\n      \"redo\": \"Opnieuw\",\n      \"code\": \"Code\",\n      \"highlight\": \"Markeren\",\n      \"align_left\": \"Links uitlijnen\",\n      \"strikethrough\": \"Doorstrepen\",\n      \"align_center\": \"Centraal uitlijnen\",\n      \"align_right\": \"Rechts uitlijnen\",\n      \"undo\": \"Ongedaan maken\"\n    },\n    \"quickly_focus\": \"Je kunt snel op dit veld focussen door op ⌘ + E te drukken\",\n    \"multiple_urls_dialog_title\": \"URL's importeren als afzonderlijke bladwijzers?\",\n    \"import_as_text\": \"Importeren als tekstbladwijzer\",\n    \"import_as_separate_bookmarks\": \"Importeren als afzonderlijke bladwijzers\",\n    \"new_item\": \"NIEUW ITEM\",\n    \"multiple_urls_dialog_desc\": \"De invoer bevat meerdere URL's op afzonderlijke regels. Wil je ze als afzonderlijke bladwijzers importeren?\",\n    \"disabled_submissions\": \"Inzendingen zijn uitgeschakeld\",\n    \"placeholder\": \"Plak hier een link of een afbeelding, schrijf een notitie of sleep een afbeelding hierheen…\",\n    \"placeholder_v2\": \"Plak een link, schrijf een notitie of sleep een afbeelding…\"\n  },\n  \"options\": {\n    \"dark_mode\": \"Donkere Modus\",\n    \"light_mode\": \"Lichte Modus\",\n    \"apps_extensions\": \"Apps & Extensies\",\n    \"documentation\": \"Documentatie\",\n    \"follow_us_on_x\": \"Volg ons op X\"\n  },\n  \"lists\": {\n    \"all_lists\": \"Alle Lijsten\",\n    \"favourites\": \"Favorieten\",\n    \"new_list\": \"Nieuwe Lijst\",\n    \"search_query_help\": \"Lees meer over de zoekopdrachttaal.\",\n    \"no_parent\": \"Geen bovenliggend item\",\n    \"smart_list\": \"Slimme lijst\",\n    \"search_query\": \"Zoekopdracht\",\n    \"edit_list\": \"Lijst bewerken\",\n    \"parent_list\": \"Bovenliggende lijst\",\n    \"list_type\": \"Type lijst\",\n    \"manual_list\": \"Handmatige lijst\",\n    \"new_nested_list\": \"Nieuwe geneste lijst\",\n    \"merge_list\": \"Lijst samenvoegen\",\n    \"destination_list\": \"Bestemmingslijst\",\n    \"delete_after_merge\": \"Originele lijst verwijderen na samenvoegen\",\n    \"no_destination\": \"Geen bestemming\",\n    \"description\": \"Beschrijving (Optioneel)\",\n    \"public_list\": {\n      \"title\": \"Openbare lijst\",\n      \"description\": \"Anderen toestaan deze lijst te bekijken\",\n      \"share_link\": \"Link delen\"\n    },\n    \"share_list\": \"Lijst delen\",\n    \"rss\": {\n      \"title\": \"RSS-feed\",\n      \"description\": \"Een RSS-feed inschakelen voor deze lijst\",\n      \"feed_url\": \"RSS-feed URL\"\n    },\n    \"delete_list\": {\n      \"title\": \"Lijst verwijderen\",\n      \"description\": \"Het verwijderen van een lijst verwijdert geen bladwijzers in die lijst.\",\n      \"delete_children\": \"Verwijder kinderlijsten (recursief)\",\n      \"delete_children_description\": \"Indien niet aangevinkt, worden alle directe kinderlijsten rootlijsten\"\n    },\n    \"shared\": \"Gedeeld\",\n    \"collaborators\": {\n      \"manage\": \"Medewerkers beheren\",\n      \"view\": \"Medewerkers bekijken\",\n      \"collaborators\": \"Medewerkers\",\n      \"add\": \"Medewerker toevoegen\",\n      \"current\": \"Huidige medewerkers\",\n      \"enter_email\": \"E-mailadres invoeren\",\n      \"please_enter_email\": \"Voer een e-mailadres in\",\n      \"added_successfully\": \"Medewerker succesvol toegevoegd\",\n      \"failed_to_add\": \"Kon medewerker niet toevoegen\",\n      \"removed\": \"Medewerker verwijderd\",\n      \"failed_to_remove\": \"Kon medewerker niet verwijderen\",\n      \"role_updated\": \"Rol bijgewerkt\",\n      \"failed_to_update_role\": \"Kon rol niet bijwerken\",\n      \"viewer\": \"Viewer\",\n      \"editor\": \"Editor\",\n      \"owner\": \"Eigenaar\",\n      \"viewer_description\": \"Kan bladwijzers in de lijst bekijken\",\n      \"editor_description\": \"Kan bladwijzers toevoegen en verwijderen\",\n      \"no_collaborators\": \"Nog geen medewerkers. Voeg iemand toe om te beginnen met samenwerken!\",\n      \"no_collaborators_readonly\": \"Geen medegebruikers voor deze lijst.\",\n      \"people_with_access\": \"Mensen die toegang hebben tot deze lijst\",\n      \"add_or_remove\": \"Mensen toevoegen of verwijderen die toegang hebben tot deze lijst\",\n      \"invitation_sent\": \"Uitnodiging succesvol verstuurd\",\n      \"invitation_revoked\": \"Uitnodiging ingetrokken\",\n      \"failed_to_revoke\": \"Kon uitnodiging niet intrekken\",\n      \"pending\": \"In behandeling\",\n      \"revoke\": \"Intrekken\",\n      \"declined\": \"Afgewezen\"\n    },\n    \"leave_list\": {\n      \"title\": \"Lijst verlaten\",\n      \"confirm_message\": \"Weet je zeker dat je {{icon}} {{name}} wilt verlaten?\",\n      \"warning\": \"Je kunt geen bladwijzers meer bekijken of openen in deze lijst. De eigenaar van de lijst kan je indien nodig weer toevoegen.\",\n      \"action\": \"Lijst verlaten\",\n      \"success\": \"Je hebt \\\"{{icon}} {{name}}\\\" verlaten\"\n    },\n    \"invitations\": {\n      \"pending\": \"Openstaande uitnodigingen\",\n      \"description\": \"Bekijk en beantwoord uitnodigingen voor samenwerking in lijsten\",\n      \"invited_by\": \"Uitgenodigd door\",\n      \"accept\": \"Accepteren\",\n      \"decline\": \"Weigeren\",\n      \"accepted\": \"Uitnodiging geaccepteerd\",\n      \"declined\": \"Uitnodiging geweigerd\",\n      \"failed_to_accept\": \"Kon uitnodiging niet accepteren\",\n      \"failed_to_decline\": \"Kon uitnodiging niet weigeren\"\n    },\n    \"shared_lists\": \"Gedeelde lijsten\"\n  },\n  \"search\": {\n    \"full_text_search\": \"Volledige tekst zoeken\",\n    \"is_favorited\": \"Is favoriet\",\n    \"is_not_favorited\": \"Is geen favoriet\",\n    \"is_archived\": \"Is gearchiveerd\",\n    \"is_not_in_any_list\": \"Staat niet in een lijst\",\n    \"type_is_not\": \"Type is niet\",\n    \"and\": \"En\",\n    \"not_created_on_or_after\": \"Niet gemaakt op of na\",\n    \"created_on_or_before\": \"Gemaakt op of voor\",\n    \"not_created_on_or_before\": \"Niet gemaakt op of voor\",\n    \"url_contains\": \"URL bevat\",\n    \"url_does_not_contain\": \"URL bevat niet\",\n    \"is_in_list\": \"Staat in lijst\",\n    \"is_not_in_list\": \"Staat niet in de lijst\",\n    \"has_tag\": \"Heeft tag\",\n    \"does_not_have_tag\": \"Heeft geen tag\",\n    \"or\": \"Of\",\n    \"is_not_archived\": \"Is niet gearchiveerd\",\n    \"has_any_tag\": \"Heeft een tag\",\n    \"has_no_tags\": \"Heeft geen tag\",\n    \"is_in_any_list\": \"Staat in een lijst\",\n    \"created_on_or_after\": \"Gemaakt op of na\",\n    \"type_is\": \"Type is\",\n    \"is_from_feed\": \"Komt uit RSS-feed\",\n    \"is_not_from_feed\": \"Komt niet uit RSS-feed\",\n    \"created_within\": \"Gemaakt binnen\",\n    \"created_earlier_than\": \"Eerder gemaakt dan\",\n    \"day_s\": \" Dagen\",\n    \"week_s\": \" Weken\",\n    \"month_s\": \" Maanden\",\n    \"year_s\": \" Jaar\",\n    \"day_s_ago\": \" Dagen geleden\",\n    \"week_s_ago\": \" Weken geleden\",\n    \"month_s_ago\": \" Maanden geleden\",\n    \"year_s_ago\": \" Jaar geleden\",\n    \"history\": \"Recente zoekopdrachten\",\n    \"title_contains\": \"Titel bevat\",\n    \"title_does_not_contain\": \"Titel bevat niet\",\n    \"is_broken_link\": \"Heeft een verbroken link\",\n    \"tags\": \"Labels\",\n    \"no_suggestions\": \"Geen suggesties\",\n    \"filters\": \"Filters\",\n    \"is_not_broken_link\": \"Heeft een werkende link\",\n    \"lists\": \"Lijsten\",\n    \"feeds\": \"Feeds\",\n    \"is_from_source\": \"Bron is\",\n    \"is_not_from_source\": \"Bron is niet\"\n  },\n  \"dialogs\": {\n    \"bookmarks\": {\n      \"delete_confirmation_description\": \"Weet je zeker dat je dit bladwijzer wilt verwijderen?\",\n      \"delete_confirmation_title\": \"Bladwijzer verwijderen?\"\n    }\n  },\n  \"toasts\": {\n    \"bookmarks\": {\n      \"refetch\": \"Opnieuw ophalen is in de wachtrij geplaatst!\",\n      \"full_page_archive\": \"Het aanmaken van een volledig pagina-archief is gestart\",\n      \"updated\": \"De bladwijzer is bijgewerkt!\",\n      \"deleted\": \"De bladwijzer is verwijderd!\",\n      \"delete_from_list\": \"De bladwijzer is uit de lijst verwijderd\",\n      \"clipboard_copied\": \"Link is naar je klembord gekopieerd!\",\n      \"preserve_pdf\": \"PDF-opslag is geactiveerd\",\n      \"update_banner\": \"Banner is bijgewerkt!\",\n      \"uploading_banner\": \"Banner uploaden...\"\n    },\n    \"lists\": {\n      \"updated\": \"Lijst is bijgewerkt!\",\n      \"created\": \"Lijst is aangemaakt!\",\n      \"merged\": \"Lijst is samengevoegd!\",\n      \"deleted\": \"Lijst is verwijderd!\"\n    },\n    \"tags\": {\n      \"created\": \"Tag is aangemaakt!\",\n      \"failed_to_create\": \"Kon tag niet aanmaken\"\n    }\n  },\n  \"cleanups\": {\n    \"cleanups\": \"Opschonen\",\n    \"duplicate_tags\": {\n      \"title\": \"Dubbele tags\",\n      \"merge_all_suggestions\": \"Alle suggesties samenvoegen?\"\n    }\n  },\n  \"highlights\": {\n    \"no_highlights\": \"Je hebt nog geen markeringen.\"\n  },\n  \"bookmark_editor\": {\n    \"save_changes\": \"Wijzigingen opslaan\",\n    \"title\": \"Bladwijzer bewerken\",\n    \"subtitle\": \"Breng wijzigingen aan in de bladwijzerdetails. Klik op opslaan als je klaar bent.\",\n    \"author\": \"Auteur\",\n    \"publisher\": \"Uitgever\",\n    \"date_published\": \"Datum gepubliceerd\",\n    \"pick_a_date\": \"Kies een datum\",\n    \"extracted_content\": \"Geëxtraheerde inhoud\"\n  },\n  \"banners\": {\n    \"no_bookmarks\": {\n      \"title\": \"Nog geen bladwijzers\",\n      \"description\": \"Sla interessante artikelen, links en pagina's op om ze later snel te kunnen openen.\"\n    }\n  },\n  \"view_options\": {\n    \"title\": \"Weergaveopties\",\n    \"layout\": \"Lay-out\",\n    \"columns\": \"Kolommen\",\n    \"display_options\": \"Weergaveopties\",\n    \"show_note_previews\": \"Notities weergeven\",\n    \"show_tags\": \"Tags weergeven\",\n    \"show_title\": \"Titel weergeven\",\n    \"image_options\": \"Afbeeldingsopties\",\n    \"image_fit_cover\": \"Cover (vullen)\",\n    \"image_fit_contain\": \"Bevatten (passen)\"\n  },\n  \"version\": {\n    \"new_release_available\": \"Nieuwe release-opmerkingen beschikbaar\",\n    \"whats_new_title\": \"Wat is er nieuw in v{{version}}\",\n    \"release_notes_description\": \"Hier zijn de meest recente updates opgehaald uit de GitHub-release-opmerkingen.\",\n    \"loading_release_notes\": \"Release-opmerkingen aan het laden…\",\n    \"unable_to_load_release_notes\": \"Kan de release-opmerkingen momenteel niet laden. Probeer het later nog eens.\",\n    \"no_release_notes\": \"Er zijn geen release-opmerkingen gepubliceerd voor deze versie.\",\n    \"release_notes_synced\": \"Release-opmerkingen worden gesynchroniseerd vanuit GitHub.\",\n    \"view_on_github\": \"Bekijken op GitHub\"\n  },\n  \"wrapped\": {\n    \"title\": \"Jouw {{year}} Overzicht\",\n    \"subtitle\": \"Een jaar in Karakeep\",\n    \"banner\": {\n      \"title\": \"Jouw 2025 Overzicht is klaar!\",\n      \"description\": \"Bekijk je jaar in bladwijzers\",\n      \"view_now\": \"Bekijk nu\"\n    },\n    \"button\": \"2025 Overzicht\",\n    \"loading\": \"Je overzicht laden...\",\n    \"failed_to_load\": \"Kon je overzichtsstatistieken niet laden\",\n    \"sections\": {\n      \"total_saves\": {\n        \"prefix\": \"Je hebt opgeslagen\",\n        \"suffix\": \"items dit jaar\",\n        \"suffix_singular\": \"item dit jaar\"\n      },\n      \"first_bookmark\": {\n        \"title\": \"Je reis is begonnen\",\n        \"description\": \"Eerste opslag van {{year}}:\"\n      },\n      \"top_domains\": \"Je top sites\",\n      \"top_tags\": \"Je top tags\",\n      \"monthly_activity\": \"Jouw jaar in opgeslagen dingen\",\n      \"most_active_day\": \"Je drukste dag\",\n      \"peak_times\": {\n        \"title\": \"Wanneer je opslaat\",\n        \"peak_hour\": \"Drukste uur\",\n        \"peak_day\": \"Drukste dag\"\n      },\n      \"how_you_save\": \"Hoe je opslaat\",\n      \"what_you_saved\": \"Wat je hebt opgeslagen\",\n      \"summary\": {\n        \"favorites\": \"Favorieten\",\n        \"tags_created\": \"Aangemaakte tags\",\n        \"highlights\": \"Hoogtepunten\"\n      },\n      \"types\": {\n        \"links\": \"Links\",\n        \"notes\": \"Notities\",\n        \"assets\": \"Activa\"\n      }\n    },\n    \"footer\": \"Gemaakt met Karakeep\",\n    \"share\": \"Delen\",\n    \"download\": \"Downloaden\",\n    \"close\": \"Sluiten\",\n    \"generating\": \"Bezig met genereren...\"\n  }\n}\n"
  },
  {
    "path": "apps/web/lib/i18n/locales/pl/translation.json",
    "content": "{\n  \"common\": {\n    \"url\": \"URL\",\n    \"email\": \"E-mail\",\n    \"password\": \"Hasło\",\n    \"name\": \"Nazwa\",\n    \"action\": \"Akcja\",\n    \"created_at\": \"Utworzono\",\n    \"actions\": \"Akcje\",\n    \"roles\": {\n      \"user\": \"Użytkownik\",\n      \"admin\": \"Administrator\"\n    },\n    \"key\": \"Klucz\",\n    \"role\": \"Rola\",\n    \"something_went_wrong\": \"Coś poszło nie tak\",\n    \"experimental\": \"Eksperymentalne\",\n    \"search\": \"Szukaj\",\n    \"tags\": \"Tagi\",\n    \"note\": \"Notatka\",\n    \"attachments\": \"Załączniki\",\n    \"screenshot\": \"Zrzut ekranu\",\n    \"video\": \"Wideo\",\n    \"archive\": \"Archiwum\",\n    \"home\": \"Strona główna\",\n    \"bookmark_types\": {\n      \"title\": \"Typ zakładki\",\n      \"text\": \"Tekst\",\n      \"media\": \"Media\",\n      \"link\": \"Link\"\n    },\n    \"type\": \"Typ\",\n    \"size\": \"Rozmiar\",\n    \"source\": \"Źródło\",\n    \"highlights\": \"Najważniejsze\",\n    \"updated_at\": \"Zaktualizowano\",\n    \"title\": \"Tytuł\",\n    \"description\": \"Opis\",\n    \"summary\": \"Podsumowanie\",\n    \"quota\": \"Limit\",\n    \"bookmarks\": \"Zakładki\",\n    \"storage\": \"Miejsce na dane\",\n    \"pdf\": \"Zarchiwizowane PDF\",\n    \"default\": \"Domyślne\",\n    \"id\": \"ID\",\n    \"last_used\": \"Ostatnio używane\"\n  },\n  \"actions\": {\n    \"remove_from_list\": \"Usuń z listy\",\n    \"summarize_with_ai\": \"Streszcz z AI\",\n    \"close\": \"Zamknij\",\n    \"ignore\": \"Ignoruj\",\n    \"change_layout\": \"Zmień układ\",\n    \"archive\": \"Zarchiwizuj\",\n    \"unarchive\": \"Odarchiwizuj\",\n    \"favorite\": \"Dodaj do ulubionych\",\n    \"delete\": \"Usuń\",\n    \"refresh\": \"Odśwież\",\n    \"recrawl\": \"Ponowne skanowanie\",\n    \"download_full_page_archive\": \"Pobierz pełne archiwum strony\",\n    \"unfavorite\": \"Usuń z ulubionych\",\n    \"edit_tags\": \"Edytuj tagi\",\n    \"add_to_list\": \"Dodaj do listy\",\n    \"select_all\": \"Zaznacz wszystko\",\n    \"unselect_all\": \"Odznacz wszystko\",\n    \"copy_link\": \"Kopiuj link\",\n    \"close_bulk_edit\": \"Zamknij masową edycję\",\n    \"bulk_edit\": \"Masowa edycja\",\n    \"manage_lists\": \"Zarządzaj listami\",\n    \"save\": \"Zapisz\",\n    \"add\": \"Dodaj\",\n    \"edit\": \"Edytuj\",\n    \"create\": \"Utwórz\",\n    \"fetch_now\": \"Pobierz teraz\",\n    \"edit_title\": \"Edytuj tytuł\",\n    \"sign_out\": \"Wyloguj się\",\n    \"merge\": \"Scal\",\n    \"cancel\": \"Anuluj\",\n    \"apply_all\": \"Zastosuj wszystko\",\n    \"sort\": {\n      \"title\": \"Sortowanie\",\n      \"newest_first\": \"Najnowsze pierwsze\",\n      \"oldest_first\": \"Najstarsze pierwsze\",\n      \"relevant_first\": \"Najbardziej trafne na początku\"\n    },\n    \"open_editor\": \"Otwórz edytor\",\n    \"toggle_show_archived\": \"Pokaż zarchiwizowane\",\n    \"confirm\": \"Potwierdź\",\n    \"regenerate\": \"Wygeneruj ponownie\",\n    \"load_more\": \"Załaduj więcej\",\n    \"edit_notes\": \"Edytuj notatki\",\n    \"preserve_as_pdf\": \"Zachowaj jako PDF\",\n    \"offline_copies\": \"Kopie offline\",\n    \"preserve_offline_archive\": \"Zachowaj archiwum offline\",\n    \"download_full_page_archive_file\": \"Pobierz plik archiwum\",\n    \"download_pdf_file\": \"Pobierz plik PDF\",\n    \"remove\": \"Usuń\",\n    \"more\": \"Więcej\",\n    \"replace_banner\": \"Zastąp baner\",\n    \"add_banner\": \"Dodaj baner\",\n    \"download\": \"Pobierz\"\n  },\n  \"settings\": {\n    \"info\": {\n      \"confirm_new_password\": \"Potwierdź nowe hasło\",\n      \"user_info\": \"Informacje o użytkowniku\",\n      \"basic_details\": \"Podstawowe dane\",\n      \"change_password\": \"Zmień hasło\",\n      \"current_password\": \"Obecne hasło\",\n      \"new_password\": \"Nowe hasło\",\n      \"options\": \"Opcje\",\n      \"interface_lang\": \"Język interfejsu\",\n      \"user_settings\": {\n        \"archive_display_behaviour\": {\n          \"show\": \"Pokaż zarchiwizowane zakładki w tagach i listach\",\n          \"hide\": \"Ukryj zarchiwizowane zakładki w tagach i listach\",\n          \"title\": \"Zarchiwizowane zakładki\"\n        },\n        \"user_settings_updated\": \"Ustawienia użytkownika zostały zaktualizowane!\",\n        \"bookmark_click_action\": {\n          \"title\": \"Akcja po kliknięciu zakładki\",\n          \"open_external_url\": \"Otwórz oryginalny URL\",\n          \"open_bookmark_details\": \"Otwórz szczegóły zakładki\"\n        }\n      },\n      \"reader_settings\": {\n        \"local_overrides_title\": \"Aktywne ustawienia specyficzne dla urządzenia\",\n        \"using_default\": \"Użyj ustawień domyślnych klienta\",\n        \"clear_override_hint\": \"Wyczyść ustawienia urządzenia, aby użyć ustawień globalnych ({{value}})\",\n        \"font_size\": \"Rozmiar czcionki\",\n        \"font_family\": \"Rodzina czcionek\",\n        \"preview_inline\": \"(podgląd)\",\n        \"tooltip_preview\": \"Niezapisane zmiany podglądu\",\n        \"save_to_all_devices\": \"Wszystkie urządzenia\",\n        \"tooltip_local\": \"Ustawienia urządzenia różnią się od globalnych\",\n        \"reset_preview\": \"Zresetuj podgląd\",\n        \"mono\": \"Monospace\",\n        \"line_height\": \"Wysokość linii\",\n        \"tooltip_default\": \"Ustawienia czytania\",\n        \"title\": \"Ustawienia czytnika\",\n        \"serif\": \"Z szeryfami\",\n        \"preview\": \"Podgląd\",\n        \"not_set\": \"Nie ustawiono\",\n        \"clear_local_overrides\": \"Wyczyść ustawienia urządzenia\",\n        \"preview_text\": \"The quick brown fox jumps over the lazy dog. Tak będzie wyglądał tekst w widoku czytnika.\",\n        \"local_overrides_cleared\": \"Ustawienia specyficzne dla urządzenia zostały wyczyszczone\",\n        \"local_overrides_description\": \"To urządzenie ma ustawienia czytnika, które różnią się od globalnych ustawień domyślnych:\",\n        \"clear_defaults\": \"Wyczyść wszystkie ustawienia domyślne\",\n        \"description\": \"Skonfiguruj domyślne ustawienia tekstu dla widoku czytnika. Ustawienia te synchronizują się na wszystkich Twoich urządzeniach.\",\n        \"defaults_cleared\": \"Ustawienia domyślne czytnika zostały wyczyszczone\",\n        \"save_hint\": \"Zapisz ustawienia tylko dla tego urządzenia lub synchronizuj na wszystkich urządzeniach\",\n        \"save_as_default\": \"Zapisz jako domyślne\",\n        \"save_to_device\": \"To urządzenie\",\n        \"sans\": \"Bezszeryfowa\",\n        \"tooltip_preview_and_local\": \"Nie zapisano zmian w podglądzie; ustawienia urządzenia różnią się od globalnych\",\n        \"adjust_hint\": \"Dostosuj powyższe ustawienia, aby wyświetlić zmiany w podglądzie\"\n      },\n      \"avatar\": {\n        \"upload\": \"Wrzuć awatar\",\n        \"change\": \"Zmień awatar\",\n        \"remove_confirm_title\": \"Usunąć awatar?\",\n        \"updated\": \"Awatar zaktualizowany\",\n        \"removed\": \"Awatar usunięty\",\n        \"description\": \"Wrzuć kwadratowy obrazek, który będzie Twoim awatarem.\",\n        \"remove_confirm_description\": \"To wyczyści Twoje aktualne zdjęcie profilowe.\",\n        \"title\": \"Zdjęcie profilowe\",\n        \"remove\": \"Usuń awatar\"\n      }\n    },\n    \"import\": {\n      \"import_bookmarks_from_html_file\": \"Importuj zakładki z pliku HTML\",\n      \"import_bookmarks_from_pocket_export\": \"Importuj zakładki z eksportu Pocket\",\n      \"import_bookmarks_from_matter_export\": \"Importuj zakładki z eksportu Matter\",\n      \"import_export\": \"Import / Eksport\",\n      \"import_export_bookmarks\": \"Import / Eksport zakładek\",\n      \"import_bookmarks_from_omnivore_export\": \"Importuj zakładki z eksportu Omnivore\",\n      \"import_bookmarks_from_karakeep_export\": \"Importuj zakładki z eksportu Karakeep\",\n      \"export_links_and_notes\": \"Eksportuj linki i notatki\",\n      \"imported_bookmarks\": \"Zaimportowane zakładki\",\n      \"import_bookmarks_from_linkwarden_export\": \"Importuj zakładki z eksportu Linkwarden\",\n      \"import_bookmarks_from_tab_session_manager_export\": \"Importuj zakładki z Tab Session Manager\",\n      \"import_bookmarks_from_mymind_export\": \"Importuj zakładki z eksportu mymind\",\n      \"import_bookmarks_from_instapaper_export\": \"Importuj zakładki z eksportu Instapaper\"\n    },\n    \"back_to_app\": \"Powrót do aplikacji\",\n    \"user_settings\": \"Ustawienia użytkownika\",\n    \"ai\": {\n      \"ai_settings\": \"Ustawienia AI\",\n      \"tagging_rules\": \"Zasady tagowania\",\n      \"tagging_rule_description\": \"Wpisane tutaj podpowiedzi będą uwzględniane jako zasady podczas generowania tagów. Możesz zobaczyć ostateczne podpowiedzi w sekcji podglądu.\",\n      \"prompt_preview\": \"Podgląd podpowiedzi\",\n      \"text_prompt\": \"Podpowiedź tekstowa\",\n      \"images_prompt\": \"Podpowiedź obrazowa\",\n      \"summarization\": \"Streszczanie\",\n      \"summarization_prompt\": \"Monit o podsumowanie\",\n      \"all_tagging\": \"Wszystkie tagi\",\n      \"text_tagging\": \"Tagowanie tekstu\",\n      \"image_tagging\": \"Tagowanie obrazów\",\n      \"tag_style\": \"Styl tagów\",\n      \"auto_summarization_description\": \"Automatycznie generuj streszczenia dla zakładek za pomocą AI.\",\n      \"auto_tagging\": \"Automatyczne tagowanie\",\n      \"titlecase_spaces\": \"Wielkie litery ze spacjami\",\n      \"lowercase_underscores\": \"Małe litery z podkreślnikami\",\n      \"inference_language\": \"Język wnioskowania\",\n      \"titlecase_hyphens\": \"Wielkie litery z myślnikami\",\n      \"lowercase_hyphens\": \"Małe litery z myślnikami\",\n      \"lowercase_spaces\": \"Małe litery ze spacjami\",\n      \"inference_language_description\": \"Wybierz język dla tagów i podsumowań generowanych przez AI.\",\n      \"tag_style_description\": \"Wybierz, jak powinny być formatowane autogenerowane tagi.\",\n      \"auto_tagging_description\": \"Automatycznie generuj tagi dla zakładek za pomocą AI.\",\n      \"camelCase\": \"camelCase\",\n      \"auto_summarization\": \"Automatyczne podsumowywanie\",\n      \"no_preference\": \"Brak preferencji\",\n      \"curated_tags\": \"Wybrane tagi\",\n      \"curated_tags_description\": \"Opcjonalnie ogranicz oznaczanie przez SI, aby używać tylko tagów z tej listy. Jeśli nie wybrano żadnych tagów, SI generuje tagi swobodnie.\",\n      \"curated_tags_updated\": \"Wybrane tagi zostały pomyślnie zaktualizowane!\",\n      \"curated_tags_update_failed\": \"Nie udało się zaktualizować wybranych tagów\"\n    },\n    \"feeds\": {\n      \"rss_subscriptions\": \"Subskrypcje RSS\",\n      \"add_a_subscription\": \"Dodaj subskrypcję\",\n      \"feed_enabled\": \"Włączono kanał RSS\",\n      \"feed_disabled\": \"Wyłączono kanał RSS\"\n    },\n    \"api_keys\": {\n      \"api_keys\": \"Klucze API\",\n      \"new_api_key\": \"Nowy klucz API\",\n      \"new_api_key_desc\": \"Nadaj swojemu kluczowi API unikalną nazwę\",\n      \"key_success\": \"Klucz został pomyślnie utworzony\",\n      \"key_success_please_copy\": \"Skopiuj klucz i przechowaj go w bezpiecznym miejscu. Po zamknięciu tego okna dialogowego nie będzie można go ponownie zobaczyć.\",\n      \"regenerate_api_key\": \"Wygeneruj ponownie klucz API\",\n      \"key_regenerated\": \"Klucz został pomyślnie wygenerowany ponownie\",\n      \"key_regenerated_please_copy\": \"Skopiuj nowy klucz i przechowuj go w bezpiecznym miejscu. Stary klucz został unieważniony i nie będzie już działać.\",\n      \"regenerate_warning\": \"Czy na pewno chcesz wygenerować ponownie klucz API \\\"{{name}}\\\"?\",\n      \"regenerate_confirmation\": \"Spowoduje to odwołanie bieżącego klucza i wygenerowanie nowego. Wszystkie aplikacje korzystające z bieżącego klucza przestaną działać.\"\n    },\n    \"broken_links\": {\n      \"broken_links\": \"Uszkodzone linki\",\n      \"last_crawled_at\": \"Ostatnie skanowanie\",\n      \"crawling_status\": \"Status skanowania\",\n      \"crawling_failed\": \"Skanowanie nieudane\"\n    },\n    \"manage_assets\": {\n      \"delete_asset_confirmation\": \"Na pewno chcesz usunąć ten zasób?\",\n      \"manage_assets\": \"Zarządzaj zasobami\",\n      \"no_assets\": \"Nie masz jeszcze żadnych zasobów.\",\n      \"asset_type\": \"Typ zasobu\",\n      \"bookmark_link\": \"Link do zakładki\",\n      \"asset_link\": \"Link do zasobu\",\n      \"delete_asset\": \"Usuń zasób\"\n    },\n    \"webhooks\": {\n      \"webhooks\": \"Webhooki\",\n      \"description\": \"Możesz użyć webhooków, aby wywoływać akcje, gdy zakładki są tworzone, zmieniane lub przeszukiwane.\",\n      \"events\": {\n        \"title\": \"Wydarzenia\",\n        \"crawled\": \"Przeszukano\",\n        \"created\": \"Utworzono\",\n        \"edited\": \"Edytowano\"\n      },\n      \"auth_token\": \"Token autoryzacyjny\",\n      \"add_auth_token\": \"Dodaj token autoryzacyjny\",\n      \"edit_auth_token\": \"Edytuj token autoryzacyjny\",\n      \"create_webhook\": \"Utwórz Webhook\",\n      \"delete_webhook\": \"Usuń Webhook\",\n      \"delete_webhook_confirmation\": \"Na pewno chcesz usunąć ten webhook?\",\n      \"edit_webhook\": \"Edytuj webhook\",\n      \"webhook_url\": \"Adres URL webhooka\"\n    },\n    \"rules\": {\n      \"rules\": \"Silnik reguł\",\n      \"rule_name\": \"Nazwa reguły\",\n      \"description\": \"Możesz użyć reguł do wyzwalania akcji, gdy zostanie uruchomione zdarzenie.\",\n      \"ceate_rule\": \"Utwórz regułę\",\n      \"edit_rule\": \"Edytuj regułę\",\n      \"save_rule\": \"Zapisz regułę\",\n      \"delete_rule\": \"Usuń regułę\",\n      \"delete_rule_confirmation\": \"Na pewno chcesz usunąć tę regułę?\",\n      \"whenever\": \"Kiedy tylko...\",\n      \"if\": \"Jeśli...\",\n      \"enter_rule_name\": \"Wprowadź nazwę reguły\",\n      \"describe_what_this_rule_does\": \"Opisz, co robi ta reguła\",\n      \"rule_has_been_created\": \"Zasada została utworzona!\",\n      \"rule_has_been_updated\": \"Reguła została zaktualizowana!\",\n      \"rule_has_been_deleted\": \"Zasada została usunięta!\",\n      \"no_rules_created_yet\": \"Nie utworzono jeszcze żadnych reguł\",\n      \"create_your_first_rule\": \"Stwórz swoją pierwszą regułę, aby zautomatyzować swoją pracę\",\n      \"conditions_types\": {\n        \"always\": \"Zawsze\",\n        \"url_contains\": \"URL zawiera\",\n        \"imported_from_feed\": \"Importowane z kanału\",\n        \"bookmark_type_is\": \"Typ zakładki to\",\n        \"has_tag\": \"Ma tag\",\n        \"is_favourited\": \"Jest ulubiona\",\n        \"is_archived\": \"Zarchiwizowany\",\n        \"and\": \"Wszystkie z poniższych są prawdziwe\",\n        \"or\": \"Którekolwiek z poniższych są prawdziwe\",\n        \"url_does_not_contain\": \"Adres URL nie zawiera\",\n        \"title_contains\": \"Tytuł zawiera\",\n        \"title_does_not_contain\": \"Tytuł nie zawiera\"\n      },\n      \"actions_types\": {\n        \"add_tag\": \"Dodaj tag\",\n        \"remove_tag\": \"Usuń tag\",\n        \"add_to_list\": \"Dodaj do listy\",\n        \"remove_from_list\": \"Usuń z listy\",\n        \"download_full_page_archive\": \"Pobierz pełną archiwizację strony\",\n        \"favourite_bookmark\": \"Ulubiona zakładka\",\n        \"archive_bookmark\": \"Zarchiwizuj zakładkę\"\n      },\n      \"events_types\": {\n        \"bookmark_added\": \"Zakładka została dodana\",\n        \"tag_added\": \"Ten tag jest dodawany do zakładki\",\n        \"tag_removed\": \"Ten tag jest usuwany z zakładki\",\n        \"added_to_list\": \"Zakładka jest dodawana do tej listy\",\n        \"removed_from_list\": \"Zakładka jest usuwana z tej listy\",\n        \"favourited\": \"Zakładka jest oznaczona jako ulubiona\",\n        \"archived\": \"Zakładka jest archiwizowana\"\n      }\n    },\n    \"stats\": {\n      \"usage_statistics\": \"Statystyki użycia\",\n      \"insights_description\": \"Informacje o Twoich nawykach dotyczących zakładek i kolekcji\",\n      \"failed_to_load\": \"Nie udało się załadować statystyk\",\n      \"overview\": {\n        \"total_bookmarks\": \"Wszystkie zakładki\",\n        \"all_saved_items\": \"Wszystkie zapisane elementy\",\n        \"favorites\": \"Ulubione\",\n        \"starred_bookmarks\": \"Zakładki oznaczone gwiazdką\",\n        \"archived\": \"Zarchiwizowane\",\n        \"archived_items\": \"Zarchiwizowane elementy\",\n        \"tags\": \"Tagi\",\n        \"unique_tags_created\": \"Utworzono unikalne tagi\",\n        \"lists\": \"Listy\",\n        \"bookmark_collections\": \"Kolekcje zakładek\",\n        \"highlights\": \"Podświetlenia\",\n        \"text_highlights\": \"Podświetlenia tekstu\",\n        \"storage_used\": \"Wykorzystane miejsce\",\n        \"total_asset_storage\": \"Całkowita ilość miejsca na zasoby\",\n        \"this_month\": \"W tym miesiącu\",\n        \"bookmarks_added\": \"Dodano zakładki\"\n      },\n      \"bookmark_types\": {\n        \"title\": \"Typy zakładek\",\n        \"links\": \"Linki\",\n        \"text_notes\": \"Notatki tekstowe\",\n        \"assets\": \"Zasoby\"\n      },\n      \"recent_activity\": {\n        \"title\": \"Ostatnia aktywność\",\n        \"this_week\": \"W tym tygodniu\",\n        \"this_month\": \"W tym miesiącu\",\n        \"this_year\": \"W tym roku\"\n      },\n      \"top_domains\": {\n        \"title\": \"Najpopularniejsze domeny\",\n        \"no_domains_found\": \"Nie znaleziono domen\"\n      },\n      \"most_used_tags\": {\n        \"title\": \"Najczęściej używane tagi\",\n        \"no_tags_found\": \"Nie znaleziono tagów\"\n      },\n      \"activity_patterns\": {\n        \"activity_by_hour\": \"Aktywność według godziny\",\n        \"activity_by_day\": \"Aktywność według dnia\"\n      },\n      \"storage_breakdown\": {\n        \"title\": \"Podział pamięci\"\n      },\n      \"bookmark_sources\": {\n        \"title\": \"Źródła zakładek\",\n        \"empty\": \"Brak dostępnych danych źródłowych\"\n      }\n    },\n    \"subscription\": {\n      \"subscription\": \"Subskrypcja\",\n      \"manage_subscription\": \"Zarządzaj subskrypcją i informacjami rozliczeniowymi\",\n      \"current_plan\": \"Aktualny plan\",\n      \"billing_period\": \"Okres rozliczeniowy\",\n      \"paid_plan\": \"Płatny plan\",\n      \"unlock_bigger_quota\": \"Odblokuj większy limit i wesprzyj projekt\",\n      \"subscribe_now\": \"Subskrybuj teraz\",\n      \"manage_billing\": \"Zarządzaj rozliczeniami\",\n      \"subscription_canceled\": \"Twoja subskrypcja została anulowana i zakończy się {{date}}. Możesz ją wznowić w dowolnym momencie.\",\n      \"usage_quotas\": \"Wykorzystanie i limity\",\n      \"track_usage\": \"Śledź swoje bieżące zużycie w porównaniu z limitami planu\",\n      \"total_bookmarks_saved\": \"Całkowita liczba zapisanych zakładek\",\n      \"assets_file_storage\": \"Zasoby i przechowywanie plików\",\n      \"unlimited_usage\": \"Nieograniczone użycie\",\n      \"quota_limit_reached\": \"Limit wykorzystania osiągnięty\",\n      \"approaching_quota_limit\": \"Zbliżasz się do limitu\",\n      \"loading_usage\": \"Ładowanie informacji o zużyciu...\",\n      \"free\": \"Darmowy\",\n      \"paid\": \"Płatny\"\n    },\n    \"import_sessions\": {\n      \"title\": \"Importuj sesje\",\n      \"description\": \"Przeglądaj i zarządzaj swoimi sesjami importu zbiorczego. Sesje są tworzone automatycznie podczas importowania zakładek.\",\n      \"load_error\": \"Nie udało się załadować sesji importu\",\n      \"no_sessions\": \"Jeszcze brak sesji importu\",\n      \"no_sessions_detail\": \"Sesje importu pojawią się tutaj automatycznie po zaimportowaniu zakładek\",\n      \"created_at\": \"Utworzono {{time}}\",\n      \"progress\": \"Postęp\",\n      \"status\": {\n        \"pending\": \"Oczekujące\",\n        \"in_progress\": \"W toku\",\n        \"completed\": \"Ukończone\",\n        \"failed\": \"Niepowodzenie\",\n        \"processing\": \"Przetwarzanie\",\n        \"staging\": \"Wersja testowa\",\n        \"running\": \"Uruchomiony\",\n        \"paused\": \"Wstrzymane\"\n      },\n      \"badges\": {\n        \"pending\": \"{{count}} oczekujących\",\n        \"processing\": \"{{count}} w toku\",\n        \"completed\": \"{{count}} ukończonych\",\n        \"failed\": \"{{count}} nieudanych\"\n      },\n      \"imported_to\": \"Zaimportowano do:\",\n      \"view_list\": \"Wyświetl listę\",\n      \"delete_dialog_title\": \"Usuń sesję importu\",\n      \"delete_dialog_description\": \"Czy na pewno chcesz usunąć \\\"{{name}}\\\"? Tej czynności nie można cofnąć. Same zakładki nie zostaną usunięte.\",\n      \"delete_session\": \"Usuń sesję\",\n      \"pause_session\": \"Wstrzymaj\",\n      \"resume_session\": \"Wznów\",\n      \"view_details\": \"Pokaż szczegóły\",\n      \"detail\": {\n        \"page_title\": \"Szczegóły sesji importu\",\n        \"back_to_import\": \"Powrót do importu\",\n        \"filter_all\": \"Wszystkie\",\n        \"filter_accepted\": \"Zaakceptowane\",\n        \"filter_rejected\": \"Odrzucone\",\n        \"filter_duplicates\": \"Duplikaty\",\n        \"filter_pending\": \"Oczekujące\",\n        \"table_title\": \"Tytuł / URL\",\n        \"table_type\": \"Typ\",\n        \"table_result\": \"Wynik\",\n        \"table_reason\": \"Powód\",\n        \"table_bookmark\": \"Zakładka\",\n        \"result_accepted\": \"Zaakceptowane\",\n        \"result_rejected\": \"Odrzucone\",\n        \"result_skipped_duplicate\": \"Duplikat\",\n        \"result_pending\": \"Oczekujące\",\n        \"result_processing\": \"Przetwarzanie\",\n        \"no_results\": \"Nie znaleziono wyników dla tego filtra.\",\n        \"view_bookmark\": \"Pokaż zakładkę\",\n        \"load_more\": \"Załaduj więcej\",\n        \"no_title\": \"Brak tytułu\"\n      }\n    },\n    \"backups\": {\n      \"backups\": \"Kopie zapasowe\",\n      \"page_title\": \"Kopie zapasowe\",\n      \"page_description\": \"Automatycznie twórz kopie zapasowe zakładek i zarządzaj nimi. Kopie zapasowe są kompresowane i bezpiecznie przechowywane.\",\n      \"configuration\": {\n        \"title\": \"Konfiguracja kopii zapasowej\",\n        \"enable_automatic_backups\": \"Włącz automatyczne kopie zapasowe\",\n        \"enable_automatic_backups_description\": \"Automatyczne tworzenie kopii zapasowych zakładek\",\n        \"backup_frequency\": \"Częstotliwość tworzenia kopii zapasowych\",\n        \"backup_frequency_description\": \"Jak często tworzyć kopie zapasowe\",\n        \"retention_period\": \"Okres przechowywania (dni)\",\n        \"retention_period_description\": \"Ile dni przechowywać kopie zapasowe przed ich usunięciem\",\n        \"frequency\": {\n          \"daily\": \"Codziennie\",\n          \"weekly\": \"Co tydzień\"\n        },\n        \"select_frequency\": \"Wybierz częstotliwość\",\n        \"save_settings\": \"Zapisz ustawienia\"\n      },\n      \"list\": {\n        \"title\": \"Twoje kopie zapasowe\",\n        \"create_backup_now\": \"Utwórz kopię zapasową teraz\",\n        \"no_backups\": \"Nie masz jeszcze żadnych kopii zapasowych. Włącz automatyczne tworzenie kopii zapasowych lub utwórz ją ręcznie.\",\n        \"table\": {\n          \"created_at\": \"Utworzono\",\n          \"bookmarks\": \"Zakładki\",\n          \"size\": \"Rozmiar\",\n          \"status\": \"Status\",\n          \"actions\": \"Akcje\"\n        },\n        \"status\": {\n          \"success\": \"Sukces\",\n          \"failed\": \"Niepowodzenie\",\n          \"pending\": \"Oczekuje\"\n        },\n        \"actions\": {\n          \"download_backup\": \"Pobierz kopię zapasową\",\n          \"delete_backup\": \"Usuń kopię zapasową\"\n        }\n      },\n      \"dialogs\": {\n        \"delete_backup_title\": \"Usunąć kopię zapasową?\",\n        \"delete_backup_description\": \"Czy na pewno chcesz usunąć tę kopię zapasową? Nie da się tego cofnąć.\"\n      },\n      \"toasts\": {\n        \"backup_queued\": \"Zadanie utworzenia kopii zapasowej zostało dodane do kolejki! Przetwarzanie rozpocznie się wkrótce.\",\n        \"backup_deleted\": \"Kopia zapasowa została usunięta!\"\n      }\n    }\n  },\n  \"admin\": {\n    \"users_list\": {\n      \"reset_password\": \"Resetuj hasło\",\n      \"num_bookmarks\": \"Liczba zakładek\",\n      \"asset_sizes\": \"Rozmiary zasobów\",\n      \"users_list\": \"Lista użytkowników\",\n      \"create_user\": \"Utwórz użytkownika\",\n      \"change_role\": \"Zmień rolę\",\n      \"delete_user\": \"Usuń użytkownika\",\n      \"local_user\": \"Lokalny użytkownik\",\n      \"confirm_password\": \"Potwierdź hasło\",\n      \"delete_user_confirm_description\": \"Na pewno chcesz usunąć użytkownika „{{name}}”?\",\n      \"unlimited\": \"Nielimitowane\"\n    },\n    \"admin_settings\": \"Ustawienia administratora\",\n    \"server_stats\": {\n      \"server_stats\": \"Statystyki serwera\",\n      \"total_users\": \"Łączna liczba użytkowników\",\n      \"total_bookmarks\": \"Łączna liczba zakładek\",\n      \"server_version\": \"Wersja serwera\"\n    },\n    \"background_jobs\": {\n      \"background_jobs\": \"Zadania w tle\",\n      \"crawler_jobs\": \"Zadania skanowania\",\n      \"indexing_jobs\": \"Zadania indeksowania\",\n      \"inference_jobs\": \"Zadania wnioskowania\",\n      \"tidy_assets_jobs\": \"Zadania porządkowania zasobów\",\n      \"job\": \"Zadanie\",\n      \"queued\": \"W kolejce\",\n      \"pending\": \"Oczekujące\",\n      \"failed\": \"Nieudane\",\n      \"feed_jobs\": \"Praca z kanałem RSS\",\n      \"webhook_jobs\": \"Zadania Webhook\",\n      \"asset_preprocessing_jobs\": \"Zadania przetwarzania zasobów\",\n      \"video_jobs\": \"Zadania pobierania wideo\",\n      \"jobs\": {\n        \"crawler\": {\n          \"title\": \"Zadania Crawlera\",\n          \"description\": \"Web crawling i wyciąganie treści z URL-i\"\n        },\n        \"inference\": {\n          \"title\": \"Zadania wnioskowania\",\n          \"description\": \"Tagowanie i podsumowywanie treści przy użyciu AI\"\n        },\n        \"indexing\": {\n          \"title\": \"Zadania indeksowania\",\n          \"description\": \"Aktualizacje indeksu wyszukiwania\"\n        },\n        \"asset_preprocessing\": {\n          \"title\": \"Zadania przetwarzania zasobów\",\n          \"description\": \"Przetwarzanie obrazów i dokumentów (zrzuty ekranu, wyciąganie tekstu, itp.)\"\n        },\n        \"tidy_assets\": {\n          \"title\": \"Zadania czyszczenia zasobów\",\n          \"description\": \"Czyszczenie zasobów i optymalizacja przechowywania\"\n        },\n        \"video\": {\n          \"title\": \"Zadania pobierania wideo\",\n          \"description\": \"Wyciąganie i pobieranie wideo\"\n        },\n        \"webhook\": {\n          \"title\": \"Zadania Webhook\",\n          \"description\": \"Zewnętrzne powiadomienia webhook\"\n        },\n        \"feed\": {\n          \"title\": \"Zadania kanału RSS\",\n          \"description\": \"Przetwarzanie kanałów RSS i aktualizacje treści\"\n        },\n        \"admin_maintenance\": {\n          \"title\": \"Zadania konserwacyjne administratora\",\n          \"description\": \"Administracyjne czyszczenie i konserwacja zasobów\"\n        }\n      },\n      \"monitor_and_manage\": \"Monitoruj i zarządzaj kolejkami zadań w tle i zadaniami przetwarzania systemu\",\n      \"active\": \"Aktywne\",\n      \"available_actions\": \"Dostępne akcje\",\n      \"status\": {\n        \"title\": \"Zrozumienie stanów zadań\",\n        \"queued\": {\n          \"title\": \"W kolejce\",\n          \"description\": \"Zadania czekają w kolejce do przetworzenia. Uruchomią się automatycznie, gdy zasoby będą dostępne.\"\n        },\n        \"unprocessed\": {\n          \"title\": \"Nieprzetworzone\",\n          \"description\": \"Zakładki, które nie zostały jeszcze przetworzone. Najprawdopodobniej są już w kolejce do przetworzenia, jeśli nie, może być konieczne ręczne ponowne umieszczenie ich w kolejce.\"\n        },\n        \"failed\": {\n          \"title\": \"Nie powiodło się\",\n          \"description\": \"Zakładki, które napotkały błędy podczas przetwarzania. Mogą one wymagać ręcznej interwencji.\"\n        }\n      },\n      \"actions\": {\n        \"recrawl_failed_links_only\": \"Przeskanuj ponownie tylko uszkodzone linki\",\n        \"recrawl_all_links\": \"Przeskanuj ponownie wszystkie linki\",\n        \"without_inference\": \"Bez wnioskowania\",\n        \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Wygeneruj ponownie tagi AI tylko dla zakładek, które się nie powiodły\",\n        \"regenerate_ai_tags_for_all_bookmarks\": \"Wygeneruj ponownie tagi AI dla wszystkich zakładek\",\n        \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Wygeneruj ponownie streszczenia AI tylko dla zakładek, które się nie powiodły\",\n        \"regenerate_ai_summaries_for_all_bookmarks\": \"Wygeneruj ponownie streszczenia AI dla wszystkich zakładek\",\n        \"reindex_all_bookmarks\": \"Zreindeksuj wszystkie zakładki\",\n        \"clean_assets\": \"Wyczyść wiszące zasoby i ponownie zsynchronizuj metadane\",\n        \"reprocess_assets_fix_mode\": \"Ponownie przetwarzaj nieprzetworzone zasoby\",\n        \"migrate_large_link_html_content\": \"Przenieś dużą zawartość HTML inline do zasobów.\",\n        \"recrawl_pending_links_only\": \"Tylko ponowne przeszukiwanie oczekujących linków\",\n        \"regenerate_ai_tags_for_pending_bookmarks_only\": \"Tylko ponowne generowanie tagów AI dla oczekujących zakładek\",\n        \"regenerate_ai_summaries_for_pending_bookmarks_only\": \"Tylko ponowne generowanie podsumowań AI dla oczekujących zakładek\"\n      }\n    },\n    \"actions\": {\n      \"recrawl_failed_links_only\": \"Ponowne skanowanie tylko nieudanych linków\",\n      \"recrawl_all_links\": \"Ponowne skanowanie wszystkich linków\",\n      \"without_inference\": \"Bez wnioskowania\",\n      \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Regeneruj tagi AI tylko dla nieudanych zakładek\",\n      \"regenerate_ai_tags_for_all_bookmarks\": \"Regeneruj tagi AI dla wszystkich zakładek\",\n      \"reindex_all_bookmarks\": \"Ponowne indeksowanie wszystkich zakładek\",\n      \"compact_assets\": \"Kompaktuj zasoby\",\n      \"reprocess_assets_fix_mode\": \"Ponowne przetwarzanie zasobów (tryb fiksny)\",\n      \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Wygeneruj ponownie podsumowania AI tylko dla nieudanych zakładek\",\n      \"regenerate_ai_summaries_for_all_bookmarks\": \"Wygeneruj ponownie podsumowania AI dla wszystkich zakładek\"\n    },\n    \"service_connections\": {\n      \"title\": \"Połączenia z usługami\",\n      \"description\": \"Monitoruj stan i łączność zewnętrznych zależności systemowych\",\n      \"search_engine\": \"Wyszukiwarka\",\n      \"browser\": \"Przeglądarka\",\n      \"queue_system\": \"System kolejek\",\n      \"status\": {\n        \"not_configured\": \"Nieskonfigurowane\",\n        \"connected\": \"Połączono\",\n        \"disconnected\": \"Odłączono\"\n      }\n    },\n    \"admin_tools\": {\n      \"admin_tools\": \"Narzędzia administratora\",\n      \"bookmark_debugger\": \"Debuger zakładek\",\n      \"bookmark_id\": \"ID zakładki\",\n      \"bookmark_id_placeholder\": \"Wprowadź ID zakładki\",\n      \"lookup\": \"Wyszukaj\",\n      \"debug_info\": \"Informacje debugowania\",\n      \"basic_info\": \"Podstawowe informacje\",\n      \"status\": \"Status\",\n      \"content\": \"Zawartość\",\n      \"html_preview\": \"Podgląd HTML (pierwsze 1000 znaków)\",\n      \"summary\": \"Podsumowanie\",\n      \"url\": \"URL\",\n      \"source_url\": \"URL źródłowy\",\n      \"asset_type\": \"Typ zasobu\",\n      \"file_name\": \"Nazwa pliku\",\n      \"owner_user_id\": \"ID użytkownika będącego właścicielem\",\n      \"tagging_status\": \"Status tagowania\",\n      \"summarization_status\": \"Status podsumowania\",\n      \"crawl_status\": \"Status indeksowania\",\n      \"crawl_status_code\": \"Kod HTTP\",\n      \"crawled_at\": \"Zindeksowano\",\n      \"recrawl\": \"Ponowne indeksowanie\",\n      \"reindex\": \"Ponowne indeksowanie\",\n      \"retag\": \"Otaguj ponownie\",\n      \"resummarize\": \"Ponowne podsumowanie\",\n      \"bookmark_not_found\": \"Nie znaleziono zakładki\",\n      \"action_success\": \"Akcja zakończona pomyślnie\",\n      \"action_failed\": \"Akcja nie powiodła się\",\n      \"recrawl_queued\": \"Zadanie ponownego indeksowania zostało dodane do kolejki\",\n      \"reindex_queued\": \"Zadanie ponownego indeksowania zostało dodane do kolejki\",\n      \"retag_queued\": \"Zadanie ponownego tagowania zostało dodane do kolejki\",\n      \"resummarize_queued\": \"Zadanie ponownego podsumowania zostało dodane do kolejki\",\n      \"view\": \"Wyświetl\",\n      \"fetch_error\": \"Błąd podczas pobierania zakładki\"\n    }\n  },\n  \"tags\": {\n    \"delete_all_unused_tags\": \"Usuń wszystkie nieużywane tagi\",\n    \"all_tags\": \"Wszystkie tagi\",\n    \"your_tags\": \"Twoje tagi\",\n    \"your_tags_info\": \"Tagi, które były przynajmniej raz przez Ciebie użyte\",\n    \"ai_tags\": \"Tagi AI\",\n    \"drag_and_drop_merging\": \"Scalanie za pomocą przeciągania\",\n    \"ai_tags_info\": \"Tagi, które zostały dodane automatycznie (przez AI)\",\n    \"unused_tags\": \"Nieużywane tagi\",\n    \"unused_tags_info\": \"Tagi, które nie są przypisane do żadnych zakładek\",\n    \"drag_and_drop_merging_info\": \"Przeciągnij i upuść tagi na siebie, aby je scalić\",\n    \"sort_by_name\": \"Sortuj według nazwy\",\n    \"create_tag\": \"Stwórz etykietę\",\n    \"create_tag_description\": \"Stwórz nową etykietę bez dołączania jej do żadnej zakładki\",\n    \"tag_name\": \"Nazwa etykiety\",\n    \"enter_tag_name\": \"Wprowadź nazwę etykiety\",\n    \"sort_by_usage\": \"Sortuj wg użycia\",\n    \"sort_by_relevance\": \"Sortuj wg trafności\",\n    \"no_custom_tags\": \"Jeszcze brak własnych tagów\",\n    \"no_ai_tags\": \"Jeszcze brak tagów AI\",\n    \"no_unused_tags\": \"Nie masz nieużywanych tagów\",\n    \"no_unused_tags_match_your_search\": \"Żadne nieużywane tagi nie pasują do wyszukiwania\",\n    \"no_tags_match_your_search\": \"Żadne tagi nie pasują do wyszukiwania\",\n    \"search_placeholder\": \"Szukaj tagów...\",\n    \"search_or_create_placeholder\": \"Szukaj lub twórz tagi...\"\n  },\n  \"editor\": {\n    \"text_toolbar\": {\n      \"markdown_shortcuts\": {\n        \"italic\": {\n          \"label\": \"Kursywa\",\n          \"example\": \"*Kursywa* lub _Kursywa_ lub CTRL+i\"\n        },\n        \"unordered_list\": {\n          \"example\": \"- Element listy\",\n          \"label\": \"Lista nieuporządkowana\"\n        },\n        \"label\": \"Skróty Markdown\",\n        \"heading\": {\n          \"label\": \"Nagłówek\",\n          \"example\": \"# H1, ## H2, ### H3\"\n        },\n        \"bold\": {\n          \"label\": \"Pogrubienie\",\n          \"example\": \"**tekst** lub CTRL+b\"\n        },\n        \"blockquote\": {\n          \"label\": \"Cytat\",\n          \"example\": \"> Cytat\"\n        },\n        \"ordered_list\": {\n          \"label\": \"Lista uporządkowana\",\n          \"example\": \"1. Element listy\"\n        },\n        \"inline_code\": {\n          \"label\": \"Kod w linii\",\n          \"example\": \"`Kod`\"\n        },\n        \"block_code\": {\n          \"label\": \"Kod blokowy\",\n          \"example\": \"``` + spacja\"\n        }\n      },\n      \"undo\": \"Cofnij\",\n      \"redo\": \"Ponów\",\n      \"bold\": \"Pogrubienie\",\n      \"italic\": \"Kursywa\",\n      \"underline\": \"Podkreślenie\",\n      \"strikethrough\": \"Przekreślenie\",\n      \"code\": \"Kod\",\n      \"highlight\": \"Podświetlenie\",\n      \"align_left\": \"Wyrównaj do lewej\",\n      \"align_center\": \"Wyśrodkuj\",\n      \"align_right\": \"Wyrównaj do prawej\"\n    },\n    \"quickly_focus\": \"Możesz szybko skupić się na tym polu, naciskając ⌘ + E\",\n    \"multiple_urls_dialog_desc\": \"Wprowadzone dane zawierają wiele adresów URL w osobnych liniach. Czy chcesz zaimportować je jako oddzielne zakładki?\",\n    \"multiple_urls_dialog_title\": \"Importować URL jako oddzielne zakładki?\",\n    \"import_as_text\": \"Importuj jako zakładkę tekstową\",\n    \"import_as_separate_bookmarks\": \"Importuj jako oddzielne zakładki\",\n    \"new_item\": \"NOWY ELEMENT\",\n    \"disabled_submissions\": \"Dodawanie jest wyłączone\",\n    \"placeholder\": \"Wklej link lub obraz, napisz notatkę lub przeciągnij obraz tutaj...\",\n    \"placeholder_v2\": \"Wklej link, napisz notatkę lub upuść obrazek…\"\n  },\n  \"cleanups\": {\n    \"duplicate_tags\": {\n      \"merge_all_suggestions\": \"Scal wszystkie sugestie?\",\n      \"title\": \"Zduplikowane tagi\"\n    },\n    \"cleanups\": \"Porządki\"\n  },\n  \"toasts\": {\n    \"lists\": {\n      \"created\": \"Lista została utworzona!\",\n      \"updated\": \"Lista została zaktualizowana!\",\n      \"merged\": \"Lista została scalona!\",\n      \"deleted\": \"Lista została usunięta!\"\n    },\n    \"bookmarks\": {\n      \"updated\": \"Zakładka została zaktualizowana!\",\n      \"deleted\": \"Zakładka została usunięta!\",\n      \"refetch\": \"Pobieranie ponownie zostało zaplanowane!\",\n      \"full_page_archive\": \"Tworzenie pełnego archiwum strony zostało rozpoczęte\",\n      \"delete_from_list\": \"Zakładka została usunięta z listy\",\n      \"clipboard_copied\": \"Link został skopiowany do schowka!\",\n      \"preserve_pdf\": \"Zapis PDF został uruchomiony\",\n      \"update_banner\": \"Baner został zaktualizowany!\",\n      \"uploading_banner\": \"Przesyłanie banera...\"\n    },\n    \"tags\": {\n      \"created\": \"Etykieta została utworzona!\",\n      \"failed_to_create\": \"Nie udało się utworzyć etykiety\"\n    }\n  },\n  \"layouts\": {\n    \"masonry\": \"Mozaika\",\n    \"grid\": \"Siatka\",\n    \"list\": \"Lista\",\n    \"compact\": \"Kompaktowy\"\n  },\n  \"options\": {\n    \"dark_mode\": \"Tryb ciemny\",\n    \"light_mode\": \"Tryb jasny\",\n    \"apps_extensions\": \"Aplikacje i rozszerzenia\",\n    \"documentation\": \"Dokumentacja\",\n    \"follow_us_on_x\": \"Obserwuj nas na X\"\n  },\n  \"lists\": {\n    \"all_lists\": \"Wszystkie listy\",\n    \"favourites\": \"Ulubione\",\n    \"new_list\": \"Nowa lista\",\n    \"new_nested_list\": \"Nowa zagnieżdżona lista\",\n    \"manual_list\": \"Ręczna lista\",\n    \"smart_list\": \"Inteligentna lista\",\n    \"search_query\": \"Zapytanie wyszukiwania\",\n    \"search_query_help\": \"Dowiedz się więcej o języku zapytań.\",\n    \"edit_list\": \"Edytuj listę\",\n    \"parent_list\": \"Lista rodziców\",\n    \"no_parent\": \"Brak rodzica\",\n    \"list_type\": \"Typ listy\",\n    \"merge_list\": \"Scal listę\",\n    \"destination_list\": \"Lista docelowa\",\n    \"delete_after_merge\": \"Usuń oryginalną listę po scaleniu\",\n    \"no_destination\": \"Brak miejsca docelowego\",\n    \"description\": \"Opis (opcjonalny)\",\n    \"public_list\": {\n      \"title\": \"Lista publiczna\",\n      \"description\": \"Zezwól innym na przeglądanie tej listy\",\n      \"share_link\": \"Udostępnij link\"\n    },\n    \"share_list\": \"Udostępnij listę\",\n    \"rss\": {\n      \"title\": \"Kanał RSS\",\n      \"description\": \"Włącz kanał RSS dla tej listy\",\n      \"feed_url\": \"Adres URL kanału RSS\"\n    },\n    \"delete_list\": {\n      \"title\": \"Usuń listę\",\n      \"description\": \"Usunięcie listy nie powoduje usunięcia żadnych zakładek z tej listy.\",\n      \"delete_children\": \"Usuń listy potomne (rekurencyjnie)\",\n      \"delete_children_description\": \"Jeśli nie zaznaczone, wszystkie bezpośrednie listy potomne staną się listami głównymi\"\n    },\n    \"shared\": \"Udostępnione\",\n    \"collaborators\": {\n      \"manage\": \"Zarządzaj Współpracownikami\",\n      \"view\": \"Wyświetl Współpracowników\",\n      \"collaborators\": \"Współpracownicy\",\n      \"add\": \"Dodaj Współpracownika\",\n      \"current\": \"Aktualni Współpracownicy\",\n      \"enter_email\": \"Wprowadź adres e-mail\",\n      \"please_enter_email\": \"Wprowadź adres e-mail.\",\n      \"added_successfully\": \"Współpracownik został dodany pomyślnie\",\n      \"failed_to_add\": \"Nie udało się dodać współpracownika\",\n      \"removed\": \"Współpracownik usunięty\",\n      \"failed_to_remove\": \"Nie udało się usunąć współpracownika\",\n      \"role_updated\": \"Rola zaktualizowana\",\n      \"failed_to_update_role\": \"Nie udało się zaktualizować roli\",\n      \"viewer\": \"Przeglądający\",\n      \"editor\": \"Edytor\",\n      \"owner\": \"Właściciel\",\n      \"viewer_description\": \"Może wyświetlać zakładki na liście\",\n      \"editor_description\": \"Może dodawać i usuwać zakładki\",\n      \"no_collaborators\": \"Jeszcze nie ma żadnych współpracowników. Dodaj kogoś, aby rozpocząć współpracę!\",\n      \"no_collaborators_readonly\": \"Brak współpracowników dla tej listy.\",\n      \"people_with_access\": \"Osoby, które mają dostęp do tej listy\",\n      \"add_or_remove\": \"Dodaj lub usuń osoby, które mogą uzyskać dostęp do tej listy\",\n      \"invitation_sent\": \"Zaproszenie wysłane, ziom!\",\n      \"invitation_revoked\": \"Cofnięto zaproszenie\",\n      \"failed_to_revoke\": \"Nie udało się cofnąć zaproszenia\",\n      \"pending\": \"Oczekuje\",\n      \"revoke\": \"Cofnij\",\n      \"declined\": \"Odrzucone\"\n    },\n    \"leave_list\": {\n      \"title\": \"Opuść listę\",\n      \"confirm_message\": \"Na pewno chcesz opuścić {{icon}} {{name}}?\",\n      \"warning\": \"Nie będziesz już mógł przeglądać ani uzyskiwać dostępu do zakładek na tej liście. Właściciel listy może Cię ponownie dodać w razie potrzeby.\",\n      \"action\": \"Opuść listę\",\n      \"success\": \"Opuściłeś \\\"{{icon}} {{name}}\\\"\"\n    },\n    \"invitations\": {\n      \"pending\": \"Zaproszenia oczekujące\",\n      \"description\": \"Sprawdź i odpowiedz na zaproszenia do współpracy na listach\",\n      \"invited_by\": \"Zaprosił(-a) przez\",\n      \"accept\": \"Akceptuj\",\n      \"decline\": \"Odrzuć\",\n      \"accepted\": \"Zaproszenie zaakceptowane\",\n      \"declined\": \"Zaproszenie odrzucone\",\n      \"failed_to_accept\": \"Nie udało się zaakceptować zaproszenia\",\n      \"failed_to_decline\": \"Nie udało się odrzucić zaproszenia\"\n    },\n    \"shared_lists\": \"Listy współdzielone\"\n  },\n  \"preview\": {\n    \"view_original\": \"Zobacz oryginał\",\n    \"cached_content\": \"Zbuforowana zawartość\",\n    \"reader_view\": \"Widok czytnika\",\n    \"tabs\": {\n      \"content\": \"Treść\",\n      \"details\": \"Szczegóły\"\n    },\n    \"archive_info\": \"Archiwa mogą się nie wyświetlać poprawnie w wierszu, jeśli wymagają Javascript. Dla najlepszych rezultatów, <1>pobierz i otwórz w przeglądarce</1>.\",\n    \"fetch_error_title\": \"Zawartość niedostępna\",\n    \"fetch_error_description\": \"Nie udało się pobrać zawartości dla tego linku. Strona może być chroniona, wymagać uwierzytelnienia, lub być tymczasowo niedostępna.\",\n    \"crawling_in_progress\": \"Pobieranie zawartości strony…\",\n    \"continue_reading\": \"Kontynuuj, gdzie skończyłeś\",\n    \"continue_reading_percent\": \"Kontynuuj tam, gdzie skończyłeś ({{percent}}%)\",\n    \"continue_button\": \"Kontynuuj\"\n  },\n  \"highlights\": {\n    \"no_highlights\": \"Nie masz jeszcze żadnych wyróżnień.\"\n  },\n  \"search\": {\n    \"is_not_favorited\": \"Nie jest ulubione\",\n    \"is_archived\": \"Jest zarchiwizowany\",\n    \"is_not_archived\": \"Nie jest zarchiwizowany\",\n    \"has_any_tag\": \"Ma dowolny tag\",\n    \"has_no_tags\": \"Nie ma tagu\",\n    \"is_in_any_list\": \"Jest na jakiejkolwiek liście\",\n    \"is_not_in_any_list\": \"Nie znajduje się na żadnej liście\",\n    \"created_on_or_after\": \"Utworzono w dniu lub po\",\n    \"not_created_on_or_after\": \"Nie utworzono w dniu lub po\",\n    \"type_is\": \"Typ to\",\n    \"created_on_or_before\": \"Utworzono w dniu lub przed\",\n    \"not_created_on_or_before\": \"Nie utworzono w dniu lub przed\",\n    \"url_contains\": \"URL zawiera\",\n    \"url_does_not_contain\": \"Adres URL nie zawiera\",\n    \"has_tag\": \"Ma tag\",\n    \"does_not_have_tag\": \"Nie ma tagu\",\n    \"full_text_search\": \"Wyszukiwanie pełnotekstowe\",\n    \"is_favorited\": \"Jest ulubione\",\n    \"or\": \"Lub\",\n    \"is_in_list\": \"Jest na liście\",\n    \"is_not_in_list\": \"Nie jest na liście\",\n    \"type_is_not\": \"Typ nie jest\",\n    \"and\": \"I\",\n    \"is_not_from_feed\": \"Nie pochodzi z kanału RSS\",\n    \"is_from_feed\": \"Pochodzi z kanału RSS\",\n    \"created_within\": \"Utworzone w ciągu\",\n    \"created_earlier_than\": \"Utworzone wcześniej niż\",\n    \"day_s\": \" Dni\",\n    \"week_s\": \" Tygodni\",\n    \"month_s\": \" Miesięcy\",\n    \"year_s\": \" Lat(a)\",\n    \"day_s_ago\": \" Dni temu\",\n    \"week_s_ago\": \" Tygodni temu\",\n    \"month_s_ago\": \" Miesięcy temu\",\n    \"year_s_ago\": \" Lat(a) temu\",\n    \"history\": \"Ostatnie wyszukiwania\",\n    \"title_contains\": \"Tytuł zawiera\",\n    \"title_does_not_contain\": \"Tytuł nie zawiera\",\n    \"is_broken_link\": \"Ma Zepsuty Link\",\n    \"tags\": \"Tagi\",\n    \"no_suggestions\": \"Brak propozycji\",\n    \"filters\": \"Filtry\",\n    \"is_not_broken_link\": \"Ma Działający Link\",\n    \"lists\": \"Listy\",\n    \"feeds\": \"Kanały\",\n    \"is_from_source\": \"Źródło to\",\n    \"is_not_from_source\": \"To nie jest źródło\"\n  },\n  \"dialogs\": {\n    \"bookmarks\": {\n      \"delete_confirmation_description\": \"Na pewno chcesz usunąć ten zakładkę?\",\n      \"delete_confirmation_title\": \"Usunąć zakładkę?\"\n    }\n  },\n  \"banners\": {\n    \"no_bookmarks\": {\n      \"title\": \"Jeszcze nie ma zakładek\",\n      \"description\": \"Zapisuj ciekawe artykuły, linki i strony, aby mieć do nich szybki dostęp później.\"\n    }\n  },\n  \"bookmark_editor\": {\n    \"title\": \"Edytuj zakładkę\",\n    \"subtitle\": \"Wprowadź zmiany w szczegółach zakładki. Kliknij Zapisz, gdy skończysz.\",\n    \"author\": \"Autor\",\n    \"publisher\": \"Wydawca\",\n    \"date_published\": \"Data publikacji\",\n    \"pick_a_date\": \"Wybierz datę\",\n    \"save_changes\": \"Zapisz zmiany\",\n    \"extracted_content\": \"Wyodrębniona zawartość\"\n  },\n  \"view_options\": {\n    \"title\": \"Opcje widoku\",\n    \"layout\": \"Układ\",\n    \"columns\": \"Kolumny\",\n    \"display_options\": \"Opcje wyświetlania\",\n    \"show_note_previews\": \"Pokaż notatki\",\n    \"show_tags\": \"Pokaż tagi\",\n    \"show_title\": \"Pokaż tytuł\",\n    \"image_options\": \"Opcje obrazu\",\n    \"image_fit_cover\": \"Okładka (Wypełnienie)\",\n    \"image_fit_contain\": \"Zawieraj (Dopasuj)\"\n  },\n  \"version\": {\n    \"new_release_available\": \"Dostępne nowe informacje o wydaniu\",\n    \"whats_new_title\": \"Co nowego w wersji {{version}}\",\n    \"release_notes_description\": \"Oto najnowsze aktualizacje pobrane z informacji o wydaniu GitHub.\",\n    \"loading_release_notes\": \"Ładowanie informacji o wydaniu…\",\n    \"unable_to_load_release_notes\": \"Nie można teraz załadować informacji o wydaniu. Spróbuj ponownie później.\",\n    \"no_release_notes\": \"Dla tej wersji nie opublikowano żadnych informacji o wydaniu.\",\n    \"release_notes_synced\": \"Informacje o wydaniu są synchronizowane z GitHub.\",\n    \"view_on_github\": \"Wyświetl na GitHub\"\n  },\n  \"wrapped\": {\n    \"title\": \"Podsumowanie {{year}}\",\n    \"subtitle\": \"Rok w Karakeep\",\n    \"banner\": {\n      \"title\": \"Twoje podsumowanie 2025 jest gotowe!\",\n      \"description\": \"Zobacz swój rok w zakładkach\",\n      \"view_now\": \"Wyświetl teraz\"\n    },\n    \"button\": \"Podsumowanie 2025\",\n    \"loading\": \"Ładowanie twojego podsumowania...\",\n    \"failed_to_load\": \"Nie udało się załadować statystyk podsumowania\",\n    \"sections\": {\n      \"total_saves\": {\n        \"prefix\": \"Zapisałeś\",\n        \"suffix\": \"rzeczy w tym roku\",\n        \"suffix_singular\": \"rzecz w tym roku\"\n      },\n      \"first_bookmark\": {\n        \"title\": \"Twoja podróż się zaczęła\",\n        \"description\": \"Pierwszy zapis w roku {{year}}:\"\n      },\n      \"top_domains\": \"Twoje najpopularniejsze strony internetowe\",\n      \"top_tags\": \"Twoje najpopularniejsze tagi\",\n      \"monthly_activity\": \"Twój rok w zapisach\",\n      \"most_active_day\": \"Twój najbardziej aktywny dzień\",\n      \"peak_times\": {\n        \"title\": \"Kiedy zapisujesz\",\n        \"peak_hour\": \"Godzina szczytu\",\n        \"peak_day\": \"Dzień szczytu\"\n      },\n      \"how_you_save\": \"Jak zapisujesz\",\n      \"what_you_saved\": \"Co zapisałeś\",\n      \"summary\": {\n        \"favorites\": \"Ulubione\",\n        \"tags_created\": \"Utworzone tagi\",\n        \"highlights\": \"Najważniejsze informacje\"\n      },\n      \"types\": {\n        \"links\": \"Linki\",\n        \"notes\": \"Notatki\",\n        \"assets\": \"Zasoby\"\n      }\n    },\n    \"footer\": \"Stworzone z Karakeep\",\n    \"share\": \"Udostępnij\",\n    \"download\": \"Pobierz\",\n    \"close\": \"Zamknij\",\n    \"generating\": \"Generowanie...\"\n  }\n}\n"
  },
  {
    "path": "apps/web/lib/i18n/locales/pt/translation.json",
    "content": "{\n  \"common\": {\n    \"url\": \"URL\",\n    \"roles\": {\n      \"admin\": \"Admin\",\n      \"user\": \"Usuário\"\n    },\n    \"video\": \"Vídeo\",\n    \"archive\": \"Arquivo\",\n    \"home\": \"Início\",\n    \"bookmark_types\": {\n      \"title\": \"Tipo de Favorito\",\n      \"link\": \"Link\",\n      \"media\": \"Mídia\",\n      \"text\": \"Texto\"\n    },\n    \"name\": \"Nome\",\n    \"email\": \"E-mail\",\n    \"password\": \"Senha\",\n    \"action\": \"Ação\",\n    \"actions\": \"Ações\",\n    \"created_at\": \"Criado em\",\n    \"key\": \"Chave\",\n    \"role\": \"Função\",\n    \"something_went_wrong\": \"Algo deu errado\",\n    \"attachments\": \"Anexos\",\n    \"experimental\": \"Experimental\",\n    \"search\": \"Pesquisar\",\n    \"tags\": \"Tags\",\n    \"note\": \"Nota\",\n    \"highlights\": \"Destaques\",\n    \"source\": \"Fonte\",\n    \"screenshot\": \"Captura de ecrã\",\n    \"type\": \"Tipo\",\n    \"size\": \"Tamanho\",\n    \"updated_at\": \"Atualizado em\",\n    \"title\": \"Título\",\n    \"description\": \"Descrição\",\n    \"summary\": \"Resumo\",\n    \"quota\": \"Quota\",\n    \"bookmarks\": \"Favoritos\",\n    \"storage\": \"Armazenamento\",\n    \"pdf\": \"PDF arquivado\",\n    \"default\": \"Padrão\",\n    \"id\": \"ID\",\n    \"last_used\": \"Usado recentemente\"\n  },\n  \"actions\": {\n    \"close\": \"Fechar\",\n    \"merge\": \"Mesclar\",\n    \"cancel\": \"Cancelar\",\n    \"download_full_page_archive\": \"Baixar arquivo da página inteira\",\n    \"create\": \"Criar\",\n    \"refresh\": \"Atualizar\",\n    \"recrawl\": \"Rastrear novamente\",\n    \"edit_tags\": \"Editar etiquetas\",\n    \"add_to_list\": \"Adicionar à lista\",\n    \"select_all\": \"Selecionar tudo\",\n    \"unselect_all\": \"Desmarcar Todos\",\n    \"copy_link\": \"Copiar Link\",\n    \"close_bulk_edit\": \"Fechar edição em massa\",\n    \"apply_all\": \"Aplicar Tudo\",\n    \"ignore\": \"Ignorar\",\n    \"sort\": {\n      \"newest_first\": \"Mais recente primeiro\",\n      \"oldest_first\": \"Mais Antigo Primeiro\",\n      \"title\": \"Ordenar\",\n      \"relevant_first\": \"Mais relevante primeiro\"\n    },\n    \"change_layout\": \"Mudar Layout\",\n    \"archive\": \"Arquivo\",\n    \"unarchive\": \"Desarquivar\",\n    \"favorite\": \"Favorito\",\n    \"unfavorite\": \"Desfavoritar\",\n    \"delete\": \"Excluir\",\n    \"bulk_edit\": \"Edição em massa\",\n    \"manage_lists\": \"Gerir Listas\",\n    \"remove_from_list\": \"Remover da Lista\",\n    \"save\": \"Salvar\",\n    \"add\": \"Adicionar\",\n    \"edit\": \"Editar\",\n    \"fetch_now\": \"Buscar agora\",\n    \"summarize_with_ai\": \"Resumir com IA\",\n    \"edit_title\": \"Editar título\",\n    \"sign_out\": \"Sair\",\n    \"open_editor\": \"Abrir editor\",\n    \"toggle_show_archived\": \"Mostrar arquivados\",\n    \"confirm\": \"Confirmar\",\n    \"regenerate\": \"Regenerar\",\n    \"load_more\": \"Carregar mais\",\n    \"edit_notes\": \"Editar notas\",\n    \"preserve_as_pdf\": \"Preservar como PDF\",\n    \"offline_copies\": \"Cópias offline\",\n    \"preserve_offline_archive\": \"Preservar Arquivos Offline\",\n    \"download_full_page_archive_file\": \"Baixar Arquivo Compactado\",\n    \"download_pdf_file\": \"Baixar Arquivo PDF\",\n    \"remove\": \"Remover\",\n    \"more\": \"Mais\",\n    \"replace_banner\": \"Substituir Banner\",\n    \"add_banner\": \"Adicionar Banner\",\n    \"download\": \"Baixar\"\n  },\n  \"settings\": {\n    \"webhooks\": {\n      \"events\": {\n        \"edited\": \"Editado\",\n        \"title\": \"Eventos\",\n        \"crawled\": \"Rastreado\",\n        \"created\": \"Criado\"\n      },\n      \"add_auth_token\": \"Adicionar token de autenticação\",\n      \"webhooks\": \"Webhooks\",\n      \"auth_token\": \"Token de Autenticação\",\n      \"description\": \"Você pode usar webhooks para acionar ações quando os favoritos são criados, alterados ou rastreados.\",\n      \"edit_auth_token\": \"Editar Token de Autenticação\",\n      \"create_webhook\": \"Criar Webhook\",\n      \"delete_webhook\": \"Excluir Webhook\",\n      \"delete_webhook_confirmation\": \"Tem certeza de que deseja excluir este webhook?\",\n      \"edit_webhook\": \"Editar Webhook\",\n      \"webhook_url\": \"URL do Webhook\"\n    },\n    \"import\": {\n      \"import_bookmarks_from_pocket_export\": \"Importar marcadores da exportação do Pocket\",\n      \"import_bookmarks_from_matter_export\": \"Importar marcadores da exportação do Matter\",\n      \"import_bookmarks_from_omnivore_export\": \"Importar marcadores da exportação do Omnivore\",\n      \"import_export\": \"Importar / Exportar\",\n      \"import_export_bookmarks\": \"Importar/Exportar Marcadores\",\n      \"import_bookmarks_from_html_file\": \"Importar favoritos de arquivo HTML\",\n      \"import_bookmarks_from_linkwarden_export\": \"Importar marcadores da exportação do Linkwarden\",\n      \"import_bookmarks_from_karakeep_export\": \"Importar marcadores da exportação do Karakeep\",\n      \"export_links_and_notes\": \"Exportar Links e Notas\",\n      \"imported_bookmarks\": \"Favoritos importados\",\n      \"import_bookmarks_from_tab_session_manager_export\": \"Importar marcadores do Tab Session Manager\",\n      \"import_bookmarks_from_mymind_export\": \"Importar favoritos da exportação do mymind\",\n      \"import_bookmarks_from_instapaper_export\": \"Importar os favoritos da exportação do Instapaper\"\n    },\n    \"info\": {\n      \"interface_lang\": \"Idioma da Interface\",\n      \"user_info\": \"Informações do usuário\",\n      \"basic_details\": \"Detalhes Básicos\",\n      \"change_password\": \"Alterar senha\",\n      \"current_password\": \"Senha Atual\",\n      \"new_password\": \"Nova senha\",\n      \"confirm_new_password\": \"Confirmar nova senha\",\n      \"options\": \"Opções\",\n      \"user_settings\": {\n        \"user_settings_updated\": \"As configurações do usuário foram atualizadas!\",\n        \"bookmark_click_action\": {\n          \"title\": \"Ação de clique no marcador\",\n          \"open_external_url\": \"Abrir URL original\",\n          \"open_bookmark_details\": \"Abrir detalhes do marcador\"\n        },\n        \"archive_display_behaviour\": {\n          \"title\": \"Marcadores arquivados\",\n          \"show\": \"Mostrar marcadores arquivados em tags e listas\",\n          \"hide\": \"Ocultar marcadores arquivados em tags e listas\"\n        }\n      },\n      \"reader_settings\": {\n        \"local_overrides_title\": \"Configurações específicas do dispositivo ativas\",\n        \"using_default\": \"Usando o padrão do cliente\",\n        \"clear_override_hint\": \"Limpar a substituição do dispositivo para usar a configuração global ({{value}})\",\n        \"font_size\": \"Tamanho da fonte\",\n        \"font_family\": \"Família da fonte\",\n        \"preview_inline\": \"(visualização)\",\n        \"tooltip_preview\": \"Alterações não salvas na pré-visualização\",\n        \"save_to_all_devices\": \"Todos os dispositivos\",\n        \"tooltip_local\": \"Configurações do dispositivo são diferentes das globais\",\n        \"reset_preview\": \"Redefinir pré-visualização\",\n        \"mono\": \"Monoespaçada\",\n        \"line_height\": \"Altura da linha\",\n        \"tooltip_default\": \"Configurações de leitura\",\n        \"title\": \"Configurações do Leitor\",\n        \"serif\": \"Com serifa\",\n        \"preview\": \"Pré-visualização\",\n        \"not_set\": \"Não definido\",\n        \"clear_local_overrides\": \"Limpar configurações do dispositivo\",\n        \"preview_text\": \"A raposa marrom rápida pula sobre o cachorro preguiçoso. É assim que o texto da visualização do leitor será exibido.\",\n        \"local_overrides_cleared\": \"As configurações específicas do dispositivo foram apagadas\",\n        \"local_overrides_description\": \"Este dispositivo tem configurações de leitor que diferem de suas configurações padrão globais:\",\n        \"clear_defaults\": \"Limpar todos os padrões\",\n        \"description\": \"Configure as configurações de texto padrão para a visualização do leitor. Essas configurações são sincronizadas em todos os seus dispositivos.\",\n        \"defaults_cleared\": \"Os padrões do leitor foram apagados\",\n        \"save_hint\": \"Salvar configurações apenas para este dispositivo ou sincronizar entre todos os dispositivos\",\n        \"save_as_default\": \"Salvar como padrão\",\n        \"save_to_device\": \"Este dispositivo\",\n        \"sans\": \"Sem serifa\",\n        \"tooltip_preview_and_local\": \"Alterações não salvas na pré-visualização; as configurações do dispositivo são diferentes das globais\",\n        \"adjust_hint\": \"Ajuste as configurações acima para visualizar as alterações\"\n      },\n      \"avatar\": {\n        \"upload\": \"Mandar avatar\",\n        \"change\": \"Trocar avatar\",\n        \"remove_confirm_title\": \"Remover avatar?\",\n        \"updated\": \"Avatar atualizado\",\n        \"removed\": \"Avatar removido\",\n        \"description\": \"Manda uma imagem quadrada para usar como teu avatar.\",\n        \"remove_confirm_description\": \"Isso vai apagar tua foto de perfil atual.\",\n        \"title\": \"Foto do perfil\",\n        \"remove\": \"Remover avatar\"\n      }\n    },\n    \"ai\": {\n      \"tagging_rules\": \"Regras de marcação\",\n      \"tagging_rule_description\": \"As instruções que você adicionar aqui serão incluídas como regras para o modelo durante a geração de tags. Você pode visualizar as instruções finais na seção de visualização de instruções.\",\n      \"prompt_preview\": \"Visualização rápida\",\n      \"text_prompt\": \"Prompt de Texto\",\n      \"images_prompt\": \"Prompt de Imagem\",\n      \"summarization_prompt\": \"Prompt de resumo\",\n      \"all_tagging\": \"Todas as Tags\",\n      \"text_tagging\": \"Marcação de texto\",\n      \"image_tagging\": \"Marcação de imagem\",\n      \"summarization\": \"Sumarização\",\n      \"ai_settings\": \"Configurações de IA\",\n      \"tag_style\": \"Estilo da etiqueta\",\n      \"auto_summarization_description\": \"Gerar automaticamente resumos para seus favoritos usando IA.\",\n      \"auto_tagging\": \"Marcação automática\",\n      \"titlecase_spaces\": \"Maiúsculas e minúsculas com espaços\",\n      \"lowercase_underscores\": \"Minúsculas com underscores\",\n      \"inference_language\": \"Linguagem de Inferência\",\n      \"titlecase_hyphens\": \"Maiúsculas e minúsculas com hífens\",\n      \"lowercase_hyphens\": \"Minúsculas com hífens\",\n      \"lowercase_spaces\": \"Minúsculas com espaços\",\n      \"inference_language_description\": \"Escolha o idioma para as tags e resumos gerados por IA.\",\n      \"tag_style_description\": \"Escolha como as suas etiquetas geradas automaticamente devem ser formatadas.\",\n      \"auto_tagging_description\": \"Gerar automaticamente tags para seus favoritos usando IA.\",\n      \"camelCase\": \"camelCase\",\n      \"auto_summarization\": \"Resumo automático\",\n      \"no_preference\": \"Sem preferência\",\n      \"curated_tags\": \"Tags selecionadas\",\n      \"curated_tags_description\": \"Opcionalmente, restrinja a marcação de IA para usar apenas tags desta lista. Quando nenhuma tag é selecionada, a IA gera tags livremente.\",\n      \"curated_tags_updated\": \"Tags selecionadas atualizadas com sucesso!\",\n      \"curated_tags_update_failed\": \"Falha ao atualizar as tags selecionadas\"\n    },\n    \"api_keys\": {\n      \"new_api_key\": \"Nova chave da API\",\n      \"new_api_key_desc\": \"Dê um nome exclusivo à sua chave de API\",\n      \"key_success\": \"A chave foi criada com sucesso\",\n      \"key_success_please_copy\": \"Por favor, copie a chave e guarde-a em um lugar seguro. Depois que você fechar a caixa de diálogo, não poderá mais acessá-la.\",\n      \"api_keys\": \"Chaves de API\",\n      \"regenerate_api_key\": \"Regenerar chave da API\",\n      \"key_regenerated\": \"Chave regenerada com sucesso\",\n      \"key_regenerated_please_copy\": \"Copie a nova chave e guarde-a em local seguro. A chave antiga foi revogada e não irá mais funcionar.\",\n      \"regenerate_warning\": \"Tem certeza de que deseja regenerar a chave da API \\\"{{name}}\\\"?\",\n      \"regenerate_confirmation\": \"Isso revogará a chave atual e gerará uma nova. Todos os aplicativos que usam a chave atual deixarão de funcionar.\"\n    },\n    \"broken_links\": {\n      \"broken_links\": \"Links Quebrados\",\n      \"last_crawled_at\": \"Última Indexação Em\",\n      \"crawling_status\": \"Estado da Indexação\",\n      \"crawling_failed\": \"Falha ao Rastrear\"\n    },\n    \"back_to_app\": \"Voltar para o App\",\n    \"user_settings\": \"Configurações do usuário\",\n    \"feeds\": {\n      \"rss_subscriptions\": \"Assinaturas RSS\",\n      \"add_a_subscription\": \"Adicionar uma Assinatura\",\n      \"feed_enabled\": \"Feed RSS ativado\",\n      \"feed_disabled\": \"Feed RSS desativado\"\n    },\n    \"manage_assets\": {\n      \"manage_assets\": \"Gerenciar Ativos\",\n      \"no_assets\": \"Você ainda não tem nenhum recurso.\",\n      \"asset_type\": \"Tipo de Ativo\",\n      \"bookmark_link\": \"Link do marcador\",\n      \"asset_link\": \"Link do recurso\",\n      \"delete_asset\": \"Excluir Ativo\",\n      \"delete_asset_confirmation\": \"Tem certeza de que deseja excluir este ativo?\"\n    },\n    \"rules\": {\n      \"rule_has_been_deleted\": \"A regra foi excluída!\",\n      \"no_rules_created_yet\": \"Nenhuma regra criada ainda\",\n      \"create_your_first_rule\": \"Crie sua primeira regra para automatizar seu fluxo de trabalho\",\n      \"conditions_types\": {\n        \"always\": \"Sempre\",\n        \"url_contains\": \"URL contém\",\n        \"or\": \"Qualquer uma das seguintes opções for verdadeira\",\n        \"imported_from_feed\": \"Importado do feed\",\n        \"bookmark_type_is\": \"Tipo de marcador é\",\n        \"has_tag\": \"Tem tag\",\n        \"is_favourited\": \"Está favoritado\",\n        \"is_archived\": \"Arquivado\",\n        \"and\": \"Tudo o que se segue é verdade\",\n        \"url_does_not_contain\": \"URL não Contém\",\n        \"title_contains\": \"Título Contém\",\n        \"title_does_not_contain\": \"Título Não Contém\"\n      },\n      \"actions_types\": {\n        \"add_tag\": \"Adicionar etiqueta\",\n        \"remove_tag\": \"Remover tag\",\n        \"add_to_list\": \"Adicionar à lista\",\n        \"remove_from_list\": \"Remover da lista\",\n        \"download_full_page_archive\": \"Baixar arquivo de página inteira\",\n        \"favourite_bookmark\": \"Favoritar marcador\",\n        \"archive_bookmark\": \"Arquivar marcador\"\n      },\n      \"rules\": \"Mecanismo de regras\",\n      \"rule_name\": \"Nome da regra\",\n      \"description\": \"Você pode usar regras para acionar ações quando um evento é disparado.\",\n      \"ceate_rule\": \"Criar regra\",\n      \"edit_rule\": \"Editar regra\",\n      \"save_rule\": \"Salvar regra\",\n      \"delete_rule\": \"Excluir regra\",\n      \"delete_rule_confirmation\": \"Tem certeza de que deseja excluir esta regra?\",\n      \"whenever\": \"Sempre que...\",\n      \"if\": \"Se...\",\n      \"enter_rule_name\": \"Insira o nome da regra\",\n      \"describe_what_this_rule_does\": \"Descreva o que esta regra faz\",\n      \"rule_has_been_created\": \"A regra foi criada!\",\n      \"rule_has_been_updated\": \"Regra foi atualizada!\",\n      \"events_types\": {\n        \"bookmark_added\": \"Um marcador é adicionado\",\n        \"tag_added\": \"Esta etiqueta é adicionada a um marcador\",\n        \"tag_removed\": \"Esta tag é removida de um marcador\",\n        \"added_to_list\": \"Um marcador é adicionado a esta lista\",\n        \"removed_from_list\": \"Um marcador é removido desta lista\",\n        \"favourited\": \"Um marcador é favoritado\",\n        \"archived\": \"Um marcador é arquivado\"\n      }\n    },\n    \"stats\": {\n      \"usage_statistics\": \"Estatísticas de uso\",\n      \"insights_description\": \"Informações sobre seus hábitos de favoritos e coleção\",\n      \"failed_to_load\": \"Falha ao carregar estatísticas\",\n      \"overview\": {\n        \"total_bookmarks\": \"Total de favoritos\",\n        \"all_saved_items\": \"Todos os itens salvos\",\n        \"favorites\": \"Favoritos\",\n        \"starred_bookmarks\": \"Favoritos marcados com estrela\",\n        \"archived\": \"Arquivado\",\n        \"archived_items\": \"Itens arquivados\",\n        \"tags\": \"Tags\",\n        \"unique_tags_created\": \"Tags únicos criados\",\n        \"lists\": \"Listas\",\n        \"bookmark_collections\": \"Coleções de favoritos\",\n        \"highlights\": \"Destaques\",\n        \"text_highlights\": \"Destaques de texto\",\n        \"storage_used\": \"Armazenamento usado\",\n        \"total_asset_storage\": \"Armazenamento total de ativos\",\n        \"this_month\": \"Este mês\",\n        \"bookmarks_added\": \"Favoritos adicionados\"\n      },\n      \"bookmark_types\": {\n        \"title\": \"Tipos de favoritos\",\n        \"links\": \"Links\",\n        \"text_notes\": \"Notas de texto\",\n        \"assets\": \"Ativos\"\n      },\n      \"recent_activity\": {\n        \"title\": \"Atividade recente\",\n        \"this_week\": \"Esta semana\",\n        \"this_month\": \"Este mês\",\n        \"this_year\": \"Este ano\"\n      },\n      \"top_domains\": {\n        \"title\": \"Domínios principais\",\n        \"no_domains_found\": \"Nenhum domínio encontrado\"\n      },\n      \"most_used_tags\": {\n        \"title\": \"Tags mais usadas\",\n        \"no_tags_found\": \"Nenhuma tag encontrada\"\n      },\n      \"activity_patterns\": {\n        \"activity_by_hour\": \"Atividade por hora\",\n        \"activity_by_day\": \"Atividade por dia\"\n      },\n      \"storage_breakdown\": {\n        \"title\": \"Detalhes do armazenamento\"\n      },\n      \"bookmark_sources\": {\n        \"title\": \"Fontes de marcadores\",\n        \"empty\": \"Nenhum dado de origem disponível\"\n      }\n    },\n    \"subscription\": {\n      \"subscription\": \"Assinatura\",\n      \"manage_subscription\": \"Gerencie sua assinatura e informações de faturamento\",\n      \"current_plan\": \"Plano atual\",\n      \"billing_period\": \"Período de faturamento\",\n      \"paid_plan\": \"Plano Pago\",\n      \"unlock_bigger_quota\": \"Desbloqueie uma cota maior e apoie o projeto\",\n      \"subscribe_now\": \"Assine agora\",\n      \"manage_billing\": \"Gerenciar faturamento\",\n      \"subscription_canceled\": \"Sua assinatura foi cancelada e terminará em {{date}}. Você pode se reinscrever a qualquer momento.\",\n      \"usage_quotas\": \"Uso e cotas\",\n      \"track_usage\": \"Acompanhe seu uso atual em relação aos limites do seu plano\",\n      \"total_bookmarks_saved\": \"Total de favoritos salvos\",\n      \"assets_file_storage\": \"Armazenamento de arquivos e ativos\",\n      \"unlimited_usage\": \"Uso ilimitado\",\n      \"quota_limit_reached\": \"Limite de cota atingido\",\n      \"approaching_quota_limit\": \"Limite de cota se aproximando\",\n      \"loading_usage\": \"Carregando informações de uso...\",\n      \"free\": \"Grátis\",\n      \"paid\": \"Pago\"\n    },\n    \"import_sessions\": {\n      \"title\": \"Importar sessões\",\n      \"description\": \"Visualize e gerencie suas sessões de importação em massa. As sessões são criadas automaticamente quando você importa favoritos.\",\n      \"load_error\": \"Falha ao carregar sessões de importação\",\n      \"no_sessions\": \"Nenhuma sessão de importação ainda\",\n      \"no_sessions_detail\": \"As sessões de importação aparecerão aqui automaticamente quando você importar favoritos\",\n      \"created_at\": \"Criado {{time}}\",\n      \"progress\": \"Progresso\",\n      \"status\": {\n        \"pending\": \"Pendente\",\n        \"in_progress\": \"Em andamento\",\n        \"completed\": \"Concluído\",\n        \"failed\": \"Falhou\",\n        \"processing\": \"Processando\",\n        \"staging\": \"Teste\",\n        \"running\": \"Em execução\",\n        \"paused\": \"Pausado\"\n      },\n      \"badges\": {\n        \"pending\": \"{{count}} pendente\",\n        \"processing\": \"{{count}} processando\",\n        \"completed\": \"{{count}} concluído\",\n        \"failed\": \"{{count}} falhou\"\n      },\n      \"imported_to\": \"Importado para:\",\n      \"view_list\": \"Ver lista\",\n      \"delete_dialog_title\": \"Excluir sessão de importação\",\n      \"delete_dialog_description\": \"Tem certeza de que deseja excluir \\\"{{name}}\\\"? Esta ação não pode ser desfeita. Os favoritos em si não serão excluídos.\",\n      \"delete_session\": \"Excluir sessão\",\n      \"pause_session\": \"Pausar\",\n      \"resume_session\": \"Retomar\",\n      \"view_details\": \"Ver Detalhes\",\n      \"detail\": {\n        \"page_title\": \"Detalhes da Sessão de Importação\",\n        \"back_to_import\": \"Voltar para Importar\",\n        \"filter_all\": \"Tudo\",\n        \"filter_accepted\": \"Aceito\",\n        \"filter_rejected\": \"Rejeitado\",\n        \"filter_duplicates\": \"Duplicados\",\n        \"filter_pending\": \"Pendente\",\n        \"table_title\": \"Título / URL\",\n        \"table_type\": \"Tipo\",\n        \"table_result\": \"Resultado\",\n        \"table_reason\": \"Razão\",\n        \"table_bookmark\": \"Favorito\",\n        \"result_accepted\": \"Aceito\",\n        \"result_rejected\": \"Rejeitado\",\n        \"result_skipped_duplicate\": \"Duplicado\",\n        \"result_pending\": \"Pendente\",\n        \"result_processing\": \"Processando\",\n        \"no_results\": \"Nenhum resultado encontrado para este filtro.\",\n        \"view_bookmark\": \"Ver Favorito\",\n        \"load_more\": \"Carregar mais\",\n        \"no_title\": \"Sem título\"\n      }\n    },\n    \"backups\": {\n      \"backups\": \"Backups\",\n      \"page_title\": \"Backups\",\n      \"page_description\": \"Crie e gerencie automaticamente backups dos seus favoritos. Os backups são compactados e armazenados de forma segura.\",\n      \"configuration\": {\n        \"title\": \"Configuração de Backup\",\n        \"enable_automatic_backups\": \"Ativar Backups Automáticos\",\n        \"enable_automatic_backups_description\": \"Criar backups automaticamente dos seus favoritos\",\n        \"backup_frequency\": \"Frequência de Backup\",\n        \"backup_frequency_description\": \"Com que frequência os backups devem ser criados\",\n        \"retention_period\": \"Período de Retenção (dias)\",\n        \"retention_period_description\": \"Quantos dias manter os backups antes de excluí-los\",\n        \"frequency\": {\n          \"daily\": \"Diariamente\",\n          \"weekly\": \"Semanalmente\"\n        },\n        \"select_frequency\": \"Selecionar frequência\",\n        \"save_settings\": \"Salvar Configurações\"\n      },\n      \"list\": {\n        \"title\": \"Seus Backups\",\n        \"create_backup_now\": \"Criar Backup Agora\",\n        \"no_backups\": \"Você ainda não tem nenhum backup. Ative os backups automáticos ou crie um manualmente.\",\n        \"table\": {\n          \"created_at\": \"Criado em\",\n          \"bookmarks\": \"Favoritos\",\n          \"size\": \"Tamanho\",\n          \"status\": \"Estado\",\n          \"actions\": \"Ações\"\n        },\n        \"status\": {\n          \"success\": \"Sucesso\",\n          \"failed\": \"Falhou\",\n          \"pending\": \"Pendente\"\n        },\n        \"actions\": {\n          \"download_backup\": \"Baixar backup\",\n          \"delete_backup\": \"Apagar backup\"\n        }\n      },\n      \"dialogs\": {\n        \"delete_backup_title\": \"Apagar backup?\",\n        \"delete_backup_description\": \"Tens a certeza que queres apagar este backup? Esta ação não pode ser desfeita.\"\n      },\n      \"toasts\": {\n        \"backup_queued\": \"O trabalho de backup foi enfileirado! Será processado em breve.\",\n        \"backup_deleted\": \"O backup foi apagado!\"\n      }\n    }\n  },\n  \"admin\": {\n    \"background_jobs\": {\n      \"crawler_jobs\": \"Tarefas do rastreador\",\n      \"video_jobs\": \"Tarefas de download de vídeo\",\n      \"feed_jobs\": \"Tarefas de feed RSS\",\n      \"failed\": \"Falhou\",\n      \"pending\": \"Pendente\",\n      \"webhook_jobs\": \"Tarefas de Webhook\",\n      \"asset_preprocessing_jobs\": \"Tarefas de Pré-processamento de Ativos\",\n      \"queued\": \"Na fila\",\n      \"background_jobs\": \"Tarefas em segundo plano\",\n      \"indexing_jobs\": \"Tarefas de Indexação\",\n      \"inference_jobs\": \"Tarefas de Inferência\",\n      \"tidy_assets_jobs\": \"Tarefas de organização de ativos\",\n      \"job\": \"Trabalho\",\n      \"jobs\": {\n        \"crawler\": {\n          \"title\": \"Tarefas do Crawler\",\n          \"description\": \"Rastreamento da web e extração de conteúdo de URLs\"\n        },\n        \"inference\": {\n          \"title\": \"Tarefas de Inferência\",\n          \"description\": \"Marcação e resumo de conteúdo alimentados por IA\"\n        },\n        \"indexing\": {\n          \"title\": \"Tarefas de Indexação\",\n          \"description\": \"Atualizações do índice de pesquisa\"\n        },\n        \"asset_preprocessing\": {\n          \"title\": \"Tarefas de Pré-processamento de Ativos\",\n          \"description\": \"Pré-processamento de imagem e documento (capturas de tela, extração de texto, etc.)\"\n        },\n        \"tidy_assets\": {\n          \"title\": \"Tarefas de Organização de Ativos\",\n          \"description\": \"Limpeza de ativos e otimização de armazenamento\"\n        },\n        \"video\": {\n          \"title\": \"Tarefas de Download de Vídeo\",\n          \"description\": \"Extração e download de vídeo\"\n        },\n        \"webhook\": {\n          \"title\": \"Tarefas de Webhook\",\n          \"description\": \"Notificações de webhook externo\"\n        },\n        \"feed\": {\n          \"title\": \"Tarefas de Feed RSS\",\n          \"description\": \"Processamento de feed RSS e atualizações de conteúdo\"\n        },\n        \"admin_maintenance\": {\n          \"title\": \"Tarefas de Manutenção do Administrador\",\n          \"description\": \"Limpeza administrativa e manutenção de ativos\"\n        }\n      },\n      \"monitor_and_manage\": \"Monitore e gerencie filas de tarefas em segundo plano e tarefas de processamento do sistema\",\n      \"active\": \"Ativo\",\n      \"available_actions\": \"Ações Disponíveis\",\n      \"status\": {\n        \"title\": \"Entendendo os estados de tarefas\",\n        \"queued\": {\n          \"title\": \"Na fila\",\n          \"description\": \"Tarefas esperando na fila para serem processadas. Elas serão iniciadas automaticamente quando os recursos estiverem disponíveis.\"\n        },\n        \"unprocessed\": {\n          \"title\": \"Não processado\",\n          \"description\": \"Favoritos que ainda não foram processados. Eles provavelmente já estão na fila para processamento, caso contrário, você pode precisar reativá-los manualmente.\"\n        },\n        \"failed\": {\n          \"title\": \"Falhou\",\n          \"description\": \"Favoritos que encontraram erros durante o processamento. Estes podem precisar de atenção manual.\"\n        }\n      },\n      \"actions\": {\n        \"recrawl_failed_links_only\": \"Rastrear novamente apenas links com falha\",\n        \"recrawl_all_links\": \"Rastrear novamente todos os links\",\n        \"without_inference\": \"Sem inferência\",\n        \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Regenerar tags de IA apenas para favoritos com falha\",\n        \"regenerate_ai_tags_for_all_bookmarks\": \"Regenerar tags de IA para todos os favoritos\",\n        \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Regenerar resumos de IA apenas para favoritos com falha\",\n        \"regenerate_ai_summaries_for_all_bookmarks\": \"Regenerar resumos de IA para todos os favoritos\",\n        \"reindex_all_bookmarks\": \"Reindexar todos os favoritos\",\n        \"clean_assets\": \"Limpar ativos soltos e resincronizar metadados\",\n        \"reprocess_assets_fix_mode\": \"Reprocessar ativos não processados\",\n        \"migrate_large_link_html_content\": \"Mover Conteúdo HTML Grande Embutido para Ativos\",\n        \"recrawl_pending_links_only\": \"Rastrear Novamente Apenas Links Pendentes\",\n        \"regenerate_ai_tags_for_pending_bookmarks_only\": \"Regenerar Tags de IA Apenas para Favoritos Pendentes\",\n        \"regenerate_ai_summaries_for_pending_bookmarks_only\": \"Regenerar Resumos de IA Apenas para Favoritos Pendentes\"\n      }\n    },\n    \"actions\": {\n      \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Regenerar tags de IA apenas para marcadores com falha\",\n      \"recrawl_failed_links_only\": \"Rastrear Apenas Links Falhos\",\n      \"recrawl_all_links\": \"Rastrear todos os links novamente\",\n      \"without_inference\": \"Sem inferência\",\n      \"regenerate_ai_tags_for_all_bookmarks\": \"Regenerar Tags de IA para Todos os Favoritos\",\n      \"reindex_all_bookmarks\": \"Reindexar Todos os Marcadores\",\n      \"compact_assets\": \"Ativos compactos\",\n      \"reprocess_assets_fix_mode\": \"Reprocessar Ativos (Modo de Correção)\",\n      \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Regenerar resumos de IA apenas para marcadores com falha\",\n      \"regenerate_ai_summaries_for_all_bookmarks\": \"Regenerar resumos de IA para todos os marcadores\"\n    },\n    \"users_list\": {\n      \"delete_user\": \"Excluir usuário\",\n      \"num_bookmarks\": \"Número de Marcadores\",\n      \"asset_sizes\": \"Tamanhos de Ativos\",\n      \"confirm_password\": \"Confirmar Senha\",\n      \"users_list\": \"Lista de usuários\",\n      \"create_user\": \"Criar usuário\",\n      \"change_role\": \"Alterar função\",\n      \"reset_password\": \"Redefinir Senha\",\n      \"local_user\": \"Usuário local\",\n      \"delete_user_confirm_description\": \"Tens a certeza que queres apagar o utilizador \\\"{{name}}\\\"?\",\n      \"unlimited\": \"Ilimitado\"\n    },\n    \"server_stats\": {\n      \"total_bookmarks\": \"Total de favoritos\",\n      \"server_version\": \"Versão do servidor\",\n      \"server_stats\": \"Estatísticas do servidor\",\n      \"total_users\": \"Total de Usuários\"\n    },\n    \"admin_settings\": \"Configurações de Administrador\",\n    \"service_connections\": {\n      \"title\": \"Conexões de serviço\",\n      \"description\": \"Monitore a saúde e a conectividade das dependências do sistema externo\",\n      \"search_engine\": \"Mecanismo de busca\",\n      \"browser\": \"Navegador\",\n      \"queue_system\": \"Sistema de fila\",\n      \"status\": {\n        \"not_configured\": \"Não configurado\",\n        \"connected\": \"Conectado\",\n        \"disconnected\": \"Desconectado\"\n      }\n    },\n    \"admin_tools\": {\n      \"admin_tools\": \"Ferramentas de Administrador\",\n      \"bookmark_debugger\": \"Depurador de Marcadores\",\n      \"bookmark_id\": \"ID do Marcador\",\n      \"bookmark_id_placeholder\": \"Insira o ID do marcador\",\n      \"lookup\": \"Pesquisar\",\n      \"debug_info\": \"Informação de Depuração\",\n      \"basic_info\": \"Informação Básica\",\n      \"status\": \"Estado\",\n      \"content\": \"Conteúdo\",\n      \"html_preview\": \"Pré-visualização HTML (Primeiros 1000 caracteres)\",\n      \"summary\": \"Resumo\",\n      \"url\": \"URL\",\n      \"source_url\": \"URL de Origem\",\n      \"asset_type\": \"Tipo de Recurso\",\n      \"file_name\": \"Nome do Ficheiro\",\n      \"owner_user_id\": \"ID do Utilizador Proprietário\",\n      \"tagging_status\": \"Estado da Etiquetagem\",\n      \"summarization_status\": \"Estado da Sumarização\",\n      \"crawl_status\": \"Estado da Indexação\",\n      \"crawl_status_code\": \"Código de status HTTP\",\n      \"crawled_at\": \"Rastreamento em\",\n      \"recrawl\": \"Rastrear novamente\",\n      \"reindex\": \"Reindexar\",\n      \"retag\": \"Retaguar\",\n      \"resummarize\": \"Resumir novamente\",\n      \"bookmark_not_found\": \"Marcador não encontrado\",\n      \"action_success\": \"Ação concluída com sucesso\",\n      \"action_failed\": \"Ação falhou\",\n      \"recrawl_queued\": \"O trabalho de rastreamento foi enfileirado\",\n      \"reindex_queued\": \"O trabalho de reindexação foi enfileirado\",\n      \"retag_queued\": \"O trabalho de marcação foi enfileirado\",\n      \"resummarize_queued\": \"O trabalho de resumo foi enfileirado\",\n      \"view\": \"Ver\",\n      \"fetch_error\": \"Erro ao buscar marcador\"\n    }\n  },\n  \"options\": {\n    \"dark_mode\": \"Modo escuro\",\n    \"light_mode\": \"Modo claro\",\n    \"apps_extensions\": \"Apps e extensões\",\n    \"documentation\": \"Documentação\",\n    \"follow_us_on_x\": \"Segue-nos no X\"\n  },\n  \"lists\": {\n    \"new_nested_list\": \"Nova Lista Aninhada\",\n    \"new_list\": \"Nova Lista\",\n    \"edit_list\": \"Editar lista\",\n    \"all_lists\": \"Todas as Listas\",\n    \"favourites\": \"Favoritos\",\n    \"parent_list\": \"Lista Principal\",\n    \"no_parent\": \"Sem Progenitor\",\n    \"list_type\": \"Tipo de Lista\",\n    \"manual_list\": \"Lista Manual\",\n    \"smart_list\": \"Lista inteligente\",\n    \"search_query\": \"Consulta de pesquisa\",\n    \"search_query_help\": \"Saiba mais sobre a linguagem de consulta de pesquisa.\",\n    \"merge_list\": \"Mesclar lista\",\n    \"destination_list\": \"Lista de destino\",\n    \"delete_after_merge\": \"Excluir lista original após a mesclagem\",\n    \"no_destination\": \"Sem destino\",\n    \"description\": \"Descrição (Opcional)\",\n    \"share_list\": \"Compartilhar lista\",\n    \"rss\": {\n      \"title\": \"Feed RSS\",\n      \"description\": \"Ativar um feed RSS para esta lista\",\n      \"feed_url\": \"URL do feed RSS\"\n    },\n    \"public_list\": {\n      \"title\": \"Lista pública\",\n      \"description\": \"Permitir que outros vejam esta lista\",\n      \"share_link\": \"Compartilhar link\"\n    },\n    \"delete_list\": {\n      \"title\": \"Eliminar lista\",\n      \"description\": \"Eliminar uma lista não elimina nenhum marcador dessa lista.\",\n      \"delete_children\": \"Eliminar listas filhas (recursivamente)\",\n      \"delete_children_description\": \"Se não estiver selecionado, todas as listas filhas diretas se tornarão listas raiz\"\n    },\n    \"shared\": \"Compartilhado\",\n    \"collaborators\": {\n      \"manage\": \"Gerenciar Colaboradores\",\n      \"view\": \"Ver Colaboradores\",\n      \"collaborators\": \"Colaboradores\",\n      \"add\": \"Adicionar Colaborador\",\n      \"current\": \"Colaboradores Atuais\",\n      \"enter_email\": \"Insira o endereço de e-mail\",\n      \"please_enter_email\": \"Por favor, insira um endereço de e-mail\",\n      \"added_successfully\": \"Colaborador adicionado com sucesso\",\n      \"failed_to_add\": \"Falha ao adicionar colaborador\",\n      \"removed\": \"Colaborador removido\",\n      \"failed_to_remove\": \"Falha ao remover colaborador\",\n      \"role_updated\": \"Função atualizada\",\n      \"failed_to_update_role\": \"Falha ao atualizar função\",\n      \"viewer\": \"Visualizador\",\n      \"editor\": \"Editor\",\n      \"owner\": \"Proprietário\",\n      \"viewer_description\": \"Pode ver os favoritos na lista\",\n      \"editor_description\": \"Pode adicionar e remover favoritos\",\n      \"no_collaborators\": \"Ainda não há colaboradores. Adicione alguém para começar a colaborar!\",\n      \"no_collaborators_readonly\": \"Nenhum colaborador para esta lista.\",\n      \"people_with_access\": \"Pessoas que têm acesso a esta lista\",\n      \"add_or_remove\": \"Adicione ou remova pessoas que podem acessar esta lista\",\n      \"invitation_sent\": \"Convite enviado com sucesso\",\n      \"invitation_revoked\": \"Convite revogado\",\n      \"failed_to_revoke\": \"Falha ao revogar o convite\",\n      \"pending\": \"Pendente\",\n      \"revoke\": \"Revogar\",\n      \"declined\": \"Recusado\"\n    },\n    \"leave_list\": {\n      \"title\": \"Sair da lista\",\n      \"confirm_message\": \"Tem certeza de que pretende sair de {{icon}} {{name}}?\",\n      \"warning\": \"Não poderá mais ver ou acessar os favoritos nesta lista. O proprietário da lista pode adicioná-lo novamente, se necessário.\",\n      \"action\": \"Sair da lista\",\n      \"success\": \"Você saiu de \\\"{{icon}} {{name}}\\\"\"\n    },\n    \"invitations\": {\n      \"pending\": \"Convites pendentes\",\n      \"description\": \"Analise e responda aos convites de colaboração da lista\",\n      \"invited_by\": \"Convidado por\",\n      \"accept\": \"Aceitar\",\n      \"decline\": \"Recusar\",\n      \"accepted\": \"Convite aceito\",\n      \"declined\": \"Convite recusado\",\n      \"failed_to_accept\": \"Falha ao aceitar o convite\",\n      \"failed_to_decline\": \"Falha ao recusar o convite\"\n    },\n    \"shared_lists\": \"Listas Partilhadas\"\n  },\n  \"tags\": {\n    \"your_tags_info\": \"Tags que foram anexadas pelo menos uma vez por você\",\n    \"sort_by_name\": \"Ordenar por nome\",\n    \"ai_tags\": \"Tags de IA\",\n    \"drag_and_drop_merging\": \"Mesclagem Arrastar e Soltar\",\n    \"drag_and_drop_merging_info\": \"Arraste e solte tags umas sobre as outras para mesclá-las\",\n    \"unused_tags_info\": \"Tags que não estão anexadas a nenhum marcador\",\n    \"delete_all_unused_tags\": \"Excluir todas as etiquetas não utilizadas\",\n    \"all_tags\": \"Todas as tags\",\n    \"your_tags\": \"Suas etiquetas\",\n    \"ai_tags_info\": \"Tags que foram anexadas apenas automaticamente (por IA)\",\n    \"unused_tags\": \"Tags não utilizadas\",\n    \"create_tag\": \"Criar etiqueta\",\n    \"create_tag_description\": \"Criar uma nova etiqueta sem anexá-la a nenhum marcador\",\n    \"tag_name\": \"Nome da etiqueta\",\n    \"enter_tag_name\": \"Insere o nome da etiqueta\",\n    \"sort_by_usage\": \"Ordenar por uso\",\n    \"sort_by_relevance\": \"Ordenar por relevância\",\n    \"no_custom_tags\": \"Ainda não há tags personalizadas\",\n    \"no_ai_tags\": \"Ainda não há tags de IA\",\n    \"no_unused_tags\": \"Você não tem nenhuma tag não usada\",\n    \"no_unused_tags_match_your_search\": \"Nenhuma tag não utilizada corresponde à sua pesquisa\",\n    \"no_tags_match_your_search\": \"Nenhuma tag corresponde à sua pesquisa\",\n    \"search_placeholder\": \"Pesquisar tags...\",\n    \"search_or_create_placeholder\": \"Pesquisar ou criar tags...\"\n  },\n  \"search\": {\n    \"full_text_search\": \"Pesquisa de Texto Completo\",\n    \"type_is\": \"Tipo é\",\n    \"or\": \"Ou\",\n    \"url_contains\": \"URL Contém\",\n    \"url_does_not_contain\": \"URL Não Contém\",\n    \"is_in_list\": \"Está Na Lista\",\n    \"is_favorited\": \"É favorito\",\n    \"is_not_favorited\": \"Não está favoritado\",\n    \"is_archived\": \"Está arquivado\",\n    \"is_not_archived\": \"Não Está Arquivado\",\n    \"is_not_in_any_list\": \"Não Está Em Nenhuma Lista\",\n    \"created_on_or_after\": \"Criado em ou após\",\n    \"not_created_on_or_before\": \"Não criado em ou antes de\",\n    \"is_not_in_list\": \"Não está na lista\",\n    \"has_tag\": \"Tem tag\",\n    \"and\": \"E\",\n    \"has_any_tag\": \"Tem Alguma Etiqueta\",\n    \"is_in_any_list\": \"Está em alguma lista\",\n    \"created_on_or_before\": \"Criado em ou antes de\",\n    \"has_no_tags\": \"Não tem tag\",\n    \"not_created_on_or_after\": \"Não criado em ou depois de\",\n    \"does_not_have_tag\": \"Não tem etiqueta\",\n    \"type_is_not\": \"Tipo não é\",\n    \"is_from_feed\": \"É de um feed RSS\",\n    \"is_not_from_feed\": \"Não é de um feed RSS\",\n    \"created_within\": \"Criado dentro de\",\n    \"created_earlier_than\": \"Criado antes de\",\n    \"day_s\": \" Dia(s)\",\n    \"week_s\": \" Semana(s)\",\n    \"month_s\": \" Mês(es)\",\n    \"year_s\": \" Ano(s)\",\n    \"day_s_ago\": \" Dia(s) atrás\",\n    \"week_s_ago\": \" Semana(s) atrás\",\n    \"month_s_ago\": \" Mês(es) atrás\",\n    \"year_s_ago\": \" Ano(s) atrás\",\n    \"history\": \"Pesquisas recentes\",\n    \"title_contains\": \"O título contém…\",\n    \"title_does_not_contain\": \"O título não contém…\",\n    \"is_broken_link\": \"Tem link quebrado\",\n    \"tags\": \"Etiquetas\",\n    \"no_suggestions\": \"Sem sugestões\",\n    \"filters\": \"Filtros\",\n    \"is_not_broken_link\": \"Tem link funcionando\",\n    \"lists\": \"Listas\",\n    \"feeds\": \"Feeds\",\n    \"is_from_source\": \"Fonte é\",\n    \"is_not_from_source\": \"Fonte não é\"\n  },\n  \"preview\": {\n    \"cached_content\": \"Conteúdo em cache\",\n    \"view_original\": \"Ver original\",\n    \"reader_view\": \"Modo Leitura\",\n    \"tabs\": {\n      \"content\": \"Conteúdo\",\n      \"details\": \"Detalhes\"\n    },\n    \"archive_info\": \"Os arquivos podem não ser renderizados corretamente embutidos se exigirem Javascript. Para obter melhores resultados, <1>baixe-o e abra-o no seu navegador</1>.\",\n    \"fetch_error_title\": \"Conteúdo Indisponível\",\n    \"fetch_error_description\": \"Não foi possível obter o conteúdo para este link. É possível que a página esteja protegida, precise de autenticação ou esteja temporariamente indisponível.\",\n    \"crawling_in_progress\": \"Buscando conteúdo da página…\",\n    \"continue_reading\": \"Continue de onde parou\",\n    \"continue_reading_percent\": \"Continue de onde parou ({{percent}}%)\",\n    \"continue_button\": \"Continuar\"\n  },\n  \"editor\": {\n    \"new_item\": \"NOVO ITEM\",\n    \"text_toolbar\": {\n      \"align_center\": \"Alinhar ao centro\",\n      \"underline\": \"Sublinhado\",\n      \"strikethrough\": \"Tachado\",\n      \"code\": \"Código\",\n      \"highlight\": \"Realçar\",\n      \"align_left\": \"Alinhar à esquerda\",\n      \"markdown_shortcuts\": {\n        \"italic\": {\n          \"example\": \"*Itálico* ou _Itálico_ ou CTRL+i\",\n          \"label\": \"Itálico\"\n        },\n        \"block_code\": {\n          \"example\": \"``` + espaço\",\n          \"label\": \"Bloco de Código\"\n        },\n        \"heading\": {\n          \"label\": \"Título\",\n          \"example\": \"# H1, ## H2, ### H3\"\n        },\n        \"bold\": {\n          \"label\": \"Negrito\",\n          \"example\": \"**texto** ou CTRL+b\"\n        },\n        \"blockquote\": {\n          \"label\": \"Bloco de Citação\",\n          \"example\": \"> Citação em bloco\"\n        },\n        \"unordered_list\": {\n          \"example\": \"- Item da lista\",\n          \"label\": \"Lista não ordenada\"\n        },\n        \"inline_code\": {\n          \"label\": \"Código embutido\",\n          \"example\": \"`Código`\"\n        },\n        \"label\": \"Atalhos de Markdown\",\n        \"ordered_list\": {\n          \"label\": \"Lista ordenada\",\n          \"example\": \"1. Item da lista\"\n        }\n      },\n      \"undo\": \"Desfazer\",\n      \"redo\": \"Refazer\",\n      \"italic\": \"Itálico\",\n      \"align_right\": \"Alinhar à Direita\",\n      \"bold\": \"Negrito\"\n    },\n    \"disabled_submissions\": \"Envios estão desativados\",\n    \"quickly_focus\": \"Você pode focar rapidamente neste campo pressionando ⌘ + E\",\n    \"multiple_urls_dialog_title\": \"Importar URLs como Marcadores separados?\",\n    \"multiple_urls_dialog_desc\": \"A entrada contém vários URLs em linhas separadas. Deseja importá-los como marcadores separados?\",\n    \"import_as_text\": \"Importar como favorito de texto\",\n    \"import_as_separate_bookmarks\": \"Importar como favoritos separados\",\n    \"placeholder\": \"Cole um link ou uma imagem, escreva uma nota ou arraste e solte uma imagem aqui…\",\n    \"placeholder_v2\": \"Cole um link, escreva uma nota ou solte uma imagem…\"\n  },\n  \"toasts\": {\n    \"bookmarks\": {\n      \"full_page_archive\": \"A criação do Arquivo de Página Inteira foi acionada\",\n      \"delete_from_list\": \"O marcador foi excluído da lista\",\n      \"clipboard_copied\": \"Link foi adicionado à sua área de transferência!\",\n      \"updated\": \"O marcador foi atualizado!\",\n      \"deleted\": \"O marcador foi excluído!\",\n      \"refetch\": \"A nova busca foi enfileirada!\",\n      \"preserve_pdf\": \"A preservação em PDF foi acionada\",\n      \"update_banner\": \"O banner foi atualizado!\",\n      \"uploading_banner\": \"Enviando banner...\"\n    },\n    \"lists\": {\n      \"updated\": \"A lista foi atualizada!\",\n      \"created\": \"Lista foi criada!\",\n      \"merged\": \"Lista foi mesclada!\",\n      \"deleted\": \"Lista foi excluída!\"\n    },\n    \"tags\": {\n      \"created\": \"Etiqueta criada!\",\n      \"failed_to_create\": \"Falha ao criar etiqueta\"\n    }\n  },\n  \"cleanups\": {\n    \"cleanups\": \"Limpezas\",\n    \"duplicate_tags\": {\n      \"title\": \"Tags Duplicadas\",\n      \"merge_all_suggestions\": \"Mesclar todas as sugestões?\"\n    }\n  },\n  \"highlights\": {\n    \"no_highlights\": \"Você ainda não tem nenhum destaque.\"\n  },\n  \"dialogs\": {\n    \"bookmarks\": {\n      \"delete_confirmation_title\": \"Excluir marcador?\",\n      \"delete_confirmation_description\": \"Tens a certeza que queres apagar este marcador?\"\n    }\n  },\n  \"layouts\": {\n    \"grid\": \"Grade\",\n    \"list\": \"Lista\",\n    \"compact\": \"Compacto\",\n    \"masonry\": \"Alvenaria\"\n  },\n  \"banners\": {\n    \"no_bookmarks\": {\n      \"title\": \"Ainda não há marcadores\",\n      \"description\": \"Salve artigos, links e páginas interessantes para acessá-los rapidamente mais tarde.\"\n    }\n  },\n  \"bookmark_editor\": {\n    \"title\": \"Editar marcador\",\n    \"subtitle\": \"Faça alterações nos detalhes do marcador. Clique em salvar quando terminar.\",\n    \"author\": \"Autor\",\n    \"publisher\": \"Editora\",\n    \"date_published\": \"Data de publicação\",\n    \"pick_a_date\": \"Escolha uma data\",\n    \"save_changes\": \"Salvar alterações\",\n    \"extracted_content\": \"Conteúdo extraído\"\n  },\n  \"view_options\": {\n    \"title\": \"Opções de visualização\",\n    \"layout\": \"Layout\",\n    \"columns\": \"Colunas\",\n    \"display_options\": \"Opções de visualização\",\n    \"show_note_previews\": \"Mostrar notas\",\n    \"show_tags\": \"Mostrar tags\",\n    \"show_title\": \"Mostrar título\",\n    \"image_options\": \"Opções de imagem\",\n    \"image_fit_cover\": \"Cobrir (preencher)\",\n    \"image_fit_contain\": \"Conter (ajustar)\"\n  },\n  \"version\": {\n    \"new_release_available\": \"Novas notas de lançamento disponíveis\",\n    \"whats_new_title\": \"O que há de novo na v{{version}}\",\n    \"release_notes_description\": \"Aqui estão as últimas atualizações obtidas das notas de lançamento do GitHub.\",\n    \"loading_release_notes\": \"Carregando as notas de lançamento…\",\n    \"unable_to_load_release_notes\": \"Não foi possível carregar as notas de lançamento neste momento. Tente novamente mais tarde.\",\n    \"no_release_notes\": \"Nenhuma nota de lançamento foi publicada para esta versão.\",\n    \"release_notes_synced\": \"As notas de lançamento são sincronizadas do GitHub.\",\n    \"view_on_github\": \"Ver no GitHub\"\n  },\n  \"wrapped\": {\n    \"title\": \"Seu Resumo de {{year}}\",\n    \"subtitle\": \"Um Ano em Karakeep\",\n    \"banner\": {\n      \"title\": \"Seu Resumo de 2025 está pronto!\",\n      \"description\": \"Veja seu ano em favoritos\",\n      \"view_now\": \"Ver Agora\"\n    },\n    \"button\": \"Resumo de 2025\",\n    \"loading\": \"Carregando seu Resumo...\",\n    \"failed_to_load\": \"Falha ao carregar suas estatísticas do Resumo\",\n    \"sections\": {\n      \"total_saves\": {\n        \"prefix\": \"Você salvou\",\n        \"suffix\": \"itens este ano\",\n        \"suffix_singular\": \"item este ano\"\n      },\n      \"first_bookmark\": {\n        \"title\": \"Sua jornada começou\",\n        \"description\": \"Primeiro salvamento de {{year}}:\"\n      },\n      \"top_domains\": \"Seus principais sites\",\n      \"top_tags\": \"Suas principais tags\",\n      \"monthly_activity\": \"Seu ano em salvamentos\",\n      \"most_active_day\": \"Seu dia mais ativo\",\n      \"peak_times\": {\n        \"title\": \"Quando você salva\",\n        \"peak_hour\": \"Hora de pico\",\n        \"peak_day\": \"Dia de pico\"\n      },\n      \"how_you_save\": \"Como você salva\",\n      \"what_you_saved\": \"O que você salvou\",\n      \"summary\": {\n        \"favorites\": \"Favoritos\",\n        \"tags_created\": \"Tags criadas\",\n        \"highlights\": \"Destaques\"\n      },\n      \"types\": {\n        \"links\": \"Links\",\n        \"notes\": \"Notas\",\n        \"assets\": \"Ativos\"\n      }\n    },\n    \"footer\": \"Feito com Karakeep\",\n    \"share\": \"Compartilhar\",\n    \"download\": \"Baixar\",\n    \"close\": \"Fechar\",\n    \"generating\": \"Gerando...\"\n  }\n}\n"
  },
  {
    "path": "apps/web/lib/i18n/locales/pt_BR/translation.json",
    "content": "{\n  \"common\": {\n    \"bookmark_types\": {\n      \"media\": \"Mídia\",\n      \"title\": \"Tipo de marcador\",\n      \"text\": \"Texto\",\n      \"link\": \"Link\"\n    },\n    \"url\": \"URL\",\n    \"name\": \"Nome\",\n    \"email\": \"Email\",\n    \"password\": \"Senha\",\n    \"action\": \"Ação\",\n    \"actions\": \"Ações\",\n    \"created_at\": \"Criado Em\",\n    \"key\": \"Chave\",\n    \"role\": \"Função\",\n    \"roles\": {\n      \"user\": \"Usuário\",\n      \"admin\": \"Administrador\"\n    },\n    \"something_went_wrong\": \"Algo deu errado\",\n    \"experimental\": \"Experimental\",\n    \"search\": \"Busca\",\n    \"tags\": \"Tags\",\n    \"note\": \"Nota\",\n    \"attachments\": \"Anexos\",\n    \"highlights\": \"Destaques\",\n    \"source\": \"Fonte\",\n    \"screenshot\": \"Captura de tela\",\n    \"video\": \"Vídeo\",\n    \"archive\": \"Arquivo\",\n    \"home\": \"Home\",\n    \"type\": \"Tipo\",\n    \"size\": \"Tamanho\",\n    \"updated_at\": \"Atualizado em\",\n    \"title\": \"Título\",\n    \"description\": \"Descrição\",\n    \"summary\": \"Resumo\",\n    \"quota\": \"Cota\",\n    \"bookmarks\": \"Favoritos\",\n    \"storage\": \"Armazenamento\",\n    \"pdf\": \"PDF Arquivado\",\n    \"default\": \"Padrão\",\n    \"id\": \"ID\",\n    \"last_used\": \"Usado recentemente\"\n  },\n  \"actions\": {\n    \"unarchive\": \"Desarquivar\",\n    \"download_full_page_archive\": \"Baixar Página Completa\",\n    \"manage_lists\": \"Gerenciar Listas\",\n    \"change_layout\": \"Mudar Layout\",\n    \"archive\": \"Arquivo\",\n    \"favorite\": \"Favorito\",\n    \"unfavorite\": \"Remover dos favoritos\",\n    \"delete\": \"Deletar\",\n    \"refresh\": \"Atualizar\",\n    \"recrawl\": \"Recrawl\",\n    \"edit_tags\": \"Editar Tags\",\n    \"add_to_list\": \"Adicionar à Lista\",\n    \"select_all\": \"Selecionar Todos\",\n    \"unselect_all\": \"Desmarcar Todos\",\n    \"copy_link\": \"Copiar Link\",\n    \"close_bulk_edit\": \"Fechar Edição em Massa\",\n    \"bulk_edit\": \"Edição em Massa\",\n    \"remove_from_list\": \"Remover da Lista\",\n    \"save\": \"Salvar\",\n    \"add\": \"Adicionar\",\n    \"edit\": \"Editar\",\n    \"create\": \"Criar\",\n    \"fetch_now\": \"Buscar Agora\",\n    \"summarize_with_ai\": \"Resumir com IA\",\n    \"edit_title\": \"Editar Título\",\n    \"sign_out\": \"Sair\",\n    \"close\": \"Fechar\",\n    \"merge\": \"Mesclar\",\n    \"cancel\": \"Cancelar\",\n    \"apply_all\": \"Aplicar Todos\",\n    \"ignore\": \"Ignorar\",\n    \"sort\": {\n      \"title\": \"Ordenar\",\n      \"newest_first\": \"Mais Recente Primeiro\",\n      \"oldest_first\": \"Mais Antigo Primeiro\",\n      \"relevant_first\": \"Mais relevantes primeiro\"\n    },\n    \"open_editor\": \"Abrir editor\",\n    \"toggle_show_archived\": \"Mostrar arquivados\",\n    \"confirm\": \"Confirmar\",\n    \"regenerate\": \"Regenerar\",\n    \"load_more\": \"Carregar mais\",\n    \"edit_notes\": \"Editar notas\",\n    \"preserve_as_pdf\": \"Preservar como PDF\",\n    \"offline_copies\": \"Cópias Offline\",\n    \"preserve_offline_archive\": \"Preservar Arquivo Offline\",\n    \"download_full_page_archive_file\": \"Baixar Arquivo do Arquivo\",\n    \"download_pdf_file\": \"Baixar Arquivo PDF\",\n    \"remove\": \"Remover\",\n    \"more\": \"Mais\",\n    \"replace_banner\": \"Substituir Banner\",\n    \"add_banner\": \"Adicionar Banner\",\n    \"download\": \"Baixar\"\n  },\n  \"settings\": {\n    \"info\": {\n      \"change_password\": \"Mudar Senha\",\n      \"basic_details\": \"Detalhes Básicos\",\n      \"user_info\": \"Informações do Usuário\",\n      \"current_password\": \"Senha Atual\",\n      \"new_password\": \"Nova Senha\",\n      \"confirm_new_password\": \"Confirmar Nova Senha\",\n      \"options\": \"Opções\",\n      \"interface_lang\": \"Idioma da Interface\",\n      \"user_settings\": {\n        \"archive_display_behaviour\": {\n          \"title\": \"Favoritos arquivados\",\n          \"show\": \"Mostrar favoritos arquivados em tags e listas\",\n          \"hide\": \"Ocultar favoritos arquivados em tags e listas\"\n        },\n        \"user_settings_updated\": \"As configurações do usuário foram atualizadas!\",\n        \"bookmark_click_action\": {\n          \"title\": \"Ação de clique do favorito\",\n          \"open_external_url\": \"Abrir URL original\",\n          \"open_bookmark_details\": \"Abrir detalhes do favorito\"\n        }\n      },\n      \"reader_settings\": {\n        \"local_overrides_title\": \"Configurações específicas do dispositivo ativas\",\n        \"using_default\": \"Usando o padrão do cliente\",\n        \"clear_override_hint\": \"Limpar substituição do dispositivo para usar a configuração global ({{value}})\",\n        \"font_size\": \"Tamanho da Fonte\",\n        \"font_family\": \"Família da Fonte\",\n        \"preview_inline\": \"(visualização)\",\n        \"tooltip_preview\": \"Alterações de visualização não salvas\",\n        \"save_to_all_devices\": \"Todos os dispositivos\",\n        \"tooltip_local\": \"Configurações do dispositivo diferentes das globais\",\n        \"reset_preview\": \"Redefinir visualização\",\n        \"mono\": \"Monoespaçado\",\n        \"line_height\": \"Altura da Linha\",\n        \"tooltip_default\": \"Configurações de leitura\",\n        \"title\": \"Configurações do Leitor\",\n        \"serif\": \"Serifa\",\n        \"preview\": \"Visualização\",\n        \"not_set\": \"Não definido\",\n        \"clear_local_overrides\": \"Limpar configurações do dispositivo\",\n        \"preview_text\": \"A raposa marrom rápida pula sobre o cão preguiçoso. É assim que o texto da sua visualização do leitor aparecerá.\",\n        \"local_overrides_cleared\": \"As configurações específicas do dispositivo foram apagadas\",\n        \"local_overrides_description\": \"Este dispositivo tem configurações de leitor que diferem de suas configurações padrão globais:\",\n        \"clear_defaults\": \"Limpar todos os padrões\",\n        \"description\": \"Configure as configurações de texto padrão para a visualização do leitor. Essas configurações são sincronizadas em todos os seus dispositivos.\",\n        \"defaults_cleared\": \"Os padrões do leitor foram apagados\",\n        \"save_hint\": \"Salvar configurações apenas para este dispositivo ou sincronizar entre todos os dispositivos\",\n        \"save_as_default\": \"Salvar como padrão\",\n        \"save_to_device\": \"Este dispositivo\",\n        \"sans\": \"Sem serifa\",\n        \"tooltip_preview_and_local\": \"Alterações de visualização não salvas; configurações do dispositivo diferentes das globais\",\n        \"adjust_hint\": \"Ajuste as configurações acima para visualizar as alterações\"\n      },\n      \"avatar\": {\n        \"upload\": \"Enviar avatar\",\n        \"change\": \"Mudar avatar\",\n        \"remove_confirm_title\": \"Remover avatar?\",\n        \"updated\": \"Avatar atualizado\",\n        \"removed\": \"Avatar removido\",\n        \"description\": \"Envie uma imagem quadrada para usar como seu avatar.\",\n        \"remove_confirm_description\": \"Isso vai apagar a foto do seu perfil atual.\",\n        \"title\": \"Foto do perfil\",\n        \"remove\": \"Remover avatar\"\n      }\n    },\n    \"back_to_app\": \"Voltar ao App\",\n    \"user_settings\": \"Configurações do Usuário\",\n    \"ai\": {\n      \"ai_settings\": \"Configurações de IA\",\n      \"tagging_rules\": \"Regras de Tags\",\n      \"tagging_rule_description\": \"Os prompts que você adicionar aqui serão incluídos como regras para o modelo durante a geração de tags. Você pode visualizar os prompts finais na seção de visualização de prompts.\",\n      \"prompt_preview\": \"Pré-visualização do Prompt\",\n      \"text_prompt\": \"Prompt de Texto\",\n      \"images_prompt\": \"Prompt de Imagem\",\n      \"summarization_prompt\": \"Prompt de Resumo\",\n      \"all_tagging\": \"Todas as Tags\",\n      \"text_tagging\": \"Tags de Texto\",\n      \"image_tagging\": \"Tags de Imagem\",\n      \"summarization\": \"Resumo\",\n      \"tag_style\": \"Estilo da etiqueta\",\n      \"auto_summarization_description\": \"Gere automaticamente resumos para seus favoritos usando IA.\",\n      \"auto_tagging\": \"Marcação automática\",\n      \"titlecase_spaces\": \"Maiúsculas e minúsculas com espaços\",\n      \"lowercase_underscores\": \"Minúsculas com sublinhados\",\n      \"inference_language\": \"Linguagem de inferência\",\n      \"titlecase_hyphens\": \"Maiúsculas e minúsculas com hífens\",\n      \"lowercase_hyphens\": \"Minúsculas com hífens\",\n      \"lowercase_spaces\": \"Minúsculas com espaços\",\n      \"inference_language_description\": \"Escolha o idioma para tags e resumos gerados por IA.\",\n      \"tag_style_description\": \"Escolha como suas tags auto-geradas devem ser formatadas.\",\n      \"auto_tagging_description\": \"Gere automaticamente tags para seus favoritos usando IA.\",\n      \"camelCase\": \"camelCase\",\n      \"auto_summarization\": \"Resumo automático\",\n      \"no_preference\": \"Sem preferência\",\n      \"curated_tags\": \"Tags selecionadas\",\n      \"curated_tags_description\": \"Restrinja opcionalmente a marcação de IA para usar apenas tags desta lista. Quando nenhuma tag é selecionada, a IA gera tags livremente.\",\n      \"curated_tags_updated\": \"Tags selecionadas atualizadas com sucesso!\",\n      \"curated_tags_update_failed\": \"Falha ao atualizar as tags selecionadas\"\n    },\n    \"feeds\": {\n      \"rss_subscriptions\": \"Assinaturas de RSS\",\n      \"add_a_subscription\": \"Adicionar uma Assinatura\",\n      \"feed_enabled\": \"Feed RSS ativado\",\n      \"feed_disabled\": \"Feed RSS desativado\"\n    },\n    \"webhooks\": {\n      \"webhooks\": \"Webhooks\",\n      \"description\": \"Você pode usar webhooks para acionar ações quando os marcadores forem criados, alterados ou rastreados.\",\n      \"events\": {\n        \"title\": \"Eventos\",\n        \"crawled\": \"Rastreados\",\n        \"created\": \"Criado\",\n        \"edited\": \"Editado\"\n      },\n      \"auth_token\": \"Token de Autenticação\",\n      \"add_auth_token\": \"Adicionar Token de Autenticação\",\n      \"edit_auth_token\": \"Editar Token de Autenticação\",\n      \"create_webhook\": \"Criar Webhook\",\n      \"delete_webhook\": \"Deletar Webhook\",\n      \"delete_webhook_confirmation\": \"Tem certeza de que deseja excluir este webhook?\",\n      \"edit_webhook\": \"Editar Webhook\",\n      \"webhook_url\": \"Webhook URL\"\n    },\n    \"import\": {\n      \"import_export\": \"Importar / Exportar\",\n      \"import_export_bookmarks\": \"Importar / Exportar Favoritos\",\n      \"import_bookmarks_from_html_file\": \"Importar Favoritos de arquivo HTML\",\n      \"import_bookmarks_from_pocket_export\": \"Importar Favoritos de exportação do Pocket\",\n      \"import_bookmarks_from_matter_export\": \"Importar Favoritos de exportação do Matter\",\n      \"import_bookmarks_from_omnivore_export\": \"Importar Favoritos de exportação do Omnivore\",\n      \"import_bookmarks_from_linkwarden_export\": \"Importar Favoritos de exportação do Linkwarden\",\n      \"import_bookmarks_from_karakeep_export\": \"Importar Favoritos de exportação do Karakeep\",\n      \"export_links_and_notes\": \"Exportar Links e Notas\",\n      \"imported_bookmarks\": \"Favoritos Importados\",\n      \"import_bookmarks_from_tab_session_manager_export\": \"Importar favoritos do Tab Session Manager\",\n      \"import_bookmarks_from_mymind_export\": \"Importar favoritos da exportação do mymind\",\n      \"import_bookmarks_from_instapaper_export\": \"Importar marcadores da exportação do Instapaper\"\n    },\n    \"api_keys\": {\n      \"api_keys\": \"Chaves API\",\n      \"new_api_key\": \"Nova Chave API\",\n      \"new_api_key_desc\": \"Dê um nome único para sua chave de API\",\n      \"key_success\": \"A Chave foi criada com sucesso\",\n      \"key_success_please_copy\": \"Por favor, copie a chave e armazene em um local seguro. Após fechar, você não poderá acessá-la novamente.\",\n      \"regenerate_api_key\": \"Regenerar chave da API\",\n      \"key_regenerated\": \"Chave regenerada com sucesso\",\n      \"key_regenerated_please_copy\": \"Copie a nova chave e guarde-a em um lugar seguro. A chave antiga foi revogada e não vai mais funcionar.\",\n      \"regenerate_warning\": \"Tem certeza de que deseja regenerar a chave da API \\\"{{name}}\\\"?\",\n      \"regenerate_confirmation\": \"Isso revogará a chave atual e gerará uma nova. Qualquer aplicativo que esteja usando a chave atual deixará de funcionar.\"\n    },\n    \"broken_links\": {\n      \"broken_links\": \"Links Quebrados\",\n      \"last_crawled_at\": \"Último Rastreamento em\",\n      \"crawling_status\": \"Status de Rastreamento\",\n      \"crawling_failed\": \"Falha no Rastreamento\"\n    },\n    \"manage_assets\": {\n      \"asset_type\": \"Tipo de recurso\",\n      \"bookmark_link\": \"Link do marcador\",\n      \"asset_link\": \"Link do recurso\",\n      \"delete_asset\": \"Excluir recurso\",\n      \"delete_asset_confirmation\": \"Tem certeza de que deseja excluir este recurso?\",\n      \"manage_assets\": \"Gerenciar recursos\",\n      \"no_assets\": \"Você ainda não tem nenhum recurso.\"\n    },\n    \"rules\": {\n      \"rules\": \"Mecanismo de regras\",\n      \"if\": \"Se ...\",\n      \"events_types\": {\n        \"bookmark_added\": \"Um marcador é adicionado\",\n        \"tag_added\": \"Esta tag é adicionada a um marcador\",\n        \"tag_removed\": \"Esta tag é removida de um marcador\",\n        \"added_to_list\": \"Um marcador é adicionado a esta lista\",\n        \"removed_from_list\": \"Um marcador é removido desta lista\",\n        \"favourited\": \"Um marcador é favoritado\",\n        \"archived\": \"Um marcador é arquivado\"\n      },\n      \"rule_name\": \"Nome da regra\",\n      \"description\": \"Você pode usar regras para acionar ações quando um evento é disparado.\",\n      \"ceate_rule\": \"Criar regra\",\n      \"edit_rule\": \"Editar regra\",\n      \"save_rule\": \"Salvar regra\",\n      \"delete_rule\": \"Excluir regra\",\n      \"delete_rule_confirmation\": \"Tem certeza de que deseja excluir esta regra?\",\n      \"whenever\": \"Sempre que ...\",\n      \"enter_rule_name\": \"Insira o nome da regra\",\n      \"describe_what_this_rule_does\": \"Descreva o que esta regra faz\",\n      \"rule_has_been_created\": \"Regra foi criada!\",\n      \"rule_has_been_updated\": \"Regra foi atualizada!\",\n      \"rule_has_been_deleted\": \"Regra foi excluída!\",\n      \"no_rules_created_yet\": \"Nenhuma regra criada ainda\",\n      \"create_your_first_rule\": \"Crie sua primeira regra para automatizar seu fluxo de trabalho\",\n      \"conditions_types\": {\n        \"always\": \"Sempre\",\n        \"url_contains\": \"URL contém\",\n        \"imported_from_feed\": \"Importado do feed\",\n        \"bookmark_type_is\": \"Tipo de marcador é\",\n        \"has_tag\": \"Tem tag\",\n        \"is_favourited\": \"É favoritado\",\n        \"is_archived\": \"Arquivado\",\n        \"and\": \"Tudo o que segue é verdade\",\n        \"or\": \"Qualquer uma das seguintes opções for verdadeira\",\n        \"url_does_not_contain\": \"URL não Contém\",\n        \"title_contains\": \"Título Contém\",\n        \"title_does_not_contain\": \"Título Não Contém\"\n      },\n      \"actions_types\": {\n        \"add_tag\": \"Adicionar tag\",\n        \"remove_tag\": \"Remover tag\",\n        \"add_to_list\": \"Adicionar à lista\",\n        \"remove_from_list\": \"Remover da lista\",\n        \"download_full_page_archive\": \"Baixar arquivo de página inteira\",\n        \"favourite_bookmark\": \"Favoritar marcador\",\n        \"archive_bookmark\": \"Arquivar marcador\"\n      }\n    },\n    \"stats\": {\n      \"usage_statistics\": \"Estatísticas de uso\",\n      \"insights_description\": \"Informações sobre seus hábitos de favoritos e coleção\",\n      \"failed_to_load\": \"Falha ao carregar estatísticas\",\n      \"overview\": {\n        \"total_bookmarks\": \"Total de favoritos\",\n        \"all_saved_items\": \"Todos os itens salvos\",\n        \"favorites\": \"Favoritos\",\n        \"starred_bookmarks\": \"Favoritos marcados com estrela\",\n        \"archived\": \"Arquivado\",\n        \"archived_items\": \"Itens arquivados\",\n        \"tags\": \"Tags\",\n        \"unique_tags_created\": \"Tags únicos criados\",\n        \"lists\": \"Listas\",\n        \"bookmark_collections\": \"Coleções de favoritos\",\n        \"highlights\": \"Destaques\",\n        \"text_highlights\": \"Destaques de texto\",\n        \"storage_used\": \"Armazenamento usado\",\n        \"total_asset_storage\": \"Armazenamento total de ativos\",\n        \"this_month\": \"Este mês\",\n        \"bookmarks_added\": \"Favoritos adicionados\"\n      },\n      \"bookmark_types\": {\n        \"title\": \"Tipos de favoritos\",\n        \"links\": \"Links\",\n        \"text_notes\": \"Notas de texto\",\n        \"assets\": \"Ativos\"\n      },\n      \"recent_activity\": {\n        \"title\": \"Atividade recente\",\n        \"this_week\": \"Esta semana\",\n        \"this_month\": \"Este mês\",\n        \"this_year\": \"Este ano\"\n      },\n      \"top_domains\": {\n        \"title\": \"Domínios principais\",\n        \"no_domains_found\": \"Nenhum domínio encontrado\"\n      },\n      \"most_used_tags\": {\n        \"title\": \"Tags mais usadas\",\n        \"no_tags_found\": \"Nenhuma tag encontrada\"\n      },\n      \"activity_patterns\": {\n        \"activity_by_hour\": \"Atividade por hora\",\n        \"activity_by_day\": \"Atividade por dia\"\n      },\n      \"storage_breakdown\": {\n        \"title\": \"Detalhes do armazenamento\"\n      },\n      \"bookmark_sources\": {\n        \"title\": \"Fontes de marcadores\",\n        \"empty\": \"Nenhum dado de origem disponível\"\n      }\n    },\n    \"subscription\": {\n      \"subscription\": \"Assinatura\",\n      \"manage_subscription\": \"Gerencie sua assinatura e informações de cobrança\",\n      \"current_plan\": \"Plano atual\",\n      \"billing_period\": \"Período de cobrança\",\n      \"paid_plan\": \"Plano Pago\",\n      \"unlock_bigger_quota\": \"Desbloqueie uma cota maior e apoie o projeto\",\n      \"subscribe_now\": \"Assine agora\",\n      \"manage_billing\": \"Gerenciar cobrança\",\n      \"subscription_canceled\": \"Sua assinatura foi cancelada e terminará em {{date}}. Você pode se reinscrever a qualquer momento.\",\n      \"usage_quotas\": \"Uso e cotas\",\n      \"track_usage\": \"Acompanhe seu uso atual em relação aos limites do seu plano\",\n      \"total_bookmarks_saved\": \"Total de favoritos salvos\",\n      \"assets_file_storage\": \"Armazenamento de arquivos e ativos\",\n      \"unlimited_usage\": \"Uso ilimitado\",\n      \"quota_limit_reached\": \"Limite de cota atingido\",\n      \"approaching_quota_limit\": \"Limite de cota se aproximando\",\n      \"loading_usage\": \"Carregando informações de uso...\",\n      \"free\": \"Grátis\",\n      \"paid\": \"Pago\"\n    },\n    \"import_sessions\": {\n      \"title\": \"Importar Sessões\",\n      \"description\": \"Visualize e gerencie suas sessões de importação em massa. As sessões são criadas automaticamente quando você importa marcadores.\",\n      \"load_error\": \"Falha ao carregar as sessões de importação\",\n      \"no_sessions\": \"Ainda não há sessões de importação\",\n      \"no_sessions_detail\": \"As sessões de importação aparecerão aqui automaticamente quando você importar marcadores\",\n      \"created_at\": \"Criado em {{time}}\",\n      \"progress\": \"Progresso\",\n      \"status\": {\n        \"pending\": \"Pendente\",\n        \"in_progress\": \"Em progresso\",\n        \"completed\": \"Concluído\",\n        \"failed\": \"Falhou\",\n        \"processing\": \"Processando\",\n        \"staging\": \"Em Preparação\",\n        \"running\": \"Executando\",\n        \"paused\": \"Pausado\"\n      },\n      \"badges\": {\n        \"pending\": \"{{count}} pendentes\",\n        \"processing\": \"{{count}} processando\",\n        \"completed\": \"{{count}} concluído\",\n        \"failed\": \"{{count}} falhou\"\n      },\n      \"imported_to\": \"Importado para:\",\n      \"view_list\": \"Ver lista\",\n      \"delete_dialog_title\": \"Excluir sessão de importação\",\n      \"delete_dialog_description\": \"Tem certeza de que deseja excluir \\\"{{name}}\\\"? Esta ação não pode ser desfeita. Os favoritos em si não serão excluídos.\",\n      \"delete_session\": \"Excluir sessão\",\n      \"pause_session\": \"Pausar\",\n      \"resume_session\": \"Retomar\",\n      \"view_details\": \"Ver Detalhes\",\n      \"detail\": {\n        \"page_title\": \"Detalhes da Sessão de Importação\",\n        \"back_to_import\": \"Voltar para Importação\",\n        \"filter_all\": \"Tudo\",\n        \"filter_accepted\": \"Aceito\",\n        \"filter_rejected\": \"Rejeitado\",\n        \"filter_duplicates\": \"Duplicatas\",\n        \"filter_pending\": \"Pendente\",\n        \"table_title\": \"Título/URL\",\n        \"table_type\": \"Tipo\",\n        \"table_result\": \"Resultado\",\n        \"table_reason\": \"Motivo\",\n        \"table_bookmark\": \"Marcador\",\n        \"result_accepted\": \"Aceito\",\n        \"result_rejected\": \"Rejeitado\",\n        \"result_skipped_duplicate\": \"Duplicado\",\n        \"result_pending\": \"Pendente\",\n        \"result_processing\": \"Processando\",\n        \"no_results\": \"Nenhum resultado encontrado para este filtro.\",\n        \"view_bookmark\": \"Ver Marcador\",\n        \"load_more\": \"Carregar mais\",\n        \"no_title\": \"Sem título\"\n      }\n    },\n    \"backups\": {\n      \"backups\": \"Backups\",\n      \"page_title\": \"Backups\",\n      \"page_description\": \"Crie e gerencie automaticamente backups dos seus favoritos. Os backups são compactados e armazenados com segurança.\",\n      \"configuration\": {\n        \"title\": \"Configuração de Backup\",\n        \"enable_automatic_backups\": \"Ativar Backups Automáticos\",\n        \"enable_automatic_backups_description\": \"Criar backups automaticamente dos seus favoritos\",\n        \"backup_frequency\": \"Frequência de Backup\",\n        \"backup_frequency_description\": \"Com que frequência os backups devem ser criados\",\n        \"retention_period\": \"Período de Retenção (dias)\",\n        \"retention_period_description\": \"Quantos dias manter os backups antes de excluí-los\",\n        \"frequency\": {\n          \"daily\": \"Diariamente\",\n          \"weekly\": \"Semanalmente\"\n        },\n        \"select_frequency\": \"Selecione a frequência\",\n        \"save_settings\": \"Salvar configurações\"\n      },\n      \"list\": {\n        \"title\": \"Seus Backups\",\n        \"create_backup_now\": \"Criar Backup Agora\",\n        \"no_backups\": \"Você ainda não tem nenhum backup. Ative os backups automáticos ou crie um manualmente.\",\n        \"table\": {\n          \"created_at\": \"Criado em\",\n          \"bookmarks\": \"Favoritos\",\n          \"size\": \"Tamanho\",\n          \"status\": \"Status\",\n          \"actions\": \"Ações\"\n        },\n        \"status\": {\n          \"success\": \"Sucesso\",\n          \"failed\": \"Falhou\",\n          \"pending\": \"Pendente\"\n        },\n        \"actions\": {\n          \"download_backup\": \"Baixar backup\",\n          \"delete_backup\": \"Excluir backup\"\n        }\n      },\n      \"dialogs\": {\n        \"delete_backup_title\": \"Excluir backup?\",\n        \"delete_backup_description\": \"Tem certeza de que deseja excluir este backup? Esta ação não pode ser desfeita.\"\n      },\n      \"toasts\": {\n        \"backup_queued\": \"O trabalho de backup foi enfileirado! Ele será processado em breve.\",\n        \"backup_deleted\": \"O backup foi excluído!\"\n      }\n    }\n  },\n  \"layouts\": {\n    \"masonry\": \"Alvenaria\",\n    \"grid\": \"Grade\",\n    \"list\": \"Lista\",\n    \"compact\": \"Compacto\"\n  },\n  \"highlights\": {\n    \"no_highlights\": \"Você ainda não tem nenhum destaque.\"\n  },\n  \"admin\": {\n    \"admin_settings\": \"Configurações de Administrador\",\n    \"server_stats\": {\n      \"server_stats\": \"Status do Servidor\",\n      \"total_users\": \"Total de Usuários\",\n      \"total_bookmarks\": \"Total de Favoritos\",\n      \"server_version\": \"Versão do Servidor\"\n    },\n    \"background_jobs\": {\n      \"background_jobs\": \"Tarefas em Segundo Plano\",\n      \"crawler_jobs\": \"Tarefas de Rastreamento\",\n      \"indexing_jobs\": \"Tarefas de Indexação\",\n      \"inference_jobs\": \"Tarefas de Inferência\",\n      \"tidy_assets_jobs\": \"Tarefas de Organização de Recursos\",\n      \"job\": \"Tarefas\",\n      \"queued\": \"Na Fila\",\n      \"pending\": \"Pendentes\",\n      \"failed\": \"Falhou\",\n      \"video_jobs\": \"Tarefas de download de vídeo\",\n      \"feed_jobs\": \"Tarefas de feed RSS\",\n      \"webhook_jobs\": \"Tarefas de Webhook\",\n      \"asset_preprocessing_jobs\": \"Tarefas de pré-processamento de recursos\",\n      \"jobs\": {\n        \"crawler\": {\n          \"title\": \"Tarefas de rastreador\",\n          \"description\": \"Rastreamento da web e extração de conteúdo de URLs\"\n        },\n        \"inference\": {\n          \"title\": \"Tarefas de inferência\",\n          \"description\": \"Marcação e resumo de conteúdo com tecnologia de IA\"\n        },\n        \"indexing\": {\n          \"title\": \"Tarefas de indexação\",\n          \"description\": \"Atualizações do índice de pesquisa\"\n        },\n        \"asset_preprocessing\": {\n          \"title\": \"Tarefas de pré-processamento de ativos\",\n          \"description\": \"Pré-processamento de imagem e documento (screenshots, extração de texto, etc.)\"\n        },\n        \"tidy_assets\": {\n          \"title\": \"Tarefas de ativos organizados\",\n          \"description\": \"Limpeza de ativos e otimização de armazenamento\"\n        },\n        \"video\": {\n          \"title\": \"Tarefas de download de vídeo\",\n          \"description\": \"Extração e download de vídeo\"\n        },\n        \"webhook\": {\n          \"title\": \"Tarefas de webhook\",\n          \"description\": \"Notificações de webhook externas\"\n        },\n        \"feed\": {\n          \"title\": \"Tarefas de feed RSS\",\n          \"description\": \"Processamento de feed RSS e atualizações de conteúdo\"\n        },\n        \"admin_maintenance\": {\n          \"title\": \"Tarefas de Manutenção do Administrador\",\n          \"description\": \"Limpeza administrativa e manutenção de ativos\"\n        }\n      },\n      \"monitor_and_manage\": \"Monitore e gerencie filas de tarefas em segundo plano e tarefas de processamento do sistema\",\n      \"active\": \"Ativo\",\n      \"available_actions\": \"Ações disponíveis\",\n      \"status\": {\n        \"title\": \"Conhecendo os estados da tarefa\",\n        \"queued\": {\n          \"title\": \"Na fila\",\n          \"description\": \"Tarefas esperando na fila para serem processadas. Elas começarão automaticamente quando os recursos estiverem disponíveis.\"\n        },\n        \"unprocessed\": {\n          \"title\": \"Não processado\",\n          \"description\": \"Favoritos que ainda não foram processados. Eles provavelmente já estão na fila para processamento, caso contrário, você pode precisar reenfileirá-los manualmente.\"\n        },\n        \"failed\": {\n          \"title\": \"Falhou\",\n          \"description\": \"Favoritos que encontraram erros durante o processamento. Estes podem precisar de atenção manual.\"\n        }\n      },\n      \"actions\": {\n        \"recrawl_failed_links_only\": \"Rastrear novamente apenas links com falha\",\n        \"recrawl_all_links\": \"Rastrear novamente todos os links\",\n        \"without_inference\": \"Sem inferência\",\n        \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Regenerar tags de IA apenas para favoritos com falha\",\n        \"regenerate_ai_tags_for_all_bookmarks\": \"Regenerar tags de IA para todos os favoritos\",\n        \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Regenerar resumos de IA apenas para favoritos com falha\",\n        \"regenerate_ai_summaries_for_all_bookmarks\": \"Regenerar resumos de IA para todos os favoritos\",\n        \"reindex_all_bookmarks\": \"Reindexar todos os favoritos\",\n        \"clean_assets\": \"Limpar ativos pendentes e resincronizar metadados\",\n        \"reprocess_assets_fix_mode\": \"Reprocessar ativos não processados\",\n        \"migrate_large_link_html_content\": \"Mover Conteúdo HTML Grande Inline para Ativos\",\n        \"recrawl_pending_links_only\": \"Rastrear novamente apenas links pendentes\",\n        \"regenerate_ai_tags_for_pending_bookmarks_only\": \"Regenerar tags de IA apenas para favoritos pendentes\",\n        \"regenerate_ai_summaries_for_pending_bookmarks_only\": \"Regenerar resumos de IA apenas para favoritos pendentes\"\n      }\n    },\n    \"actions\": {\n      \"recrawl_failed_links_only\": \"Rastrear Novamente Somente os Links que Falharam\",\n      \"recrawl_all_links\": \"Rastrear Novamente Todos os Links\",\n      \"without_inference\": \"Sem Inferência\",\n      \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Regenerar Tags de IA apenas para Favoritos que Falharam\",\n      \"regenerate_ai_tags_for_all_bookmarks\": \"Regenerar Tags de IA para Todos os Favoritos\",\n      \"reindex_all_bookmarks\": \"Reindexar Todos os Favoritos\",\n      \"compact_assets\": \"Compactar Ativos\",\n      \"reprocess_assets_fix_mode\": \"Reprocessar recursos (modo de correção)\",\n      \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Regenerar resumos de IA apenas para favoritos com falha\",\n      \"regenerate_ai_summaries_for_all_bookmarks\": \"Regenerar resumos de IA para todos os favoritos\"\n    },\n    \"users_list\": {\n      \"users_list\": \"Lista de Usuários\",\n      \"create_user\": \"Criar Usuário\",\n      \"change_role\": \"Mudar Função\",\n      \"reset_password\": \"Resetar Senha\",\n      \"delete_user\": \"Deletar Usuário\",\n      \"num_bookmarks\": \"Número de Favoritos\",\n      \"asset_sizes\": \"Tamanhos dos Ativos\",\n      \"local_user\": \"Usuário Local\",\n      \"confirm_password\": \"Confirmar Senha\",\n      \"delete_user_confirm_description\": \"Tem certeza de que deseja excluir o usuário \\\"{{name}}\\\"?\",\n      \"unlimited\": \"Ilimitado\"\n    },\n    \"service_connections\": {\n      \"title\": \"Conexões de serviço\",\n      \"description\": \"Monitore a saúde e a conectividade das dependências do sistema externo\",\n      \"search_engine\": \"Mecanismo de busca\",\n      \"browser\": \"Navegador\",\n      \"queue_system\": \"Sistema de fila\",\n      \"status\": {\n        \"not_configured\": \"Não configurado\",\n        \"connected\": \"Conectado\",\n        \"disconnected\": \"Desconectado\"\n      }\n    },\n    \"admin_tools\": {\n      \"admin_tools\": \"Ferramentas de administração\",\n      \"bookmark_debugger\": \"Debugger de Marcador\",\n      \"bookmark_id\": \"ID do Marcador\",\n      \"bookmark_id_placeholder\": \"Insira o ID do marcador\",\n      \"lookup\": \"Pesquisar\",\n      \"debug_info\": \"Informações de Debug\",\n      \"basic_info\": \"Informações básicas\",\n      \"status\": \"Status\",\n      \"content\": \"Conteúdo\",\n      \"html_preview\": \"Pré-visualização HTML (Primeiros 1000 caracteres)\",\n      \"summary\": \"Resumo\",\n      \"url\": \"URL\",\n      \"source_url\": \"URL de origem\",\n      \"asset_type\": \"Tipo de ativo\",\n      \"file_name\": \"Nome do arquivo\",\n      \"owner_user_id\": \"ID do usuário proprietário\",\n      \"tagging_status\": \"Status de marcação\",\n      \"summarization_status\": \"Status do resumo\",\n      \"crawl_status\": \"Status de rastreamento\",\n      \"crawl_status_code\": \"Código de status HTTP\",\n      \"crawled_at\": \"Rastreado em\",\n      \"recrawl\": \"Rastrear novamente\",\n      \"reindex\": \"Reindexar\",\n      \"retag\": \"Reetiquetar\",\n      \"resummarize\": \"Resumir novamente\",\n      \"bookmark_not_found\": \"Marcador não encontrado\",\n      \"action_success\": \"Ação concluída com sucesso\",\n      \"action_failed\": \"Ação falhou\",\n      \"recrawl_queued\": \"O trabalho de rastreamento foi enfileirado\",\n      \"reindex_queued\": \"O trabalho de reindexação foi enfileirado\",\n      \"retag_queued\": \"O trabalho de reetiquetagem foi enfileirado\",\n      \"resummarize_queued\": \"O trabalho de resumir novamente foi enfileirado\",\n      \"view\": \"Visualizar\",\n      \"fetch_error\": \"Erro ao buscar marcador\"\n    }\n  },\n  \"options\": {\n    \"dark_mode\": \"Modo Escuro\",\n    \"light_mode\": \"Modo Claro\",\n    \"apps_extensions\": \"Apps e extensões\",\n    \"documentation\": \"Documentação\",\n    \"follow_us_on_x\": \"Siga a gente no X\"\n  },\n  \"lists\": {\n    \"all_lists\": \"Todas as Listas\",\n    \"favourites\": \"Favoritos\",\n    \"new_list\": \"Nova Lista\",\n    \"edit_list\": \"Editar Lista\",\n    \"new_nested_list\": \"Nova Lista Aninhada\",\n    \"parent_list\": \"Lista Pai\",\n    \"no_parent\": \"Sem Pai\",\n    \"list_type\": \"Tipo de Lista\",\n    \"manual_list\": \"Lista Manual\",\n    \"smart_list\": \"Lista Inteligente\",\n    \"search_query\": \"Consulta de Pesquisa\",\n    \"search_query_help\": \"Saiba mais sobre a linguagem de consulta de pesquisa.\",\n    \"merge_list\": \"Mesclar lista\",\n    \"destination_list\": \"Lista de destino\",\n    \"delete_after_merge\": \"Excluir lista original após a mesclagem\",\n    \"no_destination\": \"Sem destino\",\n    \"description\": \"Descrição (Opcional)\",\n    \"share_list\": \"Compartilhar lista\",\n    \"rss\": {\n      \"title\": \"Feed RSS\",\n      \"description\": \"Ativar um feed RSS para esta lista\",\n      \"feed_url\": \"URL do feed RSS\"\n    },\n    \"public_list\": {\n      \"title\": \"Lista pública\",\n      \"description\": \"Permitir que outros vejam esta lista\",\n      \"share_link\": \"Compartilhar link\"\n    },\n    \"delete_list\": {\n      \"title\": \"Excluir lista\",\n      \"description\": \"Excluir uma lista não exclui nenhum marcador nessa lista.\",\n      \"delete_children\": \"Excluir listas filhas (recursivamente)\",\n      \"delete_children_description\": \"Se não estiver marcado, todas as listas filhas diretas se tornarão listas raiz\"\n    },\n    \"shared\": \"Compartilhado\",\n    \"collaborators\": {\n      \"manage\": \"Gerenciar Colaboradores\",\n      \"view\": \"Ver Colaboradores\",\n      \"collaborators\": \"Colaboradores\",\n      \"add\": \"Adicionar Colaborador\",\n      \"current\": \"Colaboradores Atuais\",\n      \"enter_email\": \"Insira o endereço de e-mail\",\n      \"please_enter_email\": \"Por favor, insira um endereço de e-mail\",\n      \"added_successfully\": \"Colaborador adicionado com sucesso\",\n      \"failed_to_add\": \"Falha ao adicionar colaborador\",\n      \"removed\": \"Colaborador removido\",\n      \"failed_to_remove\": \"Falha ao remover colaborador\",\n      \"role_updated\": \"Função atualizada\",\n      \"failed_to_update_role\": \"Falha ao atualizar a função\",\n      \"viewer\": \"Visualizador\",\n      \"editor\": \"Editor\",\n      \"owner\": \"Proprietário\",\n      \"viewer_description\": \"Pode ver os favoritos na lista\",\n      \"editor_description\": \"Pode adicionar e remover favoritos\",\n      \"no_collaborators\": \"Ainda não há colaboradores. Adicione alguém para começar a colaborar!\",\n      \"no_collaborators_readonly\": \"Nenhum colaborador para esta lista.\",\n      \"people_with_access\": \"Pessoas que têm acesso a esta lista\",\n      \"add_or_remove\": \"Adicione ou remova pessoas que podem acessar esta lista\",\n      \"invitation_sent\": \"Convite enviado com sucesso\",\n      \"invitation_revoked\": \"Convite revogado\",\n      \"failed_to_revoke\": \"Falha ao revogar o convite\",\n      \"pending\": \"Pendente\",\n      \"revoke\": \"Revogar\",\n      \"declined\": \"Recusado\"\n    },\n    \"leave_list\": {\n      \"title\": \"Sair da lista\",\n      \"confirm_message\": \"Tem certeza de que deseja sair de {{icon}} {{name}}?\",\n      \"warning\": \"Você não poderá mais visualizar ou acessar os favoritos nesta lista. O proprietário da lista pode readicioná-lo, se necessário.\",\n      \"action\": \"Sair da lista\",\n      \"success\": \"Você saiu de \\\"{{icon}} {{name}}\\\"\"\n    },\n    \"invitations\": {\n      \"pending\": \"Convites pendentes\",\n      \"description\": \"Revise e responda aos convites de colaboração de listas\",\n      \"invited_by\": \"Convidado por\",\n      \"accept\": \"Aceitar\",\n      \"decline\": \"Recusar\",\n      \"accepted\": \"Convite aceito\",\n      \"declined\": \"Convite recusado\",\n      \"failed_to_accept\": \"Falha ao aceitar o convite\",\n      \"failed_to_decline\": \"Falha ao recusar o convite\"\n    },\n    \"shared_lists\": \"Listas compartilhadas\"\n  },\n  \"tags\": {\n    \"all_tags\": \"Todas Tags\",\n    \"your_tags\": \"Suas Tags\",\n    \"your_tags_info\": \"Tags que foram anexadas pelo menos uma vez por você\",\n    \"ai_tags\": \"Tags IA\",\n    \"ai_tags_info\": \"Tags que foram anexadas automaticamente (por IA)\",\n    \"unused_tags\": \"Tags Não Utilizadas\",\n    \"unused_tags_info\": \"Tags que não estão anexadas a nenhum favorito\",\n    \"delete_all_unused_tags\": \"Excluir Todas as Tags Não Utilizadas\",\n    \"drag_and_drop_merging\": \"Arrastar e Soltar para Mesclagem\",\n    \"drag_and_drop_merging_info\": \"Arraste e solte tags uma sobre a outra para mesclá-las\",\n    \"sort_by_name\": \"Ordenar por Nome\",\n    \"create_tag\": \"Criar etiqueta\",\n    \"create_tag_description\": \"Criar uma nova etiqueta sem anexá-la a nenhum marcador\",\n    \"tag_name\": \"Nome da etiqueta\",\n    \"enter_tag_name\": \"Insira o nome da etiqueta\",\n    \"sort_by_usage\": \"Ordenar por uso\",\n    \"sort_by_relevance\": \"Ordenar por relevância\",\n    \"no_custom_tags\": \"Ainda não há tags personalizadas\",\n    \"no_ai_tags\": \"Ainda não há tags de IA\",\n    \"no_unused_tags\": \"Você não tem nenhuma tag não utilizada\",\n    \"no_unused_tags_match_your_search\": \"Nenhuma tag não utilizada corresponde à sua pesquisa\",\n    \"no_tags_match_your_search\": \"Nenhuma tag corresponde à sua pesquisa\",\n    \"search_placeholder\": \"Pesquisar tags...\",\n    \"search_or_create_placeholder\": \"Pesquisar ou criar tags...\"\n  },\n  \"search\": {\n    \"is_not_archived\": \"Não Está Arquivado\",\n    \"is_archived\": \"Está Arquivado\",\n    \"is_favorited\": \"Está Favoritado\",\n    \"is_not_favorited\": \"Não Está Favoritado\",\n    \"has_any_tag\": \"Possui Alguma Tag\",\n    \"has_no_tags\": \"Não Possui Tag\",\n    \"is_in_any_list\": \"Está em Alguma Lista\",\n    \"is_not_in_any_list\": \"Não Está em Nehuma Lista\",\n    \"created_on_or_after\": \"Criado em ou Após\",\n    \"not_created_on_or_after\": \"Não Criado em ou Após\",\n    \"created_on_or_before\": \"Criado em ou Antes\",\n    \"not_created_on_or_before\": \"Não Criado em ou Antes\",\n    \"url_contains\": \"A URL Contém\",\n    \"url_does_not_contain\": \"A URL Não Contém\",\n    \"is_in_list\": \"Está na Lista\",\n    \"is_not_in_list\": \"Não está na Lista\",\n    \"has_tag\": \"Possui Tag\",\n    \"full_text_search\": \"Pesquisar Todo o Texto\",\n    \"type_is\": \"O Tipo é\",\n    \"type_is_not\": \"O Tipo não é\",\n    \"does_not_have_tag\": \"Não Tem Tag\",\n    \"and\": \"E\",\n    \"or\": \"Ou\",\n    \"is_from_feed\": \"É de um feed RSS\",\n    \"is_not_from_feed\": \"Não é de um feed RSS\",\n    \"month_s\": \" Mês(es)\",\n    \"created_within\": \"Criado dentro de\",\n    \"created_earlier_than\": \"Criado antes de\",\n    \"day_s\": \" Dia(s)\",\n    \"week_s\": \" Semana(s)\",\n    \"year_s\": \" Ano(s)\",\n    \"day_s_ago\": \" Dia(s) atrás\",\n    \"week_s_ago\": \" Semana(s) atrás\",\n    \"month_s_ago\": \" Mês(es) atrás\",\n    \"year_s_ago\": \" Ano(s) atrás\",\n    \"history\": \"Pesquisas recentes\",\n    \"title_contains\": \"Título Contém\",\n    \"title_does_not_contain\": \"Título Não Contém\",\n    \"is_broken_link\": \"Possui link quebrado\",\n    \"tags\": \"Tags\",\n    \"no_suggestions\": \"Sem sugestões\",\n    \"filters\": \"Filtros\",\n    \"is_not_broken_link\": \"Possui link funcionando\",\n    \"lists\": \"Listas\",\n    \"feeds\": \"Feeds\",\n    \"is_from_source\": \"Fonte é\",\n    \"is_not_from_source\": \"A fonte não é\"\n  },\n  \"preview\": {\n    \"view_original\": \"Ver Original\",\n    \"cached_content\": \"Conteúdo em Cache\",\n    \"reader_view\": \"Modo Leitura\",\n    \"tabs\": {\n      \"content\": \"Conteúdo\",\n      \"details\": \"Detalhes\"\n    },\n    \"archive_info\": \"Arquivos podem não renderizar corretamente embutidos se eles exigirem Javascript. Para obter melhores resultados, <1>baixe-o e abra-o no seu navegador</1>.\",\n    \"fetch_error_title\": \"Conteúdo Indisponível\",\n    \"fetch_error_description\": \"Não foi possível buscar o conteúdo para este link. A página pode estar protegida, exigir autenticação ou estar temporariamente indisponível.\",\n    \"crawling_in_progress\": \"Buscando conteúdo da página…\",\n    \"continue_reading\": \"Continue de onde parou\",\n    \"continue_reading_percent\": \"Continue de onde parou ({{percent}}%)\",\n    \"continue_button\": \"Continuar\"\n  },\n  \"editor\": {\n    \"quickly_focus\": \"Você pode acessar rapidamente este campo pressionando ⌘ + E\",\n    \"multiple_urls_dialog_title\": \"Importando URLs como favoritos separados?\",\n    \"multiple_urls_dialog_desc\": \"A entrada contém vários URLs em linhas separadas. Deseja importá-los como favoritos separados?\",\n    \"import_as_text\": \"Importar como favoritos de texto\",\n    \"import_as_separate_bookmarks\": \"Importar como favoritos separados\",\n    \"placeholder\": \"Cole um link ou uma imagem, escreva uma nota ou arraste e solte uma imagem aqui…\",\n    \"new_item\": \"NOVO ITEM\",\n    \"text_toolbar\": {\n      \"undo\": \"Desfazer\",\n      \"redo\": \"Refazer\",\n      \"bold\": \"Negrito\",\n      \"italic\": \"Itálico\",\n      \"underline\": \"Sublinhado\",\n      \"strikethrough\": \"Tachado\",\n      \"code\": \"Código\",\n      \"highlight\": \"Destaque\",\n      \"align_left\": \"Alinhar à esquerda\",\n      \"align_center\": \"Alinhamento Central\",\n      \"align_right\": \"Alinhar à direita\",\n      \"markdown_shortcuts\": {\n        \"label\": \"Atalhos de Markdown\",\n        \"heading\": {\n          \"label\": \"Cabeçalho\",\n          \"example\": \"# H1, ## H2, ### H3\"\n        },\n        \"bold\": {\n          \"label\": \"Negrito\",\n          \"example\": \"**texto** ou CTRL+b\"\n        },\n        \"italic\": {\n          \"label\": \"Itálico\",\n          \"example\": \"*Itálico* ou _Itálico_ ou CTRL+i\"\n        },\n        \"blockquote\": {\n          \"label\": \"Citação em bloco\",\n          \"example\": \"> Citação em bloco\"\n        },\n        \"ordered_list\": {\n          \"label\": \"Lista ordenada\",\n          \"example\": \"1. Item da lista\"\n        },\n        \"unordered_list\": {\n          \"label\": \"Lista não ordenada\",\n          \"example\": \"- Item da lista\"\n        },\n        \"inline_code\": {\n          \"label\": \"Código embutido\",\n          \"example\": \"`Código`\"\n        },\n        \"block_code\": {\n          \"label\": \"Bloco de Código\",\n          \"example\": \"``` + espaço\"\n        }\n      }\n    },\n    \"disabled_submissions\": \"Os envios estão desativados\",\n    \"placeholder_v2\": \"Cole um link, escreva uma nota ou solte uma imagem…\"\n  },\n  \"dialogs\": {\n    \"bookmarks\": {\n      \"delete_confirmation_title\": \"Excluir favorito?\",\n      \"delete_confirmation_description\": \"Tem certeza de que deseja excluir este favorito?\"\n    }\n  },\n  \"toasts\": {\n    \"bookmarks\": {\n      \"deleted\": \"O favorito foi excluído!\",\n      \"updated\": \"O favorito foi atualizado!\",\n      \"refetch\": \"A nova busca foi enfileirada!\",\n      \"full_page_archive\": \"A criação do arquivo de página inteira foi acionada\",\n      \"delete_from_list\": \"O favorito foi excluído da lista\",\n      \"clipboard_copied\": \"O link foi adicionado à sua área de transferência!\",\n      \"preserve_pdf\": \"A preservação em PDF foi acionada\",\n      \"update_banner\": \"O banner foi atualizado!\",\n      \"uploading_banner\": \"Enviando banner...\"\n    },\n    \"lists\": {\n      \"created\": \"A lista foi criada!\",\n      \"updated\": \"A lista foi atualizada!\",\n      \"merged\": \"Lista foi mesclada!\",\n      \"deleted\": \"Lista foi excluída!\"\n    },\n    \"tags\": {\n      \"created\": \"Etiqueta foi criada!\",\n      \"failed_to_create\": \"Falha ao criar etiqueta\"\n    }\n  },\n  \"cleanups\": {\n    \"cleanups\": \"Limpezas\",\n    \"duplicate_tags\": {\n      \"title\": \"Tags duplicadas\",\n      \"merge_all_suggestions\": \"Mesclar todas as sugestões?\"\n    }\n  },\n  \"banners\": {\n    \"no_bookmarks\": {\n      \"title\": \"Ainda não há marcadores\",\n      \"description\": \"Salve artigos, links e páginas interessantes para acessá-los rapidamente mais tarde.\"\n    }\n  },\n  \"bookmark_editor\": {\n    \"title\": \"Editar marcador\",\n    \"subtitle\": \"Faça alterações nos detalhes do marcador. Clique em salvar quando terminar.\",\n    \"author\": \"Autor\",\n    \"publisher\": \"Editora\",\n    \"date_published\": \"Data de publicação\",\n    \"pick_a_date\": \"Escolha uma data\",\n    \"save_changes\": \"Salvar alterações\",\n    \"extracted_content\": \"Conteúdo extraído\"\n  },\n  \"view_options\": {\n    \"title\": \"Opções de visualização\",\n    \"layout\": \"Layout\",\n    \"columns\": \"Colunas\",\n    \"display_options\": \"Opções de exibição\",\n    \"show_note_previews\": \"Mostrar notas\",\n    \"show_tags\": \"Mostrar tags\",\n    \"show_title\": \"Mostrar título\",\n    \"image_options\": \"Opções de imagem\",\n    \"image_fit_cover\": \"Cobrir (Preencher)\",\n    \"image_fit_contain\": \"Conter (Ajustar)\"\n  },\n  \"version\": {\n    \"new_release_available\": \"Notas de lançamento adicionadas\",\n    \"whats_new_title\": \"O que há de novo na v{{version}}\",\n    \"release_notes_description\": \"Aqui estão as últimas atualizações buscadas nas notas de lançamento do GitHub.\",\n    \"loading_release_notes\": \"Carregando notas de lançamento…\",\n    \"unable_to_load_release_notes\": \"Não foi possível carregar as notas de lançamento agora. Tente novamente mais tarde.\",\n    \"no_release_notes\": \"Nenhuma nota de lançamento foi publicada para esta versão.\",\n    \"release_notes_synced\": \"As notas de lançamento são sincronizadas do GitHub.\",\n    \"view_on_github\": \"Ver no GitHub\"\n  },\n  \"wrapped\": {\n    \"title\": \"Seu Resumo de {{year}}\",\n    \"subtitle\": \"Um Ano em Karakeep\",\n    \"banner\": {\n      \"title\": \"Seu Resumo de 2025 está pronto!\",\n      \"description\": \"Veja seu ano em favoritos\",\n      \"view_now\": \"Ver Agora\"\n    },\n    \"button\": \"Resumo de 2025\",\n    \"loading\": \"Carregando seu Resumo...\",\n    \"failed_to_load\": \"Falha ao carregar suas estatísticas do Resumo\",\n    \"sections\": {\n      \"total_saves\": {\n        \"prefix\": \"Você salvou\",\n        \"suffix\": \"itens este ano\",\n        \"suffix_singular\": \"item este ano\"\n      },\n      \"first_bookmark\": {\n        \"title\": \"Sua jornada começou\",\n        \"description\": \"Primeiro salvamento de {{year}}:\"\n      },\n      \"top_domains\": \"Seus melhores sites\",\n      \"top_tags\": \"Suas principais tags\",\n      \"monthly_activity\": \"Seu ano em salvamentos\",\n      \"most_active_day\": \"Seu dia de maior atividade\",\n      \"peak_times\": {\n        \"title\": \"Quando você salva\",\n        \"peak_hour\": \"Horário de pico\",\n        \"peak_day\": \"Dia de pico\"\n      },\n      \"how_you_save\": \"Como você salva\",\n      \"what_you_saved\": \"O que você salvou\",\n      \"summary\": {\n        \"favorites\": \"Favoritos\",\n        \"tags_created\": \"Tags criadas\",\n        \"highlights\": \"Destaques\"\n      },\n      \"types\": {\n        \"links\": \"Links\",\n        \"notes\": \"Notas\",\n        \"assets\": \"Ativos\"\n      }\n    },\n    \"footer\": \"Feito com Karakeep\",\n    \"share\": \"Compartilhar\",\n    \"download\": \"Baixar\",\n    \"close\": \"Fechar\",\n    \"generating\": \"Gerando...\"\n  }\n}\n"
  },
  {
    "path": "apps/web/lib/i18n/locales/ru/translation.json",
    "content": "{\n  \"common\": {\n    \"roles\": {\n      \"user\": \"Пользователь\",\n      \"admin\": \"Администратор\"\n    },\n    \"role\": \"Роль\",\n    \"experimental\": \"Экспериментально\",\n    \"search\": \"Поиск\",\n    \"url\": \"URL\",\n    \"name\": \"Имя\",\n    \"email\": \"Email\",\n    \"actions\": \"Действия\",\n    \"created_at\": \"Создано\",\n    \"key\": \"Ключ\",\n    \"something_went_wrong\": \"Что-то пошло не так\",\n    \"video\": \"Видео\",\n    \"note\": \"Заметка\",\n    \"attachments\": \"Вложения\",\n    \"screenshot\": \"Снимок экрана\",\n    \"archive\": \"Архив\",\n    \"password\": \"Пароль\",\n    \"action\": \"Действие\",\n    \"tags\": \"Метки\",\n    \"home\": \"Главная\",\n    \"highlights\": \"Основные моменты\",\n    \"source\": \"Источник\",\n    \"bookmark_types\": {\n      \"title\": \"Вид закладки\",\n      \"link\": \"Ссылка\",\n      \"text\": \"Текст\",\n      \"media\": \"Медиа\"\n    },\n    \"type\": \"Тип\",\n    \"size\": \"Размер\",\n    \"updated_at\": \"Обновлено\",\n    \"title\": \"Заголовок\",\n    \"description\": \"Описание\",\n    \"summary\": \"Краткое содержание\",\n    \"quota\": \"Квота\",\n    \"bookmarks\": \"Закладки\",\n    \"storage\": \"Хранилище\",\n    \"pdf\": \"Архивированный PDF\",\n    \"default\": \"По умолчанию\",\n    \"id\": \"ID\",\n    \"last_used\": \"Последнее использование\"\n  },\n  \"lists\": {\n    \"new_list\": \"Новый список\",\n    \"new_nested_list\": \"Новый вложенный список\",\n    \"all_lists\": \"Все списки\",\n    \"favourites\": \"Избранное\",\n    \"search_query\": \"Поисковый запрос\",\n    \"search_query_help\": \"Узнайте больше о языке поисковых запросов.\",\n    \"no_parent\": \"Нет родителя\",\n    \"smart_list\": \"Умный список\",\n    \"edit_list\": \"Редактировать список\",\n    \"parent_list\": \"Список родителей\",\n    \"list_type\": \"Тип списка\",\n    \"manual_list\": \"Список вручную\",\n    \"merge_list\": \"Объединить список\",\n    \"destination_list\": \"Список назначения\",\n    \"delete_after_merge\": \"Удалить исходный список после объединения\",\n    \"no_destination\": \"Нет назначения\",\n    \"description\": \"Описание (Необязательно)\",\n    \"share_list\": \"Поделиться списком\",\n    \"rss\": {\n      \"title\": \"RSS-лента\",\n      \"description\": \"Включить RSS-ленту для этого списка\",\n      \"feed_url\": \"URL RSS-ленты\"\n    },\n    \"public_list\": {\n      \"title\": \"Публичный список\",\n      \"description\": \"Разрешить другим просматривать этот список\",\n      \"share_link\": \"Поделиться ссылкой\"\n    },\n    \"delete_list\": {\n      \"title\": \"Удалить список\",\n      \"description\": \"Удаление списка не удаляет ни одну из закладок в этом списке.\",\n      \"delete_children\": \"Удалить дочерние списки (рекурсивно)\",\n      \"delete_children_description\": \"Если не отмечено, все прямые дочерние списки станут корневыми списками\"\n    },\n    \"shared\": \"Общий\",\n    \"collaborators\": {\n      \"manage\": \"Управление сотрудниками\",\n      \"view\": \"Просмотр сотрудников\",\n      \"collaborators\": \"Сотрудники\",\n      \"add\": \"Добавить сотрудника\",\n      \"current\": \"Текущие сотрудники\",\n      \"enter_email\": \"Введите адрес электронной почты\",\n      \"please_enter_email\": \"Пожалуйста, введите адрес электронной почты\",\n      \"added_successfully\": \"Сотрудник успешно добавлен\",\n      \"failed_to_add\": \"Не удалось добавить сотрудника\",\n      \"removed\": \"Сотрудник удален\",\n      \"failed_to_remove\": \"Не удалось удалить сотрудника\",\n      \"role_updated\": \"Роль обновлена\",\n      \"failed_to_update_role\": \"Не удалось обновить роль\",\n      \"viewer\": \"Наблюдатель\",\n      \"editor\": \"Редактор\",\n      \"owner\": \"Владелец\",\n      \"viewer_description\": \"Может просматривать закладки в списке\",\n      \"editor_description\": \"Может добавлять и удалять закладки\",\n      \"no_collaborators\": \"Пока нет сотрудников. Добавьте кого-нибудь, чтобы начать совместную работу!\",\n      \"no_collaborators_readonly\": \"Нет соавторов для этого списка.\",\n      \"people_with_access\": \"Люди, у которых есть доступ к этому списку\",\n      \"add_or_remove\": \"Добавьте или удалите людей, которые могут получить доступ к этому списку\",\n      \"invitation_sent\": \"Приглашение успешно отправлено\",\n      \"invitation_revoked\": \"Приглашение отозвано\",\n      \"failed_to_revoke\": \"Не удалось отозвать приглашение\",\n      \"pending\": \"В ожидании\",\n      \"revoke\": \"Отозвать\",\n      \"declined\": \"Отклонено\"\n    },\n    \"leave_list\": {\n      \"title\": \"Покинуть список\",\n      \"confirm_message\": \"Вы уверены, что хотите покинуть {{icon}} {{name}}?\",\n      \"warning\": \"Вы больше не сможете просматривать или получать доступ к закладкам в этом списке. Владелец списка может добавить вас обратно, если это необходимо.\",\n      \"action\": \"Покинуть список\",\n      \"success\": \"Вы покинули \\\"{{icon}} {{name}}\\\"\"\n    },\n    \"invitations\": {\n      \"pending\": \"Ожидающие приглашения\",\n      \"description\": \"Просмотрите и ответьте на приглашения к совместной работе над списком\",\n      \"invited_by\": \"Пригласил\",\n      \"accept\": \"Принять\",\n      \"decline\": \"Отклонить\",\n      \"accepted\": \"Приглашение принято\",\n      \"declined\": \"Приглашение отклонено\",\n      \"failed_to_accept\": \"Не удалось принять приглашение\",\n      \"failed_to_decline\": \"Не удалось отклонить приглашение\"\n    },\n    \"shared_lists\": \"Общие списки\"\n  },\n  \"settings\": {\n    \"user_settings\": \"Настройки пользователя\",\n    \"info\": {\n      \"user_info\": \"Информация о пользователе\",\n      \"options\": \"Опции\",\n      \"basic_details\": \"Основные сведения\",\n      \"change_password\": \"Сменить пароль\",\n      \"confirm_new_password\": \"Подтвердить новый пароль\",\n      \"interface_lang\": \"Язык интерфейса\",\n      \"current_password\": \"Текущий пароль\",\n      \"new_password\": \"Новый пароль\",\n      \"user_settings\": {\n        \"user_settings_updated\": \"Настройки пользователя обновлены!\",\n        \"bookmark_click_action\": {\n          \"title\": \"Действие при нажатии на закладку\",\n          \"open_external_url\": \"Открыть оригинальный URL\",\n          \"open_bookmark_details\": \"Открыть детали закладки\"\n        },\n        \"archive_display_behaviour\": {\n          \"hide\": \"Скрыть архивированные закладки в тегах и списках\",\n          \"title\": \"Архивированные закладки\",\n          \"show\": \"Показывать архивированные закладки в тегах и списках\"\n        }\n      },\n      \"reader_settings\": {\n        \"local_overrides_title\": \"Активны настройки для этого устройства\",\n        \"using_default\": \"Используются настройки клиента по умолчанию\",\n        \"clear_override_hint\": \"Удалите переопределение устройства, чтобы использовать глобальную настройку ({{value}})\",\n        \"font_size\": \"Размер шрифта\",\n        \"font_family\": \"Тип шрифта\",\n        \"preview_inline\": \"(предпросмотр)\",\n        \"tooltip_preview\": \"Несохраненные изменения предпросмотра\",\n        \"save_to_all_devices\": \"Все устройства\",\n        \"tooltip_local\": \"Настройки устройства отличаются от глобальных\",\n        \"reset_preview\": \"Сбросить предпросмотр\",\n        \"mono\": \"Моноширинный\",\n        \"line_height\": \"Высота строки\",\n        \"tooltip_default\": \"Настройки чтения\",\n        \"title\": \"Настройки читалки\",\n        \"serif\": \"С засечками\",\n        \"preview\": \"Предварительный просмотр\",\n        \"not_set\": \"Не задано\",\n        \"clear_local_overrides\": \"Сбросить настройки для устройства\",\n        \"preview_text\": \"Шустрая бурая лиса перепрыгивает ленивого пса. Вот так будет выглядеть текст в режиме чтения.\",\n        \"local_overrides_cleared\": \"Настройки для устройства сброшены\",\n        \"local_overrides_description\": \"На этом устройстве параметры читалки отличаются от ваших глобальных настроек:\",\n        \"clear_defaults\": \"Сбросить все значения по умолчанию\",\n        \"description\": \"Настройте параметры текста по умолчанию для режима чтения. Эти параметры синхронизируются на всех ваших устройствах.\",\n        \"defaults_cleared\": \"Настройки читалки по умолчанию сброшены\",\n        \"save_hint\": \"Сохранить настройки только для этого устройства или синхронизировать на всех устройствах\",\n        \"save_as_default\": \"Сохранить как значения по умолчанию\",\n        \"save_to_device\": \"Это устройство\",\n        \"sans\": \"Без засечек\",\n        \"tooltip_preview_and_local\": \"Несохраненные изменения предпросмотра; настройки устройства отличаются от глобальных\",\n        \"adjust_hint\": \"Отрегулируйте настройки выше, чтобы просмотреть изменения\"\n      },\n      \"avatar\": {\n        \"upload\": \"Загрузить аватар\",\n        \"change\": \"Сменить аватар\",\n        \"remove_confirm_title\": \"Удалить аватар?\",\n        \"updated\": \"Аватар обновлён\",\n        \"removed\": \"Аватар удалён\",\n        \"description\": \"Загрузи квадратное изображение, которое будет твоим аватаром.\",\n        \"remove_confirm_description\": \"Текущее фото профиля будет удалено.\",\n        \"title\": \"Фото профиля\",\n        \"remove\": \"Удалить аватар\"\n      }\n    },\n    \"import\": {\n      \"import_bookmarks_from_karakeep_export\": \"Импортировать закладки из экспорта Karakeep\",\n      \"import_export\": \"Импорт / Экспорт\",\n      \"import_export_bookmarks\": \"Импорт / Экспорт закладок\",\n      \"import_bookmarks_from_pocket_export\": \"Импортировать закладки из экспорта Pocket\",\n      \"import_bookmarks_from_matter_export\": \"Импортировать закладки из экспорта Matter\",\n      \"import_bookmarks_from_omnivore_export\": \"Импортировать закладки из экспорта Omnivore\",\n      \"imported_bookmarks\": \"Импортировано закладок\",\n      \"import_bookmarks_from_html_file\": \"Импортировать закладки из HTML файла\",\n      \"export_links_and_notes\": \"Экспортировать ссылки и заметки\",\n      \"import_bookmarks_from_linkwarden_export\": \"Импортировать закладки из экспорта Linkwarden\",\n      \"import_bookmarks_from_tab_session_manager_export\": \"Импортировать закладки из Tab Session Manager\",\n      \"import_bookmarks_from_mymind_export\": \"Импортировать закладки из экспорта mymind\",\n      \"import_bookmarks_from_instapaper_export\": \"Импортировать закладки из экспорта Instapaper\"\n    },\n    \"api_keys\": {\n      \"key_success\": \"Ключ был успешно создан\",\n      \"key_success_please_copy\": \"Пожалуйста, скопируйте ключ и сохраните в безопасном месте. После закрытия окна, вы больше не сможете его посмотреть.\",\n      \"new_api_key\": \"Новый API ключ\",\n      \"new_api_key_desc\": \"Дайте вашему API ключу уникальное имя\",\n      \"api_keys\": \"API ключи\",\n      \"regenerate_api_key\": \"Обновить ключ API\",\n      \"key_regenerated\": \"Ключ успешно обновлен\",\n      \"key_regenerated_please_copy\": \"Пожалуйста, скопируй новый ключ и сохрани его в надежном месте. Старый ключ был отозван и больше не будет работать.\",\n      \"regenerate_warning\": \"Ты уверен, что хочешь обновить ключ API\\\"{{name}}\\\"?\",\n      \"regenerate_confirmation\": \"Это отзовет текущий ключ и создаст новый. Любые приложения, использующие текущий ключ, перестанут работать.\"\n    },\n    \"ai\": {\n      \"ai_settings\": \"Настройки ИИ\",\n      \"tagging_rules\": \"Правила создания меток\",\n      \"prompt_preview\": \"Предпросмотр запроса\",\n      \"text_prompt\": \"Запрос для текста\",\n      \"images_prompt\": \"Запрос для изображения\",\n      \"tagging_rule_description\": \"Запросы, которые вы добавите сюда, будут включены как правила для модели во время генерации меток. Вы можете просмотреть итоговые запросы в разделе предпросмотра запроса.\",\n      \"text_tagging\": \"Пометка текста тегами\",\n      \"image_tagging\": \"Пометка изображений тегами\",\n      \"summarization\": \"Суммирование\",\n      \"summarization_prompt\": \"Подсказка для суммирования\",\n      \"all_tagging\": \"Все теги\",\n      \"tag_style\": \"Стиль тегов\",\n      \"auto_summarization_description\": \"Автоматически генерируйте сводки для своих закладок с помощью ИИ.\",\n      \"auto_tagging\": \"Автоматическая расстановка тегов\",\n      \"titlecase_spaces\": \"Заглавные с пробелами\",\n      \"lowercase_underscores\": \"Строчные с подчеркиваниями\",\n      \"inference_language\": \"Язык логического вывода\",\n      \"titlecase_hyphens\": \"Заглавные с дефисами\",\n      \"lowercase_hyphens\": \"Строчные с дефисами\",\n      \"lowercase_spaces\": \"Строчные с пробелами\",\n      \"inference_language_description\": \"Выбери язык для тегов и саммари, которые генерит ИИ.\",\n      \"tag_style_description\": \"Выбери, как форматировать автосгенерированные теги.\",\n      \"auto_tagging_description\": \"Автоматически генерируйте теги для ваших закладок с помощью ИИ.\",\n      \"camelCase\": \"camelCase\",\n      \"auto_summarization\": \"Автоматическое создание сводок\",\n      \"no_preference\": \"Без предпочтений\",\n      \"curated_tags\": \"Подобранные теги\",\n      \"curated_tags_description\": \"При необходимости ограничьте добавление тегов ИИ только тегами из этого списка. Если теги не выбраны, ИИ генерирует теги свободно.\",\n      \"curated_tags_updated\": \"Подобранные теги успешно обновлены!\",\n      \"curated_tags_update_failed\": \"Не удалось обновить подобранные теги\"\n    },\n    \"feeds\": {\n      \"rss_subscriptions\": \"RSS подписки\",\n      \"add_a_subscription\": \"Добавить подписку\",\n      \"feed_enabled\": \"RSS-канал включён\",\n      \"feed_disabled\": \"RSS-канал отключён\"\n    },\n    \"broken_links\": {\n      \"last_crawled_at\": \"Последний раз сканирован\",\n      \"crawling_failed\": \"Сканирование не удалось\",\n      \"crawling_status\": \"Статус сканирования\",\n      \"broken_links\": \"Сломанные ссылки\"\n    },\n    \"back_to_app\": \"Вернуться в приложение\",\n    \"webhooks\": {\n      \"webhooks\": \"Веб-хуки\",\n      \"events\": {\n        \"created\": \"Создано\",\n        \"title\": \"События\",\n        \"crawled\": \"Просканировано\",\n        \"edited\": \"Отредактировано\"\n      },\n      \"description\": \"Вы можете использовать веб-хуки для запуска действий при создании, изменении или сканировании закладок.\",\n      \"auth_token\": \"Токен аутентификации\",\n      \"add_auth_token\": \"Добавить токен аутентификации\",\n      \"edit_auth_token\": \"Изменить токен авторизации\",\n      \"delete_webhook\": \"Удалить веб-хук\",\n      \"create_webhook\": \"Создать веб-хук\",\n      \"delete_webhook_confirmation\": \"Вы уверены, что хотите удалить этот веб-хук?\",\n      \"edit_webhook\": \"Редактировать веб-хук\",\n      \"webhook_url\": \"URL веб-хука\"\n    },\n    \"manage_assets\": {\n      \"bookmark_link\": \"Ссылка на закладку\",\n      \"asset_link\": \"Ссылка на ресурс\",\n      \"delete_asset\": \"Удалить ресурс\",\n      \"delete_asset_confirmation\": \"Вы уверены, что хотите удалить этот ресурс?\",\n      \"manage_assets\": \"Управление ресурсами\",\n      \"no_assets\": \"У вас пока нет ресурсов.\",\n      \"asset_type\": \"Тип ресурса\"\n    },\n    \"rules\": {\n      \"rules\": \"Механизм правил\",\n      \"rule_name\": \"Название правила\",\n      \"description\": \"Ты можешь использовать правила для запуска действий при возникновении события.\",\n      \"ceate_rule\": \"Создать правило\",\n      \"edit_rule\": \"Редактировать правило\",\n      \"save_rule\": \"Сохранить правило\",\n      \"delete_rule\": \"Удалить правило\",\n      \"delete_rule_confirmation\": \"Ты уверен, что хочешь удалить это правило?\",\n      \"whenever\": \"Когда ...\",\n      \"if\": \"Если ...\",\n      \"enter_rule_name\": \"Введите имя правила\",\n      \"describe_what_this_rule_does\": \"Опиши, что делает это правило\",\n      \"rule_has_been_created\": \"Правило создано!\",\n      \"rule_has_been_updated\": \"Правило обновлено!\",\n      \"rule_has_been_deleted\": \"Правило удалено!\",\n      \"no_rules_created_yet\": \"Пока нет правил\",\n      \"create_your_first_rule\": \"Создай свое первое правило, чтобы автоматизировать рабочий процесс\",\n      \"conditions_types\": {\n        \"always\": \"Всегда\",\n        \"url_contains\": \"URL содержит\",\n        \"imported_from_feed\": \"Импортировано из ленты\",\n        \"bookmark_type_is\": \"Тип закладки:\",\n        \"has_tag\": \"Имеет тег\",\n        \"is_favourited\": \"В избранном\",\n        \"is_archived\": \"В архиве\",\n        \"and\": \"Все следующие утверждения верны\",\n        \"or\": \"Если что-то из этого правда\",\n        \"url_does_not_contain\": \"URL не содержит\",\n        \"title_contains\": \"Заголовок содержит\",\n        \"title_does_not_contain\": \"Заголовок не содержит\"\n      },\n      \"actions_types\": {\n        \"add_tag\": \"Добавить тег\",\n        \"remove_tag\": \"Удалить тег\",\n        \"add_to_list\": \"Добавить в список\",\n        \"remove_from_list\": \"Удалить из списка\",\n        \"download_full_page_archive\": \"Скачать полный архив страницы\",\n        \"favourite_bookmark\": \"Любимая закладка\",\n        \"archive_bookmark\": \"Архивировать закладку\"\n      },\n      \"events_types\": {\n        \"bookmark_added\": \"Закладка добавлена\",\n        \"tag_added\": \"Этот тег добавляется к закладке\",\n        \"tag_removed\": \"Этот тег удален из закладки\",\n        \"added_to_list\": \"Закладка добавлена в этот список\",\n        \"removed_from_list\": \"Закладка удалена из этого списка\",\n        \"favourited\": \"Закладка добавлена в избранное\",\n        \"archived\": \"Закладка архивирована\"\n      }\n    },\n    \"stats\": {\n      \"usage_statistics\": \"Статистика использования\",\n      \"insights_description\": \"Инсайты о твоих привычках и коллекции закладок\",\n      \"failed_to_load\": \"Не удалось загрузить статистику\",\n      \"overview\": {\n        \"total_bookmarks\": \"Всего закладок\",\n        \"all_saved_items\": \"Все сохраненные элементы\",\n        \"favorites\": \"Избранное\",\n        \"starred_bookmarks\": \"Избранные закладки\",\n        \"archived\": \"В архиве\",\n        \"archived_items\": \"Архивные элементы\",\n        \"tags\": \"Теги\",\n        \"unique_tags_created\": \"Уникальные теги созданы\",\n        \"lists\": \"Списки\",\n        \"bookmark_collections\": \"Коллекции закладок\",\n        \"highlights\": \"Выделения\",\n        \"text_highlights\": \"Текстовые выделения\",\n        \"storage_used\": \"Использовано места в хранилище\",\n        \"total_asset_storage\": \"Всего места в хранилище активов\",\n        \"this_month\": \"В этом месяце\",\n        \"bookmarks_added\": \"Закладки добавлены\"\n      },\n      \"bookmark_types\": {\n        \"title\": \"Типы закладок\",\n        \"links\": \"Ссылки\",\n        \"text_notes\": \"Текстовые заметки\",\n        \"assets\": \"Активы\"\n      },\n      \"recent_activity\": {\n        \"title\": \"Недавняя активность\",\n        \"this_week\": \"На этой неделе\",\n        \"this_month\": \"В этом месяце\",\n        \"this_year\": \"В этом году\"\n      },\n      \"top_domains\": {\n        \"title\": \"Топ доменов\",\n        \"no_domains_found\": \"Домены не найдены\"\n      },\n      \"most_used_tags\": {\n        \"title\": \"Самые используемые теги\",\n        \"no_tags_found\": \"Теги не найдены\"\n      },\n      \"activity_patterns\": {\n        \"activity_by_hour\": \"Активность по часам\",\n        \"activity_by_day\": \"Активность по дням\"\n      },\n      \"storage_breakdown\": {\n        \"title\": \"Разбивка хранилища\"\n      },\n      \"bookmark_sources\": {\n        \"title\": \"Источники закладок\",\n        \"empty\": \"Нет доступных исходных данных\"\n      }\n    },\n    \"subscription\": {\n      \"subscription\": \"Подписка\",\n      \"manage_subscription\": \"Управляй своей подпиской и платёжной информацией\",\n      \"current_plan\": \"Текущий тариф\",\n      \"billing_period\": \"Расчётный период\",\n      \"paid_plan\": \"Платный тариф\",\n      \"unlock_bigger_quota\": \"Получи больше квоту и поддержи проект\",\n      \"subscribe_now\": \"Подписаться сейчас\",\n      \"manage_billing\": \"Управление оплатой\",\n      \"subscription_canceled\": \"Твоя подписка отменена и закончится {{date}}. Ты можешь возобновить её в любое время.\",\n      \"usage_quotas\": \"Использование и квоты\",\n      \"track_usage\": \"Отслеживай текущее использование по сравнению с лимитами твоего тарифного плана\",\n      \"total_bookmarks_saved\": \"Всего сохранённых закладок\",\n      \"assets_file_storage\": \"Ресурсы и хранилище файлов\",\n      \"unlimited_usage\": \"Неограниченное использование\",\n      \"quota_limit_reached\": \"Лимит квоты достигнут\",\n      \"approaching_quota_limit\": \"Приближаешься к лимиту квоты\",\n      \"loading_usage\": \"Загружаю информацию об использовании…\",\n      \"free\": \"Бесплатно\",\n      \"paid\": \"Платный\"\n    },\n    \"import_sessions\": {\n      \"title\": \"Импорт сессий\",\n      \"description\": \"Просматривай и управляй своими сессиями массового импорта. Сессии создаются автоматически, когда ты импортируешь закладки.\",\n      \"load_error\": \"Не удалось загрузить сессии импорта\",\n      \"no_sessions\": \"Пока нет сессий импорта\",\n      \"no_sessions_detail\": \"Сессии импорта появятся здесь автоматически при импорте закладок\",\n      \"created_at\": \"Создано {{time}}\",\n      \"progress\": \"Выполнение\",\n      \"status\": {\n        \"pending\": \"В ожидании\",\n        \"in_progress\": \"В процессе\",\n        \"completed\": \"Завершено\",\n        \"failed\": \"Ошибка\",\n        \"processing\": \"Обработка\",\n        \"staging\": \"Подготовка к релизу\",\n        \"running\": \"Запущено\",\n        \"paused\": \"Приостановлено\"\n      },\n      \"badges\": {\n        \"pending\": \"{{count}} в ожидании\",\n        \"processing\": \"{{count}} в обработке\",\n        \"completed\": \"{{count}} завершено\",\n        \"failed\": \"{{count}} не удалось\"\n      },\n      \"imported_to\": \"Импортировано в:\",\n      \"view_list\": \"Смотреть список\",\n      \"delete_dialog_title\": \"Удалить сессию импорта\",\n      \"delete_dialog_description\": \"Ты уверен, что хочешь удалить \\\"{{name}}\\\"? Это действие нельзя отменить. Сами закладки не будут удалены.\",\n      \"delete_session\": \"Удалить сессию\",\n      \"pause_session\": \"Приостановить\",\n      \"resume_session\": \"Возобновить\",\n      \"view_details\": \"Посмотреть детали\",\n      \"detail\": {\n        \"page_title\": \"Детали сессии импорта\",\n        \"back_to_import\": \"Вернуться к импорту\",\n        \"filter_all\": \"Все\",\n        \"filter_accepted\": \"Принято\",\n        \"filter_rejected\": \"Отклонено\",\n        \"filter_duplicates\": \"Дубликаты\",\n        \"filter_pending\": \"Ожидает обработки\",\n        \"table_title\": \"Название / URL\",\n        \"table_type\": \"Тип\",\n        \"table_result\": \"Результат\",\n        \"table_reason\": \"Причина\",\n        \"table_bookmark\": \"Закладка\",\n        \"result_accepted\": \"Принято\",\n        \"result_rejected\": \"Отклонено\",\n        \"result_skipped_duplicate\": \"Дубликат\",\n        \"result_pending\": \"Ожидает обработки\",\n        \"result_processing\": \"В обработке\",\n        \"no_results\": \"По этому фильтру ничего не найдено.\",\n        \"view_bookmark\": \"Посмотреть закладку\",\n        \"load_more\": \"Загрузить еще\",\n        \"no_title\": \"Без названия\"\n      }\n    },\n    \"backups\": {\n      \"backups\": \"Резервные копии\",\n      \"page_title\": \"Резервные копии\",\n      \"page_description\": \"Автоматическое создание резервных копий твоих закладок и управление ими. Резервные копии сжимаются и надёжно хранятся.\",\n      \"configuration\": {\n        \"title\": \"Настройка резервного копирования\",\n        \"enable_automatic_backups\": \"Включить автоматическое резервное копирование\",\n        \"enable_automatic_backups_description\": \"Автоматически создавать резервные копии твоих закладок\",\n        \"backup_frequency\": \"Периодичность резервного копирования\",\n        \"backup_frequency_description\": \"Как часто следует создавать резервные копии\",\n        \"retention_period\": \"Срок хранения (в днях)\",\n        \"retention_period_description\": \"Сколько дней хранить резервные копии до их удаления\",\n        \"frequency\": {\n          \"daily\": \"Ежедневно\",\n          \"weekly\": \"Еженедельно\"\n        },\n        \"select_frequency\": \"Выбрать периодичность\",\n        \"save_settings\": \"Сохранить настройки\"\n      },\n      \"list\": {\n        \"title\": \"Твои резервные копии\",\n        \"create_backup_now\": \"Создать резервную копию сейчас\",\n        \"no_backups\": \"У тебя ещё нет резервных копий. Включи автоматическое резервное копирование или создай его вручную.\",\n        \"table\": {\n          \"created_at\": \"Создано\",\n          \"bookmarks\": \"Закладки\",\n          \"size\": \"Размер\",\n          \"status\": \"Статус\",\n          \"actions\": \"Действия\"\n        },\n        \"status\": {\n          \"success\": \"Успешно\",\n          \"failed\": \"Ошибка\",\n          \"pending\": \"В ожидании\"\n        },\n        \"actions\": {\n          \"download_backup\": \"Скачать резервную копию\",\n          \"delete_backup\": \"Удалить резервную копию\"\n        }\n      },\n      \"dialogs\": {\n        \"delete_backup_title\": \"Удалить резервную копию?\",\n        \"delete_backup_description\": \"Вы уверены, что хотите удалить эту резервную копию? Это действие нельзя отменить.\"\n      },\n      \"toasts\": {\n        \"backup_queued\": \"Задание на резервное копирование поставлено в очередь! Оно будет обработано в ближайшее время.\",\n        \"backup_deleted\": \"Резервная копия удалена!\"\n      }\n    }\n  },\n  \"actions\": {\n    \"remove_from_list\": \"Удалить из списка\",\n    \"refresh\": \"Обновить\",\n    \"unarchive\": \"Вернуть из архива\",\n    \"favorite\": \"В избранное\",\n    \"unfavorite\": \"Удалить из избранного\",\n    \"recrawl\": \"Пересканировать\",\n    \"change_layout\": \"Изменить разметку\",\n    \"save\": \"Сохранить\",\n    \"create\": \"Создать\",\n    \"summarize_with_ai\": \"Суммировать с ИИ\",\n    \"sign_out\": \"Выйти\",\n    \"close\": \"Закрыть\",\n    \"download_full_page_archive\": \"Скачать полный архив страницы\",\n    \"add_to_list\": \"Добавить в список\",\n    \"close_bulk_edit\": \"Закрыть массовое редактирование\",\n    \"bulk_edit\": \"Массовое редактирование\",\n    \"manage_lists\": \"Управление списками\",\n    \"fetch_now\": \"Получить сейчас\",\n    \"edit_title\": \"Редактировать заголовок\",\n    \"ignore\": \"Игнорировать\",\n    \"delete\": \"Удалить\",\n    \"cancel\": \"Отменить\",\n    \"archive\": \"В архив\",\n    \"edit\": \"Редактировать\",\n    \"merge\": \"Слить\",\n    \"add\": \"Добавить\",\n    \"edit_tags\": \"Редактировать метки\",\n    \"copy_link\": \"Скопировать ссылку\",\n    \"unselect_all\": \"Отменить выбор\",\n    \"select_all\": \"Выбрать все\",\n    \"apply_all\": \"Применить всё\",\n    \"sort\": {\n      \"title\": \"Сортировка\",\n      \"newest_first\": \"Сначала новые\",\n      \"oldest_first\": \"Сначала старые\",\n      \"relevant_first\": \"Сначала самые релевантные\"\n    },\n    \"open_editor\": \"Открыть редактор\",\n    \"toggle_show_archived\": \"Показать архивированные\",\n    \"confirm\": \"Подтвердить\",\n    \"regenerate\": \"Обновить\",\n    \"load_more\": \"Загрузить еще\",\n    \"edit_notes\": \"Редактировать заметки\",\n    \"preserve_as_pdf\": \"Сохранить как PDF\",\n    \"offline_copies\": \"Автономные копии\",\n    \"preserve_offline_archive\": \"Сохранить оффлайн архив\",\n    \"download_full_page_archive_file\": \"Скачать файл архива\",\n    \"download_pdf_file\": \"Скачать PDF-файл\",\n    \"remove\": \"Удалить\",\n    \"more\": \"Больше\",\n    \"replace_banner\": \"Заменить баннер\",\n    \"add_banner\": \"Добавить баннер\",\n    \"download\": \"Скачать\"\n  },\n  \"editor\": {\n    \"text_toolbar\": {\n      \"markdown_shortcuts\": {\n        \"inline_code\": {\n          \"example\": \"`Код`\",\n          \"label\": \"Встроенный код\"\n        },\n        \"heading\": {\n          \"label\": \"Оглавление\",\n          \"example\": \"# H1, ## H2, ### H3\"\n        },\n        \"italic\": {\n          \"example\": \"*Курсив* или _Курсив_ или CTRL+i\",\n          \"label\": \"Курсив\"\n        },\n        \"blockquote\": {\n          \"label\": \"Цитата\",\n          \"example\": \"> Цитата\"\n        },\n        \"ordered_list\": {\n          \"example\": \"1. Элемент списка\",\n          \"label\": \"Упорядоченный список\"\n        },\n        \"block_code\": {\n          \"example\": \"``` + пробел\",\n          \"label\": \"Блок кода\"\n        },\n        \"label\": \"Горячие клавиши Markdown\",\n        \"bold\": {\n          \"label\": \"Жирный\",\n          \"example\": \"**текст** или CTRL+b\"\n        },\n        \"unordered_list\": {\n          \"example\": \"- Элемент списка\",\n          \"label\": \"Неупорядоченный список\"\n        }\n      },\n      \"undo\": \"Отменить\",\n      \"redo\": \"Повторить\",\n      \"bold\": \"Жирный\",\n      \"italic\": \"Курсив\",\n      \"underline\": \"Подчёркивание\",\n      \"strikethrough\": \"Зачеркнуть\",\n      \"code\": \"Код\",\n      \"highlight\": \"Выделить\",\n      \"align_left\": \"Выровнить слева\",\n      \"align_center\": \"Выровнить по центру\",\n      \"align_right\": \"Выровнить справа\"\n    },\n    \"quickly_focus\": \"Для быстрого перехода к полю нажмите ⌘ + E\",\n    \"placeholder\": \"Вставьте ссылку или изображение, напишите заметку или перетащите изображение сюда ...\",\n    \"new_item\": \"НОВЫЙ ЭЛЕМЕНТ\",\n    \"import_as_text\": \"Импортировать как текстовую закладку\",\n    \"disabled_submissions\": \"Редактирование отключено\",\n    \"multiple_urls_dialog_desc\": \"Ввод содержит несколько ссылок на разных строках. Вы хотите импортировать их как разные закладки?\",\n    \"multiple_urls_dialog_title\": \"Импрортировать ссылки как отдельные закладки?\",\n    \"import_as_separate_bookmarks\": \"Импортировать как разные закладки\",\n    \"placeholder_v2\": \"Вставьте ссылку, напишите заметку или перетащите изображение…\"\n  },\n  \"admin\": {\n    \"server_stats\": {\n      \"total_users\": \"Всего пользователей\",\n      \"server_version\": \"Версия сервера\",\n      \"total_bookmarks\": \"Всего закладок\",\n      \"server_stats\": \"Статистика сервера\"\n    },\n    \"users_list\": {\n      \"users_list\": \"Список пользователей\",\n      \"reset_password\": \"Сбросить пароль\",\n      \"asset_sizes\": \"Размеры ресурсов\",\n      \"confirm_password\": \"Подтвердить пароль\",\n      \"local_user\": \"Локальный пользователь\",\n      \"num_bookmarks\": \"Закладок\",\n      \"change_role\": \"Изменить роль\",\n      \"delete_user\": \"Удалить пользователя\",\n      \"create_user\": \"Создать пользователя\",\n      \"delete_user_confirm_description\": \"Ты уверен, что хочешь удалить пользователя \\\"{{name}}\\\"?\",\n      \"unlimited\": \"Неограниченный\"\n    },\n    \"background_jobs\": {\n      \"queued\": \"В очереди\",\n      \"tidy_assets_jobs\": \"Задачи очистки ресурсов\",\n      \"background_jobs\": \"Фоновые задачи\",\n      \"inference_jobs\": \"Задачи обработки\",\n      \"pending\": \"В ожидании\",\n      \"failed\": \"Неудачно\",\n      \"job\": \"Задача\",\n      \"indexing_jobs\": \"Задачи индексирования\",\n      \"crawler_jobs\": \"Задачи сканирования\",\n      \"video_jobs\": \"Задания на скачивание видео\",\n      \"webhook_jobs\": \"Задачи веб-хука\",\n      \"asset_preprocessing_jobs\": \"Задачи предварительной обработки ресурсов\",\n      \"feed_jobs\": \"Задания RSS-ленты\",\n      \"actions\": {\n        \"reindex_all_bookmarks\": \"Переиндексировать все закладки\",\n        \"clean_assets\": \"Очистить оборванные ресурсы и повторно синхронизировать метаданные\",\n        \"reprocess_assets_fix_mode\": \"Повторно обработать необработанные активы\",\n        \"recrawl_failed_links_only\": \"Повторно просканировать только ссылки с ошибками\",\n        \"recrawl_all_links\": \"Повторно просканировать все ссылки\",\n        \"without_inference\": \"Без инференции\",\n        \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Заново создать AI-теги только для закладок с ошибками\",\n        \"regenerate_ai_tags_for_all_bookmarks\": \"Заново создать AI-теги для всех закладок\",\n        \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Заново сгенерировать AI-сводки только для закладок с ошибками\",\n        \"regenerate_ai_summaries_for_all_bookmarks\": \"Заново сгенерировать AI-сводки для всех закладок\",\n        \"migrate_large_link_html_content\": \"Переместите Большой Встроенный HTML-контент в Ресурсы\",\n        \"recrawl_pending_links_only\": \"Переиндексировать только отложенные ссылки\",\n        \"regenerate_ai_tags_for_pending_bookmarks_only\": \"Заново сгенерировать AI-теги только для отложенных закладок\",\n        \"regenerate_ai_summaries_for_pending_bookmarks_only\": \"Заново сгенерировать AI-резюме только для отложенных закладок\"\n      },\n      \"jobs\": {\n        \"crawler\": {\n          \"title\": \"Задачи краулера\",\n          \"description\": \"Веб-сканирование и извлечение контента из URL-адресов\"\n        },\n        \"inference\": {\n          \"title\": \"Задачи логического вывода\",\n          \"description\": \"AI-тегирование и суммирование контента\"\n        },\n        \"indexing\": {\n          \"title\": \"Задачи индексирования\",\n          \"description\": \"Обновления поискового индекса\"\n        },\n        \"asset_preprocessing\": {\n          \"title\": \"Задачи предварительной обработки ресурсов\",\n          \"description\": \"Предварительная обработка изображений и документов (скриншоты, извлечение текста и т. д.)\"\n        },\n        \"tidy_assets\": {\n          \"title\": \"Упорядочение задач для ресурсов\",\n          \"description\": \"Очистка ресурсов и оптимизация хранилища\"\n        },\n        \"video\": {\n          \"title\": \"Задачи скачивания видео\",\n          \"description\": \"Извлечение и скачивание видео\"\n        },\n        \"webhook\": {\n          \"title\": \"Задачи веб-хука\",\n          \"description\": \"Внешние уведомления веб-хука\"\n        },\n        \"feed\": {\n          \"title\": \"Задачи RSS-ленты\",\n          \"description\": \"Обработка RSS-ленты и обновление контента\"\n        },\n        \"admin_maintenance\": {\n          \"title\": \"Админские работы по техобслуживанию\",\n          \"description\": \"Административная очистка и обслуживание ресурсов\"\n        }\n      },\n      \"monitor_and_manage\": \"Мониторинг и управление очередями фоновых задач и задачами системной обработки\",\n      \"active\": \"Активные\",\n      \"available_actions\": \"Доступные действия\",\n      \"status\": {\n        \"title\": \"Описание состояний задач\",\n        \"queued\": {\n          \"title\": \"В очереди\",\n          \"description\": \"Задания ждут своей очереди на обработку. Они начнутся автоматически, когда ресурсы будут доступны.\"\n        },\n        \"unprocessed\": {\n          \"title\": \"Необработанные\",\n          \"description\": \"Закладки, которые еще не были обработаны. Скорее всего, они уже поставлены в очередь на обработку, если нет, возможно, потребуется повторно поставить их в очередь вручную.\"\n        },\n        \"failed\": {\n          \"title\": \"С ошибками\",\n          \"description\": \"Закладки, при обработке которых произошли ошибки. Им может потребоваться ручная обработка.\"\n        }\n      }\n    },\n    \"actions\": {\n      \"recrawl_failed_links_only\": \"Пересканировать только неудачные ссылки\",\n      \"without_inference\": \"Без обработки\",\n      \"regenerate_ai_tags_for_all_bookmarks\": \"Перегенерировать ИИ метки всем закладкам\",\n      \"compact_assets\": \"Сжать ресурсы\",\n      \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Перегенерировать ИИ метки только для неудачных закладок\",\n      \"reindex_all_bookmarks\": \"Переиндексировать все закладки\",\n      \"recrawl_all_links\": \"Пересканировать все ссылки\",\n      \"reprocess_assets_fix_mode\": \"Перепроцессировать ресурсы (фиксный режим)\",\n      \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Перегенерировать AI-резюме только для неудачных закладок\",\n      \"regenerate_ai_summaries_for_all_bookmarks\": \"Перегенерировать AI-резюме для всех закладок\"\n    },\n    \"admin_settings\": \"Настройки администратора\",\n    \"service_connections\": {\n      \"title\": \"Подключения служб\",\n      \"description\": \"Мониторь работоспособность и связность зависимостей внешней системы\",\n      \"search_engine\": \"Поисковая система\",\n      \"browser\": \"Браузер\",\n      \"queue_system\": \"Система очередей\",\n      \"status\": {\n        \"not_configured\": \"Не настроено\",\n        \"connected\": \"Подключено\",\n        \"disconnected\": \"Отключено\"\n      }\n    },\n    \"admin_tools\": {\n      \"admin_tools\": \"Инструменты админа\",\n      \"bookmark_debugger\": \"Отладчик закладок\",\n      \"bookmark_id\": \"ID закладки\",\n      \"bookmark_id_placeholder\": \"Введи ID закладки\",\n      \"lookup\": \"Поиск\",\n      \"debug_info\": \"Отладочная инфа\",\n      \"basic_info\": \"Основная инфа\",\n      \"status\": \"Статус\",\n      \"content\": \"Контент\",\n      \"html_preview\": \"Предпросмотр HTML (первые 1000 символов)\",\n      \"summary\": \"Сводка\",\n      \"url\": \"URL\",\n      \"source_url\": \"Исходный URL\",\n      \"asset_type\": \"Тип актива\",\n      \"file_name\": \"Имя файла\",\n      \"owner_user_id\": \"ID пользователя-владельца\",\n      \"tagging_status\": \"Статус тегов\",\n      \"summarization_status\": \"Статус суммирования\",\n      \"crawl_status\": \"Статус сканирования\",\n      \"crawl_status_code\": \"Код статуса HTTP\",\n      \"crawled_at\": \"Просканировано\",\n      \"recrawl\": \"Перепроверить\",\n      \"reindex\": \"Переиндексировать\",\n      \"retag\": \"Переметить тегами\",\n      \"resummarize\": \"Перефразировать\",\n      \"bookmark_not_found\": \"Закладка не найдена\",\n      \"action_success\": \"Действие выполнено успешно\",\n      \"action_failed\": \"Действие не выполнено\",\n      \"recrawl_queued\": \"Задача перепроверки поставлена в очередь\",\n      \"reindex_queued\": \"Задача переиндексации поставлена в очередь\",\n      \"retag_queued\": \"Задача переметки тегами поставлена в очередь\",\n      \"resummarize_queued\": \"Задача перефразировки поставлена в очередь\",\n      \"view\": \"Вид\",\n      \"fetch_error\": \"Ошибка при получении закладки\"\n    }\n  },\n  \"layouts\": {\n    \"grid\": \"Сетка\",\n    \"list\": \"Список\",\n    \"compact\": \"Компактно\",\n    \"masonry\": \"Плитка\"\n  },\n  \"options\": {\n    \"dark_mode\": \"Тёмный режим\",\n    \"light_mode\": \"Светлый режим\",\n    \"apps_extensions\": \"Приложения и расширения\",\n    \"documentation\": \"Документация\",\n    \"follow_us_on_x\": \"Читай нас в X\"\n  },\n  \"tags\": {\n    \"your_tags_info\": \"Метки, которые вами были хоть раз прикреплены\",\n    \"all_tags\": \"Все метки\",\n    \"ai_tags_info\": \"Метки, которые были приклеплены только автоматически (от ИИ)\",\n    \"unused_tags_info\": \"Метки, которые не были прикреплены к любой закладке\",\n    \"ai_tags\": \"ИИ метки\",\n    \"unused_tags\": \"Неиспользованные метки\",\n    \"delete_all_unused_tags\": \"Удалить все неиспользованные метки\",\n    \"drag_and_drop_merging_info\": \"Перетащите метки друг на друга, чтобы их слить\",\n    \"sort_by_name\": \"Сортировать по имени\",\n    \"drag_and_drop_merging\": \"Слияние перетаскиванием\",\n    \"your_tags\": \"Ваши метки\",\n    \"create_tag\": \"Создать тег\",\n    \"create_tag_description\": \"Создать новый тег, не прикрепляя его ни к одной закладке\",\n    \"tag_name\": \"Имя тега\",\n    \"enter_tag_name\": \"Введите имя тега\",\n    \"sort_by_usage\": \"Сортировать по использованию\",\n    \"sort_by_relevance\": \"Сортировать по релевантности\",\n    \"no_custom_tags\": \"Пока нет пользовательских тегов\",\n    \"no_ai_tags\": \"Пока нет тегов ИИ\",\n    \"no_unused_tags\": \"У вас нет неиспользуемых тегов\",\n    \"no_unused_tags_match_your_search\": \"Нет неиспользуемых тегов, соответствующих вашему поиску\",\n    \"no_tags_match_your_search\": \"Нет тегов, соответствующих вашему поиску\",\n    \"search_placeholder\": \"Поиск тегов...\",\n    \"search_or_create_placeholder\": \"Поиск или создание тегов…\"\n  },\n  \"preview\": {\n    \"view_original\": \"Посмотреть оригинал\",\n    \"cached_content\": \"Содержимое кеша\",\n    \"reader_view\": \"Режим чтения\",\n    \"tabs\": {\n      \"content\": \"Содержание\",\n      \"details\": \"Подробности\"\n    },\n    \"archive_info\": \"Архивы могут неправильно отображаться во встроенном режиме, если для них требуется Javascript. Для достижения наилучших результатов <1>загрузите их и откройте в браузере</1>.\",\n    \"fetch_error_title\": \"Контент недоступен\",\n    \"fetch_error_description\": \"Не удалось получить контент по этой ссылке. Страница может быть защищена, требовать аутентификации или быть временно недоступна.\",\n    \"crawling_in_progress\": \"Получаю содержимое страницы…\",\n    \"continue_reading\": \"Продолжить с места, где остановились\",\n    \"continue_reading_percent\": \"Продолжить с места, где остановились ({{percent}}%)\",\n    \"continue_button\": \"Продолжить\"\n  },\n  \"toasts\": {\n    \"bookmarks\": {\n      \"refetch\": \"Повторная загрузка была добавлена в очередь!\",\n      \"full_page_archive\": \"Запущено создание полного архива страницы\",\n      \"delete_from_list\": \"Закладка была удалена из списка\",\n      \"clipboard_copied\": \"Ссылка была скопирована в буфер обмена!\",\n      \"deleted\": \"Закладка была удалена!\",\n      \"updated\": \"Закладка была обновлена!\",\n      \"preserve_pdf\": \"Сохранение в формате PDF было запущено\",\n      \"update_banner\": \"Баннер обновлён!\",\n      \"uploading_banner\": \"Загрузка баннера...\"\n    },\n    \"lists\": {\n      \"created\": \"Список был создан!\",\n      \"updated\": \"Список был обновлён!\",\n      \"merged\": \"Список объединен!\",\n      \"deleted\": \"Список удален!\"\n    },\n    \"tags\": {\n      \"created\": \"Тег создан!\",\n      \"failed_to_create\": \"Не удалось создать тег\"\n    }\n  },\n  \"cleanups\": {\n    \"cleanups\": \"Очистка\",\n    \"duplicate_tags\": {\n      \"merge_all_suggestions\": \"Слить все предложения?\",\n      \"title\": \"Дублирующиеся метки\"\n    }\n  },\n  \"search\": {\n    \"has_tag\": \"Имеет тег\",\n    \"does_not_have_tag\": \"Не имеет тега\",\n    \"or\": \"Или\",\n    \"created_on_or_before\": \"Создано до или после\",\n    \"not_created_on_or_before\": \"Не создан до или после\",\n    \"is_not_favorited\": \"Не в избранном\",\n    \"is_archived\": \"В архиве\",\n    \"is_not_archived\": \"Не в архиве\",\n    \"has_any_tag\": \"Имеет любой тег\",\n    \"has_no_tags\": \"Нет тега\",\n    \"is_in_any_list\": \"Находится в любом списке\",\n    \"is_not_in_any_list\": \"Не состоит ни в одном списке\",\n    \"created_on_or_after\": \"Создано после\",\n    \"not_created_on_or_after\": \"Не создано после\",\n    \"url_does_not_contain\": \"URL не содержит\",\n    \"is_in_list\": \"Находится в списке\",\n    \"is_not_in_list\": \"Не в списке\",\n    \"type_is\": \"Тип:\",\n    \"type_is_not\": \"Тип не\",\n    \"and\": \"И\",\n    \"is_favorited\": \"В избранном\",\n    \"url_contains\": \"URL содержит\",\n    \"full_text_search\": \"Полнотекстовый поиск\",\n    \"is_from_feed\": \"Из RSS-ленты\",\n    \"is_not_from_feed\": \"Не из RSS-ленты\",\n    \"month_s\": \" Месяцев\",\n    \"created_within\": \"Создано в течение\",\n    \"created_earlier_than\": \"Создано раньше, чем\",\n    \"day_s\": \" Дней\",\n    \"week_s\": \" Недель\",\n    \"year_s\": \" Год(а)\",\n    \"day_s_ago\": \" Дней назад\",\n    \"week_s_ago\": \" Недель назад\",\n    \"month_s_ago\": \" Месяцев назад\",\n    \"year_s_ago\": \" Год(а) назад\",\n    \"history\": \"Недавние поиски\",\n    \"title_contains\": \"Содержит в заголовке\",\n    \"title_does_not_contain\": \"Не содержит в заголовке\",\n    \"is_broken_link\": \"Битые ссылки\",\n    \"tags\": \"Теги\",\n    \"no_suggestions\": \"Нет предложений\",\n    \"filters\": \"Фильтры\",\n    \"is_not_broken_link\": \"Рабочие ссылки\",\n    \"lists\": \"Списки\",\n    \"feeds\": \"Ленты\",\n    \"is_from_source\": \"Источник:\",\n    \"is_not_from_source\": \"Источника нет\"\n  },\n  \"dialogs\": {\n    \"bookmarks\": {\n      \"delete_confirmation_title\": \"Удалить закладку?\",\n      \"delete_confirmation_description\": \"Вы уверены, что хотите удалить эту закладку?\"\n    }\n  },\n  \"highlights\": {\n    \"no_highlights\": \"У вас пока нет выделенных фрагментов.\"\n  },\n  \"banners\": {\n    \"no_bookmarks\": {\n      \"title\": \"Пока нет закладок\",\n      \"description\": \"Сохраняй интересные статьи, ссылки и страницы, чтобы быстро получить к ним доступ позже.\"\n    }\n  },\n  \"bookmark_editor\": {\n    \"title\": \"Редактировать закладку\",\n    \"subtitle\": \"Внеси изменения в детали закладки. Когда закончишь, нажми \\\"Сохранить\\\".\",\n    \"author\": \"Автор\",\n    \"publisher\": \"Издатель\",\n    \"date_published\": \"Дата публикации\",\n    \"pick_a_date\": \"Выбери дату\",\n    \"save_changes\": \"Сохранить изменения\",\n    \"extracted_content\": \"Извлеченное содержимое\"\n  },\n  \"view_options\": {\n    \"title\": \"Параметры просмотра\",\n    \"layout\": \"Расположение\",\n    \"columns\": \"Столбцы\",\n    \"display_options\": \"Параметры отображения\",\n    \"show_note_previews\": \"Показывать заметки\",\n    \"show_tags\": \"Показать теги\",\n    \"show_title\": \"Показать заголовок\",\n    \"image_options\": \"Параметры изображения\",\n    \"image_fit_cover\": \"Обложка (Заполнить)\",\n    \"image_fit_contain\": \"Содержать (По размеру)\"\n  },\n  \"version\": {\n    \"new_release_available\": \"Доступны примечания к новому выпуску\",\n    \"whats_new_title\": \"Что нового в v{{version}}\",\n    \"release_notes_description\": \"Вот последние обновления, полученные из примечаний к выпуску GitHub.\",\n    \"loading_release_notes\": \"Загрузка примечаний к выпуску…\",\n    \"unable_to_load_release_notes\": \"Не удалось загрузить примечания к выпуску прямо сейчас. Пожалуйста, повторите попытку позже.\",\n    \"no_release_notes\": \"Примечания к выпуску для этой версии не публиковались.\",\n    \"release_notes_synced\": \"Примечания к выпуску синхронизируются с GitHub.\",\n    \"view_on_github\": \"Посмотреть на GitHub\"\n  },\n  \"wrapped\": {\n    \"title\": \"Твой {{year}} Wrapped\",\n    \"subtitle\": \"Год в Karakeep\",\n    \"banner\": {\n      \"title\": \"Твой 2025 Wrapped готов!\",\n      \"description\": \"Посмотри свой год в закладках\",\n      \"view_now\": \"Посмотреть сейчас\"\n    },\n    \"button\": \"2025 Wrapped\",\n    \"loading\": \"Загружаем твой Wrapped...\",\n    \"failed_to_load\": \"Не удалось загрузить твою статистику Wrapped\",\n    \"sections\": {\n      \"total_saves\": {\n        \"prefix\": \"Ты сохранил\",\n        \"suffix\": \"штук в этом году\",\n        \"suffix_singular\": \"штука в этом году\"\n      },\n      \"first_bookmark\": {\n        \"title\": \"Твоё путешествие началось\",\n        \"description\": \"Первое сохранение в {{year}}:\"\n      },\n      \"top_domains\": \"Твои топовые сайты\",\n      \"top_tags\": \"Твои топовые теги\",\n      \"monthly_activity\": \"Твой год в сохранениях\",\n      \"most_active_day\": \"Твой самый активный день\",\n      \"peak_times\": {\n        \"title\": \"Когда ты сохраняешь\",\n        \"peak_hour\": \"Час пик\",\n        \"peak_day\": \"День пик\"\n      },\n      \"how_you_save\": \"Как ты сохраняешь\",\n      \"what_you_saved\": \"Что ты сохранил\",\n      \"summary\": {\n        \"favorites\": \"Избранное\",\n        \"tags_created\": \"Создано тегов\",\n        \"highlights\": \"Главное\"\n      },\n      \"types\": {\n        \"links\": \"Ссылки\",\n        \"notes\": \"Заметки\",\n        \"assets\": \"Ресурсы\"\n      }\n    },\n    \"footer\": \"Сделано с помощью Karakeep\",\n    \"share\": \"Поделиться\",\n    \"download\": \"Скачать\",\n    \"close\": \"Закрыть\",\n    \"generating\": \"Генерирую...\"\n  }\n}\n"
  },
  {
    "path": "apps/web/lib/i18n/locales/sk/translation.json",
    "content": "{\n  \"common\": {\n    \"name\": \"Meno\",\n    \"action\": \"Akcia\",\n    \"created_at\": \"Vytvorené\",\n    \"url\": \"URL\",\n    \"email\": \"E-mail\",\n    \"password\": \"Heslo\",\n    \"actions\": \"Akcie\",\n    \"key\": \"Kľúč\",\n    \"role\": \"Rola\",\n    \"roles\": {\n      \"user\": \"Používateľ\",\n      \"admin\": \"Admin\"\n    },\n    \"something_went_wrong\": \"Niečo sa pokazilo\",\n    \"search\": \"Hľadať\",\n    \"tags\": \"Značky\",\n    \"attachments\": \"Prílohy\",\n    \"experimental\": \"Experimentálne\",\n    \"note\": \"Poznámka\",\n    \"highlights\": \"Zvýraznenia\",\n    \"source\": \"Zdroj\",\n    \"screenshot\": \"Snímka obrazovky\",\n    \"video\": \"Video\",\n    \"home\": \"Domov\",\n    \"bookmark_types\": {\n      \"title\": \"Typ záložky\",\n      \"link\": \"Odkaz\",\n      \"text\": \"Text\",\n      \"media\": \"Médiá\"\n    },\n    \"archive\": \"Archív\",\n    \"type\": \"Typ\",\n    \"size\": \"Veľkosť\",\n    \"updated_at\": \"Aktualizované\",\n    \"title\": \"Názov\",\n    \"description\": \"Popis\",\n    \"summary\": \"Zhrnutie\",\n    \"quota\": \"Kvóta\",\n    \"bookmarks\": \"Záložky\",\n    \"storage\": \"Úložisko\",\n    \"pdf\": \"Archivované PDF\",\n    \"default\": \"Predvolené\",\n    \"id\": \"ID\",\n    \"last_used\": \"Posledné použitie\"\n  },\n  \"actions\": {\n    \"cancel\": \"Zrušiť\",\n    \"edit\": \"Upraviť\",\n    \"merge\": \"Zlúčiť\",\n    \"edit_tags\": \"Upraviť značky\",\n    \"create\": \"Vytvoriť\",\n    \"select_all\": \"Vybrať všetko\",\n    \"unfavorite\": \"Odobrať z obľúbených\",\n    \"delete\": \"Odstrániť\",\n    \"copy_link\": \"Kopírovať odkaz\",\n    \"close_bulk_edit\": \"Zavrieť hromadnú úpravu\",\n    \"bulk_edit\": \"Hromadná úprava\",\n    \"manage_lists\": \"Spravovať zoznamy\",\n    \"remove_from_list\": \"Odstrániť zo zoznamu\",\n    \"edit_title\": \"Upraviť nadpis\",\n    \"sign_out\": \"Odhlásiť sa\",\n    \"close\": \"Zavrieť\",\n    \"apply_all\": \"Všetko aplikovať\",\n    \"ignore\": \"Ignorovať\",\n    \"sort\": {\n      \"title\": \"Zoradiť\",\n      \"newest_first\": \"Od najnovších\",\n      \"oldest_first\": \"Od najstarších\",\n      \"relevant_first\": \"Najrelevantnejšie ako prvé\"\n    },\n    \"change_layout\": \"Zmena rozloženia\",\n    \"archive\": \"Archivovať\",\n    \"favorite\": \"Pridať medzi obľúbené\",\n    \"unselect_all\": \"Zrušiť výber\",\n    \"save\": \"Uložiť\",\n    \"add_to_list\": \"Pridať do zoznamu\",\n    \"add\": \"Pridať\",\n    \"refresh\": \"Obnoviť\",\n    \"unarchive\": \"Obnoviť z archívu\",\n    \"download_full_page_archive\": \"Stiahnuť archív celej stránky\",\n    \"recrawl\": \"Znova prehľadať\",\n    \"fetch_now\": \"Načítať teraz\",\n    \"summarize_with_ai\": \"Zhrnúť pomocou AI\",\n    \"open_editor\": \"Otvoriť editor\",\n    \"toggle_show_archived\": \"Zobraziť archivované\",\n    \"confirm\": \"Potvrdiť\",\n    \"regenerate\": \"Obnoviť\",\n    \"load_more\": \"Načítať viac\",\n    \"edit_notes\": \"Upraviť poznámky\",\n    \"preserve_as_pdf\": \"Uložiť ako PDF\",\n    \"offline_copies\": \"Offline kópie\",\n    \"preserve_offline_archive\": \"Uchovaj offline archív\",\n    \"download_full_page_archive_file\": \"Stiahni archivačný súbor\",\n    \"download_pdf_file\": \"Stiahni PDF súbor\",\n    \"remove\": \"Odstráň\",\n    \"more\": \"Viac\",\n    \"replace_banner\": \"Nahraď banner\",\n    \"add_banner\": \"Pridaj banner\",\n    \"download\": \"Sťahovanie\"\n  },\n  \"lists\": {\n    \"favourites\": \"Obľúbené\",\n    \"edit_list\": \"Upraviť zoznam\",\n    \"all_lists\": \"Všetky zoznamy\",\n    \"new_list\": \"Nový zoznam\",\n    \"new_nested_list\": \"Nový podzoznam\",\n    \"parent_list\": \"Nadradený zoznam\",\n    \"list_type\": \"Typ zoznamu\",\n    \"manual_list\": \"Manuálny zoznam\",\n    \"smart_list\": \"Automatický zoznam\",\n    \"search_query\": \"Vyhľadávací reťazec\",\n    \"no_parent\": \"Žiadny nadradený zoznam\",\n    \"search_query_help\": \"Zistite viac o syntaxi vyhľadávacích reťazcov.\",\n    \"merge_list\": \"Zlúčiť zoznam\",\n    \"destination_list\": \"Cieľový zoznam\",\n    \"delete_after_merge\": \"Po zlúčení odstrániť pôvodný zoznam\",\n    \"no_destination\": \"Žiadny cieľ\",\n    \"description\": \"Popis (voliteľný)\",\n    \"rss\": {\n      \"title\": \"RSS kanál\",\n      \"description\": \"Povoliť RSS kanál pre tento zoznam\",\n      \"feed_url\": \"URL RSS kanála\"\n    },\n    \"public_list\": {\n      \"title\": \"Verejný zoznam\",\n      \"description\": \"Povoliť ostatným zobraziť tento zoznam\",\n      \"share_link\": \"Zdieľať odkaz\"\n    },\n    \"share_list\": \"Zdieľať zoznam\",\n    \"delete_list\": {\n      \"title\": \"Odstrániť zoznam\",\n      \"description\": \"Odstránením zoznamu sa neodstránia žiadne záložky v tomto zozname.\",\n      \"delete_children\": \"Odstrániť podriadené zoznamy (rekurzívne)\",\n      \"delete_children_description\": \"Ak nie je zaškrtnuté, všetky priame podriadené zoznamy sa stanú koreňovými zoznamami\"\n    },\n    \"shared\": \"Zdieľané\",\n    \"collaborators\": {\n      \"manage\": \"Spravuj spolupracovníkov\",\n      \"view\": \"Zobraziť spolupracovníkov\",\n      \"collaborators\": \"Spolupracovníci\",\n      \"add\": \"Pridať spolupracovníka\",\n      \"current\": \"Súčasní spolupracovníci\",\n      \"enter_email\": \"Zadajte e-mailovú adresu\",\n      \"please_enter_email\": \"Zadajte e-mailovú adresu\",\n      \"added_successfully\": \"Spolupracovník úspešne pridaný\",\n      \"failed_to_add\": \"Nepodarilo sa pridať spolupracovníka\",\n      \"removed\": \"Spolupracovník odstránený\",\n      \"failed_to_remove\": \"Nepodarilo sa odstrániť spolupracovníka\",\n      \"role_updated\": \"Rola aktualizovaná\",\n      \"failed_to_update_role\": \"Nepodarilo sa aktualizovať rolu\",\n      \"viewer\": \"Divák\",\n      \"editor\": \"Editor\",\n      \"owner\": \"Vlastník\",\n      \"viewer_description\": \"Môže si prezerať záložky v zozname\",\n      \"editor_description\": \"Môže pridávať a odstraňovať záložky\",\n      \"no_collaborators\": \"Zatiaľ žiadni spolupracovníci. Ak chceš začať spolupracovať, niekoho pridaj!\",\n      \"no_collaborators_readonly\": \"Pre tento zoznam žiadni spolupracovníci.\",\n      \"people_with_access\": \"Ľudia, ktorí majú prístup k tomuto zoznamu\",\n      \"add_or_remove\": \"Pridajte alebo odstráňte ľudí, ktorí majú prístup k tomuto zoznamu\",\n      \"invitation_sent\": \"Pozvánka úspešne odoslaná\",\n      \"invitation_revoked\": \"Pozvánka zrušená\",\n      \"failed_to_revoke\": \"Nepodarilo sa zrušiť pozvánku\",\n      \"pending\": \"Čaká sa\",\n      \"revoke\": \"Zrušiť\",\n      \"declined\": \"Odmietnuté\"\n    },\n    \"leave_list\": {\n      \"title\": \"Opustiť zoznam\",\n      \"confirm_message\": \"Určite chceš odísť z {{icon}} {{name}}?\",\n      \"warning\": \"Už nebudeš môcť prezerať ani pristupovať k záložkám v tomto zozname. Vlastník zoznamu ťa môže znova pridať, ak to bude potrebné.\",\n      \"action\": \"Opustiť zoznam\",\n      \"success\": \"Opustil/a si \\\"{{icon}} {{name}}\\\"\"\n    },\n    \"invitations\": {\n      \"pending\": \"Pozvánky čakajúce na vybavenie\",\n      \"description\": \"Skontroluj a reaguj na pozvánky na spoluprácu v zoznamoch\",\n      \"invited_by\": \"Pozval/a\",\n      \"accept\": \"Prijať\",\n      \"decline\": \"Odmietnuť\",\n      \"accepted\": \"Pozvánka prijatá\",\n      \"declined\": \"Pozvánka odmietnutá\",\n      \"failed_to_accept\": \"Nepodarilo sa prijať pozvánku\",\n      \"failed_to_decline\": \"Nepodarilo sa odmietnuť pozvánku\"\n    },\n    \"shared_lists\": \"Zdieľané zoznamy\"\n  },\n  \"settings\": {\n    \"import\": {\n      \"import_bookmarks_from_omnivore_export\": \"Importovať záložky z Omnivore exportu\",\n      \"import_export_bookmarks\": \"Importovať / exportovať záložky\",\n      \"import_bookmarks_from_html_file\": \"Importovať záložky z HTML súboru\",\n      \"import_bookmarks_from_pocket_export\": \"Importovať záložky z Pocket exportu\",\n      \"import_bookmarks_from_matter_export\": \"Importovať záložky z Matter exportu\",\n      \"import_bookmarks_from_linkwarden_export\": \"Importovať záložky z Linkwarden exportu\",\n      \"import_bookmarks_from_karakeep_export\": \"Importovať záložky z Karakeep exportu\",\n      \"export_links_and_notes\": \"Exportovať odkazy a poznámky\",\n      \"imported_bookmarks\": \"Importovať záložky\",\n      \"import_export\": \"Importovať / exportovať\",\n      \"import_bookmarks_from_tab_session_manager_export\": \"Importovať záložky zo správcu relácií kariet\",\n      \"import_bookmarks_from_mymind_export\": \"Importovať záložky z mymind exportu\",\n      \"import_bookmarks_from_instapaper_export\": \"Importovať záložky z exportu Instapaper\"\n    },\n    \"back_to_app\": \"Naspäť do aplikácie\",\n    \"user_settings\": \"Používateľské nastavenia\",\n    \"info\": {\n      \"basic_details\": \"Základné údaje\",\n      \"change_password\": \"Zmena hesla\",\n      \"new_password\": \"Nové heslo\",\n      \"confirm_new_password\": \"Potvrdiť nové heslo\",\n      \"current_password\": \"Súčasné heslo\",\n      \"options\": \"Možnosti\",\n      \"interface_lang\": \"Jazyk rozhrania\",\n      \"user_info\": \"Informácie o používateľovi\",\n      \"user_settings\": {\n        \"user_settings_updated\": \"Používateľské nastavenia boli aktualizované!\",\n        \"bookmark_click_action\": {\n          \"title\": \"Akcia po kliknutí na záložku\",\n          \"open_external_url\": \"Otvoriť pôvodnú URL\",\n          \"open_bookmark_details\": \"Otvoriť podrobnosti záložky\"\n        },\n        \"archive_display_behaviour\": {\n          \"title\": \"Archivované záložky\",\n          \"hide\": \"Skryť archivované záložky v tagoch a zoznamoch\",\n          \"show\": \"Zobraziť archivované záložky v tagoch a zoznamoch\"\n        }\n      },\n      \"reader_settings\": {\n        \"local_overrides_title\": \"Sú aktívne nastavenia špecifické pre zariadenie\",\n        \"using_default\": \"Používa sa predvolené nastavenie pre klienta\",\n        \"clear_override_hint\": \"Vymažte prepísanie zariadenia a použite globálne nastavenie ({{value}})\",\n        \"font_size\": \"Veľkosť písma\",\n        \"font_family\": \"Rodina písma\",\n        \"preview_inline\": \"(Náhľad)\",\n        \"tooltip_preview\": \"Neuložené zmeny ukážky\",\n        \"save_to_all_devices\": \"Všetky zariadenia\",\n        \"tooltip_local\": \"Nastavenia zariadenia sa líšia od globálnych\",\n        \"reset_preview\": \"Resetovať ukážku\",\n        \"mono\": \"Neproporcionálne\",\n        \"line_height\": \"Výška riadku\",\n        \"tooltip_default\": \"Nastavenia čítania\",\n        \"title\": \"Nastavenia čítačky\",\n        \"serif\": \"Pätkové\",\n        \"preview\": \"Náhľad\",\n        \"not_set\": \"Nenastavené\",\n        \"clear_local_overrides\": \"Vymazať nastavenia zariadenia\",\n        \"preview_text\": \"The quick brown fox jumps over the lazy dog. Takto sa bude zobrazovať text v režime čítačky.\",\n        \"local_overrides_cleared\": \"Nastavenia špecifické pre zariadenie boli vymazané\",\n        \"local_overrides_description\": \"Toto zariadenie má nastavenia čítačky, ktoré sa líšia od tvojich globálnych predvolených nastavení:\",\n        \"clear_defaults\": \"Vymazať všetky predvolené\",\n        \"description\": \"Konfigurácia predvolených nastavení textu pre zobrazenie čítačky. Tieto nastavenia sa synchronizujú medzi všetkými tvojimi zariadeniami.\",\n        \"defaults_cleared\": \"Predvolené nastavenia čítačky boli vymazané\",\n        \"save_hint\": \"Uložte nastavenia iba pre toto zariadenie alebo ich synchronizujte na všetkých zariadeniach\",\n        \"save_as_default\": \"Uložiť ako predvolené\",\n        \"save_to_device\": \"Toto zariadenie\",\n        \"sans\": \"Bez pätiek\",\n        \"tooltip_preview_and_local\": \"Neuložené zmeny ukážky; nastavenia zariadenia sa líšia od globálnych\",\n        \"adjust_hint\": \"Upravte nastavenia vyššie, aby ste si prezreli zmeny\"\n      },\n      \"avatar\": {\n        \"upload\": \"Nahrať avatara\",\n        \"change\": \"Zmeniť avatara\",\n        \"remove_confirm_title\": \"Odstrániť avatara?\",\n        \"updated\": \"Avatar aktualizovaný\",\n        \"removed\": \"Avatar odstránený\",\n        \"description\": \"Nahraj štvorcový obrázok, ktorý sa použije ako tvoj avatar.\",\n        \"remove_confirm_description\": \"Týmto sa vymaže tvoja aktuálna profilová fotka.\",\n        \"title\": \"Profilová fotka\",\n        \"remove\": \"Odstrániť avatara\"\n      }\n    },\n    \"ai\": {\n      \"ai_settings\": \"Nastavenia AI\",\n      \"tagging_rules\": \"Pravidlá označovania\",\n      \"tagging_rule_description\": \"Výzvy, ktoré tu pridáte, budú zahrnuté ako pravidlá pre model počas generovania značiek. Konečné výzvy si môžete pozrieť v sekcii ukážky výziev.\",\n      \"prompt_preview\": \"Náhľad výzvy\",\n      \"text_prompt\": \"Textová výzva\",\n      \"all_tagging\": \"Všetko označené\",\n      \"text_tagging\": \"Označovanie textu\",\n      \"image_tagging\": \"Označovanie obrázkov\",\n      \"summarization\": \"Zhrnutie\",\n      \"images_prompt\": \"Výzva obrázka\",\n      \"summarization_prompt\": \"Výzva na sumarizáciu\",\n      \"tag_style\": \"Štýl tagov\",\n      \"auto_summarization_description\": \"Automaticky generujte zhrnutia pre vaše záložky pomocou AI.\",\n      \"auto_tagging\": \"Automatické označovanie štítkami\",\n      \"titlecase_spaces\": \"Veľké začiatočné písmená s medzerami\",\n      \"lowercase_underscores\": \"Malé písmená s podčiarkovníkmi\",\n      \"inference_language\": \"Jazyk inferencie\",\n      \"titlecase_hyphens\": \"Veľké začiatočné písmená s pomlčkami\",\n      \"lowercase_hyphens\": \"Malé písmená s pomlčkami\",\n      \"lowercase_spaces\": \"Malé písmená s medzerami\",\n      \"inference_language_description\": \"Vyber jazyk pre tagy a súhrny generované AI.\",\n      \"tag_style_description\": \"Vyber si, ako majú byť formátované automaticky generované tagy.\",\n      \"auto_tagging_description\": \"Automaticky generujte štítky pre vaše záložky pomocou AI.\",\n      \"camelCase\": \"camelCase\",\n      \"auto_summarization\": \"Automatické zhrnutie\",\n      \"no_preference\": \"Žiadna preferencia\",\n      \"curated_tags\": \"Vybrané štítky\",\n      \"curated_tags_description\": \"Voliteľne obmedzte označovanie pomocou umelej inteligencie iba na použitie štítkov z tohto zoznamu. Ak nie sú vybraté žiadne štítky, umelá inteligencia generuje štítky voľne.\",\n      \"curated_tags_updated\": \"Vybrané štítky boli úspešne aktualizované!\",\n      \"curated_tags_update_failed\": \"Nepodarilo sa aktualizovať vybrané štítky\"\n    },\n    \"webhooks\": {\n      \"add_auth_token\": \"Pridať autorizačný token\",\n      \"edit_auth_token\": \"Upraviť autorizačný token\",\n      \"delete_webhook\": \"Odstrániť webhook\",\n      \"edit_webhook\": \"Upraviť webhook\",\n      \"webhook_url\": \"URL webhooku\",\n      \"auth_token\": \"Autorizačný token\",\n      \"create_webhook\": \"Vytvoriť webhook\",\n      \"delete_webhook_confirmation\": \"Naozaj chcete odstrániť tento webhook?\",\n      \"description\": \"Webhooks môžeš použiť na spustenie akcií, keď sú záložky vytvorené, zmenené alebo prehľadané.\",\n      \"webhooks\": \"Webhooks\",\n      \"events\": {\n        \"crawled\": \"Prehľadané\",\n        \"created\": \"Vytvorené\",\n        \"edited\": \"Upravené\",\n        \"title\": \"Udalosti\"\n      }\n    },\n    \"api_keys\": {\n      \"api_keys\": \"API kľúče\",\n      \"new_api_key\": \"Nový API kľúč\",\n      \"key_success_please_copy\": \"Prosím, skopírujte si kľúč a uložte ho na bezpečné miesto. Keď zatvoríte dialógové okno, už k nemu nebudete mať prístup.\",\n      \"new_api_key_desc\": \"Daj svojmu API kľúču jedinečný názov\",\n      \"key_success\": \"Kľúč bol úspešne vytvorený\",\n      \"regenerate_api_key\": \"Obnoviť API kľúč\",\n      \"key_regenerated\": \"Kľúč bol úspešne obnovený\",\n      \"key_regenerated_please_copy\": \"Prosím, skopírujte si nový kľúč a uložte ho na bezpečné miesto. Starý kľúč bol zrušený a už nebude fungovať.\",\n      \"regenerate_warning\": \"Ste si istý, že chcete obnoviť API kľúč „{{name}}“?\",\n      \"regenerate_confirmation\": \"Toto zruší aktuálny kľúč a vygeneruje nový. Všetky aplikácie, ktoré používajú aktuálny kľúč, prestanú fungovať.\"\n    },\n    \"manage_assets\": {\n      \"asset_link\": \"Odkaz na majetok\",\n      \"delete_asset\": \"Odstrániť aktívum\",\n      \"delete_asset_confirmation\": \"Naozaj chceš vymazať toto aktívum?\",\n      \"bookmark_link\": \"Odkaz na záložku\",\n      \"manage_assets\": \"Spravovať aktíva\",\n      \"no_assets\": \"Zatiaľ nemáš žiadne aktíva.\",\n      \"asset_type\": \"Typ aktíva\"\n    },\n    \"feeds\": {\n      \"rss_subscriptions\": \"Odbery RSS\",\n      \"add_a_subscription\": \"Pridať odber\",\n      \"feed_enabled\": \"RSS kanál je zapnutý\",\n      \"feed_disabled\": \"RSS kanál je vypnutý\"\n    },\n    \"broken_links\": {\n      \"broken_links\": \"Nefunkčné odkazy\",\n      \"last_crawled_at\": \"Naposledy prehľadané\",\n      \"crawling_status\": \"Stav prehľadávania\",\n      \"crawling_failed\": \"Prehľadávanie zlyhalo\"\n    },\n    \"rules\": {\n      \"enter_rule_name\": \"Zadajte názov pravidla\",\n      \"describe_what_this_rule_does\": \"Popíš, čo toto pravidlo robí\",\n      \"rule_has_been_created\": \"Pravidlo bolo vytvorené!\",\n      \"conditions_types\": {\n        \"has_tag\": \"Má značku\",\n        \"is_favourited\": \"Je obľúbená\",\n        \"is_archived\": \"Archivované\",\n        \"and\": \"Platia všetky nasledujúce podmienky\",\n        \"or\": \"Ak platí niektoré z nasledujúcich\",\n        \"always\": \"Vždy\",\n        \"url_contains\": \"URL obsahuje\",\n        \"imported_from_feed\": \"Importované zo zdroja\",\n        \"bookmark_type_is\": \"Typ záložky je\",\n        \"url_does_not_contain\": \"URL Neobsahuje\",\n        \"title_contains\": \"Názov obsahuje\",\n        \"title_does_not_contain\": \"Názov neobsahuje\"\n      },\n      \"actions_types\": {\n        \"add_tag\": \"Pridať štítok\",\n        \"remove_tag\": \"Odstrániť značku\",\n        \"add_to_list\": \"Pridať do zoznamu\",\n        \"remove_from_list\": \"Odstrániť zo zoznamu\",\n        \"download_full_page_archive\": \"Stiahnuť archív celej stránky\",\n        \"favourite_bookmark\": \"Obľúbená záložka\",\n        \"archive_bookmark\": \"Archivovať záložku\"\n      },\n      \"rules\": \"Systém pravidiel\",\n      \"rule_name\": \"Názov pravidla\",\n      \"description\": \"Pravidlá môžeš použiť na spustenie akcií, keď sa spustí udalosť.\",\n      \"ceate_rule\": \"Vytvoriť pravidlo\",\n      \"edit_rule\": \"Upraviť pravidlo\",\n      \"save_rule\": \"Uložiť pravidlo\",\n      \"delete_rule\": \"Zmazať pravidlo\",\n      \"delete_rule_confirmation\": \"Naozaj chceš zmazať toto pravidlo?\",\n      \"whenever\": \"Kedykoľvek ...\",\n      \"if\": \"Ak ...\",\n      \"rule_has_been_updated\": \"Pravidlo bolo aktualizované!\",\n      \"rule_has_been_deleted\": \"Pravidlo bolo zmazané!\",\n      \"no_rules_created_yet\": \"Zatiaľ neboli vytvorené žiadne pravidlá\",\n      \"create_your_first_rule\": \"Vytvor si prvé pravidlo na automatizáciu tvojho pracovného postupu\",\n      \"events_types\": {\n        \"bookmark_added\": \"Záložka je pridaná\",\n        \"tag_added\": \"Tento štítok je pridaný do záložky\",\n        \"tag_removed\": \"Táto značka je odstránená zo záložky\",\n        \"added_to_list\": \"Záložka je pridaná do tohto zoznamu\",\n        \"removed_from_list\": \"Záložka je odstránená z tohto zoznamu\",\n        \"favourited\": \"Záložka je pridaná medzi obľúbené\",\n        \"archived\": \"Záložka je archivovaná\"\n      }\n    },\n    \"stats\": {\n      \"usage_statistics\": \"Štatistiky používania\",\n      \"insights_description\": \"Štatistiky o tvojich návykoch a zbierke záložiek\",\n      \"failed_to_load\": \"Nepodarilo sa načítať štatistiky\",\n      \"overview\": {\n        \"total_bookmarks\": \"Celkový počet záložiek\",\n        \"all_saved_items\": \"Všetky uložené položky\",\n        \"favorites\": \"Obľúbené\",\n        \"starred_bookmarks\": \"Označené záložky hviezdičkou\",\n        \"archived\": \"Archivované\",\n        \"archived_items\": \"Archivované položky\",\n        \"tags\": \"Značky\",\n        \"unique_tags_created\": \"Vytvorené jedinečné štítky\",\n        \"lists\": \"Zoznamy\",\n        \"bookmark_collections\": \"Zbierky záložiek\",\n        \"highlights\": \"Zvýraznenia\",\n        \"text_highlights\": \"Zvýraznenia textu\",\n        \"storage_used\": \"Použité úložisko\",\n        \"total_asset_storage\": \"Celkové úložisko aktív\",\n        \"this_month\": \"Tento mesiac\",\n        \"bookmarks_added\": \"Pridané záložky\"\n      },\n      \"bookmark_types\": {\n        \"title\": \"Typy záložiek\",\n        \"links\": \"Odkazy\",\n        \"text_notes\": \"Textové poznámky\",\n        \"assets\": \"Aktíva\"\n      },\n      \"recent_activity\": {\n        \"title\": \"Nedávna aktivita\",\n        \"this_week\": \"Tento týždeň\",\n        \"this_month\": \"Tento mesiac\",\n        \"this_year\": \"Tento rok\"\n      },\n      \"top_domains\": {\n        \"title\": \"Najlepšie domény\",\n        \"no_domains_found\": \"Nenašli sa žiadne domény\"\n      },\n      \"most_used_tags\": {\n        \"title\": \"Najpoužívanejšie štítky\",\n        \"no_tags_found\": \"Nenašli sa žiadne značky\"\n      },\n      \"activity_patterns\": {\n        \"activity_by_hour\": \"Aktivita podľa hodiny\",\n        \"activity_by_day\": \"Aktivita podľa dňa\"\n      },\n      \"storage_breakdown\": {\n        \"title\": \"Rozdelenie úložiska\"\n      },\n      \"bookmark_sources\": {\n        \"title\": \"Zdroje záložiek\",\n        \"empty\": \"Nie sú dostupné žiadne zdrojové dáta\"\n      }\n    },\n    \"subscription\": {\n      \"subscription\": \"Predplatné\",\n      \"manage_subscription\": \"Spravuj svoje predplatné a fakturačné údaje\",\n      \"current_plan\": \"Aktuálny plán\",\n      \"billing_period\": \"Fakturačné obdobie\",\n      \"paid_plan\": \"Platený plán\",\n      \"unlock_bigger_quota\": \"Odomkni väčšiu kvótu a podpor projekt\",\n      \"subscribe_now\": \"Prihlás sa na odber\",\n      \"manage_billing\": \"Spravovať fakturáciu\",\n      \"subscription_canceled\": \"Tvoje predplatné bolo zrušené a skončí {{date}}. Môžeš sa kedykoľvek znova prihlásiť na odber.\",\n      \"usage_quotas\": \"Využitie a kvóty\",\n      \"track_usage\": \"Sleduj aktuálne využitie v porovnaní s limitmi tvojho plánu\",\n      \"total_bookmarks_saved\": \"Celkový počet uložených záložiek\",\n      \"assets_file_storage\": \"Úložisko aktív a súborov\",\n      \"unlimited_usage\": \"Neobmedzené použitie\",\n      \"quota_limit_reached\": \"Bol dosiahnutý limit kvóty\",\n      \"approaching_quota_limit\": \"Blíži sa limit kvóty\",\n      \"loading_usage\": \"Načítavajú sa informácie o využití...\",\n      \"free\": \"Zadarmo\",\n      \"paid\": \"Platené\"\n    },\n    \"import_sessions\": {\n      \"title\": \"Importovať relácie\",\n      \"description\": \"Zobraz a spravuj svoje dávkové importy relácií. Relácie sa automaticky vytvárajú pri importovaní záložiek.\",\n      \"load_error\": \"Nepodarilo sa načítať importované relácie\",\n      \"no_sessions\": \"Zatiaľ žiadne importované relácie\",\n      \"no_sessions_detail\": \"Importované relácie sa tu automaticky zobrazia pri importovaní záložiek\",\n      \"created_at\": \"Vytvorené {{time}}\",\n      \"progress\": \"Priebeh\",\n      \"status\": {\n        \"pending\": \"Čaká sa\",\n        \"in_progress\": \"Prebieha\",\n        \"completed\": \"Dokončené\",\n        \"failed\": \"Neúspešné\",\n        \"processing\": \"Spracováva sa\",\n        \"staging\": \"Príprava\",\n        \"running\": \"Beží\",\n        \"paused\": \"Pozastavené\"\n      },\n      \"badges\": {\n        \"pending\": \"{{count}} čaká\",\n        \"processing\": \"{{count}} spracováva sa\",\n        \"completed\": \"{{count}} dokončených\",\n        \"failed\": \"{{count}} neúspešných\"\n      },\n      \"imported_to\": \"Importované do:\",\n      \"view_list\": \"Zobraziť zoznam\",\n      \"delete_dialog_title\": \"Odstrániť reláciu importu\",\n      \"delete_dialog_description\": \"Naozaj chceš vymazať \\\"{{name}}\\\"? Táto akcia sa nedá vrátiť späť. Samotné záložky nebudú vymazané.\",\n      \"delete_session\": \"Zmazať reláciu\",\n      \"pause_session\": \"Pozastaviť\",\n      \"resume_session\": \"Obnoviť\",\n      \"view_details\": \"Zobraziť podrobnosti\",\n      \"detail\": {\n        \"page_title\": \"Zobraziť podrobnosti importu relácie\",\n        \"back_to_import\": \"Späť na Import\",\n        \"filter_all\": \"Všetky\",\n        \"filter_accepted\": \"Prijaté\",\n        \"filter_rejected\": \"Odmietnuté\",\n        \"filter_duplicates\": \"Duplikáty\",\n        \"filter_pending\": \"Čakajúce\",\n        \"table_title\": \"Názov / URL\",\n        \"table_type\": \"Typ\",\n        \"table_result\": \"Výsledok\",\n        \"table_reason\": \"Dôvod\",\n        \"table_bookmark\": \"Záložka\",\n        \"result_accepted\": \"Prijaté\",\n        \"result_rejected\": \"Odmietnuté\",\n        \"result_skipped_duplicate\": \"Duplikát\",\n        \"result_pending\": \"Čakajúce\",\n        \"result_processing\": \"Spracováva sa\",\n        \"no_results\": \"Pre tento filter sa nenašli žiadne výsledky.\",\n        \"view_bookmark\": \"Zobraziť záložku\",\n        \"load_more\": \"Načítať viac\",\n        \"no_title\": \"Bez názvu\"\n      }\n    },\n    \"backups\": {\n      \"backups\": \"Zálohy\",\n      \"page_title\": \"Zálohy\",\n      \"page_description\": \"Automaticky vytváraj a spravuj zálohy svojich záložiek. Zálohy sú komprimované a bezpečne uložené.\",\n      \"configuration\": {\n        \"title\": \"Konfigurácia zálohovania\",\n        \"enable_automatic_backups\": \"Povoliť automatické zálohovanie\",\n        \"enable_automatic_backups_description\": \"Automaticky vytvárať zálohy tvojich záložiek\",\n        \"backup_frequency\": \"Frekvencia zálohovania\",\n        \"backup_frequency_description\": \"Ako často sa majú zálohy vytvárať\",\n        \"retention_period\": \"Doba uchovávania (dni)\",\n        \"retention_period_description\": \"Koľko dní uchovávať zálohy predtým, ako ich vymažeš\",\n        \"frequency\": {\n          \"daily\": \"Denne\",\n          \"weekly\": \"Týždenne\"\n        },\n        \"select_frequency\": \"Vyber frekvenciu\",\n        \"save_settings\": \"Uložiť nastavenia\"\n      },\n      \"list\": {\n        \"title\": \"Tvoje zálohy\",\n        \"create_backup_now\": \"Vytvoriť zálohu teraz\",\n        \"no_backups\": \"Zatiaľ nemáš žiadne zálohy. Povoľ automatické zálohovanie alebo vytvor manuálne.\",\n        \"table\": {\n          \"created_at\": \"Vytvorené dňa\",\n          \"bookmarks\": \"Záložky\",\n          \"size\": \"Veľkosť\",\n          \"status\": \"Stav\",\n          \"actions\": \"Akcie\"\n        },\n        \"status\": {\n          \"success\": \"Úspech\",\n          \"failed\": \"Neúspešné\",\n          \"pending\": \"Čaká sa\"\n        },\n        \"actions\": {\n          \"download_backup\": \"Stiahnuť zálohu\",\n          \"delete_backup\": \"Odstrániť zálohu\"\n        }\n      },\n      \"dialogs\": {\n        \"delete_backup_title\": \"Odstrániť zálohu?\",\n        \"delete_backup_description\": \"Určite chceš odstrániť túto zálohu? Túto akciu nie je možné vrátiť späť.\"\n      },\n      \"toasts\": {\n        \"backup_queued\": \"Úloha zálohovania bola zaradená do frontu! Bude čoskoro spracovaná.\",\n        \"backup_deleted\": \"Záloha bola odstránená!\"\n      }\n    }\n  },\n  \"search\": {\n    \"or\": \"Alebo\",\n    \"is_favorited\": \"Je obľúbené\",\n    \"is_archived\": \"Je archivované\",\n    \"is_not_archived\": \"Nie je archivované\",\n    \"has_any_tag\": \"Má nejakú značku\",\n    \"is_not_in_any_list\": \"Nie je v žiadnom zozname\",\n    \"created_on_or_after\": \"Vytvorené dňa alebo neskôr\",\n    \"not_created_on_or_after\": \"Nevytvorené dňa alebo neskôr\",\n    \"created_on_or_before\": \"Vytvorené dňa alebo pred\",\n    \"not_created_on_or_before\": \"Nevytvorené dňa alebo pred\",\n    \"url_contains\": \"URL obsahuje\",\n    \"url_does_not_contain\": \"URL neobsahuje\",\n    \"is_in_list\": \"Je v zozname\",\n    \"is_not_in_list\": \"Nie je v zozname\",\n    \"has_tag\": \"Má značku\",\n    \"type_is\": \"Typ je\",\n    \"has_no_tags\": \"Nemá žiadnu značku\",\n    \"type_is_not\": \"Typ nie je\",\n    \"and\": \"A\",\n    \"is_not_favorited\": \"Nie je obľúbené\",\n    \"full_text_search\": \"Fulltextové vyhľadávanie\",\n    \"does_not_have_tag\": \"Nemá značku\",\n    \"is_in_any_list\": \"Je v nejakom zozname\",\n    \"is_from_feed\": \"Je z RSS kanála\",\n    \"is_not_from_feed\": \"Nie je z RSS kanála\",\n    \"created_within\": \"Vytvorené v priebehu\",\n    \"created_earlier_than\": \"Vytvorené skôr ako\",\n    \"day_s\": \" Deň(dni)\",\n    \"week_s\": \" Týždeň(ne)\",\n    \"month_s\": \" Mesiac(e)\",\n    \"year_s\": \" Rok(y)\",\n    \"day_s_ago\": \" Deň(dni) dozadu\",\n    \"week_s_ago\": \" Týždeň(ne) dozadu\",\n    \"month_s_ago\": \" Mesiac(e) dozadu\",\n    \"year_s_ago\": \" Rok(y) dozadu\",\n    \"history\": \"Nedávne vyhľadávania\",\n    \"title_contains\": \"Názov obsahuje\",\n    \"title_does_not_contain\": \"Názov neobsahuje\",\n    \"is_broken_link\": \"Má nefunkčný odkaz\",\n    \"tags\": \"Značky\",\n    \"no_suggestions\": \"Žiadne návrhy\",\n    \"filters\": \"Filtre\",\n    \"is_not_broken_link\": \"Má funkčný odkaz\",\n    \"lists\": \"Zoznamy\",\n    \"feeds\": \"Kanály\",\n    \"is_from_source\": \"Zdroj je\",\n    \"is_not_from_source\": \"Zdroj nie je\"\n  },\n  \"layouts\": {\n    \"masonry\": \"Dlaždice\",\n    \"grid\": \"Mriežka\",\n    \"list\": \"Zoznam\",\n    \"compact\": \"Kompaktné\"\n  },\n  \"admin\": {\n    \"actions\": {\n      \"reindex_all_bookmarks\": \"Preindexovať všetky záložky\",\n      \"compact_assets\": \"Optimalizovať dáta\",\n      \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Pregenerovať AI značky len pre zlyhané záložky\",\n      \"regenerate_ai_tags_for_all_bookmarks\": \"Pregenerovať AI značky pre všetky záložky\",\n      \"without_inference\": \"Bez odvodzovania\",\n      \"reprocess_assets_fix_mode\": \"Spracovať aktíva znova (režim opravy)\",\n      \"recrawl_all_links\": \"Znova prehľadať všetky odkazy\",\n      \"recrawl_failed_links_only\": \"Znova prehľadať iba nefunkčné odkazy\",\n      \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Znova vygenerovať AI súhrny iba pre záložky, ktoré zlyhali\",\n      \"regenerate_ai_summaries_for_all_bookmarks\": \"Znova vygenerovať AI súhrny pre všetky záložky\"\n    },\n    \"users_list\": {\n      \"users_list\": \"Zoznam používateľov\",\n      \"reset_password\": \"Resetovať heslo\",\n      \"num_bookmarks\": \"Počet záložiek\",\n      \"local_user\": \"Lokálny používateľ\",\n      \"asset_sizes\": \"Veľkosť dát\",\n      \"change_role\": \"Zmeniť rolu\",\n      \"delete_user\": \"Odstrániť používateľa\",\n      \"confirm_password\": \"Potvrdiť heslo\",\n      \"create_user\": \"Vytvoriť používateľa\",\n      \"delete_user_confirm_description\": \"Naozaj chceš vymazať používateľa „{{name}}“?\",\n      \"unlimited\": \"Neobmedzené\"\n    },\n    \"admin_settings\": \"Nastavenia správcu\",\n    \"background_jobs\": {\n      \"background_jobs\": \"Úlohy na pozadí\",\n      \"queued\": \"Zaradené do frontu\",\n      \"video_jobs\": \"Úlohy sťahovania videa\",\n      \"crawler_jobs\": \"Úlohy prehľadávača\",\n      \"indexing_jobs\": \"Úlohy indexovania\",\n      \"inference_jobs\": \"Úlohy odvodzovania\",\n      \"tidy_assets_jobs\": \"Úlohy na upratanie aktív\",\n      \"job\": \"Úloha\",\n      \"pending\": \"Čaká sa\",\n      \"failed\": \"Neúspešné\",\n      \"webhook_jobs\": \"Úlohy Webhook\",\n      \"asset_preprocessing_jobs\": \"Úlohy predbežného spracovania aktív\",\n      \"feed_jobs\": \"Úlohy RSS kanála\",\n      \"jobs\": {\n        \"crawler\": {\n          \"title\": \"Úlohy pre prehľadávače\",\n          \"description\": \"Webové prehľadávanie a extrakcia obsahu z URL\"\n        },\n        \"inference\": {\n          \"title\": \"Úlohy odvodzovania\",\n          \"description\": \"Označovanie obsahu pomocou AI a vytváranie súhrnov\"\n        },\n        \"indexing\": {\n          \"title\": \"Úlohy indexovania\",\n          \"description\": \"Aktualizácie indexu vyhľadávania\"\n        },\n        \"asset_preprocessing\": {\n          \"title\": \"Úlohy predbežného spracovania zdrojov\",\n          \"description\": \"Predbežné spracovanie obrázkov a dokumentov (snímky obrazovky, extrakcia textu atď.)\"\n        },\n        \"tidy_assets\": {\n          \"title\": \"Úlohy na upratovanie zdrojov\",\n          \"description\": \"Čistenie zdrojov a optimalizácia úložiska\"\n        },\n        \"video\": {\n          \"title\": \"Úlohy na sťahovanie videí\",\n          \"description\": \"Extrakcia a sťahovanie videí\"\n        },\n        \"webhook\": {\n          \"title\": \"Úlohy Webhook\",\n          \"description\": \"Externé upozornenia Webhook\"\n        },\n        \"feed\": {\n          \"title\": \"Úlohy RSS Feed\",\n          \"description\": \"Spracovanie RSS kanálov a aktualizácie obsahu\"\n        },\n        \"admin_maintenance\": {\n          \"title\": \"Úlohy údržby správcu\",\n          \"description\": \"Administratívne čistenie a údržba aktív\"\n        }\n      },\n      \"monitor_and_manage\": \"Monitorujte a spravujte fronty úloh na pozadí a systémové úlohy spracovania\",\n      \"active\": \"Aktívne\",\n      \"available_actions\": \"Dostupné akcie\",\n      \"status\": {\n        \"title\": \"Porozumenie stavom úloh\",\n        \"queued\": {\n          \"title\": \"Zaradené do frontu\",\n          \"description\": \"Úlohy čakajúce v rade na spracovanie. Spustia sa automaticky, keď budú dostupné zdroje.\"\n        },\n        \"unprocessed\": {\n          \"title\": \"Nespracované\",\n          \"description\": \"Záložky, ktoré ešte neboli spracované. S najväčšou pravdepodobnosťou sú už zaradené do frontu na spracovanie, ak nie, možno ich budete musieť znova zaradiť do frontu manuálne.\"\n        },\n        \"failed\": {\n          \"title\": \"Neúspešné\",\n          \"description\": \"Záložky, pri ktorých sa počas spracovania vyskytli chyby. Môžu vyžadovať manuálnu pozornosť.\"\n        }\n      },\n      \"actions\": {\n        \"recrawl_failed_links_only\": \"Znova preliezť len neúspešné odkazy\",\n        \"recrawl_all_links\": \"Znova preliezť všetky odkazy\",\n        \"without_inference\": \"Bez odvodzovania\",\n        \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Znovu vygenerovať značky AI len pre neúspešné záložky\",\n        \"regenerate_ai_tags_for_all_bookmarks\": \"Znovu vygenerovať značky AI pre všetky záložky\",\n        \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Znovu vygenerovať súhrny AI len pre neúspešné záložky\",\n        \"regenerate_ai_summaries_for_all_bookmarks\": \"Znovu vygenerovať súhrny AI pre všetky záložky\",\n        \"reindex_all_bookmarks\": \"Preindexovať všetky záložky\",\n        \"clean_assets\": \"Vyčistiť nepotrebné prostriedky a znova synchronizovať metadáta\",\n        \"reprocess_assets_fix_mode\": \"Znova spracovať nespracované prostriedky\",\n        \"migrate_large_link_html_content\": \"Presuň rozsiahly vložený HTML obsah do assetov.\",\n        \"recrawl_pending_links_only\": \"Znova prehľadať len čakajúce odkazy\",\n        \"regenerate_ai_tags_for_pending_bookmarks_only\": \"Znova vygenerovať AI značky len pre čakajúce záložky\",\n        \"regenerate_ai_summaries_for_pending_bookmarks_only\": \"Znova vygenerovať AI súhrny len pre čakajúce záložky\"\n      }\n    },\n    \"server_stats\": {\n      \"server_version\": \"Verzia servera\",\n      \"server_stats\": \"Štatistiky servera\",\n      \"total_users\": \"Celkový počet používateľov\",\n      \"total_bookmarks\": \"Celkový počet záložiek\"\n    },\n    \"service_connections\": {\n      \"title\": \"Pripojenia služieb\",\n      \"description\": \"Monitoruj zdravie a pripojiteľnosť externých systémových závislostí\",\n      \"search_engine\": \"Vyhľadávač\",\n      \"browser\": \"Prehliadač\",\n      \"queue_system\": \"Systém front\",\n      \"status\": {\n        \"not_configured\": \"Nenakonfigurované\",\n        \"connected\": \"Pripojené\",\n        \"disconnected\": \"Odpojené\"\n      }\n    },\n    \"admin_tools\": {\n      \"admin_tools\": \"Nástroje pre správcov\",\n      \"bookmark_debugger\": \"Ladička záložiek\",\n      \"bookmark_id\": \"ID záložky\",\n      \"bookmark_id_placeholder\": \"Zadaj ID záložky\",\n      \"lookup\": \"Vyhľadať\",\n      \"debug_info\": \"Ladiace informácie\",\n      \"basic_info\": \"Základné informácie\",\n      \"status\": \"Stav\",\n      \"content\": \"Obsah\",\n      \"html_preview\": \"Náhľad HTML (prvých 1000 znakov)\",\n      \"summary\": \"Súhrn\",\n      \"url\": \"URL\",\n      \"source_url\": \"Zdrojová URL\",\n      \"asset_type\": \"Typ aktíva\",\n      \"file_name\": \"Názov súboru\",\n      \"owner_user_id\": \"ID vlastníka používateľa\",\n      \"tagging_status\": \"Stav označovania\",\n      \"summarization_status\": \"Stav sumarizácie\",\n      \"crawl_status\": \"Stav prehľadávania\",\n      \"crawl_status_code\": \"Stavový kód HTTP\",\n      \"crawled_at\": \"Prehľadané dňa\",\n      \"recrawl\": \"Pre-crawl\",\n      \"reindex\": \"Preindexovať\",\n      \"retag\": \"Pretagovať\",\n      \"resummarize\": \"Prezhrnúť\",\n      \"bookmark_not_found\": \"Záložka sa nenašla\",\n      \"action_success\": \"Akcia úspešne dokončená\",\n      \"action_failed\": \"Akcia zlyhala\",\n      \"recrawl_queued\": \"Úloha precrawlovania bola zaradená do frontu\",\n      \"reindex_queued\": \"Úloha preindexovania bola zaradená do frontu\",\n      \"retag_queued\": \"Úloha pretagovania bola zaradená do frontu\",\n      \"resummarize_queued\": \"Úloha prezhrnutia bola zaradená do frontu\",\n      \"view\": \"Zobraziť\",\n      \"fetch_error\": \"Chyba pri načítavaní záložky\"\n    }\n  },\n  \"options\": {\n    \"dark_mode\": \"Tmavý motív\",\n    \"light_mode\": \"Svetlý motív\",\n    \"apps_extensions\": \"Aplikácie a rozšírenia\",\n    \"documentation\": \"Dokumentácia\",\n    \"follow_us_on_x\": \"Sleduj nás na X\"\n  },\n  \"tags\": {\n    \"all_tags\": \"Všetky značky\",\n    \"your_tags\": \"Vaše značky\",\n    \"ai_tags\": \"AI značky\",\n    \"your_tags_info\": \"Značky priradené aspoň raz aj vami\",\n    \"unused_tags\": \"Nepoužité značky\",\n    \"drag_and_drop_merging\": \"Zlúčenie potiahnutím\",\n    \"drag_and_drop_merging_info\": \"Potiahnite značku na inú značku pre ich zlúčenie\",\n    \"sort_by_name\": \"Zoradiť podľa názvu\",\n    \"delete_all_unused_tags\": \"Odstrániť všetky nepoužité značky\",\n    \"unused_tags_info\": \"Značky nepriradené k žiadnym záložkám\",\n    \"ai_tags_info\": \"Značky priradené iba automaticky (pomocou AI)\",\n    \"create_tag\": \"Vytvoriť značku\",\n    \"create_tag_description\": \"Vytvoriť novú značku bez pripojenia k žiadnej záložke\",\n    \"tag_name\": \"Názov značky\",\n    \"enter_tag_name\": \"Zadajte názov značky\",\n    \"sort_by_usage\": \"Zoradiť podľa používania\",\n    \"sort_by_relevance\": \"Zoradiť podľa relevantnosti\",\n    \"no_custom_tags\": \"Zatiaľ žiadne vlastné štítky\",\n    \"no_ai_tags\": \"Zatiaľ žiadne štítky AI\",\n    \"no_unused_tags\": \"Nemáte žiadne nepoužité štítky\",\n    \"no_unused_tags_match_your_search\": \"Žiadne nepoužité štítky nezodpovedajú vášmu vyhľadávaniu\",\n    \"no_tags_match_your_search\": \"Žiadne štítky nezodpovedajú vášmu vyhľadávaniu\",\n    \"search_placeholder\": \"Hľadať štítky…\",\n    \"search_or_create_placeholder\": \"Hľadať alebo vytvoriť štítky…\"\n  },\n  \"editor\": {\n    \"quickly_focus\": \"Na rýchle presunutie na toto pole môžete použiť skratku CTRL + E\",\n    \"multiple_urls_dialog_title\": \"Importovať zadané URL ako samostatné záložky?\",\n    \"import_as_text\": \"Importovať ako textovú záložku\",\n    \"import_as_separate_bookmarks\": \"Importovať ako samostatné záložky\",\n    \"placeholder\": \"Vložte odkaz, napíšte poznámku alebo sem vložte či potiahnite obrázok…\",\n    \"new_item\": \"NOVÁ POLOŽKA\",\n    \"text_toolbar\": {\n      \"italic\": \"Šikmé\",\n      \"underline\": \"Podčiarknuté\",\n      \"code\": \"Kód\",\n      \"highlight\": \"Zvýraznenie\",\n      \"align_left\": \"Zarovnanie vľavo\",\n      \"align_right\": \"Zarovnanie vpravo\",\n      \"markdown_shortcuts\": {\n        \"label\": \"Skratky syntaxe Markdown\",\n        \"heading\": {\n          \"label\": \"Nadpis\",\n          \"example\": \"# Nadpis1, ## Nadpis2, ### Nadpis3\"\n        },\n        \"bold\": {\n          \"label\": \"Tučné\",\n          \"example\": \"**text** or CTRL+b\"\n        },\n        \"italic\": {\n          \"label\": \"Šikmé\",\n          \"example\": \"*text* alebo _text_ alebo CTRL+i\"\n        },\n        \"blockquote\": {\n          \"label\": \"Bloková citácia\",\n          \"example\": \"> text\"\n        },\n        \"ordered_list\": {\n          \"label\": \"Zoradený zoznam\",\n          \"example\": \"1. Položka zoznamu\"\n        },\n        \"unordered_list\": {\n          \"label\": \"Neusporiadaný zoznam\",\n          \"example\": \"- Položka zoznamu\"\n        },\n        \"inline_code\": {\n          \"label\": \"Vložený kód\",\n          \"example\": \"`Kód`\"\n        },\n        \"block_code\": {\n          \"label\": \"Blokovať kód\",\n          \"example\": \"``` + medzera\"\n        }\n      },\n      \"align_center\": \"Zarovnanie na stred\",\n      \"bold\": \"Tučné\",\n      \"strikethrough\": \"Preškrtnuté\",\n      \"undo\": \"Späť\",\n      \"redo\": \"Znova\"\n    },\n    \"multiple_urls_dialog_desc\": \"Vstup obsahuje viaceré URL na samostatných riadkoch. Chcete ich importovať ako samostatné záložky?\",\n    \"disabled_submissions\": \"Odosielanie je vypnuté\",\n    \"placeholder_v2\": \"Vložte odkaz, napíšte poznámku alebo presuňte obrázok…\"\n  },\n  \"preview\": {\n    \"view_original\": \"Zobraziť originál\",\n    \"cached_content\": \"Obsah uložený vo vyrovnávacej pamäti\",\n    \"reader_view\": \"Zobrazenie čítačky\",\n    \"tabs\": {\n      \"content\": \"Obsah\",\n      \"details\": \"Podrobnosti\"\n    },\n    \"archive_info\": \"Archívy sa nemusia vykresľovať správne priamo, ak vyžadujú Javascript. Pre dosiahnutie najlepších výsledkov si ich <1>stiahni a otvor v prehliadači</1>.\",\n    \"fetch_error_title\": \"Obsah nie je k dispozícii\",\n    \"fetch_error_description\": \"Nepodarilo sa nám získať obsah pre tento odkaz. Stránka môže byť chránená, vyžadovať autentifikáciu alebo byť dočasne nedostupná.\",\n    \"crawling_in_progress\": \"Načítava sa obsah stránky…\",\n    \"continue_reading\": \"Pokračovať tam, kde ste prestali\",\n    \"continue_reading_percent\": \"Pokračovať tam, kde ste prestali ({{percent}} %)\",\n    \"continue_button\": \"Pokračovať\"\n  },\n  \"toasts\": {\n    \"bookmarks\": {\n      \"clipboard_copied\": \"Odkaz bol pridaný do schránky!\",\n      \"updated\": \"Záložka bola aktualizovaná!\",\n      \"delete_from_list\": \"Záložka bola odstránená zo zoznamu\",\n      \"deleted\": \"Záložka bola zmazaná!\",\n      \"refetch\": \"Opätovné načítanie bolo zaradené do frontu!\",\n      \"full_page_archive\": \"Bolo spustené vytváranie archívu celej stránky\",\n      \"preserve_pdf\": \"Ukladanie do PDF bolo spustené\",\n      \"update_banner\": \"Banner bol aktualizovaný!\",\n      \"uploading_banner\": \"Nahrávanie bannera...\"\n    },\n    \"lists\": {\n      \"updated\": \"Zoznam bol aktualizovaný!\",\n      \"created\": \"Zoznam bol vytvorený!\",\n      \"merged\": \"Zoznam bol zlúčený!\",\n      \"deleted\": \"Zoznam bol odstránený!\"\n    },\n    \"tags\": {\n      \"created\": \"Značka bola vytvorená!\",\n      \"failed_to_create\": \"Nepodarilo sa vytvoriť značku\"\n    }\n  },\n  \"cleanups\": {\n    \"cleanups\": \"Čistenie\",\n    \"duplicate_tags\": {\n      \"title\": \"Duplicitné štítky\",\n      \"merge_all_suggestions\": \"Zlúčiť všetky návrhy?\"\n    }\n  },\n  \"highlights\": {\n    \"no_highlights\": \"Zatiaľ nemáš žiadne zvýraznenia.\"\n  },\n  \"dialogs\": {\n    \"bookmarks\": {\n      \"delete_confirmation_description\": \"Naozaj chceš vymazať túto záložku?\",\n      \"delete_confirmation_title\": \"Odstrániť záložku?\"\n    }\n  },\n  \"banners\": {\n    \"no_bookmarks\": {\n      \"title\": \"Zatiaľ žiadne záložky\",\n      \"description\": \"Ukladaj si zaujímavé články, odkazy a stránky, aby si sa k nim neskôr rýchlo dostal.\"\n    }\n  },\n  \"bookmark_editor\": {\n    \"title\": \"Upraviť záložku\",\n    \"subtitle\": \"Urob zmeny v podrobnostiach záložky. Keď skončíš, klikni na uložiť.\",\n    \"author\": \"Autor\",\n    \"publisher\": \"Vydavateľ\",\n    \"date_published\": \"Dátum publikovania\",\n    \"pick_a_date\": \"Vyber dátum\",\n    \"save_changes\": \"Ulož zmeny\",\n    \"extracted_content\": \"Extrahovaný obsah\"\n  },\n  \"view_options\": {\n    \"title\": \"Možnosti zobrazenia\",\n    \"layout\": \"Rozloženie\",\n    \"columns\": \"Stĺpce\",\n    \"display_options\": \"Možnosti zobrazenia\",\n    \"show_note_previews\": \"Zobraziť poznámky\",\n    \"show_tags\": \"Zobraziť značky\",\n    \"show_title\": \"Zobraziť nadpis\",\n    \"image_options\": \"Možnosti obrázka\",\n    \"image_fit_cover\": \"Obálka (vyplnenie)\",\n    \"image_fit_contain\": \"Obsiahnuť (prispôsobiť)\"\n  },\n  \"version\": {\n    \"new_release_available\": \"Sú k dispozícii nové poznámky k verzii\",\n    \"whats_new_title\": \"Čo je nové vo verzii {{version}}\",\n    \"release_notes_description\": \"Tu sú najnovšie aktualizácie prevzaté z poznámok k verzii na GitHub.\",\n    \"loading_release_notes\": \"Načítavajú sa poznámky k verzii…\",\n    \"unable_to_load_release_notes\": \"Momentálne sa nedarí načítať poznámky k verzii. Skúste to neskôr.\",\n    \"no_release_notes\": \"Pre túto verziu neboli publikované žiadne poznámky k vydaniu.\",\n    \"release_notes_synced\": \"Poznámky k verzii sú synchronizované zo služby GitHub.\",\n    \"view_on_github\": \"Zobraziť na GitHub\"\n  },\n  \"wrapped\": {\n    \"title\": \"Tvoj {{year}} Wrapped\",\n    \"subtitle\": \"Rok v Karakeep\",\n    \"banner\": {\n      \"title\": \"Tvoj 2025 Wrapped je pripravený!\",\n      \"description\": \"Pozri si svoj rok v záložkách\",\n      \"view_now\": \"Zobraziť teraz\"\n    },\n    \"button\": \"2025 Wrapped\",\n    \"loading\": \"Načítavam tvoj Wrapped...\",\n    \"failed_to_load\": \"Nepodarilo sa načítať tvoje Wrapped štatistiky\",\n    \"sections\": {\n      \"total_saves\": {\n        \"prefix\": \"Uložil(a) si\",\n        \"suffix\": \"položiek tento rok\",\n        \"suffix_singular\": \"položka tento rok\"\n      },\n      \"first_bookmark\": {\n        \"title\": \"Tvoja cesta začala\",\n        \"description\": \"Prvé uloženie tento rok: {{year}}\"\n      },\n      \"top_domains\": \"Tvoje najlepšie stránky\",\n      \"top_tags\": \"Tvoje najlepšie značky\",\n      \"monthly_activity\": \"Tvoje tohtoročné ukladania\",\n      \"most_active_day\": \"Tvoj najaktívnejší deň\",\n      \"peak_times\": {\n        \"title\": \"Kedy ukladáš\",\n        \"peak_hour\": \"Hodina špičky\",\n        \"peak_day\": \"Deň špičky\"\n      },\n      \"how_you_save\": \"Ako ukladáš\",\n      \"what_you_saved\": \"Čo si uložil/a\",\n      \"summary\": {\n        \"favorites\": \"Obľúbené\",\n        \"tags_created\": \"Vytvorené štítky\",\n        \"highlights\": \"Najzaujímavejšie\"\n      },\n      \"types\": {\n        \"links\": \"Odkazy\",\n        \"notes\": \"Poznámky\",\n        \"assets\": \"Aktíva\"\n      }\n    },\n    \"footer\": \"Vyrobené pomocou Karakeep\",\n    \"share\": \"Zdieľať\",\n    \"download\": \"Stiahnuť\",\n    \"close\": \"Zavrieť\",\n    \"generating\": \"Vytváram...\"\n  }\n}\n"
  },
  {
    "path": "apps/web/lib/i18n/locales/sl/translation.json",
    "content": "{\n  \"settings\": {\n    \"api_keys\": {\n      \"key_success_please_copy\": \"Prosimo, kopirajte ključ in ga shranite na varno mesto. Ko zaprete pogovorno okno, ključa ne boste mogli več dostopati.\",\n      \"new_api_key\": \"Nov API ključ\",\n      \"new_api_key_desc\": \"Izberite edinstveno ime za svoj API ključ\",\n      \"api_keys\": \"API Ključi\",\n      \"key_success\": \"Ključ je bil uspešno ustvarjen\",\n      \"regenerate_api_key\": \"Osveži API ključ\",\n      \"key_regenerated\": \"Ključ je bil uspešno osvežen\",\n      \"key_regenerated_please_copy\": \"Prosimo, da kopirate nov ključ in ga shranite na varnem mestu. Stari ključ je bil preklican in ne bo več deloval.\",\n      \"regenerate_warning\": \"Ali ste prepričani, da želite ponovno ustvariti API ključ \\\"{{name}}\\\"?\",\n      \"regenerate_confirmation\": \"S tem boste preklicali trenutni ključ in ustvarili novega. Vse aplikacije, ki uporabljajo trenutni ključ, bodo prenehale delovati.\"\n    },\n    \"import\": {\n      \"import_bookmarks_from_karakeep_export\": \"Uvozi zaznamke iz Karakeep izvoza\",\n      \"import_bookmarks_from_linkwarden_export\": \"Uvozi zaznamke iz Linkwarden izvoza\",\n      \"imported_bookmarks\": \"Uvoženi zaznamki\",\n      \"import_bookmarks_from_pocket_export\": \"Uvozi zaznamke iz Pocket izvoza\",\n      \"import_bookmarks_from_matter_export\": \"Uvozi zaznamke iz Matter izvoza\",\n      \"import_export_bookmarks\": \"Uvoz / Izvoz zaznamkov\",\n      \"import_bookmarks_from_omnivore_export\": \"Uvozi zaznamke iz Omnivore izvoza\",\n      \"export_links_and_notes\": \"Izvozi povezave in zapiske\",\n      \"import_export\": \"Uvoz / Izvoz\",\n      \"import_bookmarks_from_html_file\": \"Uvozi zaznamke iz datoteke HTML\",\n      \"import_bookmarks_from_tab_session_manager_export\": \"Uvozi zaznamke iz upravitelja zavihkov\",\n      \"import_bookmarks_from_mymind_export\": \"Uvozi zaznamke iz izvoza mymind\",\n      \"import_bookmarks_from_instapaper_export\": \"Uvozi zaznamke iz izvoza Instapaper\"\n    },\n    \"info\": {\n      \"options\": \"Možnosti\",\n      \"current_password\": \"Trenutno geslo\",\n      \"basic_details\": \"Osnovni podatki\",\n      \"change_password\": \"Spremeni geslo\",\n      \"new_password\": \"Novo geslo\",\n      \"confirm_new_password\": \"Potrdite novo geslo\",\n      \"interface_lang\": \"Jezik vmesnika\",\n      \"user_info\": \"Informacije o uporabniku\",\n      \"user_settings\": {\n        \"user_settings_updated\": \"Uporabniške nastavitve so bile posodobljene!\",\n        \"bookmark_click_action\": {\n          \"title\": \"Dejanje klika zaznamka\",\n          \"open_external_url\": \"Odpri izvirni URL\",\n          \"open_bookmark_details\": \"Odpri podrobnosti zaznamka\"\n        },\n        \"archive_display_behaviour\": {\n          \"title\": \"Arhivirani zaznamki\",\n          \"show\": \"Prikaži arhivirane zaznamke v oznakah in seznamih\",\n          \"hide\": \"Skrij arhivirane zaznamke v oznakah in seznamih\"\n        }\n      },\n      \"reader_settings\": {\n        \"local_overrides_title\": \"Aktivne nastavitve, specifične za napravo\",\n        \"using_default\": \"Uporaba privzete vrednosti odjemalca\",\n        \"clear_override_hint\": \"Počisti preglasitev naprave, da uporabiš globalno nastavitev ({{value}})\",\n        \"font_size\": \"Velikost pisave\",\n        \"font_family\": \"Družina pisav\",\n        \"preview_inline\": \"(predogled)\",\n        \"tooltip_preview\": \"Neshranjene spremembe predogleda\",\n        \"save_to_all_devices\": \"Vse naprave\",\n        \"tooltip_local\": \"Nastavitve naprave se razlikujejo od globalnih\",\n        \"reset_preview\": \"Ponastavi predogled\",\n        \"mono\": \"Enoprostorska\",\n        \"line_height\": \"Višina vrstice\",\n        \"tooltip_default\": \"Nastavitve branja\",\n        \"title\": \"Nastavitve bralnika\",\n        \"serif\": \"Serif\",\n        \"preview\": \"Predogled\",\n        \"not_set\": \"Ni nastavljeno\",\n        \"clear_local_overrides\": \"Počisti nastavitve naprave\",\n        \"preview_text\": \"Rjava lisica skoči čez lenega psa. Tako bo videti vaše besedilo v pogledu bralnika.\",\n        \"local_overrides_cleared\": \"Nastavitve, specifične za napravo, so bile počiscene\",\n        \"local_overrides_description\": \"Ta naprava ima nastavitve bralnika, ki se razlikujejo od vaših splošnih privzetih nastavitev:\",\n        \"clear_defaults\": \"Počisti vse privzete nastavitve\",\n        \"description\": \"Nastavite privzete nastavitve besedila za pogled bralnika. Te nastavitve se sinhronizirajo v vseh vaših napravah.\",\n        \"defaults_cleared\": \"Privzeti bralnik je bil počiščen\",\n        \"save_hint\": \"Shrani nastavitve samo za to napravo ali sinhroniziraj med vsemi napravami\",\n        \"save_as_default\": \"Shrani kot privzeto\",\n        \"save_to_device\": \"Ta naprava\",\n        \"sans\": \"Sans Serif\",\n        \"tooltip_preview_and_local\": \"Neshranjene spremembe predogleda; nastavitve naprave se razlikujejo od globalnih\",\n        \"adjust_hint\": \"Prilagodite nastavitve zgoraj za predogled sprememb\"\n      },\n      \"avatar\": {\n        \"upload\": \"Naloži avatar\",\n        \"change\": \"Spremeni avatar\",\n        \"remove_confirm_title\": \"Odstranim avatar?\",\n        \"updated\": \"Avatar posodobljen\",\n        \"removed\": \"Avatar odstranjen\",\n        \"description\": \"Naloži kvadratno sliko, ki jo boš uporabil kot svoj avatar.\",\n        \"remove_confirm_description\": \"S tem boš odstranil svojo trenutno sliko profila.\",\n        \"title\": \"Fotka profila\",\n        \"remove\": \"Odstrani avatar\"\n      }\n    },\n    \"ai\": {\n      \"text_prompt\": \"Besedilni ukaz\",\n      \"image_tagging\": \"Slikovne oznake\",\n      \"summarization\": \"Povzemanje\",\n      \"images_prompt\": \"Slikovni ukaz\",\n      \"text_tagging\": \"Besedilne oznake\",\n      \"ai_settings\": \"AI Nastavitve\",\n      \"prompt_preview\": \"Predogled ukaza\",\n      \"summarization_prompt\": \"Povzemni ukaz\",\n      \"all_tagging\": \"Vse oznake\",\n      \"tagging_rules\": \"Pravila za označevanje\",\n      \"tagging_rule_description\": \"Pozivi, ki jih dodaš tukaj, bodo vključeni kot pravila za model med ustvarjanjem oznak. Končne pozive si lahko ogledaš v razdelku za predogled pozivov.\",\n      \"tag_style\": \"Slog oznake\",\n      \"auto_summarization_description\": \"Samodejno ustvari povzetke za tvoje zaznamke z uporabo UI.\",\n      \"auto_tagging\": \"Samodejno označevanje\",\n      \"titlecase_spaces\": \"Velike začetnice s presledki\",\n      \"lowercase_underscores\": \"Male črke s podčrtaji\",\n      \"inference_language\": \"Jezik sklepanja\",\n      \"titlecase_hyphens\": \"Velike začetnice s povezaji\",\n      \"lowercase_hyphens\": \"Male črke s povezaji\",\n      \"lowercase_spaces\": \"Male črke s presledki\",\n      \"inference_language_description\": \"Izberi jezik za oznake in povzetke, ustvarjene z umetno inteligenco.\",\n      \"tag_style_description\": \"Izberi obliko samodejno ustvarjenih oznak.\",\n      \"auto_tagging_description\": \"Samodejno ustvari oznake za tvoje zaznamke z uporabo UI.\",\n      \"camelCase\": \"camelCase\",\n      \"auto_summarization\": \"Samodejno povzemanje\",\n      \"no_preference\": \"Ni mi pomembno\",\n      \"curated_tags\": \"Izbrane oznake\",\n      \"curated_tags_description\": \"Po želji omejite označevanje z umetno inteligenco, da uporablja samo oznake s tega seznama. Če ni izbranih oznak, umetna inteligenca prosto ustvarja oznake.\",\n      \"curated_tags_updated\": \"Uspešno posodobljene izbrane oznake!\",\n      \"curated_tags_update_failed\": \"Posodobitev izbranih oznak ni uspela\"\n    },\n    \"back_to_app\": \"Nazaj v aplikacijo\",\n    \"webhooks\": {\n      \"events\": {\n        \"created\": \"Ustvarjeno\",\n        \"title\": \"Dogodki\",\n        \"edited\": \"Urejeno\",\n        \"crawled\": \"Preverjeno\"\n      },\n      \"webhooks\": \"Spletne kljuke\",\n      \"description\": \"Spletne kljuke lahko uporabiš za sprožitev dejanj, ko so zaznamki ustvarjeni, spremenjeni ali prebrskani.\",\n      \"auth_token\": \"Žeton za preverjanje pristnosti\",\n      \"add_auth_token\": \"Dodaj žeton za preverjanje pristnosti\",\n      \"edit_auth_token\": \"Uredi overitveni žeton\",\n      \"create_webhook\": \"Ustvari spletno kljuko\",\n      \"edit_webhook\": \"Uredi spletno kljuko\",\n      \"webhook_url\": \"URL spletne kljuke\",\n      \"delete_webhook\": \"Izbriši spletno kljuko\",\n      \"delete_webhook_confirmation\": \"Ali ste prepričani, da želite izbrisati to spletno kljuko?\"\n    },\n    \"feeds\": {\n      \"add_a_subscription\": \"Dodaj naročnino\",\n      \"rss_subscriptions\": \"RSS Naročnine\",\n      \"feed_enabled\": \"RSS vir je omogočen\",\n      \"feed_disabled\": \"RSS vir je onemogočen\"\n    },\n    \"user_settings\": \"Uporabniške nastavitve\",\n    \"broken_links\": {\n      \"broken_links\": \"Pokvarjene povezave\",\n      \"last_crawled_at\": \"Zadnjič preverjeno ob\",\n      \"crawling_status\": \"Stanje indeksiranja\",\n      \"crawling_failed\": \"Iskanje ni uspelo\"\n    },\n    \"manage_assets\": {\n      \"asset_type\": \"Vrsta sredstva\",\n      \"bookmark_link\": \"Povezava zaznamka\",\n      \"manage_assets\": \"Upravljanje sredstev\",\n      \"no_assets\": \"Še nimaš nobenih sredstev.\",\n      \"delete_asset\": \"Izbriši sredstvo\",\n      \"delete_asset_confirmation\": \"Ali si prepričan, da želiš izbrisati to sredstvo?\",\n      \"asset_link\": \"Povezava do sredstva\"\n    },\n    \"rules\": {\n      \"create_your_first_rule\": \"Ustvari svoje prvo pravilo za avtomatizacijo poteka dela\",\n      \"conditions_types\": {\n        \"always\": \"Vedno\",\n        \"url_contains\": \"URL vsebuje\",\n        \"imported_from_feed\": \"Uvoženo iz vira\",\n        \"bookmark_type_is\": \"Vrsta zaznamka je\",\n        \"has_tag\": \"Ima oznako\",\n        \"is_favourited\": \"Je med priljubljenimi\",\n        \"is_archived\": \"Je arhivirano\",\n        \"and\": \"Vse od naslednjega je resnično\",\n        \"or\": \"Katero koli od naslednjega je resnično\",\n        \"url_does_not_contain\": \"URL naslov ne vsebuje\",\n        \"title_contains\": \"Naslov vsebuje\",\n        \"title_does_not_contain\": \"Naslov ne vsebuje\"\n      },\n      \"rules\": \"Mehanizem pravil\",\n      \"rule_name\": \"Ime pravila\",\n      \"description\": \"Uporabiš lahko pravila za sprožitev dejanj, ko se sproži dogodek.\",\n      \"ceate_rule\": \"Ustvari pravilo\",\n      \"edit_rule\": \"Uredi pravilo\",\n      \"save_rule\": \"Shrani pravilo\",\n      \"delete_rule\": \"Izbriši pravilo\",\n      \"delete_rule_confirmation\": \"Si prepričan, da želiš izbrisati to pravilo?\",\n      \"whenever\": \"Kadar ...\",\n      \"if\": \"Če ...\",\n      \"enter_rule_name\": \"Vnesite ime pravila\",\n      \"describe_what_this_rule_does\": \"Opiši, kaj to pravilo počne\",\n      \"rule_has_been_created\": \"Pravilo je bilo ustvarjeno!\",\n      \"rule_has_been_updated\": \"Pravilo je bilo posodobljeno!\",\n      \"rule_has_been_deleted\": \"Pravilo je bilo izbrisano!\",\n      \"no_rules_created_yet\": \"Še ni ustvarjenih pravil\",\n      \"actions_types\": {\n        \"add_tag\": \"Dodaj oznako\",\n        \"remove_tag\": \"Odstrani oznako\",\n        \"add_to_list\": \"Dodaj na seznam\",\n        \"remove_from_list\": \"Odstrani s seznama\",\n        \"download_full_page_archive\": \"Prenesi arhiv celotne strani\",\n        \"favourite_bookmark\": \"Priljubljeni zaznamek\",\n        \"archive_bookmark\": \"Arhiviraj zaznamek\"\n      },\n      \"events_types\": {\n        \"bookmark_added\": \"Zaznamek je dodan\",\n        \"tag_added\": \"Ta oznaka je dodana zaznamku\",\n        \"tag_removed\": \"Ta oznaka je odstranjena iz zaznamka\",\n        \"added_to_list\": \"Zaznamek je dodan na ta seznam\",\n        \"removed_from_list\": \"Zaznamek je odstranjen s tega seznama\",\n        \"favourited\": \"Zaznamek je med priljubljenimi\",\n        \"archived\": \"Zaznamek je arhiviran\"\n      }\n    },\n    \"stats\": {\n      \"usage_statistics\": \"Statistika uporabe\",\n      \"insights_description\": \"Vpogledi v tvoje navade in zbirko zaznamkov\",\n      \"failed_to_load\": \"Statistike se niso naložile\",\n      \"overview\": {\n        \"total_bookmarks\": \"Skupno število zaznamkov\",\n        \"all_saved_items\": \"Vsi shranjeni elementi\",\n        \"favorites\": \"Priljubljene\",\n        \"starred_bookmarks\": \"Zaznamki z zvezdicami\",\n        \"archived\": \"Arhivirano\",\n        \"archived_items\": \"Arhivirani elementi\",\n        \"tags\": \"Oznake\",\n        \"unique_tags_created\": \"Ustvarjene edinstvene oznake\",\n        \"lists\": \"Seznami\",\n        \"bookmark_collections\": \"Zbirke zaznamkov\",\n        \"highlights\": \"Označbe\",\n        \"text_highlights\": \"Označena besedila\",\n        \"storage_used\": \"Uporabljena shramba\",\n        \"total_asset_storage\": \"Skupna shramba sredstev\",\n        \"this_month\": \"Ta mesec\",\n        \"bookmarks_added\": \"Zaznamki dodani\"\n      },\n      \"bookmark_types\": {\n        \"title\": \"Vrste zaznamkov\",\n        \"links\": \"Povezave\",\n        \"text_notes\": \"Besedilne opombe\",\n        \"assets\": \"Sredstva\"\n      },\n      \"recent_activity\": {\n        \"title\": \"Nedavna dejavnost\",\n        \"this_week\": \"Ta teden\",\n        \"this_month\": \"Ta mesec\",\n        \"this_year\": \"Letos\"\n      },\n      \"top_domains\": {\n        \"title\": \"Najboljše domene\",\n        \"no_domains_found\": \"Nobena domena ni bila najdena\"\n      },\n      \"most_used_tags\": {\n        \"title\": \"Najpogosteje uporabljene oznake\",\n        \"no_tags_found\": \"Ni najdenih oznak\"\n      },\n      \"activity_patterns\": {\n        \"activity_by_hour\": \"Dejavnost po urah\",\n        \"activity_by_day\": \"Dejavnost po dnevih\"\n      },\n      \"storage_breakdown\": {\n        \"title\": \"Razčlenitev shrambe\"\n      },\n      \"bookmark_sources\": {\n        \"title\": \"Viri zaznamkov\",\n        \"empty\": \"Ni razpoložljivih izvornih podatkov\"\n      }\n    },\n    \"subscription\": {\n      \"subscription\": \"Naročnina\",\n      \"manage_subscription\": \"Upravljaj svojo naročnino in informacije o zaračunavanju\",\n      \"current_plan\": \"Trenutni paket\",\n      \"billing_period\": \"Obdobje zaračunavanja\",\n      \"paid_plan\": \"Plačljiv paket\",\n      \"unlock_bigger_quota\": \"Odkleni večjo kvoto in podpri projekt\",\n      \"subscribe_now\": \"Naroči se zdaj\",\n      \"manage_billing\": \"Upravljaj zaračunavanje\",\n      \"subscription_canceled\": \"Tvoja naročnina je bila preklicana in se bo končala na {{date}}. Lahko se ponovno naročiš kadarkoli.\",\n      \"usage_quotas\": \"Uporaba in kvote\",\n      \"track_usage\": \"Spremljaj trenutno uporabo glede na omejitve tvojega paketa\",\n      \"total_bookmarks_saved\": \"Skupno število shranjenih zaznamkov\",\n      \"assets_file_storage\": \"Sredstva in shramba datotek\",\n      \"unlimited_usage\": \"Neomejena uporaba\",\n      \"quota_limit_reached\": \"Dosežena omejitev kvote\",\n      \"approaching_quota_limit\": \"Se približuješ omejitvi kvote\",\n      \"loading_usage\": \"Nalaganje informacij o uporabi ...\",\n      \"free\": \"Brezplačno\",\n      \"paid\": \"Plačano\"\n    },\n    \"import_sessions\": {\n      \"title\": \"Uvozi seje\",\n      \"description\": \"Ogled in upravljanje vaših sej za množični uvoz. Seje se samodejno ustvarijo, ko uvozite zaznamke.\",\n      \"load_error\": \"Uvoz sej se ni naložil\",\n      \"no_sessions\": \"Še ni uvoznih sej\",\n      \"no_sessions_detail\": \"Uvozne seje bodo tukaj samodejno prikazane, ko uvozite zaznamke\",\n      \"created_at\": \"Ustvarjeno {{time}}\",\n      \"progress\": \"Napredek\",\n      \"status\": {\n        \"pending\": \"Čakam\",\n        \"in_progress\": \"V teku\",\n        \"completed\": \"Končano\",\n        \"failed\": \"Neuspešno\",\n        \"processing\": \"Obdelava\",\n        \"staging\": \"Priprava\",\n        \"running\": \"Izvajanje\",\n        \"paused\": \"Začasno ustavljeno\"\n      },\n      \"badges\": {\n        \"pending\": \"{{count}} čaka\",\n        \"processing\": \"{{count}} se obdeluje\",\n        \"completed\": \"{{count}} končanih\",\n        \"failed\": \"{{count}} neuspelih\"\n      },\n      \"imported_to\": \"Uvoženo v:\",\n      \"view_list\": \"Pokaži seznam\",\n      \"delete_dialog_title\": \"Izbriši sejo uvoza\",\n      \"delete_dialog_description\": \"Ste prepričani, da želite izbrisati »{{name}}«? Tega dejanja ni mogoče razveljaviti. Zaznamki sami ne bodo izbrisani.\",\n      \"delete_session\": \"Izbriši sejo\",\n      \"pause_session\": \"Premor\",\n      \"resume_session\": \"Nadaljuj\",\n      \"view_details\": \"Prikaži podrobnosti\",\n      \"detail\": {\n        \"page_title\": \"Prikaži podrobnosti uvozne seje\",\n        \"back_to_import\": \"Nazaj na uvoz\",\n        \"filter_all\": \"Vse\",\n        \"filter_accepted\": \"Sprejeto\",\n        \"filter_rejected\": \"Zavrnjeno\",\n        \"filter_duplicates\": \"Podvojeno\",\n        \"filter_pending\": \"V teku\",\n        \"table_title\": \"Naslov / URL\",\n        \"table_type\": \"Vrsta\",\n        \"table_result\": \"Rezultat\",\n        \"table_reason\": \"Razlog\",\n        \"table_bookmark\": \"Zaznamek\",\n        \"result_accepted\": \"Sprejeto\",\n        \"result_rejected\": \"Zavrnjeno\",\n        \"result_skipped_duplicate\": \"Podvojeno\",\n        \"result_pending\": \"V teku\",\n        \"result_processing\": \"Obdelava\",\n        \"no_results\": \"Za ta filter ni bilo najdenih rezultatov.\",\n        \"view_bookmark\": \"Prikaži zaznamek\",\n        \"load_more\": \"Naloži več\",\n        \"no_title\": \"Brez naslova\"\n      }\n    },\n    \"backups\": {\n      \"backups\": \"Varnostne kopije\",\n      \"page_title\": \"Varnostne kopije\",\n      \"page_description\": \"Samodejno ustvarjaj in upravljaj varnostne kopije zaznamkov. Varnostne kopije so stisnjene in varno shranjene.\",\n      \"configuration\": {\n        \"title\": \"Nastavitve varnostnega kopiranja\",\n        \"enable_automatic_backups\": \"Omogoči samodejno varnostno kopiranje\",\n        \"enable_automatic_backups_description\": \"Samodejno ustvari varnostne kopije zaznamkov\",\n        \"backup_frequency\": \"Pogostost varnostnega kopiranja\",\n        \"backup_frequency_description\": \"Kako pogosto naj se ustvarjajo varnostne kopije\",\n        \"retention_period\": \"Obdobje ohranjanja (dni)\",\n        \"retention_period_description\": \"Koliko dni hraniti varnostne kopije, preden se izbrišejo\",\n        \"frequency\": {\n          \"daily\": \"Dnevno\",\n          \"weekly\": \"Tedensko\"\n        },\n        \"select_frequency\": \"Izberi pogostost\",\n        \"save_settings\": \"Shrani nastavitve\"\n      },\n      \"list\": {\n        \"title\": \"Tvoje varnostne kopije\",\n        \"create_backup_now\": \"Ustvari varnostno kopijo zdaj\",\n        \"no_backups\": \"Še nimaš varnostnih kopij. Omogoči samodejno varnostno kopiranje ali ustvari ročno.\",\n        \"table\": {\n          \"created_at\": \"Ustvarjeno ob\",\n          \"bookmarks\": \"Zaznamki\",\n          \"size\": \"Velikost\",\n          \"status\": \"Stanje\",\n          \"actions\": \"Dejanja\"\n        },\n        \"status\": {\n          \"success\": \"Uspešno\",\n          \"failed\": \"Neuspešno\",\n          \"pending\": \"Na čakanju\"\n        },\n        \"actions\": {\n          \"download_backup\": \"Prenesi varnostno kopijo\",\n          \"delete_backup\": \"Izbriši varnostno kopijo\"\n        }\n      },\n      \"dialogs\": {\n        \"delete_backup_title\": \"Izbrišem varnostno kopijo?\",\n        \"delete_backup_description\": \"Ali ste prepričani, da želite izbrisati to varnostno kopijo? Tega dejanja ni mogoče razveljaviti.\"\n      },\n      \"toasts\": {\n        \"backup_queued\": \"Opravilo varnostne kopije je bilo dodano v čakalno vrsto! Kmalu bo obdelano.\",\n        \"backup_deleted\": \"Varnostna kopija je bila izbrisana!\"\n      }\n    }\n  },\n  \"admin\": {\n    \"server_stats\": {\n      \"server_stats\": \"Statistika strežnika\",\n      \"total_users\": \"Skupno število uporabnikov\",\n      \"total_bookmarks\": \"Skupno število zaznamkov\",\n      \"server_version\": \"Različica strežnika\"\n    },\n    \"users_list\": {\n      \"users_list\": \"Seznam uporabnikov\",\n      \"confirm_password\": \"Potrdite geslo\",\n      \"reset_password\": \"Ponastavi geslo\",\n      \"create_user\": \"Ustvari uporabnika\",\n      \"local_user\": \"Lokalni uporabnik\",\n      \"change_role\": \"Spremeni vlogo\",\n      \"delete_user\": \"Izbriši uporabnika\",\n      \"num_bookmarks\": \"Št. zaznamkov\",\n      \"asset_sizes\": \"Velikosti sredstev\",\n      \"delete_user_confirm_description\": \"Ali si prepričan, da želiš izbrisati uporabnika \\\"{{name}}\\\"?\",\n      \"unlimited\": \"Neomejeno\"\n    },\n    \"background_jobs\": {\n      \"failed\": \"Neuspešno\",\n      \"background_jobs\": \"Oprabila v ozadju\",\n      \"queued\": \"V vrsti\",\n      \"pending\": \"Čakajoče\",\n      \"job\": \"Opravilo\",\n      \"crawler_jobs\": \"Iskalna opravila\",\n      \"webhook_jobs\": \"Opravila spletne kljuke\",\n      \"indexing_jobs\": \"Opravila indeksiranja\",\n      \"tidy_assets_jobs\": \"Opravila za urejanje sredstev\",\n      \"video_jobs\": \"Opravila za prenos videoposnetkov\",\n      \"asset_preprocessing_jobs\": \"Opravila za predobdelavo sredstev\",\n      \"feed_jobs\": \"Opravila za vire RSS\",\n      \"inference_jobs\": \"Opravila za sklepanje\",\n      \"jobs\": {\n        \"crawler\": {\n          \"title\": \"Posli za pajkanje\",\n          \"description\": \"Spletno pajkanje in pridobivanje vsebine iz URLjev\"\n        },\n        \"inference\": {\n          \"title\": \"Posli sklepanja\",\n          \"description\": \"Označevanje in povzemanje vsebine s pomočjo umetne inteligence\"\n        },\n        \"indexing\": {\n          \"title\": \"Posli indeksiranja\",\n          \"description\": \"Posodobitve iskalnega indeksa\"\n        },\n        \"asset_preprocessing\": {\n          \"title\": \"Posli predobdelave sredstev\",\n          \"description\": \"Predobdelava slik in dokumentov (posnetki zaslona, izvleček besedila itd.)\"\n        },\n        \"tidy_assets\": {\n          \"title\": \"Posli urejanja sredstev\",\n          \"description\": \"Čiščenje sredstev in optimizacija shranjevanja\"\n        },\n        \"video\": {\n          \"title\": \"Posli prenosa videoposnetkov\",\n          \"description\": \"Izvleček in prenos videoposnetkov\"\n        },\n        \"webhook\": {\n          \"title\": \"Posli spletne kljuke\",\n          \"description\": \"Obvestila prek zunanje spletne kljuke\"\n        },\n        \"feed\": {\n          \"title\": \"Posli RSS-vira\",\n          \"description\": \"Obdelava RSS-vira in posodobitve vsebine\"\n        },\n        \"admin_maintenance\": {\n          \"title\": \"Administrativna vzdrževalna dela\",\n          \"description\": \"Administrativno čiščenje in vzdrževanje sredstev\"\n        }\n      },\n      \"monitor_and_manage\": \"Spremljajte in upravljajte čakalne vrste opravil v ozadju ter sistemske naloge obdelave\",\n      \"active\": \"Aktivno\",\n      \"available_actions\": \"Razpoložljiva dejanja\",\n      \"status\": {\n        \"title\": \"Razumevanje stanj opravil\",\n        \"queued\": {\n          \"title\": \"Na vrsti\",\n          \"description\": \"Opravila čakajo v vrsti za obdelavo. Samodejno se bodo začela, ko bodo na voljo viri.\"\n        },\n        \"unprocessed\": {\n          \"title\": \"Nepredelano\",\n          \"description\": \"Zaznamki, ki še niso bili obdelani. Najverjetneje so že v čakalni vrsti za obdelavo, če ne, jih boste morda morali ročno znova uvrstiti v čakalno vrsto.\"\n        },\n        \"failed\": {\n          \"title\": \"Neuspešno\",\n          \"description\": \"Zaznamki, pri katerih je prišlo do napak med obdelavo. Ti bodo morda potrebovali ročno pozornost.\"\n        }\n      },\n      \"actions\": {\n        \"recrawl_failed_links_only\": \"Ponovno preišči samo neuspele povezave\",\n        \"recrawl_all_links\": \"Ponovno preišči vse povezave\",\n        \"without_inference\": \"Brez sklepanja\",\n        \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Znova ustvari oznake AI samo za neuspele zaznamke\",\n        \"regenerate_ai_tags_for_all_bookmarks\": \"Znova ustvari oznake AI za vse zaznamke\",\n        \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Ustvari znova izvlečke AI samo za neuspele zaznamke\",\n        \"regenerate_ai_summaries_for_all_bookmarks\": \"Ustvari znova izvlečke AI za vse zaznamke\",\n        \"reindex_all_bookmarks\": \"Ponovno indeksiraj vse zaznamke\",\n        \"clean_assets\": \"Očisti viseča sredstva in ponovno sinhroniziraj metapodatke\",\n        \"reprocess_assets_fix_mode\": \"Ponovno obdelaj nepredelana sredstva\",\n        \"migrate_large_link_html_content\": \"Premakni veliko vdelano HTML-ovo kodo v sredstva.\",\n        \"recrawl_pending_links_only\": \"Ponovno indeksiraj samo povezave, ki čakajo na obdelavo\",\n        \"regenerate_ai_tags_for_pending_bookmarks_only\": \"Zgolj za zaznamke, ki čakajo na obdelavo, ponovno ustvari oznake, ustvarjene z umetno inteligenco\",\n        \"regenerate_ai_summaries_for_pending_bookmarks_only\": \"Zgolj za zaznamke, ki čakajo na obdelavo, ponovno ustvari povzetke, ustvarjene z umetno inteligenco\"\n      }\n    },\n    \"admin_settings\": \"Administratorske nastavitve\",\n    \"actions\": {\n      \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Regeneriraj AI oznake samo za neuspešne zaznamke\",\n      \"regenerate_ai_tags_for_all_bookmarks\": \"Regeneriraj AI oznake za vse zaznamke\",\n      \"recrawl_failed_links_only\": \"Ponovno preveri samo povezave, ki niso uspele\",\n      \"recrawl_all_links\": \"Ponovno poišči vse povezave\",\n      \"without_inference\": \"Brez sklepanja\",\n      \"reindex_all_bookmarks\": \"Ponovno indeksiraj vse zaznamke\",\n      \"compact_assets\": \"Kompaktna sredstva\",\n      \"reprocess_assets_fix_mode\": \"Ponovno obdelaj sredstva (način popravljanja)\",\n      \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Ponovno ustvari povzetke umetne inteligence samo za neuspele zaznamke\",\n      \"regenerate_ai_summaries_for_all_bookmarks\": \"Ponovno ustvari povzetke umetne inteligence za vse zaznamke\"\n    },\n    \"service_connections\": {\n      \"title\": \"Povezave storitev\",\n      \"description\": \"Spremljaj stanje in povezljivost zunanjih sistemskih odvisnosti\",\n      \"search_engine\": \"Iskalnik\",\n      \"browser\": \"Brskalnik\",\n      \"queue_system\": \"Sistem čakalnih vrst\",\n      \"status\": {\n        \"not_configured\": \"Ni konfigurirano\",\n        \"connected\": \"Povezano\",\n        \"disconnected\": \"Odklopljeno\"\n      }\n    },\n    \"admin_tools\": {\n      \"admin_tools\": \"Admin orodja\",\n      \"bookmark_debugger\": \"Razhroščevalnik zaznamkov\",\n      \"bookmark_id\": \"ID zaznamka\",\n      \"bookmark_id_placeholder\": \"Vnesi ID zaznamka\",\n      \"lookup\": \"Poišči\",\n      \"debug_info\": \"Podatki za razhroščevanje\",\n      \"basic_info\": \"Osnovni podatki\",\n      \"status\": \"Stanje\",\n      \"content\": \"Vsebina\",\n      \"html_preview\": \"Predogled HTML (prvih 1000 znakov)\",\n      \"summary\": \"Povzetek\",\n      \"url\": \"URL\",\n      \"source_url\": \"Izvor URL-ja\",\n      \"asset_type\": \"Vrsta sredstva\",\n      \"file_name\": \"Ime datoteke\",\n      \"owner_user_id\": \"ID uporabnika lastnika\",\n      \"tagging_status\": \"Stanje označevanja\",\n      \"summarization_status\": \"Stanje povzemanja\",\n      \"crawl_status\": \"Stanje indeksiranja\",\n      \"crawl_status_code\": \"Koda HTTP stanja\",\n      \"crawled_at\": \"Prebrano ob\",\n      \"recrawl\": \"Ponovno preberi\",\n      \"reindex\": \"Ponovno indeksiraj\",\n      \"retag\": \"Ponovno označi\",\n      \"resummarize\": \"Ponovno povzemi\",\n      \"bookmark_not_found\": \"Zaznamek ni najden\",\n      \"action_success\": \"Dejanje je bilo uspešno zaključeno\",\n      \"action_failed\": \"Dejanje je spodletelo\",\n      \"recrawl_queued\": \"Ponovno prebiranje je bilo dodano v čakalno vrsto\",\n      \"reindex_queued\": \"Ponovno indeksiranje je bilo dodano v čakalno vrsto\",\n      \"retag_queued\": \"Ponovno označevanje je bilo dodano v čakalno vrsto\",\n      \"resummarize_queued\": \"Ponovno povzemanje je bilo dodano v čakalno vrsto\",\n      \"view\": \"Poglej\",\n      \"fetch_error\": \"Napaka pri pridobivanju zaznamka\"\n    }\n  },\n  \"search\": {\n    \"is_not_archived\": \"Ni arhivirano\",\n    \"is_archived\": \"Je arhivirano\",\n    \"created_on_or_after\": \"Ustvarjeno na ali po\",\n    \"is_in_any_list\": \"Je v seznamu\",\n    \"is_not_in_any_list\": \"Ni v seznamu\",\n    \"created_on_or_before\": \"Ustvarjeno na ali prej\",\n    \"and\": \"In\",\n    \"url_contains\": \"URL Vsebuje\",\n    \"url_does_not_contain\": \"URL Ne vsebuje\",\n    \"not_created_on_or_before\": \"Ni ustvarjeno na ali pred\",\n    \"is_in_list\": \"Je v seznamu\",\n    \"does_not_have_tag\": \"Nima oznak\",\n    \"type_is_not\": \"Vrsta ni\",\n    \"not_created_on_or_after\": \"Ni ustvatjeno na ali po\",\n    \"has_tag\": \"Ima oznake\",\n    \"type_is\": \"Vrsta je\",\n    \"or\": \"Ali\",\n    \"is_not_in_list\": \"Ni v seznamu\",\n    \"is_favorited\": \"Je med priljubljenimi\",\n    \"is_not_favorited\": \"Ni med priljubljenimi\",\n    \"has_any_tag\": \"Ima katero koli oznako\",\n    \"has_no_tags\": \"Nima oznake\",\n    \"full_text_search\": \"Iskanje po celotnem besedilu\",\n    \"is_from_feed\": \"Je iz vira RSS\",\n    \"is_not_from_feed\": \"Ni iz vira RSS\",\n    \"created_within\": \"Ustvarjeno v\",\n    \"created_earlier_than\": \"Ustvarjeno prej kot\",\n    \"day_s\": \" Dnevi\",\n    \"week_s\": \" Tedni\",\n    \"month_s\": \" Meseci\",\n    \"year_s\": \" Let(a)\",\n    \"day_s_ago\": \" Dnevi nazaj\",\n    \"week_s_ago\": \" Tedni nazaj\",\n    \"month_s_ago\": \" Meseci nazaj\",\n    \"year_s_ago\": \" Let(a) nazaj\",\n    \"history\": \"Nedavna iskanja\",\n    \"title_contains\": \"Naslov vsebuje\",\n    \"title_does_not_contain\": \"Naslov ne vsebuje\",\n    \"is_broken_link\": \"Ima polomljeno povezavo\",\n    \"tags\": \"Oznake\",\n    \"no_suggestions\": \"Ni predlogov\",\n    \"filters\": \"Filtri\",\n    \"is_not_broken_link\": \"Ima delujočo povezavo\",\n    \"lists\": \"Seznami\",\n    \"feeds\": \"Viri\",\n    \"is_from_source\": \"Vir je\",\n    \"is_not_from_source\": \"Vir ni\"\n  },\n  \"tags\": {\n    \"your_tags_info\": \"Oznake, ki si jih dodelil/a vsaj enkrat\",\n    \"ai_tags_info\": \"Oznake, ki so bile dodeljene samo avtomatsko (AI)\",\n    \"drag_and_drop_merging\": \"Združevanje Potegni & Spusti\",\n    \"ai_tags\": \"AI Oznake\",\n    \"unused_tags_info\": \"Oznake, ki niso dodeljene nobenemu zaznamku\",\n    \"all_tags\": \"Vse oznake\",\n    \"sort_by_name\": \"Uredi po imenu\",\n    \"your_tags\": \"Tvoje oznake\",\n    \"unused_tags\": \"Nsuporabljene oznake\",\n    \"delete_all_unused_tags\": \"Izbriši vse neuporabljene oznake\",\n    \"drag_and_drop_merging_info\": \"Povlecite in spustite oznake drug na drugo, da jih združite\",\n    \"create_tag\": \"Ustvari oznako\",\n    \"create_tag_description\": \"Ustvari novo oznako brez pripenjanja na zaznamek\",\n    \"tag_name\": \"Ime oznake\",\n    \"enter_tag_name\": \"Vnesi ime oznake\",\n    \"sort_by_usage\": \"Razvrsti po uporabi\",\n    \"sort_by_relevance\": \"Razvrsti po ustreznosti\",\n    \"no_custom_tags\": \"Še ni oznak po meri\",\n    \"no_ai_tags\": \"Še ni oznak AI\",\n    \"no_unused_tags\": \"Nimaš neuporabljenih oznak\",\n    \"no_unused_tags_match_your_search\": \"Nobena neuporabljena oznaka ne ustreza tvojemu iskanju\",\n    \"no_tags_match_your_search\": \"Nobena oznaka ne ustreza tvojemu iskanju\",\n    \"search_placeholder\": \"Iskanje oznak ...\",\n    \"search_or_create_placeholder\": \"Poiščite ali ustvarite oznake ...\"\n  },\n  \"common\": {\n    \"archive\": \"Arhiv\",\n    \"role\": \"Vloga\",\n    \"video\": \"Video\",\n    \"email\": \"E-pošta\",\n    \"password\": \"Geslo\",\n    \"experimental\": \"Eksperimentalno\",\n    \"attachments\": \"Priponke\",\n    \"home\": \"Dom\",\n    \"something_went_wrong\": \"Nekaj je šlo narobe\",\n    \"screenshot\": \"Posnetek zaslona\",\n    \"roles\": {\n      \"user\": \"Uporabnik\",\n      \"admin\": \"Admin\"\n    },\n    \"url\": \"URL\",\n    \"name\": \"Ime\",\n    \"created_at\": \"Ustvarjeno\",\n    \"type\": \"Tip\",\n    \"size\": \"Velikost\",\n    \"bookmark_types\": {\n      \"text\": \"Besedilo\",\n      \"link\": \"Povezava\",\n      \"title\": \"Vrsta zaznamka\",\n      \"media\": \"Mediji\"\n    },\n    \"search\": \"Iskanje\",\n    \"source\": \"Vir\",\n    \"action\": \"Dejanje\",\n    \"actions\": \"Dejanja\",\n    \"key\": \"Ključ\",\n    \"tags\": \"Oznake\",\n    \"note\": \"Opomba\",\n    \"highlights\": \"Poudarki\",\n    \"updated_at\": \"Posodobljeno ob\",\n    \"title\": \"Naslov\",\n    \"description\": \"Opis\",\n    \"summary\": \"Povzetek\",\n    \"quota\": \"Količina\",\n    \"bookmarks\": \"Zaznamki\",\n    \"storage\": \"Shranjevanje\",\n    \"pdf\": \"Arhiviran PDF\",\n    \"default\": \"Privzeto\",\n    \"id\": \"ID\",\n    \"last_used\": \"Zadnja uporaba\"\n  },\n  \"actions\": {\n    \"close_bulk_edit\": \"Zapri množično urejanje\",\n    \"favorite\": \"Priljubljeno\",\n    \"copy_link\": \"Kopiraj povezavo\",\n    \"close\": \"Zapri\",\n    \"sort\": {\n      \"oldest_first\": \"Najstarejše najprej\",\n      \"title\": \"Sortiraj\",\n      \"newest_first\": \"Najnovejše najprej\",\n      \"relevant_first\": \"Najbolj pomembno najprej\"\n    },\n    \"unselect_all\": \"Od-izberi vse\",\n    \"create\": \"Ustvari\",\n    \"merge\": \"Združi\",\n    \"select_all\": \"Izberi vse\",\n    \"add\": \"Dodaj\",\n    \"unarchive\": \"Od-arhiviraj\",\n    \"remove_from_list\": \"Odstrani iz seznama\",\n    \"summarize_with_ai\": \"Povzami z AI\",\n    \"edit\": \"Uredi\",\n    \"cancel\": \"Prekliči\",\n    \"edit_title\": \"Uredi naslov\",\n    \"sign_out\": \"Odjava\",\n    \"delete\": \"Izbriši\",\n    \"manage_lists\": \"Uredi sezname\",\n    \"add_to_list\": \"Dodaj na seznam\",\n    \"archive\": \"Arhiviraj\",\n    \"download_full_page_archive\": \"Prenesi arhiv\",\n    \"refresh\": \"Osveži\",\n    \"save\": \"Shrani\",\n    \"ignore\": \"Ignoriraj\",\n    \"bulk_edit\": \"Množično urejanje\",\n    \"change_layout\": \"Spremeni postavitev\",\n    \"unfavorite\": \"Odstrani iz priljubljenih\",\n    \"recrawl\": \"Ponovno prebrskaj\",\n    \"edit_tags\": \"Uredi oznake\",\n    \"fetch_now\": \"Poberi zdaj\",\n    \"apply_all\": \"Uporabi vse\",\n    \"open_editor\": \"Odpri urejevalnik\",\n    \"toggle_show_archived\": \"Prikaži arhivirano\",\n    \"confirm\": \"Potrdi\",\n    \"regenerate\": \"Osveži\",\n    \"load_more\": \"Naloži več\",\n    \"edit_notes\": \"Uredi opombe\",\n    \"preserve_as_pdf\": \"Shrani kot PDF\",\n    \"offline_copies\": \"Kopije brez povezave\",\n    \"preserve_offline_archive\": \"Ohrani brezžično arhiviranje\",\n    \"download_full_page_archive_file\": \"Prenesi datoteko arhiva\",\n    \"download_pdf_file\": \"Prenesi datoteko PDF\",\n    \"remove\": \"Odstrani\",\n    \"more\": \"Več\",\n    \"replace_banner\": \"Zamenjaj pasico\",\n    \"add_banner\": \"Dodaj pasico\",\n    \"download\": \"Prenesi\"\n  },\n  \"layouts\": {\n    \"compact\": \"Kompaktno\",\n    \"list\": \"Seznam\",\n    \"masonry\": \"Zidarska vezava\",\n    \"grid\": \"Mreža\"\n  },\n  \"lists\": {\n    \"list_type\": \"Vrsta seznama\",\n    \"smart_list\": \"Pametni seznam\",\n    \"new_nested_list\": \"Nov gnezden seznam\",\n    \"edit_list\": \"Uredi seznam\",\n    \"search_query\": \"Iskalna poizvedba\",\n    \"all_lists\": \"Vsi seznami\",\n    \"manual_list\": \"Ročni seznam\",\n    \"new_list\": \"Nov seznam\",\n    \"favourites\": \"Priljubljene\",\n    \"parent_list\": \"Seznam staršev\",\n    \"no_parent\": \"Brez starša\",\n    \"search_query_help\": \"Izvedi več o jeziku iskalnih poizvedb.\",\n    \"delete_after_merge\": \"Izbriši izvirni seznam po združitvi\",\n    \"no_destination\": \"Ni cilja\",\n    \"description\": \"Opis (neobvezno)\",\n    \"merge_list\": \"Združi seznam\",\n    \"destination_list\": \"Ciljni seznam\",\n    \"share_list\": \"Deli seznam\",\n    \"rss\": {\n      \"title\": \"RSS vir\",\n      \"description\": \"Omogoči RSS vir za ta seznam\",\n      \"feed_url\": \"URL RSS vira\"\n    },\n    \"public_list\": {\n      \"title\": \"Javni seznam\",\n      \"description\": \"Dovoli drugim ogled tega seznama\",\n      \"share_link\": \"Povezava za deljenje\"\n    },\n    \"delete_list\": {\n      \"title\": \"Izbriši seznam\",\n      \"description\": \"Če izbrišeš seznam, ne izbrišeš zaznamkov na tem seznamu.\",\n      \"delete_children\": \"Izbriši podrejene sezname (rekurzivno)\",\n      \"delete_children_description\": \"Če ni označeno, bodo vsi neposredni podrejeni seznami postali glavni seznami\"\n    },\n    \"shared\": \"Deljeno\",\n    \"collaborators\": {\n      \"manage\": \"Upravljaj s sodelavci\",\n      \"view\": \"Poglej si sodelavce\",\n      \"collaborators\": \"Sodelavci\",\n      \"add\": \"Dodaj sodelavca\",\n      \"current\": \"Trenutni sodelavci\",\n      \"enter_email\": \"Vnesi e-poštni naslov\",\n      \"please_enter_email\": \"Prosim, vnesi e-poštni naslov\",\n      \"added_successfully\": \"Sodelavec uspešno dodan\",\n      \"failed_to_add\": \"Dodajanje sodelavca ni uspelo\",\n      \"removed\": \"Sodelavec odstranjen\",\n      \"failed_to_remove\": \"Odstranjevanje sodelavca ni uspelo\",\n      \"role_updated\": \"Vloga posodobljena\",\n      \"failed_to_update_role\": \"Posodabljanje vloge ni uspelo\",\n      \"viewer\": \"Pregledovalnik\",\n      \"editor\": \"Urednik\",\n      \"owner\": \"Lastnik\",\n      \"viewer_description\": \"Lahko si ogleda zaznamke na seznamu\",\n      \"editor_description\": \"Lahko dodaja in odstranjuje zaznamke\",\n      \"no_collaborators\": \"Še ni sodelavcev. Dodaj nekoga, da začneš sodelovati!\",\n      \"no_collaborators_readonly\": \"Ni sodelavcev za ta seznam.\",\n      \"people_with_access\": \"Osebe, ki imajo dostop do tega seznama\",\n      \"add_or_remove\": \"Dodaj ali odstrani osebe, ki imajo dostop do tega seznama\",\n      \"invitation_sent\": \"Uspešno poslano povabilo\",\n      \"invitation_revoked\": \"Povabilo preklicano\",\n      \"failed_to_revoke\": \"Preklic povabila ni uspel\",\n      \"pending\": \"Na čakanju\",\n      \"revoke\": \"Prekliči\",\n      \"declined\": \"Zavrnjeno\"\n    },\n    \"leave_list\": {\n      \"title\": \"Zapusti seznam\",\n      \"confirm_message\": \"Ali si prepričan/-a, da želiš zapustiti {{icon}} {{name}}?\",\n      \"warning\": \"Ne boš si več mogel/-a ogledati ali dostopati do zaznamkov na tem seznamu. Lastnik seznama te lahko po potrebi doda nazaj.\",\n      \"action\": \"Zapusti seznam\",\n      \"success\": \"Zapustil/-a si »{{icon}} {{name}}«\"\n    },\n    \"invitations\": {\n      \"pending\": \"Povabila na čakanju\",\n      \"description\": \"Preglejte in se odzovite na povabila za sodelovanje na seznamu\",\n      \"invited_by\": \"Povabil:\",\n      \"accept\": \"Sprejmi\",\n      \"decline\": \"Zavrni\",\n      \"accepted\": \"Povabilo sprejeto\",\n      \"declined\": \"Povabilo zavrnjeno\",\n      \"failed_to_accept\": \"Sprejem povabila ni uspel\",\n      \"failed_to_decline\": \"Zavrnitev povabila ni uspela\"\n    },\n    \"shared_lists\": \"Deljeni seznami\"\n  },\n  \"options\": {\n    \"dark_mode\": \"Temni način\",\n    \"light_mode\": \"Svetli način\",\n    \"apps_extensions\": \"Aplikacije in razširitve\",\n    \"documentation\": \"Dokumentacija\",\n    \"follow_us_on_x\": \"Sledi nam na X\"\n  },\n  \"editor\": {\n    \"text_toolbar\": {\n      \"align_left\": \"Poravnano levo\",\n      \"undo\": \"Razveljavi\",\n      \"redo\": \"Uveljavi\",\n      \"highlight\": \"Poudari\",\n      \"markdown_shortcuts\": {\n        \"ordered_list\": {\n          \"example\": \"1. Element seznama\",\n          \"label\": \"Urejen seznam\"\n        },\n        \"unordered_list\": {\n          \"label\": \"Neurejen seznam\",\n          \"example\": \"- Element seznama\"\n        },\n        \"block_code\": {\n          \"example\": \"``` + presledek\",\n          \"label\": \"Koda v bloku\"\n        },\n        \"label\": \"Markdown bližnjice\",\n        \"blockquote\": {\n          \"example\": \"> Citat\",\n          \"label\": \"Citat\"\n        },\n        \"bold\": {\n          \"label\": \"Krepko\",\n          \"example\": \"**besedilo** ali CTRL+b\"\n        },\n        \"italic\": {\n          \"label\": \"Poševno\",\n          \"example\": \"*poševno* ali _poševno_ ali CTRL+i\"\n        },\n        \"heading\": {\n          \"example\": \"# H1, ## H2, ### H3\",\n          \"label\": \"Naslov\"\n        },\n        \"inline_code\": {\n          \"label\": \"Koda v vrstici\",\n          \"example\": \"`Koda`\"\n        }\n      },\n      \"align_right\": \"Poravnano desno\",\n      \"align_center\": \"Srednisko poravnano\",\n      \"bold\": \"Krepko\",\n      \"italic\": \"Poševno\",\n      \"underline\": \"Podčrtaj\",\n      \"strikethrough\": \"Prečrtaj\",\n      \"code\": \"Koda\"\n    },\n    \"new_item\": \"NOV ELEMENT\",\n    \"import_as_separate_bookmarks\": \"Uvozi kot ločene zaznamke\",\n    \"placeholder\": \"Prilepi povezavo ali sliko, napiši zapisek ali potegni in spusti sliko tukaj…\",\n    \"multiple_urls_dialog_title\": \"Uvoz URL-jev, kot ločene zaznamke?\",\n    \"import_as_text\": \"Uvozi kot besedilni zaznamek\",\n    \"disabled_submissions\": \"Oddaje so onemogočene\",\n    \"quickly_focus\": \"Na to polje se lahko hitro osredotočiš s pritiskom na ⌘ + E\",\n    \"multiple_urls_dialog_desc\": \"Vnos vsebuje več URL-jev v ločenih vrsticah. Ali jih želite uvoziti kot ločene zaznamke?\",\n    \"placeholder_v2\": \"Prilepite povezavo, napišite opombo ali spustite sliko …\"\n  },\n  \"toasts\": {\n    \"bookmarks\": {\n      \"deleted\": \"Zaznamek je bil izbrisan!\",\n      \"delete_from_list\": \"Ta zaznamek je bil izbrisan iz seznama\",\n      \"clipboard_copied\": \"Povezava je bila kopirana v odložišče!\",\n      \"updated\": \"Zaznamek je bil posodobljen!\",\n      \"refetch\": \"Ponovno pridobivanje je bilo dodano v čakalno vrsto!\",\n      \"full_page_archive\": \"Ustvarjanje arhiva celotne strani je bilo sproženo\",\n      \"preserve_pdf\": \"Ohranjanje PDF je bilo sproženo\",\n      \"update_banner\": \"Pasica je bila posodobljena!\",\n      \"uploading_banner\": \"Nalaganje pasice ...\"\n    },\n    \"lists\": {\n      \"created\": \"Seznam je bil ustvarjen!\",\n      \"updated\": \"Seznam je bil posodobljen!\",\n      \"merged\": \"Seznam je bil združen!\",\n      \"deleted\": \"Seznam je bil izbrisan!\"\n    },\n    \"tags\": {\n      \"created\": \"Oznaka je bila ustvarjena!\",\n      \"failed_to_create\": \"Oznake ni bilo mogoče ustvariti\"\n    }\n  },\n  \"dialogs\": {\n    \"bookmarks\": {\n      \"delete_confirmation_description\": \"Si prepričan, sa želiš izbrisati ta zaznamek?\",\n      \"delete_confirmation_title\": \"Izbriši zaznamek?\"\n    }\n  },\n  \"cleanups\": {\n    \"duplicate_tags\": {\n      \"merge_all_suggestions\": \"Združi vse predloge?\",\n      \"title\": \"Podvojene oznake\"\n    },\n    \"cleanups\": \"Čiščenja\"\n  },\n  \"preview\": {\n    \"view_original\": \"Ogled izvirnika\",\n    \"cached_content\": \"Predpomnjena vsebina\",\n    \"reader_view\": \"Pogled bralnika\",\n    \"tabs\": {\n      \"content\": \"Vsebina\",\n      \"details\": \"Podrobnosti\"\n    },\n    \"archive_info\": \"Arhivi se morda ne bodo pravilno izrisali v vrstici, če zahtevajo Javascript. Za najboljše rezultate <1>jih prenesi in odpri v brskalniku</1>.\",\n    \"fetch_error_title\": \"Vsebina ni na voljo\",\n    \"fetch_error_description\": \"Ne moremo pridobiti vsebine za to povezavo. Stran je morda zaščitena, zahteva overjanje ali pa je začasno nedosegljiva.\",\n    \"crawling_in_progress\": \"Pridobivam vsebino strani …\",\n    \"continue_reading\": \"Nadaljuj, kjer si končal\",\n    \"continue_reading_percent\": \"Nadaljuj, kjer si končal ({{percent}} %)\",\n    \"continue_button\": \"Nadaljuj\"\n  },\n  \"highlights\": {\n    \"no_highlights\": \"Še nimaš nobenih poudarkov.\"\n  },\n  \"banners\": {\n    \"no_bookmarks\": {\n      \"title\": \"Še ni zaznamkov\",\n      \"description\": \"Shrani zanimive članke, povezave in strani za hiter dostop pozneje.\"\n    }\n  },\n  \"bookmark_editor\": {\n    \"save_changes\": \"Shrani spremembe\",\n    \"title\": \"Uredi zaznamek\",\n    \"subtitle\": \"Spremeni podrobnosti zaznamka. Ko končaš, klikni shrani.\",\n    \"author\": \"Avtor\",\n    \"publisher\": \"Založnik\",\n    \"date_published\": \"Datum objave\",\n    \"pick_a_date\": \"Izberi datum\",\n    \"extracted_content\": \"Izluščena vsebina\"\n  },\n  \"view_options\": {\n    \"title\": \"Možnosti prikaza\",\n    \"layout\": \"Postavitev\",\n    \"columns\": \"Stolpci\",\n    \"display_options\": \"Možnosti prikaza\",\n    \"show_note_previews\": \"Pokaži opombe\",\n    \"show_tags\": \"Pokaži oznake\",\n    \"show_title\": \"Pokaži naslov\",\n    \"image_options\": \"Možnosti slike\",\n    \"image_fit_cover\": \"Pokrov (Izpolni)\",\n    \"image_fit_contain\": \"Vsebuje (Prilagodi)\"\n  },\n  \"version\": {\n    \"new_release_available\": \"Na voljo so nove opombe ob izidu\",\n    \"whats_new_title\": \"Kaj je novega v različici v{{version}}\",\n    \"release_notes_description\": \"Tukaj so najnovejše posodobitve, pridobljene iz opomb ob izdaji GitHub.\",\n    \"loading_release_notes\": \"Nalaganje opomb ob izdaji …\",\n    \"unable_to_load_release_notes\": \"Opomb ob izdaji trenutno ni mogoče naložiti. Poskusite znova pozneje.\",\n    \"no_release_notes\": \"Za to različico niso bile objavljene nobene opombe ob izdaji.\",\n    \"release_notes_synced\": \"Opombe ob izdaji so sinhronizirane iz GitHub.\",\n    \"view_on_github\": \"Poglej si na GitHubu\"\n  },\n  \"wrapped\": {\n    \"title\": \"Tvoj zavitek {{year}}\",\n    \"subtitle\": \"Leto v Karakeepu\",\n    \"banner\": {\n      \"title\": \"Tvoj zavitek 2025 je pripravljen!\",\n      \"description\": \"Oglej si svoje leto v zaznamkih\",\n      \"view_now\": \"Poglej zdaj\"\n    },\n    \"button\": \"Zavitek 2025\",\n    \"loading\": \"Nalaganje tvojega zavitka ...\",\n    \"failed_to_load\": \"Ni uspelo naložiti tvoje zavite statistike\",\n    \"sections\": {\n      \"total_saves\": {\n        \"prefix\": \"Shranil si\",\n        \"suffix\": \"število elementov letos\",\n        \"suffix_singular\": \"element letos\"\n      },\n      \"first_bookmark\": {\n        \"title\": \"Tvoja pot se je začela\",\n        \"description\": \"Tvoj prvi shranek v letu {{year}}:\"\n      },\n      \"top_domains\": \"Tvoje najboljše spletne strani\",\n      \"top_tags\": \"Tvoje najboljše oznake\",\n      \"monthly_activity\": \"Tvoje leto v shranjenih elementih\",\n      \"most_active_day\": \"Tvoj najbolj aktiven dan\",\n      \"peak_times\": {\n        \"title\": \"Kdaj shranjuješ\",\n        \"peak_hour\": \"Ura, ko si najbolj aktiven\",\n        \"peak_day\": \"Najbolj aktiven dan\"\n      },\n      \"how_you_save\": \"Kako shranjuješ\",\n      \"what_you_saved\": \"Kaj si shranil\",\n      \"summary\": {\n        \"favorites\": \"Priljubljene\",\n        \"tags_created\": \"Ustvarjene oznake\",\n        \"highlights\": \"Poudarki\"\n      },\n      \"types\": {\n        \"links\": \"Povezave\",\n        \"notes\": \"Opombe\",\n        \"assets\": \"Sredstva\"\n      }\n    },\n    \"footer\": \"Izdelano s programom Karakeep\",\n    \"share\": \"Deli\",\n    \"download\": \"Prenesi\",\n    \"close\": \"Zapri\",\n    \"generating\": \"Ustvarjam ...\"\n  }\n}\n"
  },
  {
    "path": "apps/web/lib/i18n/locales/sv/translation.json",
    "content": "{\n  \"common\": {\n    \"url\": \"URL\",\n    \"email\": \"Email\",\n    \"password\": \"Lösenord\",\n    \"created_at\": \"Skapad den\",\n    \"key\": \"Nyckel\",\n    \"action\": \"Åtgärd\",\n    \"actions\": \"Åtgärder\",\n    \"experimental\": \"Experimentell\",\n    \"attachments\": \"Bilagor\",\n    \"screenshot\": \"Skärmdump\",\n    \"roles\": {\n      \"admin\": \"Admin\",\n      \"user\": \"Användare\"\n    },\n    \"note\": \"Anteckning\",\n    \"video\": \"Video\",\n    \"archive\": \"Arkiv\",\n    \"name\": \"Namn\",\n    \"role\": \"Roll\",\n    \"something_went_wrong\": \"Något gick fel\",\n    \"search\": \"Sök\",\n    \"tags\": \"Taggar\",\n    \"home\": \"Hem\",\n    \"highlights\": \"Höjdpunkter\",\n    \"source\": \"Källa\",\n    \"bookmark_types\": {\n      \"title\": \"Bokmärkestyp\",\n      \"link\": \"Länk\",\n      \"text\": \"Text\",\n      \"media\": \"Media\"\n    },\n    \"size\": \"Storlek\",\n    \"type\": \"Typ\",\n    \"updated_at\": \"Uppdaterad\",\n    \"title\": \"Titel\",\n    \"description\": \"Beskrivning\",\n    \"summary\": \"Sammanfattning\",\n    \"quota\": \"Kvot\",\n    \"bookmarks\": \"Bokmärken\",\n    \"storage\": \"Lagring\",\n    \"pdf\": \"Arkiverad PDF\",\n    \"default\": \"Standard\",\n    \"id\": \"ID\",\n    \"last_used\": \"Senast använt\"\n  },\n  \"layouts\": {\n    \"grid\": \"Rutnät\",\n    \"compact\": \"Kompakt\",\n    \"list\": \"Lista\",\n    \"masonry\": \"Murverk\"\n  },\n  \"actions\": {\n    \"delete\": \"Radera\",\n    \"archive\": \"Arkivera\",\n    \"unarchive\": \"Avarkivera\",\n    \"add_to_list\": \"Lägg till i lista\",\n    \"download_full_page_archive\": \"Ladda ner fullsidesarkiv\",\n    \"select_all\": \"Markera allt\",\n    \"manage_lists\": \"Hantera listor\",\n    \"remove_from_list\": \"Ta bort från lista\",\n    \"unselect_all\": \"Avmarkera allt\",\n    \"add\": \"Lägg till\",\n    \"fetch_now\": \"Hämta nu\",\n    \"edit\": \"Ändra\",\n    \"sign_out\": \"Logga ut\",\n    \"close\": \"Stäng\",\n    \"edit_title\": \"Ändra titel\",\n    \"cancel\": \"Avbryt\",\n    \"ignore\": \"Ignorera\",\n    \"refresh\": \"Uppdatera\",\n    \"favorite\": \"Favorit\",\n    \"copy_link\": \"Kopiera länk\",\n    \"create\": \"Skapa\",\n    \"change_layout\": \"Andra layout\",\n    \"edit_tags\": \"Ändra taggar\",\n    \"summarize_with_ai\": \"Sammanfatta med AI\",\n    \"save\": \"Spara\",\n    \"merge\": \"Sammanfoga\",\n    \"sort\": {\n      \"title\": \"Sortera\",\n      \"newest_first\": \"Nyast först\",\n      \"oldest_first\": \"Äldst först\",\n      \"relevant_first\": \"Mest relevant först\"\n    },\n    \"recrawl\": \"Omindexera\",\n    \"unfavorite\": \"Sluta favorisera\",\n    \"close_bulk_edit\": \"Stäng massredigering\",\n    \"bulk_edit\": \"Massredigera\",\n    \"apply_all\": \"Använd alla\",\n    \"open_editor\": \"Öppna redigeraren\",\n    \"toggle_show_archived\": \"Visa arkiverade\",\n    \"confirm\": \"Bekräfta\",\n    \"regenerate\": \"Återskapa\",\n    \"load_more\": \"Ladda mer\",\n    \"edit_notes\": \"Redigera anteckningar\",\n    \"preserve_as_pdf\": \"Spara som PDF\",\n    \"offline_copies\": \"Offlinelagrade kopior\",\n    \"preserve_offline_archive\": \"Spara offline-arkiv\",\n    \"download_full_page_archive_file\": \"Ladda ner arkivfil\",\n    \"download_pdf_file\": \"Ladda ner PDF-fil\",\n    \"remove\": \"Ta bort\",\n    \"more\": \"Mer\",\n    \"replace_banner\": \"Ersätt banderoll\",\n    \"add_banner\": \"Lägg till banderoll\",\n    \"download\": \"Ladda ner\"\n  },\n  \"settings\": {\n    \"back_to_app\": \"Tillbaka till app\",\n    \"info\": {\n      \"user_info\": \"Användarinfo\",\n      \"basic_details\": \"Grundläggande detaljer\",\n      \"interface_lang\": \"Gränsnittsspråk\",\n      \"current_password\": \"Nuvarande lösenord\",\n      \"confirm_new_password\": \"Bekräfta nytt lösenord\",\n      \"change_password\": \"Ändra lösenord\",\n      \"new_password\": \"Nytt lösenord\",\n      \"options\": \"Alternativ\",\n      \"user_settings\": {\n        \"user_settings_updated\": \"Användarinställningarna har uppdaterats!\",\n        \"bookmark_click_action\": {\n          \"title\": \"Åtgärd vid klick på bokmärke\",\n          \"open_external_url\": \"Öppna ursprunglig URL\",\n          \"open_bookmark_details\": \"Öppna bokmärkesdetaljer\"\n        },\n        \"archive_display_behaviour\": {\n          \"title\": \"Arkiverade bokmärken\",\n          \"show\": \"Visa arkiverade bokmärken i taggar och listor\",\n          \"hide\": \"Dölj arkiverade bokmärken i taggar och listor\"\n        }\n      },\n      \"reader_settings\": {\n        \"local_overrides_title\": \"Enhetsspecifika inställningar aktiva\",\n        \"using_default\": \"Använder klientstandard\",\n        \"clear_override_hint\": \"Rensa enhetsåsidosättning för att använda global inställning ({{value}})\",\n        \"font_size\": \"Teckenstorlek\",\n        \"font_family\": \"Typsnittsfamilj\",\n        \"preview_inline\": \"(förhandsvisning)\",\n        \"tooltip_preview\": \"Osparade förhandsvisningsändringar\",\n        \"save_to_all_devices\": \"Alla enheter\",\n        \"tooltip_local\": \"Enhetsinställningar skiljer sig från globala\",\n        \"reset_preview\": \"Återställ förhandsvisning\",\n        \"mono\": \"Monospace\",\n        \"line_height\": \"Radhöjd\",\n        \"tooltip_default\": \"Läsningsinställningar\",\n        \"title\": \"Läsarinställningar\",\n        \"serif\": \"Serif\",\n        \"preview\": \"Förhandsgranskning\",\n        \"not_set\": \"Ej inställt\",\n        \"clear_local_overrides\": \"Rensa enhetsinställningar\",\n        \"preview_text\": \"The quick brown fox jumps over the lazy dog. Så här kommer din läsarvytext att se ut.\",\n        \"local_overrides_cleared\": \"Enhetsspecifika inställningar har rensats\",\n        \"local_overrides_description\": \"Den här enheten har läsarinställningar som skiljer sig från dina globala standardinställningar:\",\n        \"clear_defaults\": \"Rensa alla standardvärden\",\n        \"description\": \"Konfigurera standardtextinställningar för läsarvyn. Dessa inställningar synkroniseras mellan alla dina enheter.\",\n        \"defaults_cleared\": \"Läsarstandardvärden har rensats\",\n        \"save_hint\": \"Spara inställningar endast för den här enheten eller synkronisera över alla enheter\",\n        \"save_as_default\": \"Spara som standard\",\n        \"save_to_device\": \"Den här enheten\",\n        \"sans\": \"Sans Serif\",\n        \"tooltip_preview_and_local\": \"Osparade ändringar i förhandsvisningen; enhetsinställningarna skiljer sig från de globala\",\n        \"adjust_hint\": \"Justera inställningarna ovan för att förhandsvisa ändringarna\"\n      },\n      \"avatar\": {\n        \"upload\": \"Ladda upp avatar\",\n        \"change\": \"Ändra avatar\",\n        \"remove_confirm_title\": \"Ta bort avatar?\",\n        \"updated\": \"Avatar uppdaterad\",\n        \"removed\": \"Avatar borttagen\",\n        \"description\": \"Ladda upp en kvadratisk bild för att använda som din avatar.\",\n        \"remove_confirm_description\": \"Detta kommer att ta bort ditt nuvarande profilfoto.\",\n        \"title\": \"Profilbild\",\n        \"remove\": \"Ta bort avatar\"\n      }\n    },\n    \"feeds\": {\n      \"rss_subscriptions\": \"RSS-prenumerationer\",\n      \"add_a_subscription\": \"Lägg till en prenumeration\",\n      \"feed_enabled\": \"RSS-flöde aktiverat\",\n      \"feed_disabled\": \"RSS-flöde inaktiverat\"\n    },\n    \"ai\": {\n      \"images_prompt\": \"Bildprompt\",\n      \"text_prompt\": \"Textprompt\",\n      \"tagging_rules\": \"Taggningsregler\",\n      \"ai_settings\": \"AI-inställningar\",\n      \"tagging_rule_description\": \"Prompter som du lägger till här kommer att inkluderas som regler i modellen under taggenereringen. Du kan se de slutliga prompterna i avsnittet förhandsvisning av prompter.\",\n      \"prompt_preview\": \"Förhandsvisning av prompt\",\n      \"text_tagging\": \"Texttaggning\",\n      \"image_tagging\": \"Bildtaggning\",\n      \"summarization\": \"Sammanfattning\",\n      \"summarization_prompt\": \"Sammanfattningsprompt\",\n      \"all_tagging\": \"All taggning\",\n      \"tag_style\": \"Taggstil\",\n      \"auto_summarization_description\": \"Generera automatisk sammanfattning för dina bokmärken genom att använda AI.\",\n      \"auto_tagging\": \"Automatisk taggning\",\n      \"titlecase_spaces\": \"Versala inledande bokstäver med mellanslag\",\n      \"lowercase_underscores\": \"Små bokstäver med understreck\",\n      \"inference_language\": \"Språk för inferens\",\n      \"titlecase_hyphens\": \"Versala inledande bokstäver med bindestreck\",\n      \"lowercase_hyphens\": \"Små bokstäver med bindestreck\",\n      \"lowercase_spaces\": \"Små bokstäver med mellanslag\",\n      \"inference_language_description\": \"Välj språk för AI-genererade taggar och sammanfattningar.\",\n      \"tag_style_description\": \"Välj hur dina automatiskt genererade taggar ska formateras.\",\n      \"auto_tagging_description\": \"Generera automatiskt taggar för dina bokmärken genom att använda AI.\",\n      \"camelCase\": \"camelCase\",\n      \"auto_summarization\": \"Automatisk sammanfattning\",\n      \"no_preference\": \"Ingen preferens\",\n      \"curated_tags\": \"Kurerade taggar\",\n      \"curated_tags_description\": \"Begränsa eventuellt AI-taggning till att endast använda taggar från den här listan. När inga taggar är valda genererar AI:n taggar fritt.\",\n      \"curated_tags_updated\": \"Kurerade taggar har uppdaterats!\",\n      \"curated_tags_update_failed\": \"Det gick inte att uppdatera kurerade taggar\"\n    },\n    \"import\": {\n      \"import_export\": \"Importera / exportera\",\n      \"import_export_bookmarks\": \"Exportera bokmärken\",\n      \"import_bookmarks_from_omnivore_export\": \"Importera bokmärken från Omnivore-export\",\n      \"imported_bookmarks\": \"Importerade bokmärken\",\n      \"import_bookmarks_from_karakeep_export\": \"Importera bokmärken från Karakeep-export\",\n      \"import_bookmarks_from_html_file\": \"Importera bokmärken från HTML-fil\",\n      \"import_bookmarks_from_pocket_export\": \"Importera bokmärken från Pocket-export\",\n      \"import_bookmarks_from_matter_export\": \"Importera bokmärken från Matter-export\",\n      \"export_links_and_notes\": \"Exportera länkar och anteckningar\",\n      \"import_bookmarks_from_linkwarden_export\": \"Importera bokmärken från Linkwarden-export\",\n      \"import_bookmarks_from_tab_session_manager_export\": \"Importera bokmärken från Tab Session Manager\",\n      \"import_bookmarks_from_mymind_export\": \"Importera bokmärken från min mymind-export\",\n      \"import_bookmarks_from_instapaper_export\": \"Importera bokmärken från Instapaper-export\"\n    },\n    \"api_keys\": {\n      \"api_keys\": \"API-nycklar\",\n      \"new_api_key\": \"Ny API-nyckel\",\n      \"new_api_key_desc\": \"Ge din API-nyckel ett unikt namn\",\n      \"key_success_please_copy\": \"Kopiera nyckeln och lagra den på en saker plats. Efter att du stängt denna dialogruta kommer su inte att kunna nå den igen.\",\n      \"key_success\": \"Nyckeln skapades utan problem\",\n      \"regenerate_api_key\": \"Återskapa API-nyckel\",\n      \"key_regenerated\": \"Nyckeln har återskapats\",\n      \"key_regenerated_please_copy\": \"Kopiera den nya nyckeln och spara den på ett säkert ställe. Den gamla nyckeln har återkallats och kommer inte längre att funka.\",\n      \"regenerate_warning\": \"Är du säker på att du vill återskapa API-nyckeln \\\"{{name}}\\\"?\",\n      \"regenerate_confirmation\": \"Detta kommer att återkalla den nuvarande nyckeln och skapa en ny. Alla applikationer som använder den nuvarande nyckeln kommer sluta funka.\"\n    },\n    \"user_settings\": \"Användarinställningar\",\n    \"webhooks\": {\n      \"webhook_url\": \"Webhook-URL\",\n      \"delete_webhook_confirmation\": \"Är du säker på att du vill ta bort denna webhook?\",\n      \"webhooks\": \"Webhooks\",\n      \"description\": \"Du kan använda webhooks för att utlösa åtgärder när bokmärken skapas, ändras eller genomsöks.\",\n      \"events\": {\n        \"title\": \"Händelser\",\n        \"crawled\": \"Genomsökt\",\n        \"created\": \"Skapad\",\n        \"edited\": \"Redigerad\"\n      },\n      \"auth_token\": \"Autentiseringstoken\",\n      \"add_auth_token\": \"Lägg till autentiseringstoken\",\n      \"edit_auth_token\": \"Redigera Auth-token\",\n      \"create_webhook\": \"Skapa Webhook\",\n      \"delete_webhook\": \"Ta bort webhook\",\n      \"edit_webhook\": \"Redigera Webhook\"\n    },\n    \"manage_assets\": {\n      \"bookmark_link\": \"Bokmärkeslänk\",\n      \"asset_link\": \"Tillgångslänk\",\n      \"delete_asset\": \"Ta bort tillgång\",\n      \"delete_asset_confirmation\": \"Är du säker på att du vill ta bort den här tillgången?\",\n      \"manage_assets\": \"Hantera tillgångar\",\n      \"no_assets\": \"Du har inga tillgångar än.\",\n      \"asset_type\": \"Tillgångstyp\"\n    },\n    \"broken_links\": {\n      \"crawling_status\": \"Krypstatus\",\n      \"crawling_failed\": \"Crawling misslyckades\",\n      \"broken_links\": \"Trasiga länkar\",\n      \"last_crawled_at\": \"Senaste genomsökning\"\n    },\n    \"rules\": {\n      \"rules\": \"Regelmotor\",\n      \"rule_name\": \"Regelnamn\",\n      \"description\": \"Du kan använda regler för att utlösa åtgärder när en händelse inträffar.\",\n      \"ceate_rule\": \"Skapa regel\",\n      \"edit_rule\": \"Redigera regel\",\n      \"save_rule\": \"Spara regel\",\n      \"delete_rule\": \"Ta bort regel\",\n      \"delete_rule_confirmation\": \"Är du säker på att du vill ta bort den här regeln?\",\n      \"whenever\": \"Närhelst ...\",\n      \"if\": \"Om ...\",\n      \"enter_rule_name\": \"Ange regelnamn\",\n      \"describe_what_this_rule_does\": \"Beskriv vad den här regeln gör\",\n      \"rule_has_been_created\": \"Regeln har skapats!\",\n      \"rule_has_been_updated\": \"Regeln har uppdaterats!\",\n      \"rule_has_been_deleted\": \"Regeln har tagits bort!\",\n      \"no_rules_created_yet\": \"Inga regler har skapats ännu\",\n      \"create_your_first_rule\": \"Skapa din första regel för att automatisera ditt arbetsflöde\",\n      \"conditions_types\": {\n        \"always\": \"Alltid\",\n        \"url_contains\": \"URL innehåller\",\n        \"imported_from_feed\": \"Importerad från flöde\",\n        \"bookmark_type_is\": \"Bokmärkestyp är\",\n        \"has_tag\": \"Har tagg\",\n        \"is_favourited\": \"Är favoriterad\",\n        \"is_archived\": \"Är Arkiverad\",\n        \"and\": \"Allt följande är sant\",\n        \"or\": \"Något av följande är sant\",\n        \"url_does_not_contain\": \"URL:en innehåller inte\",\n        \"title_contains\": \"Titeln innehåller\",\n        \"title_does_not_contain\": \"Titeln innehåller inte\"\n      },\n      \"actions_types\": {\n        \"add_tag\": \"Lägg till tagg\",\n        \"remove_tag\": \"Ta bort tagg\",\n        \"add_to_list\": \"Lägg till i lista\",\n        \"remove_from_list\": \"Ta bort från lista\",\n        \"download_full_page_archive\": \"Ladda ner fullständig sidarkivering\",\n        \"favourite_bookmark\": \"Favoritmarkera bokmärke\",\n        \"archive_bookmark\": \"Arkivera bokmärke\"\n      },\n      \"events_types\": {\n        \"bookmark_added\": \"Ett bokmärke läggs till\",\n        \"tag_added\": \"Den här taggen läggs till i ett bokmärke\",\n        \"tag_removed\": \"Den här taggen tas bort från ett bokmärke\",\n        \"added_to_list\": \"Ett bokmärke läggs till i den här listan\",\n        \"removed_from_list\": \"Ett bokmärke tas bort från den här listan\",\n        \"favourited\": \"Ett bokmärke är favoritmarkerat\",\n        \"archived\": \"Ett bokmärke arkiveras\"\n      }\n    },\n    \"stats\": {\n      \"usage_statistics\": \"Användningsstatistik\",\n      \"insights_description\": \"Insikter i dina bokmärkesvanor och samling\",\n      \"failed_to_load\": \"Det gick inte att läsa in statistik\",\n      \"overview\": {\n        \"total_bookmarks\": \"Totalt antal bokmärken\",\n        \"all_saved_items\": \"Alla sparade objekt\",\n        \"favorites\": \"Favoriter\",\n        \"starred_bookmarks\": \"Stjärnmärkta bokmärken\",\n        \"archived\": \"Arkiverad\",\n        \"archived_items\": \"Arkiverade objekt\",\n        \"tags\": \"Taggar\",\n        \"unique_tags_created\": \"Unika taggar skapade\",\n        \"lists\": \"Listor\",\n        \"bookmark_collections\": \"Bokmärkessamlingar\",\n        \"highlights\": \"Markeringar\",\n        \"text_highlights\": \"Textmarkeringar\",\n        \"storage_used\": \"Använt lagringsutrymme\",\n        \"total_asset_storage\": \"Total lagring av tillgångar\",\n        \"this_month\": \"Den här månaden\",\n        \"bookmarks_added\": \"Bokmärken tillagda\"\n      },\n      \"bookmark_types\": {\n        \"title\": \"Bokmärkestyper\",\n        \"links\": \"Länkar\",\n        \"text_notes\": \"Textanteckningar\",\n        \"assets\": \"Tillgångar\"\n      },\n      \"recent_activity\": {\n        \"title\": \"Senaste aktivitet\",\n        \"this_week\": \"Den här veckan\",\n        \"this_month\": \"Den här månaden\",\n        \"this_year\": \"Det här året\"\n      },\n      \"top_domains\": {\n        \"title\": \"Populäraste domäner\",\n        \"no_domains_found\": \"Inga domäner hittades\"\n      },\n      \"most_used_tags\": {\n        \"title\": \"Mest använda taggar\",\n        \"no_tags_found\": \"Inga taggar hittades\"\n      },\n      \"activity_patterns\": {\n        \"activity_by_hour\": \"Aktivitet per timme\",\n        \"activity_by_day\": \"Aktivitet per dag\"\n      },\n      \"storage_breakdown\": {\n        \"title\": \"Fördelning av lagring\"\n      },\n      \"bookmark_sources\": {\n        \"title\": \"Bokmärk källor\",\n        \"empty\": \"Ingen källdata tillgänglig\"\n      }\n    },\n    \"subscription\": {\n      \"subscription\": \"Prenumeration\",\n      \"manage_subscription\": \"Hantera din prenumeration och faktureringsinformation\",\n      \"current_plan\": \"Nuvarande plan\",\n      \"billing_period\": \"Faktureringsperiod\",\n      \"paid_plan\": \"Betalplan\",\n      \"unlock_bigger_quota\": \"Lås upp större kvot och stöd projektet\",\n      \"subscribe_now\": \"Prenumerera nu\",\n      \"manage_billing\": \"Hantera fakturering\",\n      \"subscription_canceled\": \"Din prenumeration har avbrutits och kommer att avslutas den {{date}}. Du kan prenumerera igen när som helst.\",\n      \"usage_quotas\": \"Användning och kvoter\",\n      \"track_usage\": \"Kolla din nuvarande användning jämfört med dina gränser\",\n      \"total_bookmarks_saved\": \"Totalt antal sparade bokmärken\",\n      \"assets_file_storage\": \"Tillgångar och fillagring\",\n      \"unlimited_usage\": \"Obegränsad användning\",\n      \"quota_limit_reached\": \"Kvotgränsen har nåtts\",\n      \"approaching_quota_limit\": \"Närmar sig kvotgränsen\",\n      \"loading_usage\": \"Läser in användningsinformation...\",\n      \"free\": \"Gratis\",\n      \"paid\": \"Betald\"\n    },\n    \"import_sessions\": {\n      \"title\": \"Importera sessioner\",\n      \"description\": \"Visa och hantera dina massimportsessioner. Sessioner skapas automatiskt när du importerar bokmärken.\",\n      \"load_error\": \"Det gick inte att läsa in importsessioner\",\n      \"no_sessions\": \"Inga importsessioner ännu\",\n      \"no_sessions_detail\": \"Importsessioner kommer att visas här automatiskt när du importerar bokmärken.\",\n      \"created_at\": \"Skapad {{time}}\",\n      \"progress\": \"Förlopp\",\n      \"status\": {\n        \"pending\": \"Väntar\",\n        \"in_progress\": \"Pågår\",\n        \"completed\": \"Slutförd\",\n        \"failed\": \"Misslyckades\",\n        \"processing\": \"Bearbetar\",\n        \"staging\": \"Mellanlagring\",\n        \"running\": \"Kör\",\n        \"paused\": \"Pausad\"\n      },\n      \"badges\": {\n        \"pending\": \"{{count}} väntar\",\n        \"processing\": \"{{count}} bearbetar\",\n        \"completed\": \"{{count}} slutförda\",\n        \"failed\": \"{{count}} misslyckades\"\n      },\n      \"imported_to\": \"Importerat till:\",\n      \"view_list\": \"Visa lista\",\n      \"delete_dialog_title\": \"Ta bort importsession\",\n      \"delete_dialog_description\": \"Är du säker på att du vill ta bort \\\"{{name}}\\\"? Åtgärden kan inte ångras. Själva bokmärkena kommer inte att tas bort.\",\n      \"delete_session\": \"Ta bort session\",\n      \"pause_session\": \"Pausa\",\n      \"resume_session\": \"Återuppta\",\n      \"view_details\": \"Visa detaljer\",\n      \"detail\": {\n        \"page_title\": \"Visa detaljer för importsession\",\n        \"back_to_import\": \"Tillbaka till import\",\n        \"filter_all\": \"Alla\",\n        \"filter_accepted\": \"Godkända\",\n        \"filter_rejected\": \"Avvisade\",\n        \"filter_duplicates\": \"Dubbletter\",\n        \"filter_pending\": \"Väntar\",\n        \"table_title\": \"Titel/URL\",\n        \"table_type\": \"Typ\",\n        \"table_result\": \"Resultat\",\n        \"table_reason\": \"Anledning\",\n        \"table_bookmark\": \"Bokmärke\",\n        \"result_accepted\": \"Godkända\",\n        \"result_rejected\": \"Avvisade\",\n        \"result_skipped_duplicate\": \"Dublett\",\n        \"result_pending\": \"Väntar\",\n        \"result_processing\": \"Bearbetar\",\n        \"no_results\": \"Inga resultat hittades för detta filter.\",\n        \"view_bookmark\": \"Visa bokmärke\",\n        \"load_more\": \"Ladda mer\",\n        \"no_title\": \"Ingen titel\"\n      }\n    },\n    \"backups\": {\n      \"backups\": \"Säkerhetskopior\",\n      \"page_title\": \"Säkerhetskopior\",\n      \"page_description\": \"Skapa och hantera säkerhetskopior av dina bokmärken automatiskt. Säkerhetskopiorna komprimeras och lagras säkert.\",\n      \"configuration\": {\n        \"title\": \"Konfigurera säkerhetskopiering\",\n        \"enable_automatic_backups\": \"Aktivera automatiska säkerhetskopieringar\",\n        \"enable_automatic_backups_description\": \"Skapa säkerhetskopior av dina bokmärken automatiskt\",\n        \"backup_frequency\": \"Frekvens för säkerhetskopiering\",\n        \"backup_frequency_description\": \"Hur ofta säkerhetskopieringar ska skapas\",\n        \"retention_period\": \"Kvarhållningsperiod (dagar)\",\n        \"retention_period_description\": \"Hur många dagar säkerhetskopior ska sparas innan de raderas\",\n        \"frequency\": {\n          \"daily\": \"Dagligen\",\n          \"weekly\": \"Varje vecka\"\n        },\n        \"select_frequency\": \"Välj frekvens\",\n        \"save_settings\": \"Spara inställningar\"\n      },\n      \"list\": {\n        \"title\": \"Dina säkerhetskopior\",\n        \"create_backup_now\": \"Skapa säkerhetskopia nu\",\n        \"no_backups\": \"Du har inga säkerhetskopior än. Aktivera automatiska säkerhetskopieringar eller skapa en manuellt.\",\n        \"table\": {\n          \"created_at\": \"Skapad den\",\n          \"bookmarks\": \"Bokmärken\",\n          \"size\": \"Storlek\",\n          \"status\": \"Status\",\n          \"actions\": \"Åtgärder\"\n        },\n        \"status\": {\n          \"success\": \"Lyckades\",\n          \"failed\": \"Misslyckades\",\n          \"pending\": \"Väntar\"\n        },\n        \"actions\": {\n          \"download_backup\": \"Ladda ner säkerhetskopiering\",\n          \"delete_backup\": \"Ta bort säkerhetskopiering\"\n        }\n      },\n      \"dialogs\": {\n        \"delete_backup_title\": \"Ta bort säkerhetskopiering?\",\n        \"delete_backup_description\": \"Är du säker på att du vill ta bort den här säkerhetskopieringen? Den här åtgärden kan inte ångras.\"\n      },\n      \"toasts\": {\n        \"backup_queued\": \"Säkerhetskopieringen har köats! Den kommer att behandlas inom kort.\",\n        \"backup_deleted\": \"Säkerhetskopieringen har tagits bort!\"\n      }\n    }\n  },\n  \"admin\": {\n    \"server_stats\": {\n      \"server_stats\": \"Serverstatistik\",\n      \"server_version\": \"Serverversion\",\n      \"total_bookmarks\": \"Totalt antal bokmärken\",\n      \"total_users\": \"Totalt antal användare\"\n    },\n    \"admin_settings\": \"Admin-inställningar\",\n    \"background_jobs\": {\n      \"background_jobs\": \"Bakgrundsjobb\",\n      \"job\": \"Jobb\",\n      \"pending\": \"Pågående\",\n      \"indexing_jobs\": \"Indexeringsjobb\",\n      \"inference_jobs\": \"Inferensjobb\",\n      \"queued\": \"Köade\",\n      \"asset_preprocessing_jobs\": \"Jobb för förbearbetning av tillgångar\",\n      \"crawler_jobs\": \"Crawler-jobb\",\n      \"tidy_assets_jobs\": \"Jobb för att städa upp tillgångar\",\n      \"failed\": \"Misslyckades\",\n      \"video_jobs\": \"Nedladdningsjobb för video\",\n      \"webhook_jobs\": \"Webhook-jobb\",\n      \"feed_jobs\": \"RSS-flödesjobb\",\n      \"jobs\": {\n        \"crawler\": {\n          \"title\": \"Crawler Jobs\",\n          \"description\": \"Web crawling och innehållsextrahering från URL:er\"\n        },\n        \"inference\": {\n          \"title\": \"Inference Jobs\",\n          \"description\": \"AI-driven taggning och sammanfattning av innehåll\"\n        },\n        \"indexing\": {\n          \"title\": \"Indexeringsjobb\",\n          \"description\": \"Uppdateringar av sökindex\"\n        },\n        \"asset_preprocessing\": {\n          \"title\": \"Tillgång Förbehandlingsjobb\",\n          \"description\": \"Bild- och dokumentförbehandling (skärmbilder, textutvinning, etc.)\"\n        },\n        \"tidy_assets\": {\n          \"title\": \"Städa Upp Tillgångar Jobb\",\n          \"description\": \"Rensning av tillgångar och lagringsoptimering\"\n        },\n        \"video\": {\n          \"title\": \"Nedladdningsjobb för video\",\n          \"description\": \"Video extrahering och nedladdning\"\n        },\n        \"webhook\": {\n          \"title\": \"Webhook-jobb\",\n          \"description\": \"Externa webhook-aviseringar\"\n        },\n        \"feed\": {\n          \"title\": \"RSS-flödesjobb\",\n          \"description\": \"RSS-flödesbearbetning och innehållsuppdateringar\"\n        },\n        \"admin_maintenance\": {\n          \"title\": \"Admin Underhållsjobb\",\n          \"description\": \"Administrativ rensning och underhåll av tillgångar\"\n        }\n      },\n      \"monitor_and_manage\": \"Övervaka och hantera bakgrundsjobbköer och systembearbetningsuppgifter\",\n      \"active\": \"Aktiv\",\n      \"available_actions\": \"Tillgängliga åtgärder\",\n      \"status\": {\n        \"title\": \"Förstå Jobbstatusar\",\n        \"queued\": {\n          \"title\": \"Köad\",\n          \"description\": \"Jobb som väntar i kön på att behandlas. De startar automatiskt när resurser finns tillgängliga.\"\n        },\n        \"unprocessed\": {\n          \"title\": \"Obehandlade\",\n          \"description\": \"Bokmärken som ännu inte behandlats. De är troligen redan köade för behandling, om inte kan du behöva ställa dem i kö manuellt igen.\"\n        },\n        \"failed\": {\n          \"title\": \"Misslyckades\",\n          \"description\": \"Bokmärken som stött på fel under bearbetningen. Dessa kan behöva manuell uppmärksamhet.\"\n        }\n      },\n      \"actions\": {\n        \"recrawl_failed_links_only\": \"Sök om bara misslyckade länkar\",\n        \"recrawl_all_links\": \"Sök om alla länkar\",\n        \"without_inference\": \"Utan inferens\",\n        \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Generera om AI-taggar bara för misslyckade bokmärken\",\n        \"regenerate_ai_tags_for_all_bookmarks\": \"Generera om AI-taggar för alla bokmärken\",\n        \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Generera om AI-sammanfattningar bara för misslyckade bokmärken\",\n        \"regenerate_ai_summaries_for_all_bookmarks\": \"Generera om AI-sammanfattningar för alla bokmärken\",\n        \"reindex_all_bookmarks\": \"Indexera om alla bokmärken\",\n        \"clean_assets\": \"Rensa hängande tillgångar och synkronisera om metadata\",\n        \"reprocess_assets_fix_mode\": \"Behandla om obehandlade tillgångar\",\n        \"migrate_large_link_html_content\": \"Flytta stort HTML-innehåll i text till resurser\",\n        \"recrawl_pending_links_only\": \"Kryp bara om väntande länkar\",\n        \"regenerate_ai_tags_for_pending_bookmarks_only\": \"Generera AI-taggar bara för väntande bokmärken\",\n        \"regenerate_ai_summaries_for_pending_bookmarks_only\": \"Generera AI-sammanfattningar bara för väntande bokmärken\"\n      }\n    },\n    \"actions\": {\n      \"reindex_all_bookmarks\": \"Återindexera alla bokmärken\",\n      \"without_inference\": \"Utan inferens\",\n      \"regenerate_ai_tags_for_all_bookmarks\": \"Återskapa AI-taggar för alla bokmärken\",\n      \"reprocess_assets_fix_mode\": \"Bearbeta om tillgångar (fixläge)\",\n      \"recrawl_failed_links_only\": \"Genomsök endast misslyckade länkar igen\",\n      \"recrawl_all_links\": \"Omkryp alla länkar\",\n      \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Återskapa AI-taggar endast för misslyckade bokmärken\",\n      \"compact_assets\": \"Komprimera tillgångar\",\n      \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Återskapa AI-sammanfattningar endast för misslyckade bokmärken\",\n      \"regenerate_ai_summaries_for_all_bookmarks\": \"Återskapa AI-sammanfattningar för alla bokmärken\"\n    },\n    \"users_list\": {\n      \"reset_password\": \"Återställ lösenord\",\n      \"delete_user\": \"Radera användare\",\n      \"users_list\": \"Användarlista\",\n      \"local_user\": \"Lokal användare\",\n      \"confirm_password\": \"Bekräfta lösenord\",\n      \"create_user\": \"Skapa användare\",\n      \"change_role\": \"Ändra roll\",\n      \"num_bookmarks\": \"Antal bokmärken\",\n      \"asset_sizes\": \"Tillgångsstorlekar\",\n      \"delete_user_confirm_description\": \"Är du säker på att du vill ta bort användaren \\\"{{name}}\\\"?\",\n      \"unlimited\": \"Obegränsat\"\n    },\n    \"service_connections\": {\n      \"title\": \"Serviceanslutningar\",\n      \"description\": \"Övervaka hälsan och anslutningen för externa systemberoenden\",\n      \"search_engine\": \"Sökmotor\",\n      \"browser\": \"Webbläsare\",\n      \"queue_system\": \"Kösystem\",\n      \"status\": {\n        \"not_configured\": \"Inte konfigurerad\",\n        \"connected\": \"Ansluten\",\n        \"disconnected\": \"Frånkopplad\"\n      }\n    },\n    \"admin_tools\": {\n      \"admin_tools\": \"Adminverktyg\",\n      \"bookmark_debugger\": \"Bokmärkesfelsökare\",\n      \"bookmark_id\": \"Bokmärkes-ID\",\n      \"bookmark_id_placeholder\": \"Ange bokmärkes-ID\",\n      \"lookup\": \"Slå upp\",\n      \"debug_info\": \"Felsökningsinformation\",\n      \"basic_info\": \"Grundläggande information\",\n      \"status\": \"Status\",\n      \"content\": \"Innehåll\",\n      \"html_preview\": \"HTML-förhandsvisning (första 1000 tecknen)\",\n      \"summary\": \"Sammanfattning\",\n      \"url\": \"URL\",\n      \"source_url\": \"Käll-URL\",\n      \"asset_type\": \"Tillgångstyp\",\n      \"file_name\": \"Filnamn\",\n      \"owner_user_id\": \"Ägar-ID\",\n      \"tagging_status\": \"Taggstatus\",\n      \"summarization_status\": \"Summeringsstatus\",\n      \"crawl_status\": \"Crawlningsstatus\",\n      \"crawl_status_code\": \"HTTP-statuskod\",\n      \"crawled_at\": \"Genomsöktes\",\n      \"recrawl\": \"Genomsök igen\",\n      \"reindex\": \"Indexera om\",\n      \"retag\": \"Tagga om\",\n      \"resummarize\": \"Sammanfatta om\",\n      \"bookmark_not_found\": \"Bokmärket hittades inte\",\n      \"action_success\": \"Åtgärden slutfördes\",\n      \"action_failed\": \"Åtgärden misslyckades\",\n      \"recrawl_queued\": \"Jobbet för att genomsöka igen har köats\",\n      \"reindex_queued\": \"Jobbet för att indexera om har köats\",\n      \"retag_queued\": \"Jobbet för att tagga om har köats\",\n      \"resummarize_queued\": \"Jobbet för att sammanfatta om har köats\",\n      \"view\": \"Visa\",\n      \"fetch_error\": \"Fel vid hämtning av bokmärke\"\n    }\n  },\n  \"options\": {\n    \"light_mode\": \"Ljust läge\",\n    \"dark_mode\": \"Mörkt läge\",\n    \"apps_extensions\": \"Appar och tillägg\",\n    \"documentation\": \"Dokumentation\",\n    \"follow_us_on_x\": \"Följ oss på X\"\n  },\n  \"lists\": {\n    \"new_nested_list\": \"Ny nästlad lista\",\n    \"all_lists\": \"Alla listor\",\n    \"favourites\": \"Favoriter\",\n    \"new_list\": \"Ny lista\",\n    \"search_query_help\": \"Lär dig mer om sökspråket.\",\n    \"edit_list\": \"Redigera lista\",\n    \"smart_list\": \"Smart lista\",\n    \"list_type\": \"Listtyp\",\n    \"manual_list\": \"Manuell lista\",\n    \"parent_list\": \"Föräldralista\",\n    \"no_parent\": \"Ingen förälder\",\n    \"search_query\": \"Sökfråga\",\n    \"no_destination\": \"Ingen destination\",\n    \"description\": \"Beskrivning (Valfritt)\",\n    \"merge_list\": \"Slå samman lista\",\n    \"destination_list\": \"Destinationslista\",\n    \"delete_after_merge\": \"Ta bort originallistan efter sammanslagning\",\n    \"share_list\": \"Dela lista\",\n    \"rss\": {\n      \"title\": \"RSS-flöde\",\n      \"description\": \"Aktivera ett RSS-flöde för den här listan\",\n      \"feed_url\": \"URL för RSS-flöde\"\n    },\n    \"public_list\": {\n      \"title\": \"Offentlig lista\",\n      \"description\": \"Tillåt andra att visa den här listan\",\n      \"share_link\": \"Dela länk\"\n    },\n    \"delete_list\": {\n      \"title\": \"Ta bort lista\",\n      \"description\": \"Om du tar bort en lista tas inga bokmärken i den listan bort.\",\n      \"delete_children\": \"Ta bort underordnade listor (rekursivt)\",\n      \"delete_children_description\": \"Om den inte är markerad kommer alla direkt underordnade listor att bli rotlistor\"\n    },\n    \"shared\": \"Delad\",\n    \"collaborators\": {\n      \"manage\": \"Hantera medarbetare\",\n      \"view\": \"Visa medarbetare\",\n      \"collaborators\": \"Medarbetare\",\n      \"add\": \"Lägg till medarbetare\",\n      \"current\": \"Nuvarande medarbetare\",\n      \"enter_email\": \"Ange e-postadress\",\n      \"please_enter_email\": \"Vänligen ange en e-postadress\",\n      \"added_successfully\": \"Medarbetare tillagd\",\n      \"failed_to_add\": \"Det gick inte att lägga till medarbetare\",\n      \"removed\": \"Medarbetare borttagen\",\n      \"failed_to_remove\": \"Det gick inte att ta bort medarbetare\",\n      \"role_updated\": \"Roll uppdaterad\",\n      \"failed_to_update_role\": \"Det gick inte att uppdatera rollen\",\n      \"viewer\": \"Visare\",\n      \"editor\": \"Redigerare\",\n      \"owner\": \"Ägare\",\n      \"viewer_description\": \"Kan visa bokmärken i listan\",\n      \"editor_description\": \"Kan lägga till och ta bort bokmärken\",\n      \"no_collaborators\": \"Inga medarbetare ännu. Lägg till någon för att börja samarbeta!\",\n      \"no_collaborators_readonly\": \"Inga samarbetspartners för den här listan.\",\n      \"people_with_access\": \"Personer som har tillgång till den här listan\",\n      \"add_or_remove\": \"Lägg till eller ta bort personer som kan komma åt den här listan\",\n      \"invitation_sent\": \"Inbjudan skickad!\",\n      \"invitation_revoked\": \"Inbjudan återkallad\",\n      \"failed_to_revoke\": \"Kunde inte återkalla inbjudan\",\n      \"pending\": \"Väntar\",\n      \"revoke\": \"Återkalla\",\n      \"declined\": \"Nekad\"\n    },\n    \"leave_list\": {\n      \"title\": \"Lämna lista\",\n      \"confirm_message\": \"Är du säker på att du vill lämna {{icon}} {{name}}?\",\n      \"warning\": \"Du kommer inte längre att kunna visa eller komma åt bokmärken i den här listan. Listans ägare kan lägga till dig igen om det behövs.\",\n      \"action\": \"Lämna lista\",\n      \"success\": \"Du har lämnat \\\"{{icon}} {{name}}\\\"\"\n    },\n    \"invitations\": {\n      \"pending\": \"Väntande inbjudningar\",\n      \"description\": \"Granska och svara på inbjudningar för listsamarbete\",\n      \"invited_by\": \"Inbjuden av\",\n      \"accept\": \"Acceptera\",\n      \"decline\": \"Neka\",\n      \"accepted\": \"Inbjudan är accepterad\",\n      \"declined\": \"Inbjudan är nekad\",\n      \"failed_to_accept\": \"Kunde inte acceptera inbjudan\",\n      \"failed_to_decline\": \"Kunde inte neka inbjudan\"\n    },\n    \"shared_lists\": \"Delade listor\"\n  },\n  \"tags\": {\n    \"drag_and_drop_merging_info\": \"Dra och släpp taggar på varandra för att sammanfoga dem\",\n    \"delete_all_unused_tags\": \"Radera alla oanvända taggar\",\n    \"all_tags\": \"Alla taggar\",\n    \"unused_tags\": \"Oanvända taggar\",\n    \"sort_by_name\": \"Sortera efter namn\",\n    \"your_tags\": \"Dina taggar\",\n    \"ai_tags\": \"AI-taggar\",\n    \"drag_and_drop_merging\": \"Dra och släpp-sammanslagning\",\n    \"your_tags_info\": \"Taggar som du har bifogat minst en gång\",\n    \"ai_tags_info\": \"Taggar som bara lades till automatiskt (av AI)\",\n    \"unused_tags_info\": \"Taggar som inte är kopplade till några bokmärken\",\n    \"create_tag\": \"Skapa tagg\",\n    \"create_tag_description\": \"Skapa en ny tagg utan att koppla den till ett bokmärke\",\n    \"tag_name\": \"Taggnamn\",\n    \"enter_tag_name\": \"Ange taggnamn\",\n    \"sort_by_usage\": \"Sortera efter användning\",\n    \"sort_by_relevance\": \"Sortera efter relevans\",\n    \"no_custom_tags\": \"Inga anpassade taggar ännu\",\n    \"no_ai_tags\": \"Inga AI-taggar ännu\",\n    \"no_unused_tags\": \"Du har inga oanvända taggar\",\n    \"no_unused_tags_match_your_search\": \"Inga oanvända taggar matchar din sökning\",\n    \"no_tags_match_your_search\": \"Inga taggar matchar din sökning\",\n    \"search_placeholder\": \"Sök taggar...\",\n    \"search_or_create_placeholder\": \"Sök eller skapa taggar...\"\n  },\n  \"editor\": {\n    \"import_as_separate_bookmarks\": \"Importera som separata bokmärken\",\n    \"import_as_text\": \"Importera som textbokmärke\",\n    \"placeholder\": \"Klistra in en länk eller bild, skriv en anteckning eller dra och släpp en bild här...\",\n    \"new_item\": \"NY POST\",\n    \"multiple_urls_dialog_title\": \"Importera URL:er som separata bokmärken?\",\n    \"multiple_urls_dialog_desc\": \"Inmatningen innehåller flera URL:er på separata rader. Vill du importera dem som separata bokmärken?\",\n    \"text_toolbar\": {\n      \"code\": \"Kod\",\n      \"markdown_shortcuts\": {\n        \"italic\": {\n          \"label\": \"Kursiv\",\n          \"example\": \"*Kursiv* eller _Kursiv_ eller CTRL+i\"\n        },\n        \"inline_code\": {\n          \"example\": \"`Kod`\",\n          \"label\": \"Inbäddad kod\"\n        },\n        \"unordered_list\": {\n          \"example\": \"- Listobjekt\",\n          \"label\": \"Osorterad lista\"\n        },\n        \"blockquote\": {\n          \"label\": \"Blockcitat\",\n          \"example\": \"> Blockcitat\"\n        },\n        \"block_code\": {\n          \"label\": \"Blockkod\",\n          \"example\": \"``` + mellanslag\"\n        },\n        \"label\": \"Genvägar för Markdown\",\n        \"heading\": {\n          \"label\": \"Rubrik\",\n          \"example\": \"# H1, ## H2, ### H3\"\n        },\n        \"bold\": {\n          \"label\": \"Fet\",\n          \"example\": \"**text** eller CTRL+b\"\n        },\n        \"ordered_list\": {\n          \"label\": \"Ordnad lista\",\n          \"example\": \"1. Listobjekt\"\n        }\n      },\n      \"undo\": \"Ångra\",\n      \"redo\": \"Gör om\",\n      \"bold\": \"Fet\",\n      \"italic\": \"Kursiv\",\n      \"underline\": \"Understrykning\",\n      \"strikethrough\": \"Genomstruken\",\n      \"highlight\": \"Markera\",\n      \"align_left\": \"Vänsterjustera\",\n      \"align_center\": \"Centrera\",\n      \"align_right\": \"Högerjustera\"\n    },\n    \"disabled_submissions\": \"Inlämningar är inaktiverade\",\n    \"quickly_focus\": \"Du kan snabbt fokusera på det här fältet genom att trycka på ⌘ + E\",\n    \"placeholder_v2\": \"Klistra in en länk, skriv en anteckning eller släpp en bild…\"\n  },\n  \"toasts\": {\n    \"bookmarks\": {\n      \"updated\": \"Bokmärket har uppdaters!\",\n      \"full_page_archive\": \"Skapande av fullsidesarkiv har startats\",\n      \"deleted\": \"Bokmärket har raderats!\",\n      \"delete_from_list\": \"Bokmärket har raderats från listan\",\n      \"clipboard_copied\": \"Länken har lags till i ditt urklipp!\",\n      \"refetch\": \"Hämtning har köats!\",\n      \"preserve_pdf\": \"PDF-sparande har triggats\",\n      \"update_banner\": \"Banderollen har uppdaterats!\",\n      \"uploading_banner\": \"Laddar upp banderoll...\"\n    },\n    \"lists\": {\n      \"created\": \"Listan har skapats!\",\n      \"updated\": \"Listan har uppdaterats!\",\n      \"merged\": \"Listan har slagits samman!\",\n      \"deleted\": \"Listan har tagits bort!\"\n    },\n    \"tags\": {\n      \"created\": \"Taggen har skapats!\",\n      \"failed_to_create\": \"Kunde inte skapa tagg\"\n    }\n  },\n  \"cleanups\": {\n    \"duplicate_tags\": {\n      \"title\": \"Dublettaggar\",\n      \"merge_all_suggestions\": \"Slå ihop alla förslag?\"\n    },\n    \"cleanups\": \"Rensningar\"\n  },\n  \"preview\": {\n    \"view_original\": \"Visa orginal\",\n    \"cached_content\": \"Cachelagrat innehåll\",\n    \"reader_view\": \"Läsarvy\",\n    \"tabs\": {\n      \"content\": \"Innehåll\",\n      \"details\": \"Detaljer\"\n    },\n    \"archive_info\": \"Arkiv kanske inte återges korrekt inbäddade om de kräver Javascript. För bästa resultat, <1>ladda ner den och öppna den i din webbläsare</1>.\",\n    \"fetch_error_title\": \"Innehållet är inte tillgängligt\",\n    \"fetch_error_description\": \"Vi kunde inte hämta innehållet för den här länken. Sidan kan vara skyddad, kräva autentisering eller vara tillfälligt otillgänglig.\",\n    \"crawling_in_progress\": \"Hämtar sidans innehåll …\",\n    \"continue_reading\": \"Fortsätt där du slutade\",\n    \"continue_reading_percent\": \"Fortsätt där du slutade ({{percent}} %)\",\n    \"continue_button\": \"Fortsätt\"\n  },\n  \"dialogs\": {\n    \"bookmarks\": {\n      \"delete_confirmation_title\": \"Ta bort bokmärke?\",\n      \"delete_confirmation_description\": \"Är du säker på att du vill ta bort det här bokmärket?\"\n    }\n  },\n  \"search\": {\n    \"is_archived\": \"Är arkiverad\",\n    \"is_not_archived\": \"Är inte arkiverad\",\n    \"is_in_any_list\": \"Finns i någon lista\",\n    \"is_not_in_any_list\": \"Finns inte i någon lista\",\n    \"or\": \"Eller\",\n    \"is_not_favorited\": \"Är inte favoritmarkerad\",\n    \"is_favorited\": \"Är favoriterad\",\n    \"has_any_tag\": \"Har någon tagg\",\n    \"has_no_tags\": \"Har ingen tagg\",\n    \"url_contains\": \"URL innehåller\",\n    \"has_tag\": \"Har tagg\",\n    \"does_not_have_tag\": \"Har inte taggen\",\n    \"full_text_search\": \"Fulltextsökning\",\n    \"type_is\": \"Typ är\",\n    \"is_in_list\": \"Finns i lista\",\n    \"is_not_in_list\": \"Finns inte i listan\",\n    \"and\": \"Och\",\n    \"created_on_or_before\": \"Skapad på eller före\",\n    \"created_on_or_after\": \"Skapad på eller efter\",\n    \"not_created_on_or_after\": \"Inte skapad på eller efter\",\n    \"not_created_on_or_before\": \"Inte skapad på eller före\",\n    \"url_does_not_contain\": \"URL innehåller inte\",\n    \"type_is_not\": \"Typen är inte\",\n    \"is_from_feed\": \"Kommer från RSS-flöde\",\n    \"is_not_from_feed\": \"Kommer inte från RSS-flöde\",\n    \"created_within\": \"Skapad inom\",\n    \"created_earlier_than\": \"Skapad tidigare än\",\n    \"day_s\": \" Dagar\",\n    \"week_s\": \" Veckor\",\n    \"month_s\": \" Månader\",\n    \"year_s\": \" År\",\n    \"day_s_ago\": \" Dagar sedan\",\n    \"week_s_ago\": \" Veckor sedan\",\n    \"month_s_ago\": \" Månader sedan\",\n    \"year_s_ago\": \" År sedan\",\n    \"history\": \"Senaste sökningar\",\n    \"title_contains\": \"Titeln innehåller\",\n    \"title_does_not_contain\": \"Titeln innehåller inte\",\n    \"is_broken_link\": \"Har trasig länk\",\n    \"tags\": \"Taggar\",\n    \"no_suggestions\": \"Inga förslag\",\n    \"filters\": \"Filter\",\n    \"is_not_broken_link\": \"Har fungerande länk\",\n    \"lists\": \"Listor\",\n    \"feeds\": \"Feeds\",\n    \"is_from_source\": \"Källa är\",\n    \"is_not_from_source\": \"Källa är inte\"\n  },\n  \"highlights\": {\n    \"no_highlights\": \"Du har inga markeringar ännu.\"\n  },\n  \"banners\": {\n    \"no_bookmarks\": {\n      \"title\": \"Inga bokmärken än\",\n      \"description\": \"Spara intressanta artiklar, länkar och sidor för att snabbt komma åt dem senare.\"\n    }\n  },\n  \"bookmark_editor\": {\n    \"title\": \"Redigera bokmärke\",\n    \"subtitle\": \"Gör ändringar i bokmärkesinformationen. Klicka på spara när du är klar.\",\n    \"author\": \"Författare\",\n    \"publisher\": \"Utgivare\",\n    \"date_published\": \"Publiceringsdatum\",\n    \"pick_a_date\": \"Välj ett datum\",\n    \"save_changes\": \"Spara ändringar\",\n    \"extracted_content\": \"Extraherat innehåll\"\n  },\n  \"view_options\": {\n    \"title\": \"Visa alternativ\",\n    \"layout\": \"Layout\",\n    \"columns\": \"Kolumner\",\n    \"display_options\": \"Visningsalternativ\",\n    \"show_note_previews\": \"Visa anteckningar\",\n    \"show_tags\": \"Visa taggar\",\n    \"show_title\": \"Visa titel\",\n    \"image_options\": \"Bildalternativ\",\n    \"image_fit_cover\": \"Täck (Fyll)\",\n    \"image_fit_contain\": \"Innehåll (Passa)\"\n  },\n  \"version\": {\n    \"new_release_available\": \"Nya versionsanmärkningar tillgängliga\",\n    \"whats_new_title\": \"Vad är nytt i v{{version}}\",\n    \"release_notes_description\": \"Här är de senaste uppdateringarna hämtade från GitHub versionsanmärkningar.\",\n    \"loading_release_notes\": \"Läser in versionsanmärkningar…\",\n    \"unable_to_load_release_notes\": \"Det gick inte att läsa in versionsanmärkningar just nu. Försök igen senare.\",\n    \"no_release_notes\": \"Inga versionsanmärkningar publicerades för den här versionen.\",\n    \"release_notes_synced\": \"Versionsanmärkningar synkroniseras från GitHub.\",\n    \"view_on_github\": \"Visa på GitHub\"\n  },\n  \"wrapped\": {\n    \"title\": \"Din sammanfattning för {{year}}\",\n    \"subtitle\": \"Ett år i Karakeep\",\n    \"banner\": {\n      \"title\": \"Din sammanfattning för 2025 är klar!\",\n      \"description\": \"Se ditt år i bokmärken\",\n      \"view_now\": \"Visa nu\"\n    },\n    \"button\": \"Sammanfattning för 2025\",\n    \"loading\": \"Läser in din sammanfattning...\",\n    \"failed_to_load\": \"Det gick inte att läsa in din sammanfattningsstatistik\",\n    \"sections\": {\n      \"total_saves\": {\n        \"prefix\": \"Du sparade\",\n        \"suffix\": \"Saker detta år\",\n        \"suffix_singular\": \"Sak detta år\"\n      },\n      \"first_bookmark\": {\n        \"title\": \"Din resa började\",\n        \"description\": \"Första sparningen under {{year}}:\"\n      },\n      \"top_domains\": \"Dina populäraste webbplatser\",\n      \"top_tags\": \"Dina populäraste taggar\",\n      \"monthly_activity\": \"Ditt sparår\",\n      \"most_active_day\": \"Din mest aktiva dag\",\n      \"peak_times\": {\n        \"title\": \"När du sparar\",\n        \"peak_hour\": \"Topptimme\",\n        \"peak_day\": \"Toppdag\"\n      },\n      \"how_you_save\": \"Så här sparar du\",\n      \"what_you_saved\": \"Det här har du sparat\",\n      \"summary\": {\n        \"favorites\": \"Favoriter\",\n        \"tags_created\": \"Taggar som skapats\",\n        \"highlights\": \"Höjdpunkter\"\n      },\n      \"types\": {\n        \"links\": \"Länkar\",\n        \"notes\": \"Anteckningar\",\n        \"assets\": \"Tillgångar\"\n      }\n    },\n    \"footer\": \"Gjord med Karakeep\",\n    \"share\": \"Dela\",\n    \"download\": \"Ladda ner\",\n    \"close\": \"Stäng\",\n    \"generating\": \"Genererar...\"\n  }\n}\n"
  },
  {
    "path": "apps/web/lib/i18n/locales/tr/translation.json",
    "content": "{\n  \"common\": {\n    \"url\": \"URL\",\n    \"name\": \"İsim\",\n    \"email\": \"E-posta\",\n    \"password\": \"Şifre\",\n    \"action\": \"Eylem\",\n    \"actions\": \"Eylemler\",\n    \"created_at\": \"Oluşturulma Tarihi\",\n    \"key\": \"Anahtar\",\n    \"role\": \"Rol\",\n    \"roles\": {\n      \"user\": \"Kullanıcı\",\n      \"admin\": \"Yönetici\"\n    },\n    \"something_went_wrong\": \"Bir şeyler ters gitti\",\n    \"experimental\": \"Deneysel\",\n    \"search\": \"Ara\",\n    \"tags\": \"Etiketler\",\n    \"note\": \"Not\",\n    \"attachments\": \"Ekler\",\n    \"highlights\": \"Öne Çıkanlar\",\n    \"source\": \"Kaynak\",\n    \"screenshot\": \"Ekran Görüntüsü\",\n    \"video\": \"Video\",\n    \"archive\": \"Arşiv\",\n    \"home\": \"Ana Sayfa\",\n    \"size\": \"Boyut\",\n    \"bookmark_types\": {\n      \"text\": \"Metin\",\n      \"link\": \"Bağlantı\",\n      \"media\": \"Medya\",\n      \"title\": \"Yer İşareti Türü\"\n    },\n    \"type\": \"Yaz\",\n    \"updated_at\": \"Güncellenme Zamanı\",\n    \"title\": \"Başlık\",\n    \"description\": \"Açıklama\",\n    \"summary\": \"Özet\",\n    \"quota\": \"Kota\",\n    \"bookmarks\": \"Yer İmleri\",\n    \"storage\": \"Depolama\",\n    \"pdf\": \"Arşivlenmiş PDF\",\n    \"default\": \"Varsayılan\",\n    \"id\": \"Kimlik\",\n    \"last_used\": \"Son Kullanılan\"\n  },\n  \"layouts\": {\n    \"masonry\": \"Döşeme\",\n    \"grid\": \"Izgara\",\n    \"list\": \"Liste\",\n    \"compact\": \"Kompakt\"\n  },\n  \"actions\": {\n    \"change_layout\": \"Düzeni Değiştir\",\n    \"archive\": \"Arşivle\",\n    \"unarchive\": \"Arşivden Çıkar\",\n    \"favorite\": \"Favorile\",\n    \"unfavorite\": \"Favorilerden Kaldır\",\n    \"delete\": \"Sil\",\n    \"refresh\": \"Yenile\",\n    \"recrawl\": \"Tekrar Tara\",\n    \"download_full_page_archive\": \"Tüm Sayfa Arşivini İndir\",\n    \"edit_tags\": \"Etiketleri Düzenle\",\n    \"add_to_list\": \"Listeye Ekle\",\n    \"select_all\": \"Tümünü Seç\",\n    \"unselect_all\": \"Seçimi Kaldır\",\n    \"copy_link\": \"Bağlantıyı Kopyala\",\n    \"close_bulk_edit\": \"Toplu Düzenlemeyi Kapat\",\n    \"bulk_edit\": \"Toplu Düzenleme\",\n    \"manage_lists\": \"Listeleri Yönet\",\n    \"remove_from_list\": \"Listeden Kaldır\",\n    \"save\": \"Kaydet\",\n    \"add\": \"Ekle\",\n    \"edit\": \"Düzenle\",\n    \"create\": \"Oluştur\",\n    \"fetch_now\": \"Şimdi Getir\",\n    \"summarize_with_ai\": \"Yapay Zeka ile Özetle\",\n    \"edit_title\": \"Başlığı Düzenle\",\n    \"sign_out\": \"Oturumu Kapat\",\n    \"close\": \"Kapat\",\n    \"merge\": \"Birleştir\",\n    \"cancel\": \"İptal\",\n    \"apply_all\": \"Hepsine Uygula\",\n    \"ignore\": \"Yoksay\",\n    \"sort\": {\n      \"title\": \"Sırala\",\n      \"newest_first\": \"En yeni önce\",\n      \"oldest_first\": \"En eski önce\",\n      \"relevant_first\": \"En İlgili Olan Önce\"\n    },\n    \"open_editor\": \"Düzenleyiciyi Aç\",\n    \"toggle_show_archived\": \"Arşivlenmiş Olanları Göster\",\n    \"confirm\": \"Onayla\",\n    \"regenerate\": \"Yeniden oluştur\",\n    \"load_more\": \"Daha Fazla Yükle\",\n    \"edit_notes\": \"Notları Düzenle\",\n    \"preserve_as_pdf\": \"PDF olarak sakla\",\n    \"offline_copies\": \"Çevrimdışı Kopyalar\",\n    \"preserve_offline_archive\": \"Çevrimdışı Arşivi Koru\",\n    \"download_full_page_archive_file\": \"Arşiv Dosyasını İndir\",\n    \"download_pdf_file\": \"PDF Dosyasını İndir\",\n    \"remove\": \"Kaldır\",\n    \"more\": \"Daha fazla\",\n    \"replace_banner\": \"Başlığı Değiştir\",\n    \"add_banner\": \"Başlık Ekle\",\n    \"download\": \"İndir\"\n  },\n  \"highlights\": {\n    \"no_highlights\": \"Henüz hiçbir öne çıkarılmış içeriğiniz yok.\"\n  },\n  \"settings\": {\n    \"back_to_app\": \"Uygulamaya Geri Dön\",\n    \"user_settings\": \"Kullanıcı Ayarları\",\n    \"info\": {\n      \"user_info\": \"Kullanıcı Bilgileri\",\n      \"basic_details\": \"Temel Bilgiler\",\n      \"change_password\": \"Şifre Değiştir\",\n      \"current_password\": \"Mevcut Şifre\",\n      \"new_password\": \"Yeni Şifre\",\n      \"confirm_new_password\": \"Yeni Şifreyi Onaylayın\",\n      \"options\": \"Seçenekler\",\n      \"interface_lang\": \"Arayüz Dili\",\n      \"user_settings\": {\n        \"user_settings_updated\": \"Kullanıcı ayarları güncellendi!\",\n        \"bookmark_click_action\": {\n          \"title\": \"Yer İşareti Tıklama Eylemi\",\n          \"open_external_url\": \"Orijinal URL'yi Aç\",\n          \"open_bookmark_details\": \"Yer İşareti Ayrıntılarını Aç\"\n        },\n        \"archive_display_behaviour\": {\n          \"title\": \"Arşivlenmiş Yer İmleri\",\n          \"show\": \"Arşivlenmiş yer imlerini etiketlerde ve listelerde göster\",\n          \"hide\": \"Arşivlenmiş yer imlerini etiketlerde ve listelerde gizle\"\n        }\n      },\n      \"reader_settings\": {\n        \"local_overrides_title\": \"Cihaza özel ayarlar etkin\",\n        \"using_default\": \"İstemci varsayılanı kullanılıyor\",\n        \"clear_override_hint\": \"Genel ayarı ({{value}}) kullanmak için cihaz geçersiz kılmasını temizle\",\n        \"font_size\": \"Yazı Tipi Boyutu\",\n        \"font_family\": \"Yazı Tipi Ailesi\",\n        \"preview_inline\": \"(önizleme)\",\n        \"tooltip_preview\": \"Kaydedilmemiş önizleme değişiklikleri\",\n        \"save_to_all_devices\": \"Tüm cihazlar\",\n        \"tooltip_local\": \"Cihaz ayarları, genel ayarlardan farklı\",\n        \"reset_preview\": \"Önizlemeyi sıfırla\",\n        \"mono\": \"Tek Aralık\",\n        \"line_height\": \"Satır Yüksekliği\",\n        \"tooltip_default\": \"Okuma ayarları\",\n        \"title\": \"Okuyucu Ayarları\",\n        \"serif\": \"Serif\",\n        \"preview\": \"Önizleme\",\n        \"not_set\": \"Ayarlanmadı\",\n        \"clear_local_overrides\": \"Cihaz ayarlarını temizle\",\n        \"preview_text\": \"Hızlı kahverengi tilki tembel köpeğin üzerinden atlar. Okuyucu görünümü metniniz bu şekilde görünecek.\",\n        \"local_overrides_cleared\": \"Cihaza özel ayarlar temizlendi\",\n        \"local_overrides_description\": \"Bu cihaz, genel varsayılanlarınızdan farklı okuyucu ayarlarına sahiptir:\",\n        \"clear_defaults\": \"Tüm varsayılanları temizle\",\n        \"description\": \"Okuyucu görünümü için varsayılan metin ayarlarını yapılandır. Bu ayarlar tüm cihazlarınızda senkronize edilir.\",\n        \"defaults_cleared\": \"Okuyucu varsayılanları temizlendi\",\n        \"save_hint\": \"Ayarları yalnızca bu cihaz için kaydet veya tüm cihazlarda senkronize et\",\n        \"save_as_default\": \"Varsayılan olarak kaydet\",\n        \"save_to_device\": \"Bu cihaz\",\n        \"sans\": \"Sans Serif\",\n        \"tooltip_preview_and_local\": \"Kaydedilmemiş önizleme değişiklikleri; cihaz ayarları genel ayarlardan farklı\",\n        \"adjust_hint\": \"Değişiklikleri önizlemek için yukarıdaki ayarları düzenle\"\n      },\n      \"avatar\": {\n        \"upload\": \"Avatar yükle\",\n        \"change\": \"Avatarı değiştir\",\n        \"remove_confirm_title\": \"Avatarı kaldırılsın mı?\",\n        \"updated\": \"Avatar güncellendi\",\n        \"removed\": \"Avatar silindi\",\n        \"description\": \"Avatarınız olarak kullanmak için kare bir resim yükleyin.\",\n        \"remove_confirm_description\": \"Bu, mevcut profil fotoğrafınızı temizleyecek.\",\n        \"title\": \"Profil Fotoğrafı\",\n        \"remove\": \"Avatarı kaldır\"\n      }\n    },\n    \"ai\": {\n      \"ai_settings\": \"Yapay Zeka Ayarları\",\n      \"tagging_rules\": \"Etiketleme Kuralları\",\n      \"tagging_rule_description\": \"Buraya eklediğiniz istemler, etiket oluşturma sırasında modele ek kurallar olarak dahil edilecektir. Son istemleri, istem önizleme bölümünde görebilirsiniz.\",\n      \"prompt_preview\": \"İstem Önizlemesi\",\n      \"text_prompt\": \"Metin İstemi\",\n      \"images_prompt\": \"Görseller İstemi\",\n      \"image_tagging\": \"Resim Etiketleme\",\n      \"summarization_prompt\": \"Özetleme İstemi\",\n      \"all_tagging\": \"Tüm Etiketleme\",\n      \"text_tagging\": \"Metin Etiketleme\",\n      \"summarization\": \"Özetleme\",\n      \"tag_style\": \"Etiket Stili\",\n      \"auto_summarization_description\": \"Yapay zeka kullanarak yer işaretlerin için otomatik olarak özet oluştur.\",\n      \"auto_tagging\": \"Otomatik etiketleme\",\n      \"titlecase_spaces\": \"Büyük harf ve boşluklu\",\n      \"lowercase_underscores\": \"Küçük harf ve alt çizgili\",\n      \"inference_language\": \"Çıkarım Dili\",\n      \"titlecase_hyphens\": \"Büyük harf ve tireli\",\n      \"lowercase_hyphens\": \"Küçük harf ve tireli\",\n      \"lowercase_spaces\": \"Küçük harf ve boşluklu\",\n      \"inference_language_description\": \"Yapay zeka tarafından oluşturulan etiketler ve özetler için dili seç.\",\n      \"tag_style_description\": \"Otomatik oluşturulan etiketlerinin nasıl biçimlendirileceğini seç.\",\n      \"auto_tagging_description\": \"Yapay zeka kullanarak yer işaretlerin için otomatik olarak etiket oluştur.\",\n      \"camelCase\": \"camelCase\",\n      \"auto_summarization\": \"Otomatik özetleme\",\n      \"no_preference\": \"Tercih Yok\",\n      \"curated_tags\": \"Seçilmiş Etiketler\",\n      \"curated_tags_description\": \"İsteğe bağlı olarak, AI etiketlemesini yalnızca bu listedeki etiketleri kullanacak şekilde kısıtlayın. Hiçbir etiket seçilmediğinde, AI etiketleri serbestçe oluşturur.\",\n      \"curated_tags_updated\": \"Seçilmiş etiketler başarıyla güncellendi!\",\n      \"curated_tags_update_failed\": \"Seçilmiş etiketler güncellenemedi\"\n    },\n    \"feeds\": {\n      \"rss_subscriptions\": \"RSS Abonelikleri\",\n      \"add_a_subscription\": \"Bir Abonelik Ekle\",\n      \"feed_enabled\": \"RSS Beslemesi etkin\",\n      \"feed_disabled\": \"RSS Beslemesi devre dışı\"\n    },\n    \"import\": {\n      \"import_export\": \"İçe Aktar / Dışa Aktar\",\n      \"import_export_bookmarks\": \"Yer İşaretlerini İçe / Dışa Aktar\",\n      \"import_bookmarks_from_html_file\": \"HTML Dosyasından Yer İşaretlerini İçe Aktar\",\n      \"import_bookmarks_from_pocket_export\": \"Pocket Dışa Aktarımından Yer İşaretlerini İçe Aktar\",\n      \"import_bookmarks_from_matter_export\": \"Matter Dışa Aktarımından Yer İşaretlerini İçe Aktar\",\n      \"import_bookmarks_from_omnivore_export\": \"Omnivore Dışa Aktarımından Yer İşaretlerini İçe Aktar\",\n      \"import_bookmarks_from_karakeep_export\": \"Karakeep Dışa Aktarımından Yer İşaretlerini İçe Aktar\",\n      \"export_links_and_notes\": \"Bağlantı ve Notları Dışa Aktar\",\n      \"imported_bookmarks\": \"İçe Aktarılan Yer İşaretleri\",\n      \"import_bookmarks_from_linkwarden_export\": \"Linkwarden dışa aktarımından Yer İşaretlerini İçe Aktar\",\n      \"import_bookmarks_from_tab_session_manager_export\": \"Sekme Oturum Yöneticisi'nden Yer İşaretlerini İçe Aktar\",\n      \"import_bookmarks_from_mymind_export\": \"Mymind dışa aktarımından Yer İşaretlerini İçe Aktar\",\n      \"import_bookmarks_from_instapaper_export\": \"Instapaper dışa aktarımından Yer İşaretlerini İçe Aktar\"\n    },\n    \"api_keys\": {\n      \"api_keys\": \"API Anahtarları\",\n      \"new_api_key\": \"Yeni API Anahtarı\",\n      \"new_api_key_desc\": \"API anahtarınıza benzersiz bir ad verin\",\n      \"key_success\": \"Anahtar başarıyla oluşturuldu\",\n      \"key_success_please_copy\": \"Lütfen anahtarı kopyalayın ve güvenli bir yerde saklayın. Diyaloğu kapattıktan sonra tekrar erişemezsiniz.\",\n      \"regenerate_api_key\": \"API Anahtarını Yeniden Oluştur\",\n      \"key_regenerated\": \"Anahtar başarıyla yeniden oluşturuldu\",\n      \"key_regenerated_please_copy\": \"Lütfen yeni anahtarı kopyalayın ve güvenli bir yerde saklayın. Eski anahtar iptal edildi ve artık çalışmayacak.\",\n      \"regenerate_warning\": \"\\\"{{name}}\\\" API anahtarını yeniden oluşturmak istediğinizden emin misiniz?\",\n      \"regenerate_confirmation\": \"Bu, mevcut anahtarı iptal edecek ve yeni bir tane oluşturacaktır. Mevcut anahtarı kullanan uygulamaların hiçbiri çalışmayacak.\"\n    },\n    \"broken_links\": {\n      \"broken_links\": \"Bozuk Bağlantılar\",\n      \"last_crawled_at\": \"Son Tarama Tarihi\",\n      \"crawling_status\": \"Tarama Durumu\",\n      \"crawling_failed\": \"Tarama Başarısız Oldu\"\n    },\n    \"webhooks\": {\n      \"edit_auth_token\": \"Kimlik Doğrulama Jetonunu Düzenle\",\n      \"create_webhook\": \"Web Kancası Oluştur\",\n      \"delete_webhook\": \"Web Kancasını Sil\",\n      \"delete_webhook_confirmation\": \"Bu web kancasını silmek istediğinizden emin misiniz?\",\n      \"edit_webhook\": \"Web Kancasını Düzenle\",\n      \"webhook_url\": \"Web Kancası URL'si\",\n      \"add_auth_token\": \"Kimlik Doğrulama Jetonu Ekle\",\n      \"auth_token\": \"Kimlik Doğrulama Jetonu\",\n      \"webhooks\": \"Web kancaları\",\n      \"description\": \"Web kancaları kullanarak yer imleri oluşturulduğunda, değiştirildiğinde veya tarandığında eylemleri tetikleyebilirsin.\",\n      \"events\": {\n        \"title\": \"Etkinlikler\",\n        \"crawled\": \"Taranmış\",\n        \"created\": \"Oluşturuldu\",\n        \"edited\": \"Düzenlendi\"\n      }\n    },\n    \"manage_assets\": {\n      \"manage_assets\": \"Varlıkları Yönet\",\n      \"no_assets\": \"Henüz hiçbir varlığın yok.\",\n      \"asset_type\": \"Varlık Türü\",\n      \"bookmark_link\": \"Yer İşareti Bağlantısı\",\n      \"asset_link\": \"Varlık Bağlantısı\",\n      \"delete_asset\": \"Varlığı Sil\",\n      \"delete_asset_confirmation\": \"Bu varlığı silmek istediğinizden emin misiniz?\"\n    },\n    \"rules\": {\n      \"rules\": \"Kural Motoru\",\n      \"conditions_types\": {\n        \"is_archived\": \"Arşivlendi\",\n        \"always\": \"Her zaman\",\n        \"url_contains\": \"URL İçeriyor\",\n        \"imported_from_feed\": \"Beslemeden İçe Aktarıldı\",\n        \"bookmark_type_is\": \"Yer İşareti Türü Şudur\",\n        \"has_tag\": \"Etikete Sahip\",\n        \"is_favourited\": \"Favorilere Eklendi\",\n        \"and\": \"Aşağıdakilerin hepsi doğru\",\n        \"or\": \"Aşağıdakilerden herhangi biri doğruysa\",\n        \"url_does_not_contain\": \"URL İçermiyor\",\n        \"title_contains\": \"Başlık İçeriyor\",\n        \"title_does_not_contain\": \"Başlık İçermiyor\"\n      },\n      \"actions_types\": {\n        \"add_tag\": \"Etiket Ekle\",\n        \"remove_tag\": \"Etiketi Kaldır\",\n        \"add_to_list\": \"Listeye Ekle\",\n        \"remove_from_list\": \"Listeden Kaldır\",\n        \"download_full_page_archive\": \"Tam Sayfa Arşivini İndir\",\n        \"favourite_bookmark\": \"Favori Yer İşareti\",\n        \"archive_bookmark\": \"Yer İşaretini Arşivle\"\n      },\n      \"rule_name\": \"Kural Adı\",\n      \"description\": \"Bir olay tetiklendiğinde eylemleri tetiklemek için kuralları kullanabilirsiniz.\",\n      \"ceate_rule\": \"Kural Oluştur\",\n      \"edit_rule\": \"Kuralı Düzenle\",\n      \"save_rule\": \"Kuralı Kaydet\",\n      \"delete_rule\": \"Kuralı Sil\",\n      \"delete_rule_confirmation\": \"Bu kuralı silmek istediğinden emin misin?\",\n      \"whenever\": \"Ne zaman ...\",\n      \"if\": \"Eğer ...\",\n      \"enter_rule_name\": \"Kural adı girin\",\n      \"describe_what_this_rule_does\": \"Bu kuralın ne yaptığını açıklayın\",\n      \"rule_has_been_created\": \"Kural oluşturuldu!\",\n      \"rule_has_been_updated\": \"Kural güncellendi!\",\n      \"rule_has_been_deleted\": \"Kural silindi!\",\n      \"no_rules_created_yet\": \"Henüz kural oluşturulmadı\",\n      \"create_your_first_rule\": \"İş akışınızı otomatikleştirmek için ilk kuralınızı oluşturun\",\n      \"events_types\": {\n        \"bookmark_added\": \"Bir yer işareti eklendi\",\n        \"tag_added\": \"Bu etiket bir yer işaretine eklendi\",\n        \"tag_removed\": \"Bu etiket bir yer işaretinden kaldırıldı\",\n        \"added_to_list\": \"Bu listeye bir yer işareti eklendi\",\n        \"removed_from_list\": \"Bir yer işareti bu listeden kaldırıldı\",\n        \"favourited\": \"Bir yer işareti favorilere eklendi\",\n        \"archived\": \"Bir yer işareti arşivlendi\"\n      }\n    },\n    \"stats\": {\n      \"usage_statistics\": \"Kullanım İstatistikleri\",\n      \"insights_description\": \"Yer imi alışkanlıklarınız ve koleksiyonunuz hakkında bilgiler\",\n      \"failed_to_load\": \"İstatistikler yüklenemedi\",\n      \"overview\": {\n        \"total_bookmarks\": \"Toplam Yer İmi\",\n        \"all_saved_items\": \"Kaydedilen tüm öğeler\",\n        \"favorites\": \"Favoriler\",\n        \"starred_bookmarks\": \"Yıldızlı yer imleri\",\n        \"archived\": \"Arşivlendi\",\n        \"archived_items\": \"Arşivlenmiş öğeler\",\n        \"tags\": \"Etiketler\",\n        \"unique_tags_created\": \"Benzersiz etiketler oluşturuldu\",\n        \"lists\": \"Listeler\",\n        \"bookmark_collections\": \"Yer imi koleksiyonları\",\n        \"highlights\": \"Öne Çıkanlar\",\n        \"text_highlights\": \"Metin vurguları\",\n        \"storage_used\": \"Kullanılan Depolama Alanı\",\n        \"total_asset_storage\": \"Toplam varlık depolama\",\n        \"this_month\": \"Bu Ay\",\n        \"bookmarks_added\": \"Yer imleri eklendi\"\n      },\n      \"bookmark_types\": {\n        \"title\": \"Yer İmi Türleri\",\n        \"links\": \"Bağlantılar\",\n        \"text_notes\": \"Metin Notları\",\n        \"assets\": \"Varlıklar\"\n      },\n      \"recent_activity\": {\n        \"title\": \"Son Etkinlik\",\n        \"this_week\": \"Bu Hafta\",\n        \"this_month\": \"Bu Ay\",\n        \"this_year\": \"Bu Yıl\"\n      },\n      \"top_domains\": {\n        \"title\": \"En İyi Alan Adları\",\n        \"no_domains_found\": \"Alan adı bulunamadı\"\n      },\n      \"most_used_tags\": {\n        \"title\": \"En Çok Kullanılan Etiketler\",\n        \"no_tags_found\": \"Etiket bulunamadı\"\n      },\n      \"activity_patterns\": {\n        \"activity_by_hour\": \"Saate Göre Etkinlik\",\n        \"activity_by_day\": \"Güne Göre Etkinlik\"\n      },\n      \"storage_breakdown\": {\n        \"title\": \"Depolama Dökümü\"\n      },\n      \"bookmark_sources\": {\n        \"title\": \"Kaynakları işaretle\",\n        \"empty\": \"Kullanılabilir kaynak verisi yok\"\n      }\n    },\n    \"subscription\": {\n      \"subscription\": \"Abonelik\",\n      \"manage_subscription\": \"Aboneliğinizi ve fatura bilgilerinizi yönetin\",\n      \"current_plan\": \"Mevcut Plan\",\n      \"billing_period\": \"Faturalandırma Dönemi\",\n      \"paid_plan\": \"Ücretli Plan\",\n      \"unlock_bigger_quota\": \"Daha büyük kota açın ve projeyi destekleyin\",\n      \"subscribe_now\": \"Şimdi Abone Ol\",\n      \"manage_billing\": \"Faturalandırmayı Yönet\",\n      \"subscription_canceled\": \"Aboneliğiniz iptal edildi ve {{date}} tarihinde sona erecek. İstediğiniz zaman yeniden abone olabilirsiniz.\",\n      \"usage_quotas\": \"Kullanım ve Kotalar\",\n      \"track_usage\": \"Mevcut kullanımınızı plan limitlerinize göre takip edin\",\n      \"total_bookmarks_saved\": \"Kaydedilen toplam yer imi\",\n      \"assets_file_storage\": \"Varlıklar ve dosya depolama\",\n      \"unlimited_usage\": \"Sınırsız kullanım\",\n      \"quota_limit_reached\": \"Kota sınırına ulaşıldı\",\n      \"approaching_quota_limit\": \"Kota sınırına yaklaşılıyor\",\n      \"loading_usage\": \"Kullanım bilgileri yükleniyor...\",\n      \"free\": \"Ücretsiz\",\n      \"paid\": \"Ücretli\"\n    },\n    \"import_sessions\": {\n      \"title\": \"İçe Aktarma Oturumları\",\n      \"description\": \"Toplu içe aktarma oturumlarını görüntüle ve yönet. Oturumlar, yer imlerini içe aktardığında otomatik olarak oluşturulur.\",\n      \"load_error\": \"İçe aktarma oturumları yüklenemedi\",\n      \"no_sessions\": \"Henüz içe aktarma oturumu yok\",\n      \"no_sessions_detail\": \"Yer imlerini içe aktardığında, içe aktarma oturumları burada otomatik olarak görünecektir.\",\n      \"created_at\": \"Oluşturulma zamanı: {{time}}\",\n      \"progress\": \"İlerleme durumu\",\n      \"status\": {\n        \"pending\": \"Bekliyor\",\n        \"in_progress\": \"Devam ediyor\",\n        \"completed\": \"Tamamlandı\",\n        \"failed\": \"Başarısız\",\n        \"processing\": \"İşleniyor\",\n        \"staging\": \"Hazırlanıyor\",\n        \"running\": \"Çalışıyor\",\n        \"paused\": \"Durduruldu\"\n      },\n      \"badges\": {\n        \"pending\": \"{{count}} beklemede\",\n        \"processing\": \"{{count}} işleniyor\",\n        \"completed\": \"{{count}} tamamlandı\",\n        \"failed\": \"{{count}} başarısız\"\n      },\n      \"imported_to\": \"İçe aktarıldığı yer:\",\n      \"view_list\": \"Listeyi Görüntüle\",\n      \"delete_dialog_title\": \"İçe Aktarma Oturumunu Sil\",\n      \"delete_dialog_description\": \"\\\"{{name}}\\\" öğesini silmek istediğinden emin misin? Bu işlem geri alınamaz. Yer imlerinin kendileri silinmeyecektir.\",\n      \"delete_session\": \"Oturumu Sil\",\n      \"pause_session\": \"Durdur\",\n      \"resume_session\": \"Devam Et\",\n      \"view_details\": \"Ayrıntıları Görüntüle\",\n      \"detail\": {\n        \"page_title\": \"İçe Aktarma Oturumu Ayrıntıları\",\n        \"back_to_import\": \"İçe Aktarmaya Geri Dön\",\n        \"filter_all\": \"Tümü\",\n        \"filter_accepted\": \"Kabul Edilenler\",\n        \"filter_rejected\": \"Reddedilenler\",\n        \"filter_duplicates\": \"Yinelenenler\",\n        \"filter_pending\": \"Bekleyen\",\n        \"table_title\": \"Başlık / URL\",\n        \"table_type\": \"Tür\",\n        \"table_result\": \"Sonuç\",\n        \"table_reason\": \"Gerekçe\",\n        \"table_bookmark\": \"Yer İşareti\",\n        \"result_accepted\": \"Kabul Edilenler\",\n        \"result_rejected\": \"Reddedilenler\",\n        \"result_skipped_duplicate\": \"Yinelenen\",\n        \"result_pending\": \"Bekleyen\",\n        \"result_processing\": \"İşleniyor\",\n        \"no_results\": \"Bu filtre için sonuç bulunamadı.\",\n        \"view_bookmark\": \"Yer işaretini görüntüle\",\n        \"load_more\": \"Daha Fazla Yükle\",\n        \"no_title\": \"Başlık yok\"\n      }\n    },\n    \"backups\": {\n      \"backups\": \"Yedeklemeler\",\n      \"page_title\": \"Yedeklemeler\",\n      \"page_description\": \"Yer işaretlerinizin otomatik olarak yedeklerini oluşturun ve yönetin. Yedeklemeler sıkıştırılır ve güvenli bir şekilde saklanır.\",\n      \"configuration\": {\n        \"title\": \"Yedekleme Yapılandırması\",\n        \"enable_automatic_backups\": \"Otomatik Yedeklemeleri Etkinleştir\",\n        \"enable_automatic_backups_description\": \"Yer işaretlerinizin otomatik olarak yedeklerini oluştur.\",\n        \"backup_frequency\": \"Yedekleme Sıklığı\",\n        \"backup_frequency_description\": \"Yedeklemelerin ne sıklıkla oluşturulması gerektiği\",\n        \"retention_period\": \"Saklama Süresi (gün)\",\n        \"retention_period_description\": \"Yedekleri silmeden önce kaç gün saklanacağı\",\n        \"frequency\": {\n          \"daily\": \"Günlük\",\n          \"weekly\": \"Haftalık\"\n        },\n        \"select_frequency\": \"Sıklık seçin\",\n        \"save_settings\": \"Ayarları Kaydet\"\n      },\n      \"list\": {\n        \"title\": \"Yedeklemeleriniz\",\n        \"create_backup_now\": \"Şimdi Yedekleme Oluştur\",\n        \"no_backups\": \"Henüz herhangi bir yedeğiniz yok. Otomatik yedeklemeyi etkinleştirin veya manuel olarak bir tane oluşturun.\",\n        \"table\": {\n          \"created_at\": \"Oluşturulma Zamanı\",\n          \"bookmarks\": \"Yer İşaretleri\",\n          \"size\": \"Boyut\",\n          \"status\": \"Durum\",\n          \"actions\": \"Eylemler\"\n        },\n        \"status\": {\n          \"success\": \"Başarılı\",\n          \"failed\": \"Başarısız\",\n          \"pending\": \"Beklemede\"\n        },\n        \"actions\": {\n          \"download_backup\": \"Yedeklemeyi İndir\",\n          \"delete_backup\": \"Yedeklemeyi Sil\"\n        }\n      },\n      \"dialogs\": {\n        \"delete_backup_title\": \"Yedeklemeyi silmek mi istiyorsun?\",\n        \"delete_backup_description\": \"Bu yedeklemeyi silmek istediğinden emin misin? Bu işlem geri alınamaz.\"\n      },\n      \"toasts\": {\n        \"backup_queued\": \"Yedekleme işi sıraya alındı! Kısa süre içinde işlenecek.\",\n        \"backup_deleted\": \"Yedekleme silindi!\"\n      }\n    }\n  },\n  \"admin\": {\n    \"admin_settings\": \"Yönetici Ayarları\",\n    \"server_stats\": {\n      \"server_stats\": \"Sunucu İstatistikleri\",\n      \"total_users\": \"Toplam Kullanıcı\",\n      \"total_bookmarks\": \"Toplam Yer İşareti\",\n      \"server_version\": \"Sunucu Sürümü\"\n    },\n    \"background_jobs\": {\n      \"background_jobs\": \"Arka Plan Görevleri\",\n      \"crawler_jobs\": \"Tarayıcı Görevleri\",\n      \"indexing_jobs\": \"Dizine Alma Görevleri\",\n      \"inference_jobs\": \"Çıkarım Görevleri\",\n      \"tidy_assets_jobs\": \"Varlıkları Düzenleme Görevleri\",\n      \"job\": \"Görev\",\n      \"queued\": \"Sıraya Alındı\",\n      \"pending\": \"Beklemede\",\n      \"failed\": \"Başarısız\",\n      \"video_jobs\": \"Video İndirme İşleri\",\n      \"feed_jobs\": \"RSS Besleme İşleri\",\n      \"webhook_jobs\": \"Webhook İşleri\",\n      \"asset_preprocessing_jobs\": \"Varlık Ön İşleme İşleri\",\n      \"jobs\": {\n        \"crawler\": {\n          \"title\": \"Crawler İşleri\",\n          \"description\": \"URL'lerden web kazıma ve içerik çıkarma\"\n        },\n        \"inference\": {\n          \"title\": \"Çıkarım İşleri\",\n          \"description\": \"İçeriğin yapay zeka destekli etiketlenmesi ve özetlenmesi\"\n        },\n        \"indexing\": {\n          \"title\": \"İndeksleme İşleri\",\n          \"description\": \"Arama indeksi güncellemeleri\"\n        },\n        \"asset_preprocessing\": {\n          \"title\": \"Varlık Ön İşleme İşleri\",\n          \"description\": \"Resim ve belge ön işleme (ekran görüntüleri, metin çıkarma vb.)\"\n        },\n        \"tidy_assets\": {\n          \"title\": \"Varlıkları Düzenleme İşleri\",\n          \"description\": \"Varlık temizleme ve depolama optimizasyonu\"\n        },\n        \"video\": {\n          \"title\": \"Video İndirme İşleri\",\n          \"description\": \"Video çıkarma ve indirme\"\n        },\n        \"webhook\": {\n          \"title\": \"Webhook İşleri\",\n          \"description\": \"Harici webhook bildirimleri\"\n        },\n        \"feed\": {\n          \"title\": \"RSS Besleme İşleri\",\n          \"description\": \"RSS besleme işleme ve içerik güncellemeleri\"\n        },\n        \"admin_maintenance\": {\n          \"title\": \"Yönetici Bakım İşleri\",\n          \"description\": \"Yönetimsel temizlik ve varlık bakımı\"\n        }\n      },\n      \"monitor_and_manage\": \"Arka plan iş kuyruklarını ve sistem işleme görevlerini izleyin ve yönetin\",\n      \"active\": \"Etkin\",\n      \"available_actions\": \"Kullanılabilir Eylemler\",\n      \"status\": {\n        \"title\": \"İş Durumlarını Anlama\",\n        \"queued\": {\n          \"title\": \"Sıraya alındı\",\n          \"description\": \"İşlenmeyi bekleyen işler. Kaynaklar kullanılabilir olduğunda otomatik olarak başlayacaklar.\"\n        },\n        \"unprocessed\": {\n          \"title\": \"İşlenmemiş\",\n          \"description\": \"Henüz işlenmemiş yer imleri. Büyük olasılıkla zaten işlenmek üzere sıraya alınmış durumdalar, değilse manuel olarak yeniden sıraya almanız gerekebilir.\"\n        },\n        \"failed\": {\n          \"title\": \"Başarısız oldu\",\n          \"description\": \"İşleme sırasında hatalarla karşılaşan yer imleri. Bunlar manuel müdahale gerektirebilir.\"\n        }\n      },\n      \"actions\": {\n        \"recrawl_failed_links_only\": \"Yalnızca Başarısız Bağlantıları Yeniden Tara\",\n        \"recrawl_all_links\": \"Tüm Bağlantıları Yeniden Tara\",\n        \"without_inference\": \"Çıkarım olmadan\",\n        \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Yalnızca Başarısız Yer İmleri için Yapay Zeka Etiketlerini Yeniden Oluştur\",\n        \"regenerate_ai_tags_for_all_bookmarks\": \"Tüm Yer İmleri için Yapay Zeka Etiketlerini Yeniden Oluştur\",\n        \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Yalnızca Başarısız Yer İmleri için Yapay Zeka Özetlerini Yeniden Oluştur\",\n        \"regenerate_ai_summaries_for_all_bookmarks\": \"Tüm Yer İmleri için Yapay Zeka Özetlerini Yeniden Oluştur\",\n        \"reindex_all_bookmarks\": \"Tüm Yer İmlerini Yeniden İndeksle\",\n        \"clean_assets\": \"Sallanan Varlıkları Temizle ve Meta Verileri Yeniden Eşitle\",\n        \"reprocess_assets_fix_mode\": \"İşlenmemiş Varlıkları Yeniden İşle\",\n        \"migrate_large_link_html_content\": \"Büyük Satır İçi HTML İçeriğini Varlıklara Taşı\",\n        \"recrawl_pending_links_only\": \"Sadece Bekleyen Bağlantıları Yeniden Tara\",\n        \"regenerate_ai_tags_for_pending_bookmarks_only\": \"Sadece Bekleyen Yer İşaretleri için Yapay Zeka Etiketlerini Yeniden Oluştur\",\n        \"regenerate_ai_summaries_for_pending_bookmarks_only\": \"Sadece Bekleyen Yer İşaretleri için Yapay Zeka Özetlerini Yeniden Oluştur\"\n      }\n    },\n    \"actions\": {\n      \"recrawl_failed_links_only\": \"Sadece Başarısız Bağlantıları Tekrar Tara\",\n      \"recrawl_all_links\": \"Tüm Bağlantıları Tekrar Tara\",\n      \"without_inference\": \"Çıkarım Olmadan\",\n      \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Yalnızca Başarısız Yer İşaretleri için Yapay Zeka Etiketlerini Yeniden Oluştur\",\n      \"regenerate_ai_tags_for_all_bookmarks\": \"Tüm Yer İşaretleri için Yapay Zeka Etiketlerini Yeniden Oluştur\",\n      \"reindex_all_bookmarks\": \"Tüm Yer İşaretlerini Yeniden Dizine Al\",\n      \"compact_assets\": \"Varlıkları Sıkıştır\",\n      \"reprocess_assets_fix_mode\": \"Varlıkları Yeniden İşle (Fix Mod)\",\n      \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Yalnızca Başarısız Yer İmleri için Yapay Zeka Özetlerini Yeniden Oluştur\",\n      \"regenerate_ai_summaries_for_all_bookmarks\": \"Tüm Yer İmleri için Yapay Zeka Özetlerini Yeniden Oluştur\"\n    },\n    \"users_list\": {\n      \"users_list\": \"Kullanıcı Listesi\",\n      \"create_user\": \"Kullanıcı Oluştur\",\n      \"change_role\": \"Rol Değiştir\",\n      \"reset_password\": \"Şifreyi Sıfırla\",\n      \"delete_user\": \"Kullanıcıyı Sil\",\n      \"num_bookmarks\": \"Yer İşareti Sayısı\",\n      \"asset_sizes\": \"Varlık Boyutları\",\n      \"local_user\": \"Yerel Kullanıcı\",\n      \"confirm_password\": \"Şifreyi Onayla\",\n      \"delete_user_confirm_description\": \"{{name}} kullanıcısını silmek istediğine emin misin?\",\n      \"unlimited\": \"Sınırsız\"\n    },\n    \"service_connections\": {\n      \"title\": \"Hizmet Bağlantıları\",\n      \"description\": \"Harici sistem bağımlılıklarının sağlığını ve bağlantısını izle\",\n      \"search_engine\": \"Arama Motoru\",\n      \"browser\": \"Tarayıcı\",\n      \"queue_system\": \"Sıra Sistemi\",\n      \"status\": {\n        \"not_configured\": \"Yapılandırılmamış\",\n        \"connected\": \"Bağlı\",\n        \"disconnected\": \"Bağlantı Kesildi\"\n      }\n    },\n    \"admin_tools\": {\n      \"admin_tools\": \"Yönetici Araçları\",\n      \"bookmark_debugger\": \"Yer İmi Hata Ayıklayıcısı\",\n      \"bookmark_id\": \"Yer İmi Kimliği\",\n      \"bookmark_id_placeholder\": \"Yer imi kimliğini girin\",\n      \"lookup\": \"Arama\",\n      \"debug_info\": \"Hata Ayıklama Bilgileri\",\n      \"basic_info\": \"Temel Bilgiler\",\n      \"status\": \"Durum\",\n      \"content\": \"İçerik\",\n      \"html_preview\": \"HTML Önizlemesi (İlk 1000 karakter)\",\n      \"summary\": \"Özet\",\n      \"url\": \"URL\",\n      \"source_url\": \"Kaynak URL'si\",\n      \"asset_type\": \"Varlık Türü\",\n      \"file_name\": \"Dosya Adı\",\n      \"owner_user_id\": \"Sahip Kullanıcı Kimliği\",\n      \"tagging_status\": \"Etiketleme Durumu\",\n      \"summarization_status\": \"Özetleme Durumu\",\n      \"crawl_status\": \"Tarama Durumu\",\n      \"crawl_status_code\": \"HTTP Durum Kodu\",\n      \"crawled_at\": \"Taranma Zamanı\",\n      \"recrawl\": \"Yeniden tarama\",\n      \"reindex\": \"Yeniden indeksleme\",\n      \"retag\": \"Yeniden etiketleme\",\n      \"resummarize\": \"Yeniden özetleme\",\n      \"bookmark_not_found\": \"Yer işareti bulunamadı\",\n      \"action_success\": \"İşlem başarıyla tamamlandı\",\n      \"action_failed\": \"İşlem başarısız oldu\",\n      \"recrawl_queued\": \"Yeniden tarama işi sıraya alındı\",\n      \"reindex_queued\": \"Yeniden indeksleme işi sıraya alındı\",\n      \"retag_queued\": \"Yeniden etiketleme işi sıraya alındı\",\n      \"resummarize_queued\": \"Yeniden özetleme işi sıraya alındı\",\n      \"view\": \"Görüntüle\",\n      \"fetch_error\": \"Yer işareti alınırken hata oluştu\"\n    }\n  },\n  \"options\": {\n    \"dark_mode\": \"Koyu Mod\",\n    \"light_mode\": \"Açık Mod\",\n    \"apps_extensions\": \"Uygulamalar & Uzantılar\",\n    \"documentation\": \"Belgeleme\",\n    \"follow_us_on_x\": \"Bizi X'te takip edin\"\n  },\n  \"lists\": {\n    \"all_lists\": \"Tüm Listeler\",\n    \"favourites\": \"Favoriler\",\n    \"new_list\": \"Yeni Liste\",\n    \"new_nested_list\": \"Yeni İç İçe Liste\",\n    \"no_parent\": \"Üst Öğe Yok\",\n    \"list_type\": \"Liste Türü\",\n    \"manual_list\": \"Manuel Liste\",\n    \"smart_list\": \"Akıllı Liste\",\n    \"search_query\": \"Arama Sorgusu\",\n    \"search_query_help\": \"Arama sorgu dili hakkında daha fazla bilgi edinin.\",\n    \"parent_list\": \"Üst Öğe Listesi\",\n    \"edit_list\": \"Listeyi Düzenle\",\n    \"merge_list\": \"Listeyi Birleştir\",\n    \"destination_list\": \"Hedef Liste\",\n    \"delete_after_merge\": \"Birleştirmeden sonra orijinal listeyi sil\",\n    \"no_destination\": \"Hedef Yok\",\n    \"description\": \"Açıklama (İsteğe Bağlı)\",\n    \"share_list\": \"Listeyi Paylaş\",\n    \"rss\": {\n      \"title\": \"RSS Beslemesi\",\n      \"description\": \"Bu liste için bir RSS beslemesi etkinleştir\",\n      \"feed_url\": \"RSS Besleme URL'si\"\n    },\n    \"public_list\": {\n      \"title\": \"Herkese Açık Liste\",\n      \"description\": \"Başkalarının bu listeyi görüntülemesine izin ver\",\n      \"share_link\": \"Bağlantıyı Paylaş\"\n    },\n    \"delete_list\": {\n      \"title\": \"Listeyi Sil\",\n      \"description\": \"Bir listeyi silmek, o listedeki hiçbir yer işaretini silmez.\",\n      \"delete_children\": \"Alt listeleri sil (özyinelemeli olarak)\",\n      \"delete_children_description\": \"İşaretli değilse, tüm doğrudan alt listeler kök listelere dönüşür\"\n    },\n    \"shared\": \"Paylaşılan\",\n    \"collaborators\": {\n      \"manage\": \"Ortak Çalışanları Yönet\",\n      \"view\": \"Ortak Çalışanları Görüntüle\",\n      \"collaborators\": \"Ortak Çalışanlar\",\n      \"add\": \"Ortak Çalışan Ekle\",\n      \"current\": \"Mevcut Ortak Çalışanlar\",\n      \"enter_email\": \"E-posta adresini girin\",\n      \"please_enter_email\": \"Lütfen bir e-posta adresi girin\",\n      \"added_successfully\": \"Ortak çalışan başarıyla eklendi\",\n      \"failed_to_add\": \"Ortak çalışan eklenemedi\",\n      \"removed\": \"Ortak çalışan kaldırıldı\",\n      \"failed_to_remove\": \"Ortak çalışan kaldırılamadı\",\n      \"role_updated\": \"Rol güncellendi\",\n      \"failed_to_update_role\": \"Rol güncellenemedi\",\n      \"viewer\": \"Görüntüleyici\",\n      \"editor\": \"Düzenleyici\",\n      \"owner\": \"Sahip\",\n      \"viewer_description\": \"Listedeki yer işaretlerini görüntüleyebilir\",\n      \"editor_description\": \"Yer işaretleri ekleyebilir ve kaldırabilir\",\n      \"no_collaborators\": \"Henüz ortak çalışan yok. İşbirliğine başlamak için birini ekleyin!\",\n      \"no_collaborators_readonly\": \"Bu listede işbirlikçi yok.\",\n      \"people_with_access\": \"Bu listeye erişimi olan kişiler\",\n      \"add_or_remove\": \"Bu listeye erişebilecek kişileri ekle veya kaldır\",\n      \"invitation_sent\": \"Davetiye başarıyla gönderildi\",\n      \"invitation_revoked\": \"Davetiye iptal edildi\",\n      \"failed_to_revoke\": \"Davetiye iptal edilemedi\",\n      \"pending\": \"Beklemede\",\n      \"revoke\": \"İptal et\",\n      \"declined\": \"Reddedildi\"\n    },\n    \"leave_list\": {\n      \"title\": \"Listeden Ayrıl\",\n      \"confirm_message\": \"{{icon}} {{name}} listesinden ayrılmak istediğinden emin misin?\",\n      \"warning\": \"Artık bu listedeki yer işaretlerini görüntüleyemeyecek veya bunlara erişemeyeceksin. Liste sahibi gerekirse seni geri ekleyebilir.\",\n      \"action\": \"Listeden Ayrıl\",\n      \"success\": \"\\\"{{icon}} {{name}}\\\" listesinden ayrıldın\"\n    },\n    \"invitations\": {\n      \"pending\": \"Bekleyen Davetiyeler\",\n      \"description\": \"Liste işbirliği davetlerini inceleyip yanıtlayın\",\n      \"invited_by\": \"Davet Eden\",\n      \"accept\": \"Kabul Et\",\n      \"decline\": \"Reddet\",\n      \"accepted\": \"Davetiye kabul edildi\",\n      \"declined\": \"Davetiye reddedildi\",\n      \"failed_to_accept\": \"Davetiye kabul edilemedi\",\n      \"failed_to_decline\": \"Davetiye reddedilemedi\"\n    },\n    \"shared_lists\": \"Paylaşılan Listeler\"\n  },\n  \"tags\": {\n    \"all_tags\": \"Tüm Etiketler\",\n    \"your_tags\": \"Kendi Etiketleriniz\",\n    \"your_tags_info\": \"En az bir kez tarafınızdan eklenmiş etiketler\",\n    \"ai_tags\": \"Yapay Zeka Etiketleri\",\n    \"ai_tags_info\": \"Yalnızca otomatik olarak (Yapay Zeka tarafından) eklenen etiketler\",\n    \"unused_tags\": \"Kullanılmayan Etiketler\",\n    \"unused_tags_info\": \"Hiçbir yer işaretine eklenmemiş etiketler\",\n    \"delete_all_unused_tags\": \"Kullanılmayan Tüm Etiketleri Sil\",\n    \"drag_and_drop_merging\": \"Sürükle ve Bırak Birleştirme\",\n    \"drag_and_drop_merging_info\": \"Etiketleri birbirlerinin üzerine sürükleyerek birleştirin\",\n    \"sort_by_name\": \"İsme Göre Sırala\",\n    \"create_tag\": \"Etiket Oluştur\",\n    \"create_tag_description\": \"Herhangi bir yer işaretine iliştirmeden yeni bir etiket oluştur\",\n    \"tag_name\": \"Etiket Adı\",\n    \"enter_tag_name\": \"Etiket adını girin\",\n    \"sort_by_usage\": \"Kullanıma Göre Sırala\",\n    \"sort_by_relevance\": \"Alaka Düzeyine Göre Sırala\",\n    \"no_custom_tags\": \"Henüz özel etiket yok\",\n    \"no_ai_tags\": \"Henüz yapay zeka etiketi yok\",\n    \"no_unused_tags\": \"Kullanılmayan etiketiniz yok\",\n    \"no_unused_tags_match_your_search\": \"Aramanızla eşleşen kullanılmayan etiket yok\",\n    \"no_tags_match_your_search\": \"Aramanızla eşleşen etiket yok\",\n    \"search_placeholder\": \"Etiketleri ara...\",\n    \"search_or_create_placeholder\": \"Etiket ara veya oluştur...\"\n  },\n  \"preview\": {\n    \"view_original\": \"Orijinali Görüntüle\",\n    \"cached_content\": \"Önbelleğe Alınmış İçerik\",\n    \"reader_view\": \"Okuyucu Görünümü\",\n    \"tabs\": {\n      \"content\": \"İçerik\",\n      \"details\": \"Ayrıntılar\"\n    },\n    \"archive_info\": \"Arşivler Javascript gerektiriyorsa satır içi olarak doğru şekilde işlenmeyebilir. En iyi sonuçlar için, <1>indirin ve tarayıcınızda açın</1>.\",\n    \"fetch_error_title\": \"İçerik Kullanılamıyor\",\n    \"fetch_error_description\": \"Bu bağlantı için içerik alınamadı. Sayfa korumalı olabilir, kimlik doğrulaması gerektirebilir veya geçici olarak kullanılamıyor olabilir.\",\n    \"crawling_in_progress\": \"Sayfa içeriği getiriliyor…\",\n    \"continue_reading\": \"Kaldığın yerden devam et\",\n    \"continue_reading_percent\": \"Kaldığınız yerden devam edin ({{percent}}%)\",\n    \"continue_button\": \"Devam et\"\n  },\n  \"editor\": {\n    \"quickly_focus\": \"Bu alana hızlıca odaklanmak için ⌘ + E tuşlarına basabilirsiniz\",\n    \"multiple_urls_dialog_title\": \"URL'leri ayrı Yer İşaretleri olarak mı içe aktarılıyor?\",\n    \"multiple_urls_dialog_desc\": \"Girdi, ayrı satırlarda birden fazla URL içeriyor. Bunları ayrı yer işaretleri olarak içe aktarmak ister misiniz?\",\n    \"import_as_text\": \"Metin Yer İşareti Olarak İçe Aktar\",\n    \"import_as_separate_bookmarks\": \"Ayrı Yer İşaretleri Olarak İçe Aktar\",\n    \"placeholder\": \"Bir bağlantı veya görsel yapıştırın, not yazın ya da buraya bir görsel sürükleyip bırakın ...\",\n    \"new_item\": \"YENİ ÖGE\",\n    \"disabled_submissions\": \"Gönderimler Devre Dışı\",\n    \"text_toolbar\": {\n      \"undo\": \"Geri Al\",\n      \"redo\": \"Yinele\",\n      \"bold\": \"Kalın\",\n      \"italic\": \"İtalik\",\n      \"underline\": \"Altı Çizili\",\n      \"strikethrough\": \"Üstü Çizili\",\n      \"code\": \"Kod\",\n      \"highlight\": \"Vurgula\",\n      \"align_left\": \"Sola Hizala\",\n      \"align_center\": \"Ortaya Hizala\",\n      \"align_right\": \"Sağa Hizala\",\n      \"markdown_shortcuts\": {\n        \"label\": \"Markdown Kısayolları\",\n        \"heading\": {\n          \"label\": \"Başlık\",\n          \"example\": \"# H1, ## H2, ### H3\"\n        },\n        \"bold\": {\n          \"label\": \"Kalın\",\n          \"example\": \"**metin** veya CTRL+b\"\n        },\n        \"italic\": {\n          \"label\": \"İtalik\",\n          \"example\": \"*İtalik* veya _İtalik_ veya CTRL+i\"\n        },\n        \"blockquote\": {\n          \"label\": \"Blok Alıntı\",\n          \"example\": \"> Blok alıntı\"\n        },\n        \"ordered_list\": {\n          \"label\": \"Sıralı Liste\",\n          \"example\": \"1. Liste elemanı\"\n        },\n        \"unordered_list\": {\n          \"label\": \"Sırasız Liste\",\n          \"example\": \"- Liste elemanı\"\n        },\n        \"inline_code\": {\n          \"label\": \"Satır İçi Kod\",\n          \"example\": \"`Kod`\"\n        },\n        \"block_code\": {\n          \"label\": \"Blok Kod\",\n          \"example\": \"``` + boşluk\"\n        }\n      }\n    },\n    \"placeholder_v2\": \"Bir bağlantı yapıştırın, bir not yazın veya bir resim bırakın…\"\n  },\n  \"toasts\": {\n    \"bookmarks\": {\n      \"updated\": \"Yer işareti güncellendi!\",\n      \"deleted\": \"Yer işareti silindi!\",\n      \"refetch\": \"Yeniden getir kuyruğa alındı!\",\n      \"full_page_archive\": \"Tüm Sayfa Arşivi oluşturma başlatıldı\",\n      \"delete_from_list\": \"Yer işareti listeden silindi\",\n      \"clipboard_copied\": \"Bağlantı panonuza eklendi!\",\n      \"preserve_pdf\": \"PDF olarak saklama tetiklendi\",\n      \"update_banner\": \"Başlık güncellendi!\",\n      \"uploading_banner\": \"Başlık yükleniyor...\"\n    },\n    \"lists\": {\n      \"created\": \"Liste oluşturuldu!\",\n      \"updated\": \"Liste güncellendi!\",\n      \"merged\": \"Liste birleştirildi!\",\n      \"deleted\": \"Liste silindi!\"\n    },\n    \"tags\": {\n      \"created\": \"Etiket oluşturuldu!\",\n      \"failed_to_create\": \"Etiket oluşturulamadı\"\n    }\n  },\n  \"cleanups\": {\n    \"cleanups\": \"Temizleme\",\n    \"duplicate_tags\": {\n      \"title\": \"Yinelenen Etiketler\",\n      \"merge_all_suggestions\": \"Tüm önerileri birleştir?\"\n    }\n  },\n  \"search\": {\n    \"is_not_in_any_list\": \"Herhangi Bir Listede Değil\",\n    \"created_on_or_before\": \"Şu Tarihte veya Daha Önce Oluşturuldu\",\n    \"not_created_on_or_before\": \"Şu Tarihte veya Daha Önce Oluşturulmadı\",\n    \"url_contains\": \"URL İçeriyor\",\n    \"url_does_not_contain\": \"URL İçermiyor\",\n    \"full_text_search\": \"Tam Metin Arama\",\n    \"and\": \"Ve\",\n    \"is_favorited\": \"Favorilere Eklendi mi\",\n    \"is_not_in_list\": \"Listede Değil\",\n    \"not_created_on_or_after\": \"Şu Tarihte veya Sonrasında Oluşturulmadı\",\n    \"has_no_tags\": \"Etiketi Yok\",\n    \"is_in_any_list\": \"Herhangi Bir Listede mi\",\n    \"is_in_list\": \"Listede mi\",\n    \"or\": \"Veya\",\n    \"is_not_favorited\": \"Favorilere Eklenmemiş\",\n    \"created_on_or_after\": \"Şu Tarihte veya Sonrasında Oluşturuldu\",\n    \"has_tag\": \"Etiketi Var\",\n    \"does_not_have_tag\": \"Etiketi Yok\",\n    \"is_archived\": \"Arşivlendi\",\n    \"is_not_archived\": \"Arşivlenmedi\",\n    \"has_any_tag\": \"Herhangi Bir Etikete Sahip\",\n    \"type_is\": \"Türü\",\n    \"type_is_not\": \"Tür şu değil\",\n    \"is_from_feed\": \"RSS Beslemesinden mi?\",\n    \"is_not_from_feed\": \"RSS Beslemesinden değil\",\n    \"created_within\": \"Şu Zaman İçinde Oluşturuldu\",\n    \"year_s_ago\": \" Yıl Önce\",\n    \"created_earlier_than\": \"Şundan Daha Önce Oluşturuldu\",\n    \"day_s\": \" Gün(ler)\",\n    \"week_s\": \" Hafta(lar)\",\n    \"month_s\": \" Ay(lar)\",\n    \"year_s\": \" Yıl(lar)\",\n    \"day_s_ago\": \" Gün Önce\",\n    \"week_s_ago\": \" Hafta Önce\",\n    \"month_s_ago\": \" Ay Önce\",\n    \"history\": \"Son Aramalar\",\n    \"title_contains\": \"Başlık İçeriyor\",\n    \"title_does_not_contain\": \"Başlık İçermiyor\",\n    \"is_broken_link\": \"Bozuk Bağlantısı Var\",\n    \"tags\": \"Etiketler\",\n    \"no_suggestions\": \"Öneri yok\",\n    \"filters\": \"Filtreler\",\n    \"is_not_broken_link\": \"Çalışan Bağlantısı Var\",\n    \"lists\": \"Listeler\",\n    \"feeds\": \"Akışlar\",\n    \"is_from_source\": \"Kaynak\",\n    \"is_not_from_source\": \"Kaynak değil\"\n  },\n  \"dialogs\": {\n    \"bookmarks\": {\n      \"delete_confirmation_description\": \"Bu yer işaretini silmek istediğinden emin misin?\",\n      \"delete_confirmation_title\": \"Yer İşaretini Silmek İstiyor Musun?\"\n    }\n  },\n  \"banners\": {\n    \"no_bookmarks\": {\n      \"title\": \"Henüz yer işareti yok\",\n      \"description\": \"İlgini çeken makaleleri, bağlantıları ve sayfaları kaydet ve daha sonra hızlıca eriş.\"\n    }\n  },\n  \"bookmark_editor\": {\n    \"title\": \"Yer İşaretini Düzenle\",\n    \"subtitle\": \"Yer işareti ayrıntılarında değişiklik yap. İşin bitince kaydet'e tıkla.\",\n    \"author\": \"Yazar\",\n    \"publisher\": \"Yayımcı\",\n    \"date_published\": \"Yayınlanma Tarihi\",\n    \"pick_a_date\": \"Bir tarih seç\",\n    \"save_changes\": \"Değişiklikleri kaydet\",\n    \"extracted_content\": \"Çıkarılan İçerik\"\n  },\n  \"view_options\": {\n    \"title\": \"Görünüm Seçenekleri\",\n    \"layout\": \"Yerleşim\",\n    \"columns\": \"Sütunlar\",\n    \"display_options\": \"Görüntüleme Seçenekleri\",\n    \"show_note_previews\": \"Notları Göster\",\n    \"show_tags\": \"Etiketleri Göster\",\n    \"show_title\": \"Başlığı Göster\",\n    \"image_options\": \"Görüntü Seçenekleri\",\n    \"image_fit_cover\": \"Kapak (Doldur)\",\n    \"image_fit_contain\": \"İçer (Sığdır)\"\n  },\n  \"version\": {\n    \"new_release_available\": \"Yeni sürüm notları mevcut\",\n    \"whats_new_title\": \"v{{version}} sürümünde ne var?\",\n    \"release_notes_description\": \"GitHub sürüm notlarından alınan en son güncellemeler burada.\",\n    \"loading_release_notes\": \"Sürüm notları yükleniyor…\",\n    \"unable_to_load_release_notes\": \"Şu anda sürüm notları yüklenemiyor. Lütfen daha sonra tekrar deneyin.\",\n    \"no_release_notes\": \"Bu sürüm için yayınlanmış sürüm notu yok.\",\n    \"release_notes_synced\": \"Sürüm notları GitHub'dan senkronize edilir.\",\n    \"view_on_github\": \"GitHub'da görüntüle\"\n  },\n  \"wrapped\": {\n    \"title\": \"{{year}} Özetin\",\n    \"subtitle\": \"Karakeep'te Bir Yıl\",\n    \"banner\": {\n      \"title\": \"2025 Özetin hazır!\",\n      \"description\": \"Yılını yer işaretlerinde gör\",\n      \"view_now\": \"Şimdi Görüntüle\"\n    },\n    \"button\": \"2025 Özeti\",\n    \"loading\": \"Özetin yükleniyor...\",\n    \"failed_to_load\": \"Özet istatistiklerini yükleme başarısız oldu\",\n    \"sections\": {\n      \"total_saves\": {\n        \"prefix\": \"Kaydettiğin\",\n        \"suffix\": \"bu yılki öğeler\",\n        \"suffix_singular\": \"bu yılki öğe\"\n      },\n      \"first_bookmark\": {\n        \"title\": \"Yolculuğun Başladı\",\n        \"description\": \"{{year}} yılının ilk kaydı:\"\n      },\n      \"top_domains\": \"En Popüler Sitelerin\",\n      \"top_tags\": \"En Popüler Etiketlerin\",\n      \"monthly_activity\": \"Kaydettiklerinle Geçen Yılın\",\n      \"most_active_day\": \"En Aktif Günün\",\n      \"peak_times\": {\n        \"title\": \"Ne Zaman Kaydediyorsun\",\n        \"peak_hour\": \"En Yoğun Saat\",\n        \"peak_day\": \"En Yoğun Gün\"\n      },\n      \"how_you_save\": \"Nasıl Kaydediyorsun\",\n      \"what_you_saved\": \"Neler Kaydettin\",\n      \"summary\": {\n        \"favorites\": \"Favoriler\",\n        \"tags_created\": \"Oluşturulan Etiketler\",\n        \"highlights\": \"Öne Çıkanlar\"\n      },\n      \"types\": {\n        \"links\": \"Bağlantılar\",\n        \"notes\": \"Notlar\",\n        \"assets\": \"Varlıklar\"\n      }\n    },\n    \"footer\": \"Karakeep ile yapıldı\",\n    \"share\": \"Paylaş\",\n    \"download\": \"İndir\",\n    \"close\": \"Kapat\",\n    \"generating\": \"Oluşturuluyor...\"\n  }\n}\n"
  },
  {
    "path": "apps/web/lib/i18n/locales/uk/translation.json",
    "content": "{\n  \"common\": {\n    \"url\": \"URL\",\n    \"name\": \"Ім'я\",\n    \"email\": \"Email\",\n    \"password\": \"Пароль\",\n    \"action\": \"Дія\",\n    \"actions\": \"Дії\",\n    \"created_at\": \"Створено\",\n    \"key\": \"Ключ\",\n    \"role\": \"Роль\",\n    \"type\": \"Тип\",\n    \"size\": \"Розмір\",\n    \"roles\": {\n      \"user\": \"Користувач\",\n      \"admin\": \"Адміністратор\"\n    },\n    \"experimental\": \"Експериментальний\",\n    \"search\": \"Пошук\",\n    \"something_went_wrong\": \"Щось пішло не так\",\n    \"bookmark_types\": {\n      \"title\": \"Тип закладки\",\n      \"link\": \"Посилання\",\n      \"text\": \"Текст\",\n      \"media\": \"Медіа\"\n    },\n    \"tags\": \"Теги\",\n    \"note\": \"Примітка\",\n    \"attachments\": \"Вкладення\",\n    \"highlights\": \"Основні моменти\",\n    \"source\": \"Джерело\",\n    \"screenshot\": \"Скріншот\",\n    \"video\": \"Відео\",\n    \"archive\": \"Архів\",\n    \"home\": \"Домашня сторінка\",\n    \"updated_at\": \"Оновлено\",\n    \"title\": \"Заголовок\",\n    \"description\": \"Опис\",\n    \"summary\": \"Короткий зміст\",\n    \"quota\": \"Квота\",\n    \"bookmarks\": \"Закладки\",\n    \"storage\": \"Сховище\",\n    \"pdf\": \"Архівні PDF\",\n    \"default\": \"За замовчуванням\",\n    \"id\": \"ID\",\n    \"last_used\": \"Востаннє використано\"\n  },\n  \"actions\": {\n    \"sign_out\": \"Вийти\",\n    \"change_layout\": \"Змінити макет\",\n    \"archive\": \"Архів\",\n    \"unarchive\": \"Розархівувати\",\n    \"favorite\": \"Улюблене\",\n    \"unfavorite\": \"Видалити з улюбленого\",\n    \"refresh\": \"Оновити\",\n    \"recrawl\": \"Повторне сканування\",\n    \"download_full_page_archive\": \"Завантажити повний архів сторінки\",\n    \"edit_tags\": \"Редагувати теги\",\n    \"add_to_list\": \"Додати до списку\",\n    \"select_all\": \"Вибрати все\",\n    \"unselect_all\": \"Зняти виділення з усіх\",\n    \"copy_link\": \"Копіювати посилання\",\n    \"close_bulk_edit\": \"Закрити масове редагування\",\n    \"bulk_edit\": \"Редагування декількох\",\n    \"manage_lists\": \"Керування списками\",\n    \"remove_from_list\": \"Видалити зі списку\",\n    \"add\": \"Додати\",\n    \"edit\": \"Редагувати\",\n    \"create\": \"Створити\",\n    \"fetch_now\": \"Отримати зараз\",\n    \"summarize_with_ai\": \"Підсумувати за допомогою ШІ\",\n    \"edit_title\": \"Редагувати заголовок\",\n    \"close\": \"Закрити\",\n    \"merge\": \"Злиття\",\n    \"cancel\": \"Скасувати\",\n    \"apply_all\": \"Застосувати все\",\n    \"ignore\": \"Ігнорувати\",\n    \"sort\": {\n      \"title\": \"Сортувати\",\n      \"newest_first\": \"Спочатку новіші\",\n      \"oldest_first\": \"Спочатку найстаріші\",\n      \"relevant_first\": \"Спочатку найрелевантніші\"\n    },\n    \"delete\": \"Видалити\",\n    \"save\": \"Зберегти\",\n    \"open_editor\": \"Відкрити редактор\",\n    \"toggle_show_archived\": \"Показати заархівовані\",\n    \"confirm\": \"Підтвердити\",\n    \"regenerate\": \"Відновити\",\n    \"load_more\": \"Завантажити більше\",\n    \"edit_notes\": \"Редагувати примітки\",\n    \"preserve_as_pdf\": \"Зберегти як PDF\",\n    \"offline_copies\": \"Офлайн копії\",\n    \"preserve_offline_archive\": \"Зберегти офлайн-архів\",\n    \"download_full_page_archive_file\": \"Завантажити файл архіву\",\n    \"download_pdf_file\": \"Завантажити PDF-файл\",\n    \"remove\": \"Видалити\",\n    \"more\": \"Більше\",\n    \"replace_banner\": \"Замінити банер\",\n    \"add_banner\": \"Додати банер\",\n    \"download\": \"Завантажити\"\n  },\n  \"settings\": {\n    \"webhooks\": {\n      \"events\": {\n        \"title\": \"Події\",\n        \"crawled\": \"Проскановано\",\n        \"created\": \"Створено\",\n        \"edited\": \"Відредаговано\"\n      },\n      \"webhooks\": \"Вебхуки\",\n      \"description\": \"Ви можете використовувати вебхуки для запуску дій, коли закладки створюються, змінюються або скануються.\",\n      \"auth_token\": \"Токен автентифікації\",\n      \"add_auth_token\": \"Додати токен автентифікації\",\n      \"edit_auth_token\": \"Редагувати токен автентифікації\",\n      \"create_webhook\": \"Створити вебхук\",\n      \"delete_webhook_confirmation\": \"Ви впевнені, що хочете видалити цей вебхук?\",\n      \"edit_webhook\": \"Редагувати вебхук\",\n      \"webhook_url\": \"URL-адреса веб-перехоплювача\",\n      \"delete_webhook\": \"Видалити вебхук\"\n    },\n    \"back_to_app\": \"Назад до застосунку\",\n    \"user_settings\": \"Налаштування користувача\",\n    \"info\": {\n      \"user_info\": \"Інформація про користувача\",\n      \"basic_details\": \"Основні відомості\",\n      \"change_password\": \"Змінити пароль\",\n      \"current_password\": \"Поточний пароль\",\n      \"new_password\": \"Новий пароль\",\n      \"confirm_new_password\": \"Підтвердьте новий пароль\",\n      \"options\": \"Параметри\",\n      \"interface_lang\": \"Мова інтерфейсу\",\n      \"user_settings\": {\n        \"user_settings_updated\": \"Налаштування користувача оновлено!\",\n        \"bookmark_click_action\": {\n          \"title\": \"Дія при кліку на закладку\",\n          \"open_external_url\": \"Відкрити оригінальний URL\",\n          \"open_bookmark_details\": \"Відкрити деталі закладки\"\n        },\n        \"archive_display_behaviour\": {\n          \"title\": \"Заархівовані закладки\",\n          \"show\": \"Показувати заархівовані закладки в тегах і списках\",\n          \"hide\": \"Приховувати заархівовані закладки в тегах і списках\"\n        }\n      },\n      \"reader_settings\": {\n        \"local_overrides_title\": \"Активні налаштування для конкретного пристрою\",\n        \"using_default\": \"Використовується типове значення клієнта\",\n        \"clear_override_hint\": \"Очистити переналаштування пристрою, щоб використовувати глобальні налаштування ({{value}})\",\n        \"font_size\": \"Розмір шрифту\",\n        \"font_family\": \"Сімейство шрифтів\",\n        \"preview_inline\": \"(попередній перегляд)\",\n        \"tooltip_preview\": \"Не збережені зміни попереднього перегляду\",\n        \"save_to_all_devices\": \"Усі пристрої\",\n        \"tooltip_local\": \"Налаштування пристрою відрізняються від глобальних\",\n        \"reset_preview\": \"Скинути попередній перегляд\",\n        \"mono\": \"Моноширинний\",\n        \"line_height\": \"міжрядковий інтервал\",\n        \"tooltip_default\": \"Налаштування читання\",\n        \"title\": \"Параметри читання\",\n        \"serif\": \"Serif\",\n        \"preview\": \"Перегляд\",\n        \"not_set\": \"Не встановлено\",\n        \"clear_local_overrides\": \"Очистити налаштування пристрою\",\n        \"preview_text\": \"Швидкий бурий лис стрибає через ледачого пса. Ось як виглядатиме ваш текст у режимі читання.\",\n        \"local_overrides_cleared\": \"Налаштування для конкретного пристрою очищено\",\n        \"local_overrides_description\": \"На цьому пристрої параметри читання відрізняються від ваших глобальних типових значень:\",\n        \"clear_defaults\": \"Очистити всі типові налаштування\",\n        \"description\": \"Налаштуйте параметри тексту для перегляду в режимі читання. Ці параметри синхронізуються на всіх ваших пристроях.\",\n        \"defaults_cleared\": \"Типові значення читання очищено\",\n        \"save_hint\": \"Зберегти налаштування тільки для цього пристрою або синхронізувати на всіх пристроях\",\n        \"save_as_default\": \"Зберегти як типові\",\n        \"save_to_device\": \"Цей пристрій\",\n        \"sans\": \"Sans Serif\",\n        \"tooltip_preview_and_local\": \"Не збережені зміни попереднього перегляду; налаштування пристрою відрізняються від глобальних\",\n        \"adjust_hint\": \"Налаштуйте параметри вище, щоб попередньо переглянути зміни\"\n      },\n      \"avatar\": {\n        \"upload\": \"Завантажити аватар\",\n        \"change\": \"Змінити аватар\",\n        \"remove_confirm_title\": \"Видалити аватар?\",\n        \"updated\": \"Аватар оновлено\",\n        \"removed\": \"Аватар видалено\",\n        \"description\": \"Завантаж квадратне зображення, щоб використовувати його як свій аватар.\",\n        \"remove_confirm_description\": \"Це видалить поточне фото профілю.\",\n        \"title\": \"Фото профілю\",\n        \"remove\": \"Видалити аватар\"\n      }\n    },\n    \"ai\": {\n      \"ai_settings\": \"Налаштування ШІ\",\n      \"tagging_rule_description\": \"Підказки, які ви додаєте тут, будуть включені як правила до моделі під час створення тегів. Ви можете переглянути остаточні підказки в розділі попереднього перегляду підказок.\",\n      \"text_prompt\": \"Текстова підказка\",\n      \"images_prompt\": \"Підказка для зображення\",\n      \"summarization_prompt\": \"Підказка для підсумовування\",\n      \"all_tagging\": \"Усі теги\",\n      \"text_tagging\": \"Текстові теги\",\n      \"image_tagging\": \"Тегування зображень\",\n      \"summarization\": \"Підсумовування\",\n      \"prompt_preview\": \"Попередній перегляд підказки\",\n      \"tagging_rules\": \"Правила тегів\",\n      \"tag_style\": \"Стиль тегів\",\n      \"auto_summarization_description\": \"Автоматично створюйте підсумки для закладок, використовуючи штучний інтелект.\",\n      \"auto_tagging\": \"Автоматичне тегування\",\n      \"titlecase_spaces\": \"З великої літери з пробілами\",\n      \"lowercase_underscores\": \"З маленької літери з підкресленнями\",\n      \"inference_language\": \"Мова висновування\",\n      \"titlecase_hyphens\": \"З великої літери з дефісами\",\n      \"lowercase_hyphens\": \"З маленької літери з дефісами\",\n      \"lowercase_spaces\": \"З маленької літери з пробілами\",\n      \"inference_language_description\": \"Вибери мову для тегів і підсумків, згенерованих ШІ.\",\n      \"tag_style_description\": \"Обери, як форматуватимуться твої автоматично створені теги.\",\n      \"auto_tagging_description\": \"Автоматично генеруйте теги для своїх закладок за допомогою штучного інтелекту.\",\n      \"camelCase\": \"camelCase\",\n      \"auto_summarization\": \"Автоматичне підсумовування\",\n      \"no_preference\": \"Без переваг\",\n      \"curated_tags\": \"Підібрані теги\",\n      \"curated_tags_description\": \"За бажанням, обмежте AI-тегування, щоб використовувати лише теги зі списку. Коли не вибрано жодного тегу, AI генерує теги вільно.\",\n      \"curated_tags_updated\": \"Підібрані теги успішно оновлено!\",\n      \"curated_tags_update_failed\": \"Не вдалося оновити підібрані теги\"\n    },\n    \"feeds\": {\n      \"rss_subscriptions\": \"RSS-підписки\",\n      \"add_a_subscription\": \"Додати підписку\",\n      \"feed_enabled\": \"RSS-канал увімкнено\",\n      \"feed_disabled\": \"RSS-канал вимкнено\"\n    },\n    \"import\": {\n      \"import_export\": \"Імпорт / Експорт\",\n      \"import_export_bookmarks\": \"Імпорт / Експорт закладок\",\n      \"import_bookmarks_from_html_file\": \"Імпортувати закладки з HTML-файлу\",\n      \"import_bookmarks_from_pocket_export\": \"Імпортувати закладки з експорту Pocket\",\n      \"import_bookmarks_from_matter_export\": \"Імпортувати закладки з експорту Matter\",\n      \"import_bookmarks_from_omnivore_export\": \"Імпорт закладок з експорту Omnivore\",\n      \"import_bookmarks_from_linkwarden_export\": \"Імпортувати закладки з експорту Linkwarden\",\n      \"import_bookmarks_from_karakeep_export\": \"Імпортувати закладки з експорту Karakeep\",\n      \"export_links_and_notes\": \"Експорт посилань і нотаток\",\n      \"imported_bookmarks\": \"Імпортовані закладки\",\n      \"import_bookmarks_from_tab_session_manager_export\": \"Імпортувати закладки з Tab Session Manager\",\n      \"import_bookmarks_from_mymind_export\": \"Імпортувати закладки з експорту mymind\",\n      \"import_bookmarks_from_instapaper_export\": \"Імпортувати закладки з експорту Instapaper\"\n    },\n    \"api_keys\": {\n      \"api_keys\": \"Ключі API\",\n      \"new_api_key\": \"Новий ключ API\",\n      \"new_api_key_desc\": \"Дайте своєму API-ключу унікальне ім'я\",\n      \"key_success_please_copy\": \"Будь ласка, скопіюйте ключ і збережіть його в надійному місці. Після закриття діалогового вікна ви більше не зможете отримати до нього доступ.\",\n      \"key_success\": \"Ключ успішно створено\",\n      \"regenerate_api_key\": \"Відновити ключ API\",\n      \"key_regenerated\": \"Ключ успішно відновлено\",\n      \"key_regenerated_please_copy\": \"Будь ласка, скопіюйте новий ключ і збережіть його десь у безпечному місці. Старий ключ було відкликано, і він більше не працюватиме.\",\n      \"regenerate_warning\": \"Ви впевнені, що хочете відновити ключ API \\\"{{name}}\\\"?\",\n      \"regenerate_confirmation\": \"Це призведе до скасування поточного ключа та створення нового. Будь-які програми, які використовують поточний ключ, перестануть працювати.\"\n    },\n    \"broken_links\": {\n      \"broken_links\": \"Неробочі посилання\",\n      \"last_crawled_at\": \"Останнє сканування\",\n      \"crawling_failed\": \"Помилка сканування\",\n      \"crawling_status\": \"Статус сканування\"\n    },\n    \"manage_assets\": {\n      \"manage_assets\": \"Керування ресурсами\",\n      \"no_assets\": \"У вас ще немає жодних ресурсів.\",\n      \"asset_type\": \"Тип активу\",\n      \"bookmark_link\": \"Посилання на закладку\",\n      \"asset_link\": \"Посилання на ресурс\",\n      \"delete_asset\": \"Видалити ресурс\",\n      \"delete_asset_confirmation\": \"Ви впевнені, що хочете видалити цей актив?\"\n    },\n    \"rules\": {\n      \"conditions_types\": {\n        \"always\": \"Завжди\",\n        \"url_contains\": \"URL містить\",\n        \"imported_from_feed\": \"Імпортовано з каналу\",\n        \"bookmark_type_is\": \"Тип закладки\",\n        \"has_tag\": \"Має тег\",\n        \"is_favourited\": \"В обраних\",\n        \"is_archived\": \"В архіві\",\n        \"and\": \"Усі наступні умови є істинними\",\n        \"or\": \"Будь-яке з наступного є правдою\",\n        \"url_does_not_contain\": \"URL не містить\",\n        \"title_contains\": \"Назва містить\",\n        \"title_does_not_contain\": \"Назва не містить\"\n      },\n      \"enter_rule_name\": \"Введіть назву правила\",\n      \"rules\": \"Механізм правил\",\n      \"rule_name\": \"Назва правила\",\n      \"description\": \"Ти можеш використовувати правила для запуску дій, коли відбувається подія.\",\n      \"ceate_rule\": \"Створити правило\",\n      \"edit_rule\": \"Редагувати правило\",\n      \"save_rule\": \"Зберегти правило\",\n      \"delete_rule\": \"Видалити правило\",\n      \"delete_rule_confirmation\": \"Ти впевнений, що хочеш видалити це правило?\",\n      \"whenever\": \"Коли ...\",\n      \"if\": \"Якщо ...\",\n      \"describe_what_this_rule_does\": \"Опиши, що робить це правило\",\n      \"rule_has_been_created\": \"Правило створено!\",\n      \"rule_has_been_updated\": \"Правило оновлено!\",\n      \"rule_has_been_deleted\": \"Правило видалено!\",\n      \"no_rules_created_yet\": \"Ще немає жодних правил\",\n      \"create_your_first_rule\": \"Створи своє перше правило, щоб автоматизувати робочий процес\",\n      \"actions_types\": {\n        \"add_tag\": \"Додати тег\",\n        \"remove_tag\": \"Видалити тег\",\n        \"add_to_list\": \"Додати до списку\",\n        \"remove_from_list\": \"Видалити зі списку\",\n        \"download_full_page_archive\": \"Завантажити повний архів сторінки\",\n        \"favourite_bookmark\": \"Улюблена закладка\",\n        \"archive_bookmark\": \"Архівувати закладку\"\n      },\n      \"events_types\": {\n        \"bookmark_added\": \"Закладку додано\",\n        \"tag_added\": \"Цей тег додано до закладки\",\n        \"tag_removed\": \"Цей тег видалено із закладки\",\n        \"added_to_list\": \"Закладку додано до цього списку\",\n        \"removed_from_list\": \"Закладку видалено з цього списку\",\n        \"favourited\": \"Закладка додана до улюблених\",\n        \"archived\": \"Закладку заархівовано\"\n      }\n    },\n    \"stats\": {\n      \"usage_statistics\": \"Статистика використання\",\n      \"insights_description\": \"Аналіз твоїх звичок і колекції закладок\",\n      \"failed_to_load\": \"Не вдалося завантажити статистику\",\n      \"overview\": {\n        \"total_bookmarks\": \"Всього закладок\",\n        \"all_saved_items\": \"Усі збережені елементи\",\n        \"favorites\": \"Улюблене\",\n        \"starred_bookmarks\": \"Відмічені закладки\",\n        \"archived\": \"В архіві\",\n        \"archived_items\": \"Архівні елементи\",\n        \"tags\": \"Теги\",\n        \"unique_tags_created\": \"Створено унікальних тегів\",\n        \"lists\": \"Списки\",\n        \"bookmark_collections\": \"Колекції закладок\",\n        \"highlights\": \"Виділення\",\n        \"text_highlights\": \"Текстові виділення\",\n        \"storage_used\": \"Використано сховище\",\n        \"total_asset_storage\": \"Загальний обсяг сховища активів\",\n        \"this_month\": \"Цього місяця\",\n        \"bookmarks_added\": \"Закладки додано\"\n      },\n      \"bookmark_types\": {\n        \"title\": \"Типи закладок\",\n        \"links\": \"Посилання\",\n        \"text_notes\": \"Текстові нотатки\",\n        \"assets\": \"Активи\"\n      },\n      \"recent_activity\": {\n        \"title\": \"Остання активність\",\n        \"this_week\": \"Цього тижня\",\n        \"this_month\": \"Цього місяця\",\n        \"this_year\": \"Цього року\"\n      },\n      \"top_domains\": {\n        \"title\": \"Топ доменів\",\n        \"no_domains_found\": \"Доменів не знайдено\"\n      },\n      \"most_used_tags\": {\n        \"title\": \"Теги, що використовуються найчастіше\",\n        \"no_tags_found\": \"Тегів не знайдено\"\n      },\n      \"activity_patterns\": {\n        \"activity_by_hour\": \"Активність за годину\",\n        \"activity_by_day\": \"Активність за днями\"\n      },\n      \"storage_breakdown\": {\n        \"title\": \"Розбивка сховища\"\n      },\n      \"bookmark_sources\": {\n        \"title\": \"Джерела закладок\",\n        \"empty\": \"Відсутні вихідні дані\"\n      }\n    },\n    \"subscription\": {\n      \"subscription\": \"Підписка\",\n      \"manage_subscription\": \"Керуй своєю підпискою та платіжною інформацією\",\n      \"current_plan\": \"Поточний план\",\n      \"billing_period\": \"Розрахунковий період\",\n      \"paid_plan\": \"Платний план\",\n      \"unlock_bigger_quota\": \"Розблокуй більшу квоту та підтримай проєкт\",\n      \"subscribe_now\": \"Підписатися зараз\",\n      \"manage_billing\": \"Керувати виставленням рахунків\",\n      \"subscription_canceled\": \"Твою підписку скасовано, і вона закінчиться {{date}}. Ти можеш відновити підписку в будь-який час.\",\n      \"usage_quotas\": \"Використання та квоти\",\n      \"track_usage\": \"Відстежуй поточне використання відповідно до лімітів твого плану\",\n      \"total_bookmarks_saved\": \"Загальна кількість збережених закладок\",\n      \"assets_file_storage\": \"Активи та місце для зберігання файлів\",\n      \"unlimited_usage\": \"Необмежене використання\",\n      \"quota_limit_reached\": \"Ліміт квоти досягнуто\",\n      \"approaching_quota_limit\": \"Наближаємось до ліміту квоти\",\n      \"loading_usage\": \"Завантажую інформацію про використання...\",\n      \"free\": \"Безкоштовно\",\n      \"paid\": \"Платний\"\n    },\n    \"import_sessions\": {\n      \"title\": \"Імпорт сеансів\",\n      \"description\": \"Переглядайте та керуйте своїми сеансами масового імпорту. Сеанси створюються автоматично під час імпорту закладок.\",\n      \"load_error\": \"Не вдалося завантажити сеанси імпорту\",\n      \"no_sessions\": \"Поки що немає сеансів імпорту\",\n      \"no_sessions_detail\": \"Сеанси імпорту з'являться тут автоматично, коли ви імпортуєте закладки\",\n      \"created_at\": \"Створено {{time}}\",\n      \"progress\": \"Прогрес\",\n      \"status\": {\n        \"pending\": \"В очікуванні\",\n        \"in_progress\": \"В процесі виконання\",\n        \"completed\": \"Завершено\",\n        \"failed\": \"Не вдалося\",\n        \"processing\": \"Обробка\",\n        \"staging\": \"Підготовка\",\n        \"running\": \"Працює\",\n        \"paused\": \"Призупинено\"\n      },\n      \"badges\": {\n        \"pending\": \"{{count}} в очікуванні\",\n        \"processing\": \"{{count}} в обробці\",\n        \"completed\": \"{{count}} завершено\",\n        \"failed\": \"{{count}} не вдалося\"\n      },\n      \"imported_to\": \"Імпортовано в:\",\n      \"view_list\": \"Перегляд списку\",\n      \"delete_dialog_title\": \"Видалити сеанс імпорту\",\n      \"delete_dialog_description\": \"Ви впевнені, що хочете видалити \\\"{{name}}\\\"? Цю дію неможливо скасувати. Самі закладки не буде видалено.\",\n      \"delete_session\": \"Видалити сесію\",\n      \"pause_session\": \"Призупинити\",\n      \"resume_session\": \"Відновити\",\n      \"view_details\": \"Перегляд деталей\",\n      \"detail\": {\n        \"page_title\": \"Деталі сеансу імпорту\",\n        \"back_to_import\": \"Назад до імпорту\",\n        \"filter_all\": \"Усі\",\n        \"filter_accepted\": \"Прийнято\",\n        \"filter_rejected\": \"Відхилено\",\n        \"filter_duplicates\": \"Дублікати\",\n        \"filter_pending\": \"В очікуванні\",\n        \"table_title\": \"Заголовок / URL\",\n        \"table_type\": \"Тип\",\n        \"table_result\": \"Результат\",\n        \"table_reason\": \"Причина\",\n        \"table_bookmark\": \"Закладка\",\n        \"result_accepted\": \"Прийнято\",\n        \"result_rejected\": \"Відхилено\",\n        \"result_skipped_duplicate\": \"Дублікат\",\n        \"result_pending\": \"В очікуванні\",\n        \"result_processing\": \"Обробка\",\n        \"no_results\": \"За цим фільтром нічого не знайдено.\",\n        \"view_bookmark\": \"Переглянути закладку\",\n        \"load_more\": \"Завантажити ще\",\n        \"no_title\": \"Без назви\"\n      }\n    },\n    \"backups\": {\n      \"backups\": \"Резервні копії\",\n      \"page_title\": \"Резервні копії\",\n      \"page_description\": \"Автоматично створюй та керуй резервними копіями своїх закладок. Резервні копії стискаються та надійно зберігаються.\",\n      \"configuration\": {\n        \"title\": \"Налаштування резервного копіювання\",\n        \"enable_automatic_backups\": \"Увімкнути автоматичне резервне копіювання\",\n        \"enable_automatic_backups_description\": \"Автоматично створювати резервні копії твоїх закладок\",\n        \"backup_frequency\": \"Періодичність резервного копіювання\",\n        \"backup_frequency_description\": \"Як часто потрібно створювати резервні копії\",\n        \"retention_period\": \"Термін зберігання (дні)\",\n        \"retention_period_description\": \"Скільки днів зберігати резервні копії перед їх видаленням\",\n        \"frequency\": {\n          \"daily\": \"Щодня\",\n          \"weekly\": \"Щотижня\"\n        },\n        \"select_frequency\": \"Виберіть періодичність\",\n        \"save_settings\": \"Зберегти налаштування\"\n      },\n      \"list\": {\n        \"title\": \"Твої резервні копії\",\n        \"create_backup_now\": \"Створити резервну копію зараз\",\n        \"no_backups\": \"У тебе ще немає жодних резервних копій. Увімкни автоматичне резервне копіювання або створи її вручну.\",\n        \"table\": {\n          \"created_at\": \"Створено\",\n          \"bookmarks\": \"Закладки\",\n          \"size\": \"Розмір\",\n          \"status\": \"Статус\",\n          \"actions\": \"Дії\"\n        },\n        \"status\": {\n          \"success\": \"Успішно\",\n          \"failed\": \"Не вдалося\",\n          \"pending\": \"В очікуванні\"\n        },\n        \"actions\": {\n          \"download_backup\": \"Завантажити резервну копію\",\n          \"delete_backup\": \"Видалити резервну копію\"\n        }\n      },\n      \"dialogs\": {\n        \"delete_backup_title\": \"Видалити резервну копію?\",\n        \"delete_backup_description\": \"Ви впевнені, що хочете видалити цю резервну копію? Цю дію неможливо буде скасувати.\"\n      },\n      \"toasts\": {\n        \"backup_queued\": \"Завдання резервного копіювання поставлено в чергу! Воно буде оброблено найближчим часом.\",\n        \"backup_deleted\": \"Резервну копію видалено!\"\n      }\n    }\n  },\n  \"search\": {\n    \"or\": \"Або\",\n    \"is_favorited\": \"В обраних\",\n    \"is_not_favorited\": \"Не в обраних\",\n    \"is_not_archived\": \"Не заархівовано\",\n    \"has_no_tags\": \"Немає теґа\",\n    \"is_in_any_list\": \"Є в будь-якому списку\",\n    \"not_created_on_or_after\": \"Не створено після\",\n    \"created_on_or_before\": \"Створено або до\",\n    \"not_created_on_or_before\": \"Не створено до або раніше\",\n    \"is_in_list\": \"Є у списку\",\n    \"is_not_in_list\": \"Не в списку\",\n    \"full_text_search\": \"Повнотекстовий пошук\",\n    \"type_is\": \"Тип є\",\n    \"type_is_not\": \"Тип не є\",\n    \"and\": \"І\",\n    \"has_any_tag\": \"Має будь-який тег\",\n    \"is_archived\": \"В архіві\",\n    \"is_not_in_any_list\": \"Немає в жодному списку\",\n    \"created_on_or_after\": \"Створено або після\",\n    \"url_contains\": \"URL містить\",\n    \"url_does_not_contain\": \"URL не містить\",\n    \"has_tag\": \"Має тег\",\n    \"does_not_have_tag\": \"Не має теґа\",\n    \"is_from_feed\": \"З RSS-каналу\",\n    \"is_not_from_feed\": \"Не з RSS-каналу\",\n    \"created_within\": \"Створено протягом\",\n    \"created_earlier_than\": \"Створено раніше ніж\",\n    \"day_s\": \" День(ів)\",\n    \"week_s\": \" Тижнів\",\n    \"month_s\": \" Місяць(ів)\",\n    \"year_s\": \" Рік(ів)\",\n    \"day_s_ago\": \" Днів тому\",\n    \"week_s_ago\": \" Тижнів тому\",\n    \"month_s_ago\": \" Місяців тому\",\n    \"year_s_ago\": \" Років тому\",\n    \"history\": \"Нещодавні пошуки\",\n    \"title_contains\": \"Назва містить\",\n    \"title_does_not_contain\": \"Назва не містить\",\n    \"is_broken_link\": \"Має недійсне посилання\",\n    \"tags\": \"Теги\",\n    \"no_suggestions\": \"Немає пропозицій\",\n    \"filters\": \"Фільтри\",\n    \"is_not_broken_link\": \"Має дійсне посилання\",\n    \"lists\": \"Списки\",\n    \"feeds\": \"Стрічки новин\",\n    \"is_from_source\": \"Джерелом є\",\n    \"is_not_from_source\": \"Джерелом не є\"\n  },\n  \"preview\": {\n    \"cached_content\": \"Кешований вміст\",\n    \"view_original\": \"Переглянути оригінал\",\n    \"reader_view\": \"Режим читання\",\n    \"tabs\": {\n      \"details\": \"Деталі\",\n      \"content\": \"Вміст\"\n    },\n    \"archive_info\": \"Архіви можуть неправильно відображатися вбудовано, якщо їм потрібен Javascript. Для кращого результату, <1>завантажте їх і відкрийте у своєму браузері</1>.\",\n    \"fetch_error_title\": \"Вміст недоступний\",\n    \"fetch_error_description\": \"Не вдалося отримати вміст за цим посиланням. Можливо, сторінка захищена, вимагає автентифікації або тимчасово недоступна.\",\n    \"crawling_in_progress\": \"Отримую вміст сторінки…\",\n    \"continue_reading\": \"Продовжити з місця, де зупинилися\",\n    \"continue_reading_percent\": \"Продовжити з місця, де зупинились ({{percent}}%)\",\n    \"continue_button\": \"Продовжити\"\n  },\n  \"layouts\": {\n    \"masonry\": \"Кам'яна кладка\",\n    \"grid\": \"Сітка\",\n    \"list\": \"Список\",\n    \"compact\": \"Компактний\"\n  },\n  \"highlights\": {\n    \"no_highlights\": \"У вас ще немає виділень.\"\n  },\n  \"admin\": {\n    \"admin_settings\": \"Налаштування адміністратора\",\n    \"server_stats\": {\n      \"server_stats\": \"Статистика сервера\",\n      \"total_users\": \"Всього користувачів\",\n      \"total_bookmarks\": \"Всього закладок\",\n      \"server_version\": \"Версія сервера\"\n    },\n    \"background_jobs\": {\n      \"video_jobs\": \"Завдання завантаження відео\",\n      \"feed_jobs\": \"Завдання для RSS-каналу\",\n      \"webhook_jobs\": \"Завдання вебхуків\",\n      \"asset_preprocessing_jobs\": \"Завдання попередньої обробки ресурсів\",\n      \"inference_jobs\": \"Завдання висновування\",\n      \"tidy_assets_jobs\": \"Завдання з очищення ресурсів\",\n      \"job\": \"Завдання\",\n      \"queued\": \"У черзі\",\n      \"background_jobs\": \"Фонові завдання\",\n      \"crawler_jobs\": \"Завдання сканування\",\n      \"indexing_jobs\": \"Завдання індексації\",\n      \"pending\": \"В очікуванні\",\n      \"failed\": \"Не вдалося\",\n      \"jobs\": {\n        \"crawler\": {\n          \"title\": \"Завдання для краулера\",\n          \"description\": \"Веб-сканування та видобуток контенту з URL-адрес\"\n        },\n        \"inference\": {\n          \"title\": \"Завдання висновування\",\n          \"description\": \"Тегування та узагальнення контенту на основі штучного інтелекту\"\n        },\n        \"indexing\": {\n          \"title\": \"Завдання індексації\",\n          \"description\": \"Оновлення пошукового індексу\"\n        },\n        \"asset_preprocessing\": {\n          \"title\": \"Завдання попередньої обробки ресурсів\",\n          \"description\": \"Попередня обробка зображень і документів (скріншоти, вилучення тексту тощо)\"\n        },\n        \"tidy_assets\": {\n          \"title\": \"Завдання з очищення ресурсів\",\n          \"description\": \"Очищення ресурсів та оптимізація зберігання\"\n        },\n        \"video\": {\n          \"title\": \"Завдання завантаження відео\",\n          \"description\": \"Видобуток та завантаження відео\"\n        },\n        \"webhook\": {\n          \"title\": \"Завдання вебперехоплювача\",\n          \"description\": \"Зовнішні сповіщення вебперехоплювача\"\n        },\n        \"feed\": {\n          \"title\": \"Завдання RSS-каналу\",\n          \"description\": \"Обробка RSS-каналу та оновлення контенту\"\n        },\n        \"admin_maintenance\": {\n          \"title\": \"Завдання з адміністрування\",\n          \"description\": \"Адміністративне очищення та обслуговування майна\"\n        }\n      },\n      \"monitor_and_manage\": \"Моніторинг та управління чергами фонових завдань і системними задачами обробки\",\n      \"active\": \"Активний\",\n      \"available_actions\": \"Доступні дії\",\n      \"status\": {\n        \"title\": \"Розуміння станів завдань\",\n        \"queued\": {\n          \"title\": \"В черзі\",\n          \"description\": \"Завдання чекають у черзі на обробку. Вони почнуться автоматично, коли будуть доступні ресурси.\"\n        },\n        \"unprocessed\": {\n          \"title\": \"Не оброблено\",\n          \"description\": \"Закладки, які ще не були оброблені. Скоріш за все, вони вже в черзі на обробку, але якщо ні, можливо, вам доведеться повторно додати їх до черги вручну.\"\n        },\n        \"failed\": {\n          \"title\": \"Помилка\",\n          \"description\": \"Закладки, під час обробки яких виникли помилки. Вони можуть потребувати ручної уваги.\"\n        }\n      },\n      \"actions\": {\n        \"recrawl_failed_links_only\": \"Повторно сканувати тільки пошкоджені посилання\",\n        \"recrawl_all_links\": \"Повторно сканувати всі посилання\",\n        \"without_inference\": \"Без висновків\",\n        \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Відтворити теги ШІ тільки для пошкоджених закладок\",\n        \"regenerate_ai_tags_for_all_bookmarks\": \"Відтворити теги ШІ для всіх закладок\",\n        \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Відтворити стислий виклад ШІ тільки для пошкоджених закладок\",\n        \"regenerate_ai_summaries_for_all_bookmarks\": \"Відтворити стислий виклад ШІ для всіх закладок\",\n        \"reindex_all_bookmarks\": \"Переіндексувати всі закладки\",\n        \"clean_assets\": \"Очистити завислі активи та повторно синхронізувати метадані\",\n        \"reprocess_assets_fix_mode\": \"Повторно обробити необроблені активи\",\n        \"migrate_large_link_html_content\": \"Перемістіть великий вбудований HTML-вміст до ресурсів\",\n        \"recrawl_pending_links_only\": \"Повторно сканувати лише відкладені посилання\",\n        \"regenerate_ai_tags_for_pending_bookmarks_only\": \"Згенерувати теги ШІ лише для відкладених закладок\",\n        \"regenerate_ai_summaries_for_pending_bookmarks_only\": \"Згенерувати зведення ШІ лише для відкладених закладок\"\n      }\n    },\n    \"actions\": {\n      \"recrawl_all_links\": \"Повторно просканувати всі посилання\",\n      \"without_inference\": \"Без висновків\",\n      \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Згенерувати теги ШІ тільки для невдалих закладок\",\n      \"reprocess_assets_fix_mode\": \"Переобробити ресурси (режим виправлення)\",\n      \"reindex_all_bookmarks\": \"Переіндексувати всі закладки\",\n      \"compact_assets\": \"Компактні активи\",\n      \"regenerate_ai_tags_for_all_bookmarks\": \"Відновити теги ШІ для всіх закладок\",\n      \"recrawl_failed_links_only\": \"Повторно просканувати лише пошкоджені посилання\",\n      \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Згенерувати AI-резюме лише для невдалих закладок\",\n      \"regenerate_ai_summaries_for_all_bookmarks\": \"Згенерувати AI-резюме для всіх закладок\"\n    },\n    \"users_list\": {\n      \"users_list\": \"Список користувачів\",\n      \"create_user\": \"Створити користувача\",\n      \"change_role\": \"Змінити роль\",\n      \"reset_password\": \"Скинути пароль\",\n      \"delete_user\": \"Видалити користувача\",\n      \"num_bookmarks\": \"Кількість закладок\",\n      \"asset_sizes\": \"Розміри ресурсів\",\n      \"local_user\": \"Локальний користувач\",\n      \"confirm_password\": \"Підтвердьте пароль\",\n      \"delete_user_confirm_description\": \"Ти впевнений, що хочеш видалити користувача \\\"{{name}}\\\"?\",\n      \"unlimited\": \"Необмежений\"\n    },\n    \"service_connections\": {\n      \"title\": \"Підключення до сервісів\",\n      \"description\": \"Стежте за здоров'ям і підключенням зовнішніх системних залежностей\",\n      \"search_engine\": \"Пошукова система\",\n      \"browser\": \"Браузер\",\n      \"queue_system\": \"Системна черга\",\n      \"status\": {\n        \"not_configured\": \"Не налаштовано\",\n        \"connected\": \"Підключено\",\n        \"disconnected\": \"Відключено\"\n      }\n    },\n    \"admin_tools\": {\n      \"admin_tools\": \"Інструменти адміністратора\",\n      \"bookmark_debugger\": \"Налагоджувач закладок\",\n      \"bookmark_id\": \"ID закладки\",\n      \"bookmark_id_placeholder\": \"Введіть ID закладки\",\n      \"lookup\": \"Пошук\",\n      \"debug_info\": \"Інформація для налагодження\",\n      \"basic_info\": \"Основна інформація\",\n      \"status\": \"Статус\",\n      \"content\": \"Вміст\",\n      \"html_preview\": \"Попередній перегляд HTML (перші 1000 символів)\",\n      \"summary\": \"Короткий зміст\",\n      \"url\": \"URL\",\n      \"source_url\": \"Вихідний URL\",\n      \"asset_type\": \"Тип активу\",\n      \"file_name\": \"Ім'я файлу\",\n      \"owner_user_id\": \"ID користувача-власника\",\n      \"tagging_status\": \"Статус тегування\",\n      \"summarization_status\": \"Статус підсумовування\",\n      \"crawl_status\": \"Статус сканування\",\n      \"crawl_status_code\": \"Код стану HTTP\",\n      \"crawled_at\": \"Проскановано\",\n      \"recrawl\": \"Повторне сканування\",\n      \"reindex\": \"Повторна індексація\",\n      \"retag\": \"Повторне позначення теґами\",\n      \"resummarize\": \"Повторне підсумовування\",\n      \"bookmark_not_found\": \"Закладку не знайдено\",\n      \"action_success\": \"Дію успішно завершено\",\n      \"action_failed\": \"Помилка дії\",\n      \"recrawl_queued\": \"Завдання повторного сканування додано до черги\",\n      \"reindex_queued\": \"Завдання повторної індексації додано до черги\",\n      \"retag_queued\": \"Завдання повторного позначення теґами додано до черги\",\n      \"resummarize_queued\": \"Завдання повторного підсумовування додано до черги\",\n      \"view\": \"Перегляд\",\n      \"fetch_error\": \"Помилка отримання закладки\"\n    }\n  },\n  \"lists\": {\n    \"all_lists\": \"Усі списки\",\n    \"favourites\": \"Улюблене\",\n    \"new_list\": \"Новий список\",\n    \"edit_list\": \"Редагувати список\",\n    \"new_nested_list\": \"Новий вкладений список\",\n    \"list_type\": \"Тип списку\",\n    \"manual_list\": \"Список вручну\",\n    \"smart_list\": \"Розумний список\",\n    \"search_query_help\": \"Дізнайтеся більше про мову пошукових запитів.\",\n    \"search_query\": \"Пошуковий запит\",\n    \"parent_list\": \"Батьківський список\",\n    \"no_parent\": \"Немає батьківського елемента\",\n    \"merge_list\": \"Об'єднати список\",\n    \"destination_list\": \"Список призначення\",\n    \"delete_after_merge\": \"Видалити оригінальний список після об'єднання\",\n    \"no_destination\": \"Немає призначення\",\n    \"description\": \"Опис (Необов'язково)\",\n    \"public_list\": {\n      \"share_link\": \"Поділитися посиланням\",\n      \"title\": \"Публічний список\",\n      \"description\": \"Дозволити іншим переглядати цей список\"\n    },\n    \"share_list\": \"Поділитися списком\",\n    \"rss\": {\n      \"title\": \"RSS-канал\",\n      \"description\": \"Увімкнути RSS-канал для цього списку\",\n      \"feed_url\": \"URL RSS-каналу\"\n    },\n    \"delete_list\": {\n      \"title\": \"Видалити список\",\n      \"description\": \"Видалення списку не призведе до видалення жодної із закладок у цьому списку.\",\n      \"delete_children\": \"Видалити дочірні списки (рекурсивно)\",\n      \"delete_children_description\": \"Якщо не позначено, усі прямі дочірні списки стануть кореневими списками\"\n    },\n    \"shared\": \"Спільне\",\n    \"collaborators\": {\n      \"manage\": \"Управління співавторами\",\n      \"view\": \"Перегляд співавторів\",\n      \"collaborators\": \"Співавтори\",\n      \"add\": \"Додати співавтора\",\n      \"current\": \"Поточні співавтори\",\n      \"enter_email\": \"Введіть адресу електронної пошти\",\n      \"please_enter_email\": \"Будь ласка, введіть адресу електронної пошти\",\n      \"added_successfully\": \"Співавтора успішно додано\",\n      \"failed_to_add\": \"Не вдалося додати співавтора\",\n      \"removed\": \"Співавтора видалено\",\n      \"failed_to_remove\": \"Не вдалося видалити співавтора\",\n      \"role_updated\": \"Роль оновлено\",\n      \"failed_to_update_role\": \"Не вдалося оновити роль\",\n      \"viewer\": \"Глядач\",\n      \"editor\": \"Редактор\",\n      \"owner\": \"Власник\",\n      \"viewer_description\": \"Може переглядати закладки в списку\",\n      \"editor_description\": \"Може додавати та видаляти закладки\",\n      \"no_collaborators\": \"Поки що немає співавторів. Додайте когось, щоб почати співпрацювати!\",\n      \"no_collaborators_readonly\": \"Немає співавторів для цього списку.\",\n      \"people_with_access\": \"Люди, які мають доступ до цього списку\",\n      \"add_or_remove\": \"Додайте або видаліть людей, які можуть отримати доступ до цього списку\",\n      \"invitation_sent\": \"Запрошення успішно надіслано\",\n      \"invitation_revoked\": \"Запрошення відкликано\",\n      \"failed_to_revoke\": \"Не вдалося відкликати запрошення\",\n      \"pending\": \"В очікуванні\",\n      \"revoke\": \"Відкликати\",\n      \"declined\": \"Відхилено\"\n    },\n    \"leave_list\": {\n      \"title\": \"Покинути список\",\n      \"confirm_message\": \"Ви впевнені, що хочете покинути {{icon}} {{name}}?\",\n      \"warning\": \"Ви більше не зможете переглядати або отримувати доступ до закладок у цьому списку. Власник списку може додати вас назад, якщо потрібно.\",\n      \"action\": \"Покинути список\",\n      \"success\": \"Ви покинули \\\"{{icon}} {{name}}\\\"\"\n    },\n    \"invitations\": {\n      \"pending\": \"Запрошення, що очікують на розгляд\",\n      \"description\": \"Перегляньте та дайте відповідь на запрошення до співпраці над списком\",\n      \"invited_by\": \"Запросив\",\n      \"accept\": \"Прийняти\",\n      \"decline\": \"Відхилити\",\n      \"accepted\": \"Запрошення прийнято\",\n      \"declined\": \"Запрошення відхилено\",\n      \"failed_to_accept\": \"Не вдалося прийняти запрошення\",\n      \"failed_to_decline\": \"Не вдалося відхилити запрошення\"\n    },\n    \"shared_lists\": \"Спільні списки\"\n  },\n  \"tags\": {\n    \"your_tags\": \"Ваші теги\",\n    \"your_tags_info\": \"Теги, які ви прикріплювали хоча б один раз\",\n    \"ai_tags\": \"Теги AI\",\n    \"ai_tags_info\": \"Теги, які були додані лише автоматично (за допомогою AI)\",\n    \"unused_tags\": \"Невикористані теги\",\n    \"unused_tags_info\": \"Теги, які не прив'язані до жодної закладки\",\n    \"delete_all_unused_tags\": \"Видалити всі невикористані теги\",\n    \"drag_and_drop_merging\": \"Перетягування для об'єднання\",\n    \"drag_and_drop_merging_info\": \"Перетягніть теги один на одного, щоб об'єднати їх\",\n    \"sort_by_name\": \"Сортувати за назвою\",\n    \"all_tags\": \"Усі теги\",\n    \"create_tag\": \"Створити тег\",\n    \"create_tag_description\": \"Створити новий тег, не прив’язуючи його до жодної закладки\",\n    \"tag_name\": \"Назва тегу\",\n    \"enter_tag_name\": \"Введіть назву тегу\",\n    \"sort_by_usage\": \"Сортувати за використанням\",\n    \"sort_by_relevance\": \"Сортувати за релевантністю\",\n    \"no_custom_tags\": \"Поки що немає користувацьких теґів\",\n    \"no_ai_tags\": \"Ще немає теґів AI\",\n    \"no_unused_tags\": \"У вас немає невикористаних теґів\",\n    \"no_unused_tags_match_your_search\": \"Жоден з невикористаних теґів не відповідає вашому пошуку\",\n    \"no_tags_match_your_search\": \"Жоден теґ не відповідає вашому пошуку\",\n    \"search_placeholder\": \"Пошук тегів...\",\n    \"search_or_create_placeholder\": \"Шукайте або створюйте теги...\"\n  },\n  \"editor\": {\n    \"multiple_urls_dialog_title\": \"Імпортувати URL-адреси як окремі закладки?\",\n    \"multiple_urls_dialog_desc\": \"Вхідні дані містять декілька URL-адрес в окремих рядках. Хочете імпортувати їх як окремі закладки?\",\n    \"text_toolbar\": {\n      \"markdown_shortcuts\": {\n        \"heading\": {\n          \"example\": \"# H1, ## H2, ### H3\",\n          \"label\": \"Заголовок\"\n        },\n        \"bold\": {\n          \"label\": \"Жирний\",\n          \"example\": \"**текст** або CTRL+b\"\n        },\n        \"blockquote\": {\n          \"example\": \"> Цитата\",\n          \"label\": \"Цитата\"\n        },\n        \"ordered_list\": {\n          \"label\": \"Упорядкований список\",\n          \"example\": \"1. Пункт списку\"\n        },\n        \"block_code\": {\n          \"example\": \"``` + space\",\n          \"label\": \"Блок коду\"\n        },\n        \"unordered_list\": {\n          \"example\": \"- Елемент списку\",\n          \"label\": \"Невпорядкований список\"\n        },\n        \"inline_code\": {\n          \"label\": \"Вбудований код\",\n          \"example\": \"`Код`\"\n        },\n        \"label\": \"Markdown-шорткати\",\n        \"italic\": {\n          \"label\": \"Курсив\",\n          \"example\": \"*Курсив* або _Курсив_ або CTRL+i\"\n        }\n      },\n      \"align_center\": \"Вирівняти по центру\",\n      \"align_right\": \"Вирівнювання по правому краю\",\n      \"bold\": \"Жирний\",\n      \"italic\": \"Курсив\",\n      \"underline\": \"Підкреслення\",\n      \"strikethrough\": \"Перекреслений\",\n      \"code\": \"Код\",\n      \"highlight\": \"Підсвічування\",\n      \"align_left\": \"Вирівняти по лівому краю\",\n      \"undo\": \"Скасувати\",\n      \"redo\": \"Повторити\"\n    },\n    \"quickly_focus\": \"Ви можете швидко зосередитися на цьому полі, натиснувши ⌘ + E\",\n    \"import_as_text\": \"Імпортувати як текстову закладку\",\n    \"import_as_separate_bookmarks\": \"Імпортувати як окремі закладки\",\n    \"placeholder\": \"Вставте посилання або зображення, напишіть нотатку або перетягніть зображення сюди…\",\n    \"new_item\": \"НОВИЙ ЕЛЕМЕНТ\",\n    \"disabled_submissions\": \"Надсилання вимкнено\",\n    \"placeholder_v2\": \"Вставте посилання, напишіть нотатку або перетягніть зображення…\"\n  },\n  \"dialogs\": {\n    \"bookmarks\": {\n      \"delete_confirmation_title\": \"Видалити закладку?\",\n      \"delete_confirmation_description\": \"Ви впевнені, що хочете видалити цю закладку?\"\n    }\n  },\n  \"options\": {\n    \"dark_mode\": \"Темний режим\",\n    \"light_mode\": \"Світлий режим\",\n    \"apps_extensions\": \"Застосунки та розширення\",\n    \"documentation\": \"Документація\",\n    \"follow_us_on_x\": \"Слідкуйте за нами в X\"\n  },\n  \"toasts\": {\n    \"bookmarks\": {\n      \"refetch\": \"Повторне отримання поставлено в чергу!\",\n      \"full_page_archive\": \"Створення повного архіву сторінки було запущено\",\n      \"delete_from_list\": \"Закладку видалено зі списку\",\n      \"clipboard_copied\": \"Посилання додано до вашого буфера обміну!\",\n      \"updated\": \"Закладку оновлено!\",\n      \"deleted\": \"Закладку видалено!\",\n      \"preserve_pdf\": \"Збереження PDF ініційовано\",\n      \"update_banner\": \"Банер оновлено!\",\n      \"uploading_banner\": \"Завантаження банера...\"\n    },\n    \"lists\": {\n      \"created\": \"Список створено!\",\n      \"updated\": \"Список оновлено!\",\n      \"merged\": \"Список об'єднано!\",\n      \"deleted\": \"Список видалено!\"\n    },\n    \"tags\": {\n      \"created\": \"Тег створено!\",\n      \"failed_to_create\": \"Не вдалося створити тег\"\n    }\n  },\n  \"cleanups\": {\n    \"cleanups\": \"Очищення\",\n    \"duplicate_tags\": {\n      \"title\": \"Дублікати тегів\",\n      \"merge_all_suggestions\": \"Об'єднати всі пропозиції?\"\n    }\n  },\n  \"banners\": {\n    \"no_bookmarks\": {\n      \"title\": \"Ще немає закладок\",\n      \"description\": \"Зберігай цікаві статті, посилання та сторінки, щоб мати до них швидкий доступ пізніше.\"\n    }\n  },\n  \"bookmark_editor\": {\n    \"title\": \"Редагувати закладку\",\n    \"subtitle\": \"Внесіть зміни до деталей закладки. Коли закінчите, натисніть «Зберегти».\",\n    \"author\": \"Автор\",\n    \"publisher\": \"Видавець\",\n    \"date_published\": \"Дата публікації\",\n    \"pick_a_date\": \"Виберіть дату\",\n    \"save_changes\": \"Зберегти зміни\",\n    \"extracted_content\": \"Видобутий вміст\"\n  },\n  \"view_options\": {\n    \"title\": \"Параметри перегляду\",\n    \"layout\": \"Розмітка\",\n    \"columns\": \"Стовпці\",\n    \"display_options\": \"Параметри відображення\",\n    \"show_note_previews\": \"Показувати примітки\",\n    \"show_tags\": \"Покажи теги\",\n    \"show_title\": \"Покажи назву\",\n    \"image_options\": \"Параметри зображення\",\n    \"image_fit_cover\": \"Обкладинка (Заповнення)\",\n    \"image_fit_contain\": \"Вмістити (За розміром)\"\n  },\n  \"version\": {\n    \"new_release_available\": \"Доступні нові нотатки до випуску\",\n    \"whats_new_title\": \"Що нового у v{{version}}\",\n    \"release_notes_description\": \"Тут є останні оновлення, отримані з нотаток до випуску GitHub.\",\n    \"loading_release_notes\": \"Завантаження нотаток до випуску…\",\n    \"unable_to_load_release_notes\": \"Не вдалося завантажити нотатки до випуску зараз. Спробуйте пізніше.\",\n    \"no_release_notes\": \"Для цієї версії не було опубліковано жодних нотаток до випуску.\",\n    \"release_notes_synced\": \"Нотатки до випуску синхронізовані з GitHub.\",\n    \"view_on_github\": \"Переглянути на GitHub\"\n  },\n  \"wrapped\": {\n    \"title\": \"Твій підсумок за {{year}} рік\",\n    \"subtitle\": \"Рік у Karakeep\",\n    \"banner\": {\n      \"title\": \"Твій підсумок за 2025 рік готовий!\",\n      \"description\": \"Подивись свій рік у закладках\",\n      \"view_now\": \"Подивитись зараз\"\n    },\n    \"button\": \"Підсумок 2025\",\n    \"loading\": \"Завантаження підсумку...\",\n    \"failed_to_load\": \"Не вдалося завантажити статистику підсумку\",\n    \"sections\": {\n      \"total_saves\": {\n        \"prefix\": \"Ти зберіг\",\n        \"suffix\": \"штук цього року\",\n        \"suffix_singular\": \"штука цього року\"\n      },\n      \"first_bookmark\": {\n        \"title\": \"Твоя подорож почалася\",\n        \"description\": \"Перше збереження за {{year}}:\"\n      },\n      \"top_domains\": \"Топ твоїх сайтів\",\n      \"top_tags\": \"Твої головні теґи\",\n      \"monthly_activity\": \"Твій рік у збереженнях\",\n      \"most_active_day\": \"Твій найактивніший день\",\n      \"peak_times\": {\n        \"title\": \"Коли ти зберігаєш\",\n        \"peak_hour\": \"Година пік\",\n        \"peak_day\": \"День пік\"\n      },\n      \"how_you_save\": \"Як ти зберігаєш\",\n      \"what_you_saved\": \"Що ти зберігав\",\n      \"summary\": {\n        \"favorites\": \"Улюблене\",\n        \"tags_created\": \"Створено теґів\",\n        \"highlights\": \"Основні моменти\"\n      },\n      \"types\": {\n        \"links\": \"Посилання\",\n        \"notes\": \"Нотатки\",\n        \"assets\": \"Активи\"\n      }\n    },\n    \"footer\": \"Зроблено з допомогою Karakeep\",\n    \"share\": \"Поділитися\",\n    \"download\": \"Завантажити\",\n    \"close\": \"Закрити\",\n    \"generating\": \"Генерую...\"\n  }\n}\n"
  },
  {
    "path": "apps/web/lib/i18n/locales/vi/translation.json",
    "content": "{\n  \"actions\": {\n    \"save\": \"Lưu\",\n    \"manage_lists\": \"Quản lý danh sách\",\n    \"edit\": \"Sửa\",\n    \"unselect_all\": \"Bỏ chọn tất cả\",\n    \"bulk_edit\": \"Sửa hàng loạt\",\n    \"create\": \"Tạo mới\",\n    \"copy_link\": \"Sao chép liên kết\",\n    \"favorite\": \"Ưa thích\",\n    \"unfavorite\": \"Bỏ ưa thích\",\n    \"change_layout\": \"Thay đổi cách sắp xếp\",\n    \"delete\": \"Xóa\",\n    \"archive\": \"Lưu trữ\",\n    \"unarchive\": \"Bỏ lưu trữ\",\n    \"edit_tags\": \"Sửa nhãn\",\n    \"refresh\": \"Làm mới\",\n    \"select_all\": \"Chọn tất cả\",\n    \"add_to_list\": \"Thêm vào danh sách\",\n    \"download_full_page_archive\": \"Tải xuống toàn bộ trang lưu trữ\",\n    \"recrawl\": \"Quét lại\",\n    \"close_bulk_edit\": \"Đóng sửa hàng loạt\",\n    \"remove_from_list\": \"Xóa khỏi danh sách\",\n    \"cancel\": \"Hủy\",\n    \"close\": \"Đóng\",\n    \"sign_out\": \"Đăng xuất\",\n    \"summarize_with_ai\": \"Tóm tắt bằng AI\",\n    \"sort\": {\n      \"title\": \"Sắp xếp\",\n      \"newest_first\": \"Sắp xếp theo mới nhất\",\n      \"oldest_first\": \"Sắp xếp theo cũ nhất\",\n      \"relevant_first\": \"Liên quan nhất trước\"\n    },\n    \"ignore\": \"Bỏ qua\",\n    \"apply_all\": \"Áp dụng cho tất cả\",\n    \"merge\": \"Hợp nhất\",\n    \"edit_title\": \"Sửa tiêu đề\",\n    \"add\": \"Thêm\",\n    \"fetch_now\": \"Lấy dữ liệu ngay\",\n    \"open_editor\": \"Mở trình chỉnh sửa\",\n    \"toggle_show_archived\": \"Hiển thị đã lưu trữ\",\n    \"confirm\": \"Xác nhận\",\n    \"regenerate\": \"Tạo lại\",\n    \"load_more\": \"Tải thêm\",\n    \"edit_notes\": \"Sửa ghi chú\",\n    \"preserve_as_pdf\": \"Lưu giữ dưới dạng PDF\",\n    \"offline_copies\": \"Bản sao ngoại tuyến\",\n    \"preserve_offline_archive\": \"Lưu Trữ Ngoại Tuyến\",\n    \"download_full_page_archive_file\": \"Tải File Lưu Trữ\",\n    \"download_pdf_file\": \"Tải File PDF\",\n    \"remove\": \"Gỡ\",\n    \"more\": \"Thêm\",\n    \"replace_banner\": \"Thay Banner\",\n    \"add_banner\": \"Thêm Banner\",\n    \"download\": \"Tải xuống\"\n  },\n  \"layouts\": {\n    \"list\": \"Danh sách\",\n    \"grid\": \"Lưới\",\n    \"masonry\": \"Masonry\",\n    \"compact\": \"Gọn\"\n  },\n  \"settings\": {\n    \"feeds\": {\n      \"add_a_subscription\": \"Thêm đăng kí\",\n      \"rss_subscriptions\": \"Đăng kí RSS\",\n      \"feed_enabled\": \"Đã bật Nguồn cấp RSS\",\n      \"feed_disabled\": \"Đã tắt Nguồn cấp RSS\"\n    },\n    \"import\": {\n      \"imported_bookmarks\": \"Đánh dấu trang đã được nhập\",\n      \"import_bookmarks_from_html_file\": \"Nhập đánh dấu trang từ file HTML\",\n      \"import_export\": \"Nhập / Xuất dữ liệu\",\n      \"export_links_and_notes\": \"Xuất liên kết và ghi chú\",\n      \"import_export_bookmarks\": \"Nhập / Xuất đánh dấu trang\",\n      \"import_bookmarks_from_linkwarden_export\": \"Nhập dấu trang từ bản xuất Linkwarden\",\n      \"import_bookmarks_from_pocket_export\": \"Nhập dấu trang từ bản xuất Pocket\",\n      \"import_bookmarks_from_matter_export\": \"Nhập dấu trang từ bản xuất Matter\",\n      \"import_bookmarks_from_omnivore_export\": \"Nhập dấu trang từ xuất Omnivore\",\n      \"import_bookmarks_from_karakeep_export\": \"Nhập dấu trang từ bản xuất Karakeep\",\n      \"import_bookmarks_from_tab_session_manager_export\": \"Nhập dấu trang từ Tab Session Manager\",\n      \"import_bookmarks_from_mymind_export\": \"Nhập dấu trang từ tệp xuất mymind\",\n      \"import_bookmarks_from_instapaper_export\": \"Nhập dấu trang từ bản xuất Instapaper\"\n    },\n    \"webhooks\": {\n      \"edit_auth_token\": \"Sửa mã xác thực\",\n      \"description\": \"Bạn có thể sử dụng webhooks để kích hoạt hành động khi các dấu trang được tạo, thay đổi hoặc quét.\",\n      \"delete_webhook\": \"Xóa Webhook\",\n      \"create_webhook\": \"Tạo Webhook\",\n      \"delete_webhook_confirmation\": \"Bạn có chắc là muốn xóa webhook này không?\",\n      \"events\": {\n        \"created\": \"Đã tạo mới\",\n        \"edited\": \"Đã chỉnh sửa\",\n        \"crawled\": \"Đã quét\",\n        \"title\": \"Sự kiện\"\n      },\n      \"webhooks\": \"Webhooks\",\n      \"auth_token\": \"Mã xác thực\",\n      \"add_auth_token\": \"Thêm mã xác thực\",\n      \"edit_webhook\": \"Sửa Webhook\",\n      \"webhook_url\": \"Webhook URL\"\n    },\n    \"api_keys\": {\n      \"key_success_please_copy\": \"Vui lòng sao chép khóa và lưu trữ nó ở nơi an toàn. Sau khi bạn đóng hộp thoại, bạn sẽ không thể truy cập lại nó.\",\n      \"api_keys\": \"Khóa API\",\n      \"key_success\": \"Khóa đã được tạo thành công\",\n      \"new_api_key\": \"Khóa API mới\",\n      \"new_api_key_desc\": \"Đặt 1 tên độc nhất cho khóa API\",\n      \"regenerate_api_key\": \"Tạo lại API Key\",\n      \"key_regenerated\": \"Đã tạo lại key thành công\",\n      \"key_regenerated_please_copy\": \"Vui lòng copy key mới và lưu trữ nó ở nơi an toàn. Key cũ đã bị thu hồi và sẽ không còn hoạt động nữa.\",\n      \"regenerate_warning\": \"Bạn có chắc chắn muốn tạo lại API key \\\"{{name}}\\\" không?\",\n      \"regenerate_confirmation\": \"Hành động này sẽ thu hồi key hiện tại và tạo một key mới. Bất kỳ ứng dụng nào đang sử dụng key hiện tại sẽ ngừng hoạt động.\"\n    },\n    \"ai\": {\n      \"tagging_rule_description\": \"Các gợi ý mà bạn thêm vào đây sẽ được đưa vào làm quy tắc cho mô hình trong quá trình tạo nhãn. Bạn có thể xem các gợi ý cuối cùng trong phần xem trước gợi ý.\",\n      \"ai_settings\": \"Cài đặt AI\",\n      \"prompt_preview\": \"Xem trước lệnh gợi ý\",\n      \"tagging_rules\": \"Các quy tắc dán nhãn\",\n      \"text_prompt\": \"Lệnh gợi ý văn bản\",\n      \"images_prompt\": \"Lệnh gợi ý hình ảnh\",\n      \"summarization_prompt\": \"Lệnh gợi ý tóm tắt\",\n      \"summarization\": \"Tóm tắt\",\n      \"all_tagging\": \"Tất cả nhãn\",\n      \"image_tagging\": \"Nhãn cho hình ảnh\",\n      \"text_tagging\": \"Nhãn cho văn bản\",\n      \"tag_style\": \"Kiểu Thẻ\",\n      \"auto_summarization_description\": \"Tự động tạo bản tóm tắt cho dấu trang bằng AI.\",\n      \"auto_tagging\": \"Tự động gắn thẻ\",\n      \"titlecase_spaces\": \"Tiêu đề viết hoa có dấu cách\",\n      \"lowercase_underscores\": \"Chữ thường có dấu gạch dưới\",\n      \"inference_language\": \"Ngôn ngữ Suy luận\",\n      \"titlecase_hyphens\": \"Tiêu đề viết hoa có dấu gạch ngang\",\n      \"lowercase_hyphens\": \"Chữ thường có dấu gạch ngang\",\n      \"lowercase_spaces\": \"Chữ thường có dấu cách\",\n      \"inference_language_description\": \"Chọn ngôn ngữ cho các thẻ và tóm tắt do AI tạo.\",\n      \"tag_style_description\": \"Chọn cách định dạng các thẻ tự động tạo của bạn.\",\n      \"auto_tagging_description\": \"Tự động tạo thẻ cho dấu trang bằng AI.\",\n      \"camelCase\": \"camelCase\",\n      \"auto_summarization\": \"Tự động tóm tắt\",\n      \"no_preference\": \"Không có tùy chọn nào cả\",\n      \"curated_tags\": \"Các thẻ được tuyển chọn\",\n      \"curated_tags_description\": \"Bạn có thể giới hạn gắn thẻ AI để chỉ sử dụng các thẻ từ danh sách này. Nếu không có thẻ nào được chọn, AI sẽ tự do tạo thẻ.\",\n      \"curated_tags_updated\": \"Đã cập nhật thành công các thẻ được tuyển chọn!\",\n      \"curated_tags_update_failed\": \"Không cập nhật được các thẻ được tuyển chọn\"\n    },\n    \"info\": {\n      \"basic_details\": \"Thông tin cơ bản\",\n      \"user_info\": \"Thông tin người dùng\",\n      \"change_password\": \"Thay đổi mật khẩu\",\n      \"current_password\": \"Mật khẩu hiện tại\",\n      \"new_password\": \"Mật khẩu mới\",\n      \"confirm_new_password\": \"Xác nhận mật khẩu mới\",\n      \"options\": \"Tùy chọn\",\n      \"interface_lang\": \"Ngôn ngữ giao diện\",\n      \"user_settings\": {\n        \"user_settings_updated\": \"Đã cập nhật cài đặt người dùng!\",\n        \"bookmark_click_action\": {\n          \"title\": \"Hành động khi nhấp vào bookmark\",\n          \"open_external_url\": \"Mở URL gốc\",\n          \"open_bookmark_details\": \"Mở chi tiết bookmark\"\n        },\n        \"archive_display_behaviour\": {\n          \"title\": \"Bookmark đã lưu trữ\",\n          \"show\": \"Hiển thị các bookmark đã lưu trữ trong tag và danh sách\",\n          \"hide\": \"Ẩn các bookmark đã lưu trữ trong tag và danh sách\"\n        }\n      },\n      \"reader_settings\": {\n        \"local_overrides_title\": \"Đã kích hoạt cài đặt dành riêng cho thiết bị\",\n        \"using_default\": \"Sử dụng mặc định của ứng dụng\",\n        \"clear_override_hint\": \"Xóa ghi đè thiết bị để sử dụng cài đặt chung ({{value}})\",\n        \"font_size\": \"Cỡ chữ\",\n        \"font_family\": \"Họ phông chữ\",\n        \"preview_inline\": \"(xem trước)\",\n        \"tooltip_preview\": \"Các thay đổi xem trước chưa được lưu\",\n        \"save_to_all_devices\": \"Tất cả các thiết bị\",\n        \"tooltip_local\": \"Cài đặt thiết bị khác với cài đặt chung\",\n        \"reset_preview\": \"Đặt lại bản xem trước\",\n        \"mono\": \"Đơn cách\",\n        \"line_height\": \"Chiều cao dòng\",\n        \"tooltip_default\": \"Cài đặt đọc\",\n        \"title\": \"Cài đặt Trình đọc\",\n        \"serif\": \"Chân phương\",\n        \"preview\": \"Xem trước\",\n        \"not_set\": \"Chưa đặt\",\n        \"clear_local_overrides\": \"Xóa cài đặt thiết bị\",\n        \"preview_text\": \"Một con cáo nâu nhanh chóng nhảy qua con chó lười biếng. Đây là cách mà văn bản chế độ xem trình đọc của bạn sẽ hiển thị.\",\n        \"local_overrides_cleared\": \"Đã xóa cài đặt cho thiết bị\",\n        \"local_overrides_description\": \"Thiết bị này có các cài đặt trình đọc khác với cài đặt mặc định toàn cầu của bạn:\",\n        \"clear_defaults\": \"Xóa tất cả mặc định\",\n        \"description\": \"Cấu hình cài đặt văn bản mặc định cho chế độ xem trình đọc. Các cài đặt này đồng bộ hóa trên tất cả các thiết bị của bạn.\",\n        \"defaults_cleared\": \"Đã xóa các mặc định của trình đọc\",\n        \"save_hint\": \"Lưu cài đặt chỉ cho thiết bị này hoặc đồng bộ hóa trên tất cả các thiết bị\",\n        \"save_as_default\": \"Lưu làm mặc định\",\n        \"save_to_device\": \"Thiết bị này\",\n        \"sans\": \"Không chân phương\",\n        \"tooltip_preview_and_local\": \"Các thay đổi xem trước chưa được lưu; cài đặt thiết bị khác với cài đặt chung\",\n        \"adjust_hint\": \"Điều chỉnh các cài đặt ở trên để xem trước các thay đổi\"\n      },\n      \"avatar\": {\n        \"upload\": \"Tải lên ảnh đại diện\",\n        \"change\": \"Đổi ảnh đại diện\",\n        \"remove_confirm_title\": \"Xóa ảnh đại diện?\",\n        \"updated\": \"Đã cập nhật ảnh đại diện\",\n        \"removed\": \"Đã xóa ảnh đại diện\",\n        \"description\": \"Tải lên ảnh vuông để dùng làm ảnh đại diện nha.\",\n        \"remove_confirm_description\": \"Hành động này sẽ xóa ảnh hồ sơ hiện tại của bạn đó.\",\n        \"title\": \"Ảnh hồ sơ\",\n        \"remove\": \"Xóa ảnh đại diện\"\n      }\n    },\n    \"user_settings\": \"Cài đặt người dùng\",\n    \"back_to_app\": \"Quay lại ứng dụng\",\n    \"broken_links\": {\n      \"broken_links\": \"Liên kết hỏng\",\n      \"last_crawled_at\": \"Lần quét cuối lúc\",\n      \"crawling_status\": \"Tình trạng quét\",\n      \"crawling_failed\": \"Quét thất bại\"\n    },\n    \"manage_assets\": {\n      \"asset_type\": \"Loại tài sản\",\n      \"bookmark_link\": \"Liên kết dấu trang\",\n      \"asset_link\": \"Liên kết tài sản\",\n      \"delete_asset\": \"Xóa tài sản\",\n      \"delete_asset_confirmation\": \"Bạn có chắc chắn muốn xóa tài sản này không?\",\n      \"manage_assets\": \"Quản lý tài sản\",\n      \"no_assets\": \"Mày chưa có tài sản mẹ gì cả.\"\n    },\n    \"rules\": {\n      \"actions_types\": {\n        \"archive_bookmark\": \"Lưu trữ dấu trang\",\n        \"add_tag\": \"Thêm thẻ\",\n        \"remove_tag\": \"Xóa thẻ\",\n        \"add_to_list\": \"Thêm vào danh sách\",\n        \"remove_from_list\": \"Xóa khỏi danh sách\",\n        \"download_full_page_archive\": \"Tải xuống bản lưu trữ toàn trang\",\n        \"favourite_bookmark\": \"Đánh dấu trang yêu thích\"\n      },\n      \"rules\": \"Công cụ quy tắc\",\n      \"rule_name\": \"Tên quy tắc\",\n      \"description\": \"Bạn có thể sử dụng các quy tắc để kích hoạt các hành động khi một sự kiện xảy ra.\",\n      \"ceate_rule\": \"Tạo quy tắc\",\n      \"edit_rule\": \"Chỉnh sửa quy tắc\",\n      \"save_rule\": \"Lưu quy tắc\",\n      \"delete_rule\": \"Xóa quy tắc\",\n      \"delete_rule_confirmation\": \"Bạn có chắc chắn muốn xóa quy tắc này không?\",\n      \"whenever\": \"Bất cứ khi nào ...\",\n      \"if\": \"Nếu ...\",\n      \"enter_rule_name\": \"Nhập tên quy tắc\",\n      \"describe_what_this_rule_does\": \"Mô tả quy tắc này làm gì\",\n      \"rule_has_been_created\": \"Quy tắc đã được tạo!\",\n      \"rule_has_been_updated\": \"Quy tắc đã được cập nhật!\",\n      \"rule_has_been_deleted\": \"Quy tắc đã bị xóa!\",\n      \"no_rules_created_yet\": \"Chưa có quy tắc nào được tạo\",\n      \"create_your_first_rule\": \"Tạo quy tắc đầu tiên của bạn để tự động hóa quy trình làm việc\",\n      \"conditions_types\": {\n        \"always\": \"Luôn luôn\",\n        \"url_contains\": \"URL chứa\",\n        \"imported_from_feed\": \"Đã nhập từ nguồn cấp dữ liệu\",\n        \"bookmark_type_is\": \"Loại dấu trang là\",\n        \"has_tag\": \"Có thẻ\",\n        \"is_favourited\": \"Được yêu thích\",\n        \"is_archived\": \"Đã lưu trữ\",\n        \"and\": \"Tất cả những điều sau đây là đúng\",\n        \"or\": \"Bất kỳ điều nào sau đây là đúng\",\n        \"url_does_not_contain\": \"URL Không Chứa\",\n        \"title_contains\": \"Chứa trong tiêu đề\",\n        \"title_does_not_contain\": \"Không chứa trong tiêu đề\"\n      },\n      \"events_types\": {\n        \"bookmark_added\": \"Một dấu trang được thêm vào\",\n        \"tag_added\": \"Thẻ này được thêm vào một dấu trang\",\n        \"tag_removed\": \"Thẻ này bị xóa khỏi dấu trang\",\n        \"added_to_list\": \"Một dấu trang được thêm vào danh sách này\",\n        \"removed_from_list\": \"Một dấu trang bị xóa khỏi danh sách này\",\n        \"favourited\": \"Một dấu trang được yêu thích\",\n        \"archived\": \"Một dấu trang được lưu trữ\"\n      }\n    },\n    \"stats\": {\n      \"usage_statistics\": \"Thống kê sử dụng\",\n      \"insights_description\": \"Thông tin chi tiết về thói quen đánh dấu trang và bộ sưu tập của bạn\",\n      \"failed_to_load\": \"Không tải được số liệu thống kê\",\n      \"overview\": {\n        \"total_bookmarks\": \"Tổng số dấu trang\",\n        \"all_saved_items\": \"Tất cả các mục đã lưu\",\n        \"favorites\": \"Ưa thích\",\n        \"starred_bookmarks\": \"Các dấu trang đã gắn dấu sao\",\n        \"archived\": \"Đã lưu trữ\",\n        \"archived_items\": \"Các mục đã lưu trữ\",\n        \"tags\": \"Thẻ\",\n        \"unique_tags_created\": \"Đã tạo các thẻ duy nhất\",\n        \"lists\": \"Danh sách\",\n        \"bookmark_collections\": \"Bộ sưu tập dấu trang\",\n        \"highlights\": \"Điểm nổi bật\",\n        \"text_highlights\": \"Đánh dấu văn bản\",\n        \"storage_used\": \"Bộ nhớ đã dùng\",\n        \"total_asset_storage\": \"Tổng dung lượng lưu trữ tài sản\",\n        \"this_month\": \"Tháng này\",\n        \"bookmarks_added\": \"Đã thêm dấu trang\"\n      },\n      \"bookmark_types\": {\n        \"title\": \"Loại dấu trang\",\n        \"links\": \"Liên kết\",\n        \"text_notes\": \"Ghi chú văn bản\",\n        \"assets\": \"Tài sản\"\n      },\n      \"recent_activity\": {\n        \"title\": \"Hoạt động gần đây\",\n        \"this_week\": \"Tuần này\",\n        \"this_month\": \"Tháng này\",\n        \"this_year\": \"Năm nay\"\n      },\n      \"top_domains\": {\n        \"title\": \"Các miền hàng đầu\",\n        \"no_domains_found\": \"Không tìm thấy miền nào\"\n      },\n      \"most_used_tags\": {\n        \"title\": \"Các thẻ được dùng nhiều nhất\",\n        \"no_tags_found\": \"Không tìm thấy thẻ nào\"\n      },\n      \"activity_patterns\": {\n        \"activity_by_hour\": \"Hoạt động theo giờ\",\n        \"activity_by_day\": \"Hoạt động theo ngày\"\n      },\n      \"storage_breakdown\": {\n        \"title\": \"Phân tích bộ nhớ\"\n      },\n      \"bookmark_sources\": {\n        \"title\": \"Nguồn đánh dấu trang\",\n        \"empty\": \"Không có dữ liệu nguồn\"\n      }\n    },\n    \"subscription\": {\n      \"subscription\": \"Đăng ký\",\n      \"manage_subscription\": \"Quản lý thông tin thanh toán và đăng ký của bạn\",\n      \"current_plan\": \"Gói hiện tại\",\n      \"billing_period\": \"Thời gian thanh toán\",\n      \"paid_plan\": \"Gói trả phí\",\n      \"unlock_bigger_quota\": \"Mở khóa hạn ngạch lớn hơn và hỗ trợ dự án\",\n      \"subscribe_now\": \"Đăng ký ngay\",\n      \"manage_billing\": \"Quản lý thanh toán\",\n      \"subscription_canceled\": \"Đăng ký của bạn đã bị hủy và sẽ kết thúc vào {{date}}. Bạn có thể đăng ký lại bất cứ lúc nào.\",\n      \"usage_quotas\": \"Mức sử dụng & Hạn ngạch\",\n      \"track_usage\": \"Theo dõi mức sử dụng hiện tại so với giới hạn gói của bạn\",\n      \"total_bookmarks_saved\": \"Tổng số dấu trang đã lưu\",\n      \"assets_file_storage\": \"Tài sản và dung lượng lưu trữ tệp\",\n      \"unlimited_usage\": \"Sử dụng không giới hạn\",\n      \"quota_limit_reached\": \"Đã đạt đến giới hạn hạn ngạch\",\n      \"approaching_quota_limit\": \"Đang tiến gần đến giới hạn hạn ngạch\",\n      \"loading_usage\": \"Đang tải thông tin sử dụng...\",\n      \"free\": \"Miễn phí\",\n      \"paid\": \"Trả phí\"\n    },\n    \"import_sessions\": {\n      \"title\": \"Nhập Phiên\",\n      \"description\": \"Xem và quản lý các phiên nhập hàng loạt của bạn. Các phiên được tạo tự động khi bạn nhập dấu trang.\",\n      \"load_error\": \"Không tải được phiên nhập\",\n      \"no_sessions\": \"Chưa có phiên nhập nào\",\n      \"no_sessions_detail\": \"Các phiên nhập sẽ tự động xuất hiện ở đây khi bạn nhập dấu trang\",\n      \"created_at\": \"Đã tạo {{time}}\",\n      \"progress\": \"Tiến trình\",\n      \"status\": {\n        \"pending\": \"Đang chờ xử lý\",\n        \"in_progress\": \"Đang thực hiện\",\n        \"completed\": \"Đã hoàn thành\",\n        \"failed\": \"Thất bại\",\n        \"processing\": \"Đang xử lý\",\n        \"staging\": \"Đang dàn dựng\",\n        \"running\": \"Đang chạy\",\n        \"paused\": \"Đang tạm dừng\"\n      },\n      \"badges\": {\n        \"pending\": \"{{count}} đang chờ xử lý\",\n        \"processing\": \"{{count}} đang xử lý\",\n        \"completed\": \"{{count}} đã hoàn thành\",\n        \"failed\": \"{{count}} thất bại\"\n      },\n      \"imported_to\": \"Đã nhập vào:\",\n      \"view_list\": \"Xem Danh sách\",\n      \"delete_dialog_title\": \"Xóa Phiên Nhập\",\n      \"delete_dialog_description\": \"Bạn có chắc chắn muốn xóa \\\"{{name}}\\\" không? Thao tác này không thể hoàn tác. Bản thân các dấu trang sẽ không bị xóa.\",\n      \"delete_session\": \"Xóa Phiên\",\n      \"pause_session\": \"Tạm dừng\",\n      \"resume_session\": \"Tiếp tục\",\n      \"view_details\": \"Xem chi tiết\",\n      \"detail\": {\n        \"page_title\": \"Xem chi tiết phiên nhập\",\n        \"back_to_import\": \"Quay lại Nhập\",\n        \"filter_all\": \"Tất cả\",\n        \"filter_accepted\": \"Đã chấp nhận\",\n        \"filter_rejected\": \"Đã từ chối\",\n        \"filter_duplicates\": \"Bản sao\",\n        \"filter_pending\": \"Đang chờ xử lý\",\n        \"table_title\": \"Tiêu đề / URL\",\n        \"table_type\": \"Loại\",\n        \"table_result\": \"Kết quả\",\n        \"table_reason\": \"Lý do\",\n        \"table_bookmark\": \"Đánh dấu trang\",\n        \"result_accepted\": \"Đã chấp nhận\",\n        \"result_rejected\": \"Đã từ chối\",\n        \"result_skipped_duplicate\": \"Bản sao\",\n        \"result_pending\": \"Đang chờ xử lý\",\n        \"result_processing\": \"Đang xử lý\",\n        \"no_results\": \"Không tìm thấy kết quả nào cho bộ lọc này.\",\n        \"view_bookmark\": \"Xem dấu trang\",\n        \"load_more\": \"Tải thêm\",\n        \"no_title\": \"Không có tiêu đề\"\n      }\n    },\n    \"backups\": {\n      \"backups\": \"Sao lưu\",\n      \"page_title\": \"Sao lưu\",\n      \"page_description\": \"Tự động tạo và quản lý sao lưu dấu trang của bạn. Bản sao lưu được nén và lưu trữ an toàn.\",\n      \"configuration\": {\n        \"title\": \"Cấu hình sao lưu\",\n        \"enable_automatic_backups\": \"Bật sao lưu tự động\",\n        \"enable_automatic_backups_description\": \"Tự động tạo bản sao lưu dấu trang của bạn\",\n        \"backup_frequency\": \"Tần suất sao lưu\",\n        \"backup_frequency_description\": \"Tần suất tạo bản sao lưu\",\n        \"retention_period\": \"Thời gian lưu giữ (ngày)\",\n        \"retention_period_description\": \"Số ngày giữ bản sao lưu trước khi xóa\",\n        \"frequency\": {\n          \"daily\": \"Hàng ngày\",\n          \"weekly\": \"Hàng tuần\"\n        },\n        \"select_frequency\": \"Chọn tần suất\",\n        \"save_settings\": \"Lưu cài đặt\"\n      },\n      \"list\": {\n        \"title\": \"Các bản sao lưu của bạn\",\n        \"create_backup_now\": \"Tạo bản sao lưu ngay\",\n        \"no_backups\": \"Bạn chưa có bản sao lưu nào. Bật sao lưu tự động hoặc tạo thủ công.\",\n        \"table\": {\n          \"created_at\": \"Đã tạo vào\",\n          \"bookmarks\": \"Dấu trang\",\n          \"size\": \"Kích thước\",\n          \"status\": \"Trạng thái\",\n          \"actions\": \"Hành động\"\n        },\n        \"status\": {\n          \"success\": \"Thành công\",\n          \"failed\": \"Thất bại\",\n          \"pending\": \"Đang chờ\"\n        },\n        \"actions\": {\n          \"download_backup\": \"Tải xuống bản sao lưu\",\n          \"delete_backup\": \"Xóa bản sao lưu\"\n        }\n      },\n      \"dialogs\": {\n        \"delete_backup_title\": \"Xóa bản sao lưu?\",\n        \"delete_backup_description\": \"Mày có chắc là muốn xóa bản sao lưu này không? Hành động này không thể hoàn tác đâu đó.\"\n      },\n      \"toasts\": {\n        \"backup_queued\": \"Công việc sao lưu đã được đưa vào hàng đợi! Nó sẽ sớm được xử lý thôi.\",\n        \"backup_deleted\": \"Đã xóa bản sao lưu!\"\n      }\n    }\n  },\n  \"admin\": {\n    \"server_stats\": {\n      \"total_users\": \"Tất cả người dùng\",\n      \"total_bookmarks\": \"Tất cả đánh dấu trang\",\n      \"server_version\": \"Phiên bản máy chủ\",\n      \"server_stats\": \"Thông số máy chủ\"\n    },\n    \"background_jobs\": {\n      \"background_jobs\": \"Tiến trình chạy nền\",\n      \"failed\": \"Thất bại\",\n      \"inference_jobs\": \"Việc suy luận\",\n      \"video_jobs\": \"Công việc tải video xuống\",\n      \"webhook_jobs\": \"Công việc webhook\",\n      \"asset_preprocessing_jobs\": \"Công việc tiền xử lý tài sản\",\n      \"feed_jobs\": \"Việc làm RSS Feed\",\n      \"tidy_assets_jobs\": \"Công việc sắp xếp tài sản\",\n      \"job\": \"Việc làm\",\n      \"queued\": \"Đã xếp hàng\",\n      \"pending\": \"Đang chờ xử lý\",\n      \"crawler_jobs\": \"Việc làm của trình thu thập dữ liệu\",\n      \"indexing_jobs\": \"Đang lập chỉ mục công việc\",\n      \"jobs\": {\n        \"crawler\": {\n          \"title\": \"Các Công Việc Thu Thập Dữ Liệu\",\n          \"description\": \"Thu thập dữ liệu web và trích xuất nội dung từ các URL\"\n        },\n        \"inference\": {\n          \"title\": \"Các Công Việc Suy Luận\",\n          \"description\": \"Gắn thẻ và tóm tắt nội dung bằng AI\"\n        },\n        \"indexing\": {\n          \"title\": \"Các Công Việc Lập Chỉ Mục\",\n          \"description\": \"Cập nhật chỉ mục tìm kiếm\"\n        },\n        \"asset_preprocessing\": {\n          \"title\": \"Các Công Việc Tiền Xử Lý Tài Sản\",\n          \"description\": \"Xử lý trước hình ảnh và tài liệu (chụp ảnh màn hình, trích xuất văn bản, v.v.)\"\n        },\n        \"tidy_assets\": {\n          \"title\": \"Các Công Việc Dọn Dẹp Tài Sản\",\n          \"description\": \"Dọn dẹp tài sản và tối ưu hóa lưu trữ\"\n        },\n        \"video\": {\n          \"title\": \"Các Công Việc Tải Xuống Video\",\n          \"description\": \"Trích xuất và tải xuống video\"\n        },\n        \"webhook\": {\n          \"title\": \"Các Công Việc Webhook\",\n          \"description\": \"Thông báo webhook bên ngoài\"\n        },\n        \"feed\": {\n          \"title\": \"Các Công Việc Nguồn Cấp RSS\",\n          \"description\": \"Xử lý nguồn cấp RSS và cập nhật nội dung\"\n        },\n        \"admin_maintenance\": {\n          \"title\": \"Công việc bảo trì quản trị\",\n          \"description\": \"Dọn dẹp hành chính và bảo trì tài sản\"\n        }\n      },\n      \"monitor_and_manage\": \"Theo dõi và quản lý hàng đợi công việc nền và các tác vụ xử lý hệ thống\",\n      \"active\": \"Đang hoạt động\",\n      \"available_actions\": \"Các Hành Động Khả Dụng\",\n      \"status\": {\n        \"title\": \"Tìm Hiểu Trạng Thái Công Việc\",\n        \"queued\": {\n          \"title\": \"Đã xếp hàng chờ\",\n          \"description\": \"Các công việc đang chờ trong hàng đợi để được xử lý. Chúng sẽ tự động bắt đầu khi có tài nguyên.\"\n        },\n        \"unprocessed\": {\n          \"title\": \"Chưa xử lý\",\n          \"description\": \"Các dấu trang chưa được xử lý. Chúng rất có thể đã được xếp hàng chờ xử lý, nếu không, bạn có thể cần phải xếp hàng lại theo cách thủ công.\"\n        },\n        \"failed\": {\n          \"title\": \"Thất bại\",\n          \"description\": \"Các dấu trang gặp lỗi trong quá trình xử lý. Chúng có thể cần được chú ý thủ công.\"\n        }\n      },\n      \"actions\": {\n        \"recrawl_failed_links_only\": \"Thu thập lại thông tin chỉ các liên kết bị lỗi\",\n        \"recrawl_all_links\": \"Thu thập lại thông tin tất cả các liên kết\",\n        \"without_inference\": \"Không suy luận\",\n        \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Tạo lại thẻ AI chỉ cho các dấu trang bị lỗi\",\n        \"regenerate_ai_tags_for_all_bookmarks\": \"Tạo lại thẻ AI cho tất cả các dấu trang\",\n        \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Tạo lại tóm tắt AI chỉ cho các dấu trang bị lỗi\",\n        \"regenerate_ai_summaries_for_all_bookmarks\": \"Tạo lại tóm tắt AI cho tất cả các dấu trang\",\n        \"reindex_all_bookmarks\": \"Lập chỉ mục lại tất cả các dấu trang\",\n        \"clean_assets\": \"Xóa các tài sản treo lơ lửng & Đồng bộ lại siêu dữ liệu\",\n        \"reprocess_assets_fix_mode\": \"Xử lý lại các tài sản chưa được xử lý\",\n        \"migrate_large_link_html_content\": \"Chuyển Nội Dung HTML Lớn Nội Tuyến sang Tài Sản\",\n        \"recrawl_pending_links_only\": \"Chỉ thu thập lại các liên kết đang chờ xử lý\",\n        \"regenerate_ai_tags_for_pending_bookmarks_only\": \"Chỉ tạo lại thẻ AI cho các dấu trang đang chờ xử lý\",\n        \"regenerate_ai_summaries_for_pending_bookmarks_only\": \"Chỉ tạo lại tóm tắt AI cho các dấu trang đang chờ xử lý\"\n      }\n    },\n    \"admin_settings\": \"Cài đặt quản trị viên\",\n    \"users_list\": {\n      \"delete_user\": \"Xóa người dùng\",\n      \"users_list\": \"Danh sách người dùng\",\n      \"create_user\": \"Tạo người dùng\",\n      \"change_role\": \"Đổi vai trò\",\n      \"reset_password\": \"Đặt lại mật khẩu\",\n      \"confirm_password\": \"Xác nhận mật khẩu\",\n      \"num_bookmarks\": \"Số lượng dấu trang\",\n      \"asset_sizes\": \"Kích thước tài sản\",\n      \"local_user\": \"Người dùng cục bộ\",\n      \"delete_user_confirm_description\": \"Mày có chắc là muốn xóa người dùng \\\"{{name}}\\\" không?\",\n      \"unlimited\": \"Không giới hạn\"\n    },\n    \"actions\": {\n      \"recrawl_failed_links_only\": \"Chỉ quét lại liên kết hỏng\",\n      \"recrawl_all_links\": \"Quét lại mọi liên kết\",\n      \"regenerate_ai_tags_for_failed_bookmarks_only\": \"Tạo lại thẻ AI chỉ cho các dấu trang bị lỗi\",\n      \"regenerate_ai_tags_for_all_bookmarks\": \"Tạo lại thẻ AI cho tất cả dấu trang\",\n      \"reindex_all_bookmarks\": \"Lập chỉ mục lại tất cả dấu trang\",\n      \"compact_assets\": \"Gộp tài sản\",\n      \"reprocess_assets_fix_mode\": \"Xử lý lại tài sản (Chế độ sửa lỗi)\",\n      \"without_inference\": \"Không có suy luận\",\n      \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"Tạo lại tóm tắt AI chỉ cho các bookmark bị lỗi\",\n      \"regenerate_ai_summaries_for_all_bookmarks\": \"Tạo lại tóm tắt AI cho tất cả các bookmark\"\n    },\n    \"service_connections\": {\n      \"title\": \"Kết nối Dịch vụ\",\n      \"description\": \"Theo dõi tình trạng và khả năng kết nối của các phụ thuộc hệ thống bên ngoài\",\n      \"search_engine\": \"Công cụ Tìm kiếm\",\n      \"browser\": \"Trình duyệt\",\n      \"queue_system\": \"Hệ thống Hàng đợi\",\n      \"status\": {\n        \"not_configured\": \"Chưa Cấu hình\",\n        \"connected\": \"Đã kết nối\",\n        \"disconnected\": \"Đã ngắt kết nối\"\n      }\n    },\n    \"admin_tools\": {\n      \"admin_tools\": \"Công cụ quản trị\",\n      \"bookmark_debugger\": \"Trình gỡ lỗi đánh dấu trang\",\n      \"bookmark_id\": \"ID đánh dấu trang\",\n      \"bookmark_id_placeholder\": \"Nhập ID đánh dấu trang\",\n      \"lookup\": \"Tra cứu\",\n      \"debug_info\": \"Thông tin gỡ lỗi\",\n      \"basic_info\": \"Thông tin cơ bản\",\n      \"status\": \"Trạng thái\",\n      \"content\": \"Nội dung\",\n      \"html_preview\": \"Xem trước HTML (1000 ký tự đầu tiên)\",\n      \"summary\": \"Tóm tắt\",\n      \"url\": \"URL\",\n      \"source_url\": \"URL nguồn\",\n      \"asset_type\": \"Loại tài sản\",\n      \"file_name\": \"Tên tập tin\",\n      \"owner_user_id\": \"ID người dùng sở hữu\",\n      \"tagging_status\": \"Trạng thái gắn thẻ\",\n      \"summarization_status\": \"Trạng thái tóm tắt\",\n      \"crawl_status\": \"Trạng thái thu thập dữ liệu\",\n      \"crawl_status_code\": \"Mã trạng thái HTTP\",\n      \"crawled_at\": \"Đã thu thập dữ liệu lúc\",\n      \"recrawl\": \"Thu thập dữ liệu lại\",\n      \"reindex\": \"Lập chỉ mục lại\",\n      \"retag\": \"Gắn thẻ lại\",\n      \"resummarize\": \"Tóm tắt lại\",\n      \"bookmark_not_found\": \"Không tìm thấy dấu trang\",\n      \"action_success\": \"Đã hoàn tất hành động thành công\",\n      \"action_failed\": \"Hành động thất bại\",\n      \"recrawl_queued\": \"Đã xếp hàng chờ việc thu thập dữ liệu lại\",\n      \"reindex_queued\": \"Đã xếp hàng chờ việc lập chỉ mục lại\",\n      \"retag_queued\": \"Đã xếp hàng chờ việc gắn thẻ lại\",\n      \"resummarize_queued\": \"Đã xếp hàng chờ việc tóm tắt lại\",\n      \"view\": \"Xem\",\n      \"fetch_error\": \"Lỗi khi tìm nạp dấu trang\"\n    }\n  },\n  \"common\": {\n    \"screenshot\": \"Ảnh chụp màn hình\",\n    \"action\": \"Hành động\",\n    \"url\": \"URL\",\n    \"name\": \"Họ Tên\",\n    \"email\": \"Địa chỉ Email\",\n    \"password\": \"Mật khẩu\",\n    \"search\": \"Tìm kiếm\",\n    \"experimental\": \"Thử nghiệm\",\n    \"note\": \"Ghi chú\",\n    \"tags\": \"Dán nhãn\",\n    \"something_went_wrong\": \"Đã có lỗi xảy ra\",\n    \"attachments\": \"Tệp đính kèm\",\n    \"source\": \"Nguồn\",\n    \"roles\": {\n      \"user\": \"Người dùng\",\n      \"admin\": \"Quản trị viên\"\n    },\n    \"role\": \"Quyền hạn\",\n    \"key\": \"Khóa\",\n    \"created_at\": \"Được tạo vào\",\n    \"highlights\": \"Đánh dấu\",\n    \"home\": \"Trang chủ\",\n    \"bookmark_types\": {\n      \"link\": \"Liên kết\",\n      \"title\": \"Loại đánh dấu\",\n      \"text\": \"Văn bản\",\n      \"media\": \"Phương tiện\"\n    },\n    \"video\": \"Video\",\n    \"archive\": \"Lưu trữ\",\n    \"actions\": \"Các hành động\",\n    \"type\": \"Loại\",\n    \"size\": \"Kích thước\",\n    \"updated_at\": \"Cập nhật lúc\",\n    \"title\": \"Tiêu đề\",\n    \"description\": \"Mô tả\",\n    \"summary\": \"Tóm tắt\",\n    \"quota\": \"Hạn ngạch\",\n    \"bookmarks\": \"Dấu trang\",\n    \"storage\": \"Lưu trữ\",\n    \"pdf\": \"PDF đã lưu trữ\",\n    \"default\": \"Mặc định\",\n    \"id\": \"ID\",\n    \"last_used\": \"Lần cuối sử dụng\"\n  },\n  \"highlights\": {\n    \"no_highlights\": \"Bạn chưa có đánh dấu nào.\"\n  },\n  \"lists\": {\n    \"all_lists\": \"Tất cả danh sách\",\n    \"new_list\": \"Danh sách mới\",\n    \"edit_list\": \"Sửa danh sách\",\n    \"list_type\": \"Loại danh sách\",\n    \"manual_list\": \"Danh sách thủ công\",\n    \"smart_list\": \"Danh sách thông minh\",\n    \"favourites\": \"Ưa thích\",\n    \"parent_list\": \"Danh sách gốc\",\n    \"no_parent\": \"Không có thư mục gốc\",\n    \"search_query\": \"Truy vấn tìm kiếm\",\n    \"new_nested_list\": \"Danh sách lồng nhau mới\",\n    \"search_query_help\": \"Tìm hiểu thêm về ngôn ngữ truy vấn tìm kiếm.\",\n    \"merge_list\": \"Hợp nhất danh sách\",\n    \"destination_list\": \"Danh sách đích\",\n    \"delete_after_merge\": \"Xóa danh sách gốc sau khi hợp nhất\",\n    \"no_destination\": \"Không có đích đến\",\n    \"description\": \"Mô tả (Tùy chọn)\",\n    \"share_list\": \"Chia sẻ danh sách\",\n    \"rss\": {\n      \"title\": \"Nguồn cấp RSS\",\n      \"description\": \"Bật nguồn cấp RSS cho danh sách này\",\n      \"feed_url\": \"URL nguồn cấp RSS\"\n    },\n    \"public_list\": {\n      \"title\": \"Danh sách công khai\",\n      \"description\": \"Cho phép người khác xem danh sách này\",\n      \"share_link\": \"Chia sẻ liên kết\"\n    },\n    \"delete_list\": {\n      \"title\": \"Xóa Danh Sách\",\n      \"description\": \"Xóa một danh sách không xóa bất kỳ dấu trang nào trong danh sách đó.\",\n      \"delete_children\": \"Xóa danh sách con (đệ quy)\",\n      \"delete_children_description\": \"Nếu không được chọn, tất cả các danh sách con trực tiếp sẽ trở thành danh sách gốc\"\n    },\n    \"shared\": \"Được chia sẻ\",\n    \"collaborators\": {\n      \"manage\": \"Quản lý Cộng tác viên\",\n      \"view\": \"Xem Cộng tác viên\",\n      \"collaborators\": \"Cộng tác viên\",\n      \"add\": \"Thêm Cộng tác viên\",\n      \"current\": \"Cộng tác viên Hiện tại\",\n      \"enter_email\": \"Nhập địa chỉ email\",\n      \"please_enter_email\": \"Vui lòng nhập địa chỉ email\",\n      \"added_successfully\": \"Đã thêm cộng tác viên thành công\",\n      \"failed_to_add\": \"Không thêm được cộng tác viên\",\n      \"removed\": \"Đã xóa cộng tác viên\",\n      \"failed_to_remove\": \"Không xóa được cộng tác viên\",\n      \"role_updated\": \"Đã cập nhật vai trò\",\n      \"failed_to_update_role\": \"Không cập nhật được vai trò\",\n      \"viewer\": \"Người xem\",\n      \"editor\": \"Biên tập viên\",\n      \"owner\": \"Chủ sở hữu\",\n      \"viewer_description\": \"Có thể xem dấu trang trong danh sách\",\n      \"editor_description\": \"Có thể thêm và xóa dấu trang\",\n      \"no_collaborators\": \"Chưa có cộng tác viên nào. Thêm ai đó để bắt đầu cộng tác!\",\n      \"no_collaborators_readonly\": \"Không có cộng tác viên nào cho danh sách này.\",\n      \"people_with_access\": \"Những người có quyền truy cập vào danh sách này\",\n      \"add_or_remove\": \"Thêm hoặc xóa những người có thể truy cập danh sách này\",\n      \"invitation_sent\": \"Đã gửi lời mời thành công\",\n      \"invitation_revoked\": \"Đã thu hồi lời mời\",\n      \"failed_to_revoke\": \"Không thu hồi được lời mời\",\n      \"pending\": \"Đang chờ\",\n      \"revoke\": \"Thu hồi\",\n      \"declined\": \"Đã từ chối\"\n    },\n    \"leave_list\": {\n      \"title\": \"Rời khỏi danh sách\",\n      \"confirm_message\": \"Bạn có chắc chắn muốn rời khỏi {{icon}} {{name}} không?\",\n      \"warning\": \"Bạn sẽ không còn có thể xem hoặc truy cập dấu trang trong danh sách này nữa. Chủ sở hữu danh sách có thể thêm bạn lại nếu cần.\",\n      \"action\": \"Rời khỏi danh sách\",\n      \"success\": \"Bạn đã rời khỏi \\\"{{icon}} {{name}}\\\"\"\n    },\n    \"invitations\": {\n      \"pending\": \"Lời mời đang chờ\",\n      \"description\": \"Xem lại và phản hồi lời mời cộng tác danh sách\",\n      \"invited_by\": \"Được mời bởi\",\n      \"accept\": \"Chấp nhận\",\n      \"decline\": \"Từ chối\",\n      \"accepted\": \"Đã chấp nhận lời mời\",\n      \"declined\": \"Đã từ chối lời mời\",\n      \"failed_to_accept\": \"Không chấp nhận được lời mời\",\n      \"failed_to_decline\": \"Không từ chối được lời mời\"\n    },\n    \"shared_lists\": \"Danh sách dùng chung\"\n  },\n  \"search\": {\n    \"is_favorited\": \"Được ưa thích\",\n    \"created_on_or_before\": \"Được tạo vào hoặc trước lúc\",\n    \"has_tag\": \"Được gán nhãn\",\n    \"not_created_on_or_after\": \"Không được tạo vào hoặc sau lúc\",\n    \"is_archived\": \"Được lưu trữ\",\n    \"is_in_any_list\": \"Ở trong danh sách nào đó\",\n    \"created_on_or_after\": \"Được tạo vào hoặc sau lúc\",\n    \"url_contains\": \"URL chứa\",\n    \"is_not_favorited\": \"Không được ưa thích\",\n    \"is_not_archived\": \"Không được lưu trữ\",\n    \"is_not_in_any_list\": \"Không ở trong danh sách nào\",\n    \"has_any_tag\": \"Được gán nhãn\",\n    \"has_no_tags\": \"Không được gán nhãn\",\n    \"not_created_on_or_before\": \"Không được tạo vào hoặc trước lúc\",\n    \"url_does_not_contain\": \"URL không chứa\",\n    \"is_in_list\": \"Ở trong danh sách\",\n    \"is_not_in_list\": \"Không ở trong danh sách\",\n    \"does_not_have_tag\": \"Không được gán nhãn\",\n    \"type_is\": \"Thể loại là\",\n    \"type_is_not\": \"Thể loại không phải là\",\n    \"full_text_search\": \"Tìm kiếm toàn văn bản\",\n    \"and\": \"Và\",\n    \"or\": \"Hoặc\",\n    \"is_from_feed\": \"Lấy từ RSS Feed\",\n    \"is_not_from_feed\": \"Không lấy từ RSS Feed\",\n    \"created_within\": \"Được tạo trong vòng\",\n    \"day_s\": \" Ngày\",\n    \"created_earlier_than\": \"Được tạo trước\",\n    \"week_s\": \" Tuần\",\n    \"month_s\": \" Tháng\",\n    \"year_s\": \" Năm\",\n    \"day_s_ago\": \" Ngày trước\",\n    \"week_s_ago\": \" Tuần trước\",\n    \"month_s_ago\": \" Tháng trước\",\n    \"year_s_ago\": \" Năm trước\",\n    \"history\": \"Tìm kiếm gần đây\",\n    \"title_contains\": \"Chứa trong tiêu đề\",\n    \"title_does_not_contain\": \"Không chứa trong tiêu đề\",\n    \"is_broken_link\": \"Có liên kết hỏng\",\n    \"tags\": \"Thẻ\",\n    \"no_suggestions\": \"Không có đề xuất nào\",\n    \"filters\": \"Bộ lọc\",\n    \"is_not_broken_link\": \"Có liên kết hoạt động\",\n    \"lists\": \"Danh sách\",\n    \"feeds\": \"Nguồn cấp dữ liệu\",\n    \"is_from_source\": \"Nguồn là\",\n    \"is_not_from_source\": \"Nguồn không phải là\"\n  },\n  \"tags\": {\n    \"all_tags\": \"Tất cả nhãn\",\n    \"your_tags\": \"Nhãn của bạn\",\n    \"delete_all_unused_tags\": \"Xóa những nhãn không được sử dụng\",\n    \"unused_tags\": \"Nhãn không được sử dụng\",\n    \"sort_by_name\": \"Sắp xếp theo Tên\",\n    \"unused_tags_info\": \"Các thẻ không được đính kèm vào bất kỳ dấu trang nào\",\n    \"drag_and_drop_merging\": \"Kéo & Thả để Gộp\",\n    \"drag_and_drop_merging_info\": \"Kéo và thả các thẻ lên nhau để hợp nhất chúng\",\n    \"your_tags_info\": \"Các thẻ đã được bạn đính kèm ít nhất một lần\",\n    \"ai_tags\": \"Thẻ AI\",\n    \"ai_tags_info\": \"Các thẻ chỉ được đính kèm tự động (bởi AI)\",\n    \"create_tag\": \"Tạo Thẻ\",\n    \"create_tag_description\": \"Tạo một thẻ mới mà không cần đính kèm nó vào bất kỳ dấu trang nào\",\n    \"tag_name\": \"Tên Thẻ\",\n    \"enter_tag_name\": \"Nhập tên thẻ\",\n    \"sort_by_usage\": \"Sắp xếp theo mức độ sử dụng\",\n    \"sort_by_relevance\": \"Sắp xếp theo mức độ liên quan\",\n    \"no_custom_tags\": \"Chưa có thẻ tùy chỉnh nào\",\n    \"no_ai_tags\": \"Chưa có thẻ AI nào\",\n    \"no_unused_tags\": \"Bạn không có thẻ chưa sử dụng nào\",\n    \"no_unused_tags_match_your_search\": \"Không có thẻ chưa sử dụng nào phù hợp với tìm kiếm của bạn\",\n    \"no_tags_match_your_search\": \"Không có thẻ nào phù hợp với tìm kiếm của bạn\",\n    \"search_placeholder\": \"Tìm kiếm thẻ...\",\n    \"search_or_create_placeholder\": \"Tìm kiếm hoặc tạo thẻ...\"\n  },\n  \"options\": {\n    \"dark_mode\": \"Chế độ tối\",\n    \"light_mode\": \"Chế độ sáng\",\n    \"apps_extensions\": \"Ứng dụng & Tiện ích mở rộng\",\n    \"documentation\": \"Tài liệu\",\n    \"follow_us_on_x\": \"Theo dõi chúng tôi trên X\"\n  },\n  \"editor\": {\n    \"text_toolbar\": {\n      \"italic\": \"In nghiêng\",\n      \"redo\": \"Làm lại\",\n      \"undo\": \"Hoàn tác\",\n      \"bold\": \"In đậm\",\n      \"underline\": \"Gạch dưới\",\n      \"highlight\": \"Đánh dấu nổi bật\",\n      \"align_left\": \"Căn trái\",\n      \"align_center\": \"Căn giữa\",\n      \"align_right\": \"Căn phải\",\n      \"markdown_shortcuts\": {\n        \"label\": \"Phím tắt Markdown\",\n        \"heading\": {\n          \"label\": \"Tiêu đề\",\n          \"example\": \"# H1, ## H2, ### H3\"\n        },\n        \"italic\": {\n          \"example\": \"*In nghiêng* hoặc _In nghiêng_ hoặc CTRL+i\",\n          \"label\": \"In nghiêng\"\n        },\n        \"blockquote\": {\n          \"label\": \"Trích dẫn\",\n          \"example\": \"> Trích dẫn khối\"\n        },\n        \"ordered_list\": {\n          \"label\": \"Danh sách có thứ tự\",\n          \"example\": \"1. Mục danh sách\"\n        },\n        \"unordered_list\": {\n          \"label\": \"Danh sách không có thứ tự\",\n          \"example\": \"- Mục danh sách\"\n        },\n        \"bold\": {\n          \"example\": \"**text** hoặc CTRL+b\",\n          \"label\": \"In đậm\"\n        },\n        \"inline_code\": {\n          \"label\": \"Mã nội dòng\",\n          \"example\": \"`Mã`\"\n        },\n        \"block_code\": {\n          \"label\": \"Chặn mã\",\n          \"example\": \"``` + space\"\n        }\n      },\n      \"strikethrough\": \"Gạch ngang\",\n      \"code\": \"Mã\"\n    },\n    \"quickly_focus\": \"Bạn có thể nhanh chóng tập trung vào trường này bằng cách nhấn ⌘ + E\",\n    \"disabled_submissions\": \"Đã tắt tính năng gửi\",\n    \"multiple_urls_dialog_desc\": \"Đầu vào chứa nhiều URL trên các dòng riêng biệt. Bạn có muốn nhập chúng dưới dạng các dấu trang riêng biệt không?\",\n    \"import_as_separate_bookmarks\": \"Nhập dưới dạng Dấu trang riêng biệt\",\n    \"multiple_urls_dialog_title\": \"Nhập URL dưới dạng Dấu trang riêng biệt?\",\n    \"import_as_text\": \"Nhập dưới dạng Dấu trang văn bản\",\n    \"placeholder\": \"Dán một liên kết hoặc một hình ảnh, viết một ghi chú hoặc kéo và thả một hình ảnh vào đây…\",\n    \"new_item\": \"MỤC MỚI\",\n    \"placeholder_v2\": \"Dán liên kết, viết ghi chú hoặc thả ảnh…\"\n  },\n  \"cleanups\": {\n    \"duplicate_tags\": {\n      \"title\": \"Thẻ trùng lặp\",\n      \"merge_all_suggestions\": \"Hợp nhất tất cả các đề xuất?\"\n    },\n    \"cleanups\": \"Dọn dẹp\"\n  },\n  \"dialogs\": {\n    \"bookmarks\": {\n      \"delete_confirmation_title\": \"Xóa dấu trang?\",\n      \"delete_confirmation_description\": \"Bạn có chắc chắn muốn xóa dấu trang này không?\"\n    }\n  },\n  \"toasts\": {\n    \"bookmarks\": {\n      \"updated\": \"Đã cập nhật dấu trang!\",\n      \"deleted\": \"Đã xóa dấu trang!\",\n      \"refetch\": \"Đã xếp hàng tìm nạp lại!\",\n      \"full_page_archive\": \"Đã kích hoạt tạo bản lưu trữ toàn trang\",\n      \"delete_from_list\": \"Đã xóa dấu trang khỏi danh sách\",\n      \"clipboard_copied\": \"Liên kết đã được thêm vào bảng nhớ tạm của bạn!\",\n      \"preserve_pdf\": \"Đã kích hoạt lưu giữ PDF\",\n      \"update_banner\": \"Banner đã được cập nhật!\",\n      \"uploading_banner\": \"Đang tải banner lên...\"\n    },\n    \"lists\": {\n      \"created\": \"Đã tạo danh sách!\",\n      \"updated\": \"Danh sách đã được cập nhật!\",\n      \"merged\": \"Đã hợp nhất danh sách!\",\n      \"deleted\": \"Đã xóa danh sách!\"\n    },\n    \"tags\": {\n      \"created\": \"Đã tạo thẻ thành công!\",\n      \"failed_to_create\": \"Không tạo được thẻ\"\n    }\n  },\n  \"preview\": {\n    \"view_original\": \"Xem bản gốc\",\n    \"cached_content\": \"Nội dung được lưu trong bộ nhớ cache\",\n    \"reader_view\": \"Chế độ đọc\",\n    \"tabs\": {\n      \"content\": \"Nội dung\",\n      \"details\": \"Chi tiết\"\n    },\n    \"archive_info\": \"Các bản lưu trữ có thể không hiển thị chính xác nội dòng nếu chúng yêu cầu Javascript. Để có kết quả tốt nhất, <1>hãy tải xuống và mở trong trình duyệt của bạn</1>.\",\n    \"fetch_error_title\": \"Không thể tìm thấy nội dung\",\n    \"fetch_error_description\": \"Chúng tôi không thể tìm nạp nội dung cho liên kết này. Trang có thể được bảo vệ, yêu cầu xác thực hoặc tạm thời không khả dụng.\",\n    \"crawling_in_progress\": \"Đang tìm nạp nội dung trang…\",\n    \"continue_reading\": \"Tiếp tục từ chỗ bạn đã dừng lại\",\n    \"continue_reading_percent\": \"Tiếp tục từ chỗ bạn đã dừng lại ({{percent}}%)\",\n    \"continue_button\": \"Tiếp tục\"\n  },\n  \"bookmark_editor\": {\n    \"title\": \"Sửa dấu trang\",\n    \"subtitle\": \"Thay đổi chi tiết dấu trang. Nhấn lưu khi xong việc nhé.\",\n    \"author\": \"Tác giả\",\n    \"publisher\": \"Nhà xuất bản\",\n    \"date_published\": \"Ngày xuất bản\",\n    \"pick_a_date\": \"Chọn ngày\",\n    \"save_changes\": \"Lưu thay đổi\",\n    \"extracted_content\": \"Nội dung đã trích xuất\"\n  },\n  \"banners\": {\n    \"no_bookmarks\": {\n      \"title\": \"Chưa có dấu trang nào\",\n      \"description\": \"Lưu các bài viết, liên kết và trang thú vị để truy cập nhanh sau này.\"\n    }\n  },\n  \"view_options\": {\n    \"title\": \"Tùy chọn xem\",\n    \"layout\": \"Bố cục\",\n    \"columns\": \"Cột\",\n    \"display_options\": \"Tùy chọn hiển thị\",\n    \"show_note_previews\": \"Hiện ghi chú\",\n    \"show_tags\": \"Hiển thị thẻ\",\n    \"show_title\": \"Hiển thị tiêu đề\",\n    \"image_options\": \"Tùy chọn hình ảnh\",\n    \"image_fit_cover\": \"Bìa (Lấp đầy)\",\n    \"image_fit_contain\": \"Chứa (Vừa vặn)\"\n  },\n  \"version\": {\n    \"new_release_available\": \"Có ghi chú phát hành mới đó\",\n    \"whats_new_title\": \"Có gì mới trong v{{version}}?\",\n    \"release_notes_description\": \"Đây là những cập nhật mới nhất lấy từ ghi chú phát hành trên GitHub.\",\n    \"loading_release_notes\": \"Đang tải ghi chú phát hành…\",\n    \"unable_to_load_release_notes\": \"Không thể tải ghi chú phát hành ngay bây giờ. Lát nữa thử lại nha.\",\n    \"no_release_notes\": \"Không có ghi chú phát hành nào cho phiên bản này cả.\",\n    \"release_notes_synced\": \"Ghi chú phát hành được đồng bộ hóa từ GitHub đó.\",\n    \"view_on_github\": \"Xem trên GitHub nè\"\n  },\n  \"wrapped\": {\n    \"sections\": {\n      \"total_saves\": {\n        \"suffix\": \"{{items}} trong năm nay\",\n        \"suffix_singular\": \"{{item}} trong năm nay\",\n        \"prefix\": \"Bạn đã lưu\"\n      },\n      \"first_bookmark\": {\n        \"title\": \"Hành trình của bạn bắt đầu\",\n        \"description\": \"Lưu đầu tiên của năm {{year}}:\"\n      },\n      \"top_domains\": \"Các trang web hàng đầu của bạn\",\n      \"top_tags\": \"Các thẻ hàng đầu của bạn\",\n      \"monthly_activity\": \"Năm lưu trữ của bạn\",\n      \"most_active_day\": \"Ngày hoạt động tích cực nhất của bạn\",\n      \"peak_times\": {\n        \"title\": \"Khi bạn lưu\",\n        \"peak_hour\": \"Giờ cao điểm\",\n        \"peak_day\": \"Ngày cao điểm\"\n      },\n      \"how_you_save\": \"Cách bạn lưu\",\n      \"what_you_saved\": \"Những gì bạn đã lưu\",\n      \"summary\": {\n        \"favorites\": \"Yêu thích\",\n        \"tags_created\": \"Thẻ đã tạo\",\n        \"highlights\": \"Điểm nổi bật\"\n      },\n      \"types\": {\n        \"links\": \"Liên kết\",\n        \"notes\": \"Ghi chú\",\n        \"assets\": \"Tài sản\"\n      }\n    },\n    \"footer\": \"Được tạo bằng Karakeep\",\n    \"share\": \"Chia sẻ\",\n    \"download\": \"Tải xuống\",\n    \"close\": \"Đóng\",\n    \"generating\": \"Đang tạo...\",\n    \"title\": \"Tổng Kết {{year}} Của Bạn\",\n    \"subtitle\": \"Một Năm Ở Karakeep\",\n    \"banner\": {\n      \"title\": \"Tổng Kết Năm 2025 Của Bạn đã sẵn sàng!\",\n      \"description\": \"Xem năm của bạn qua các bookmark\",\n      \"view_now\": \"Xem Ngay\"\n    },\n    \"button\": \"Tổng Kết Năm 2025\",\n    \"loading\": \"Đang tải Tổng Kết của bạn...\",\n    \"failed_to_load\": \"Không tải được thống kê Tổng Kết của bạn\"\n  }\n}\n"
  },
  {
    "path": "apps/web/lib/i18n/locales/zh/translation.json",
    "content": "{\n  \"common\": {\n    \"url\": \"URL\",\n    \"name\": \"名称\",\n    \"email\": \"电子邮件\",\n    \"password\": \"密码\",\n    \"action\": \"操作\",\n    \"actions\": \"操作\",\n    \"created_at\": \"创建于\",\n    \"key\": \"键\",\n    \"role\": \"角色\",\n    \"roles\": {\n      \"user\": \"用户\",\n      \"admin\": \"管理员\"\n    },\n    \"something_went_wrong\": \"出了些问题\",\n    \"experimental\": \"实验性\",\n    \"search\": \"搜索\",\n    \"tags\": \"标签\",\n    \"note\": \"笔记\",\n    \"attachments\": \"附件\",\n    \"screenshot\": \"截图\",\n    \"video\": \"视频\",\n    \"archive\": \"归档\",\n    \"home\": \"主页\",\n    \"highlights\": \"高亮\",\n    \"source\": \"来源\",\n    \"bookmark_types\": {\n      \"title\": \"书签类型\",\n      \"link\": \"链接\",\n      \"text\": \"文字\",\n      \"media\": \"媒体\"\n    },\n    \"type\": \"类型\",\n    \"size\": \"大小\",\n    \"updated_at\": \"更新于\",\n    \"title\": \"标题\",\n    \"description\": \"描述\",\n    \"summary\": \"摘要\",\n    \"quota\": \"配额\",\n    \"bookmarks\": \"书签\",\n    \"storage\": \"存储\",\n    \"pdf\": \"已存档的 PDF\",\n    \"default\": \"默认\",\n    \"id\": \"ID\",\n    \"last_used\": \"上次使用\"\n  },\n  \"layouts\": {\n    \"masonry\": \"砌体\",\n    \"grid\": \"网格\",\n    \"list\": \"列表\",\n    \"compact\": \"紧凑\"\n  },\n  \"actions\": {\n    \"change_layout\": \"更改布局\",\n    \"archive\": \"归档\",\n    \"unarchive\": \"取消归档\",\n    \"favorite\": \"收藏\",\n    \"unfavorite\": \"取消收藏\",\n    \"delete\": \"删除\",\n    \"refresh\": \"刷新\",\n    \"download_full_page_archive\": \"下载完整页面归档\",\n    \"edit_tags\": \"编辑标签\",\n    \"add_to_list\": \"添加到列表\",\n    \"select_all\": \"全选\",\n    \"unselect_all\": \"取消全选\",\n    \"copy_link\": \"复制链接\",\n    \"close_bulk_edit\": \"关闭批量编辑\",\n    \"bulk_edit\": \"批量编辑\",\n    \"manage_lists\": \"管理列表\",\n    \"remove_from_list\": \"从列表中移除\",\n    \"save\": \"保存\",\n    \"add\": \"添加\",\n    \"edit\": \"编辑\",\n    \"create\": \"创建\",\n    \"fetch_now\": \"立即获取\",\n    \"summarize_with_ai\": \"使用AI总结\",\n    \"edit_title\": \"编辑标题\",\n    \"sign_out\": \"登出\",\n    \"close\": \"关闭\",\n    \"merge\": \"合并\",\n    \"cancel\": \"取消\",\n    \"apply_all\": \"全部应用\",\n    \"ignore\": \"忽略\",\n    \"recrawl\": \"重新抓取\",\n    \"sort\": {\n      \"title\": \"排序\",\n      \"newest_first\": \"最新优先\",\n      \"oldest_first\": \"最早优先\",\n      \"relevant_first\": \"最相关优先\"\n    },\n    \"open_editor\": \"打开编辑器\",\n    \"toggle_show_archived\": \"显示已存档\",\n    \"confirm\": \"确认\",\n    \"regenerate\": \"重新生成\",\n    \"load_more\": \"加载更多\",\n    \"edit_notes\": \"编辑备注\",\n    \"preserve_as_pdf\": \"另存为 PDF\",\n    \"offline_copies\": \"离线副本\",\n    \"preserve_offline_archive\": \"保留离线存档\",\n    \"download_full_page_archive_file\": \"下载存档文件\",\n    \"download_pdf_file\": \"下载 PDF 文件\",\n    \"remove\": \"移除\",\n    \"more\": \"更多\",\n    \"replace_banner\": \"替换横幅\",\n    \"add_banner\": \"添加横幅\",\n    \"download\": \"下载\"\n  },\n  \"settings\": {\n    \"back_to_app\": \"返回应用\",\n    \"user_settings\": \"用户设置\",\n    \"info\": {\n      \"user_info\": \"用户信息\",\n      \"basic_details\": \"基本详情\",\n      \"change_password\": \"更改密码\",\n      \"current_password\": \"当前密码\",\n      \"new_password\": \"新密码\",\n      \"confirm_new_password\": \"确认新密码\",\n      \"options\": \"选项\",\n      \"interface_lang\": \"界面语言\",\n      \"user_settings\": {\n        \"user_settings_updated\": \"用户设置已更新！\",\n        \"bookmark_click_action\": {\n          \"title\": \"书签点击操作\",\n          \"open_external_url\": \"打开原始 URL\",\n          \"open_bookmark_details\": \"打开书签详情\"\n        },\n        \"archive_display_behaviour\": {\n          \"title\": \"已存档的书签\",\n          \"show\": \"在标签和列表中显示已存档的书签\",\n          \"hide\": \"在标签和列表中隐藏已存档的书签\"\n        }\n      },\n      \"reader_settings\": {\n        \"local_overrides_title\": \"设备特定的设置已激活\",\n        \"using_default\": \"正在使用客户端默认值\",\n        \"clear_override_hint\": \"清除设备覆盖以使用全局设置（{{value}}）\",\n        \"font_size\": \"字体大小\",\n        \"font_family\": \"字体系列\",\n        \"preview_inline\": \"（预览）\",\n        \"tooltip_preview\": \"未保存的预览更改\",\n        \"save_to_all_devices\": \"所有设备\",\n        \"tooltip_local\": \"设备设置与全局设置不同\",\n        \"reset_preview\": \"重置预览\",\n        \"mono\": \"等宽\",\n        \"line_height\": \"行高\",\n        \"tooltip_default\": \"阅读设置\",\n        \"title\": \"阅读器设置\",\n        \"serif\": \"衬线\",\n        \"preview\": \"预览\",\n        \"not_set\": \"未设置\",\n        \"clear_local_overrides\": \"清除设备设置\",\n        \"preview_text\": \"敏捷的棕色狐狸跳过懒惰的狗。这是您的阅读器视图文本的显示方式。\",\n        \"local_overrides_cleared\": \"设备特定的设置已清除\",\n        \"local_overrides_description\": \"此设备上的阅读器设置与您的全局默认值不同：\",\n        \"clear_defaults\": \"清除所有默认值\",\n        \"description\": \"配置阅读器视图的默认文本设置。这些设置将在您的所有设备上同步。\",\n        \"defaults_cleared\": \"阅读器默认值已清除\",\n        \"save_hint\": \"仅保存此设备的设置，还是在所有设备同步\",\n        \"save_as_default\": \"保存为默认值\",\n        \"save_to_device\": \"此设备\",\n        \"sans\": \"无衬线\",\n        \"tooltip_preview_and_local\": \"未保存的预览更改；设备设置与全局设置不同\",\n        \"adjust_hint\": \"调整以上设置以预览更改\"\n      },\n      \"avatar\": {\n        \"upload\": \"上传虚拟形象\",\n        \"change\": \"更改虚拟形象\",\n        \"remove_confirm_title\": \"移除虚拟形象？\",\n        \"updated\": \"虚拟形象已更新\",\n        \"removed\": \"虚拟形象已移除\",\n        \"description\": \"上传一张方形图片作为您的虚拟形象。\",\n        \"remove_confirm_description\": \"这会清除您当前的头像照片。\",\n        \"title\": \"头像照片\",\n        \"remove\": \"移除虚拟形象\"\n      }\n    },\n    \"ai\": {\n      \"ai_settings\": \"AI设置\",\n      \"tagging_rules\": \"标签规则\",\n      \"tagging_rule_description\": \"在此添加的提示将包含在模型生成标签的规则中。您可以在提示预览部分查看最终的提示。\",\n      \"prompt_preview\": \"提示预览\",\n      \"text_prompt\": \"文本提示\",\n      \"images_prompt\": \"图像提示\",\n      \"summarization\": \"总结\",\n      \"image_tagging\": \"图片标记\",\n      \"text_tagging\": \"文字标记\",\n      \"all_tagging\": \"所有标记\",\n      \"summarization_prompt\": \"摘要生成提示\",\n      \"tag_style\": \"标签样式\",\n      \"auto_summarization_description\": \"使用 AI 自动为你的书签生成摘要。\",\n      \"auto_tagging\": \"自动添加标签\",\n      \"titlecase_spaces\": \"带空格的首字母大写\",\n      \"lowercase_underscores\": \"带下划线的小写\",\n      \"inference_language\": \"推理语言\",\n      \"titlecase_hyphens\": \"带连字符的首字母大写\",\n      \"lowercase_hyphens\": \"带连字符的小写\",\n      \"lowercase_spaces\": \"带空格的小写\",\n      \"inference_language_description\": \"为 AI 生成的标签和摘要选择语言。\",\n      \"tag_style_description\": \"选择自动生成的标签应如何格式化。\",\n      \"auto_tagging_description\": \"使用 AI 自动为你的书签生成标签。\",\n      \"camelCase\": \"驼峰式命名\",\n      \"auto_summarization\": \"自动摘要\",\n      \"no_preference\": \"无偏好\",\n      \"curated_tags\": \"精选标签\",\n      \"curated_tags_description\": \"（可选）将 AI 标签限制为仅使用此列表中的标签。如果未选择任何标签，AI 将随意生成标签。\",\n      \"curated_tags_updated\": \"精选标签已成功更新！\",\n      \"curated_tags_update_failed\": \"更新精选标签失败\"\n    },\n    \"feeds\": {\n      \"rss_subscriptions\": \"RSS订阅\",\n      \"add_a_subscription\": \"添加订阅\",\n      \"feed_enabled\": \"RSS 订阅已启用\",\n      \"feed_disabled\": \"RSS 订阅已禁用\"\n    },\n    \"import\": {\n      \"import_export\": \"导入/导出\",\n      \"import_export_bookmarks\": \"导入/导出书签\",\n      \"import_bookmarks_from_html_file\": \"从HTML文件导入书签\",\n      \"import_bookmarks_from_pocket_export\": \"从Pocket导出导入书签\",\n      \"import_bookmarks_from_matter_export\": \"从Matter导出导入书签\",\n      \"import_bookmarks_from_omnivore_export\": \"从Omnivore导出导入书签\",\n      \"import_bookmarks_from_karakeep_export\": \"从Karakeep导出导入书签\",\n      \"export_links_and_notes\": \"导出链接和笔记\",\n      \"imported_bookmarks\": \"已导入书签\",\n      \"import_bookmarks_from_linkwarden_export\": \"导入 Linkwarden 导出的书签\",\n      \"import_bookmarks_from_tab_session_manager_export\": \"从 Tab Session Manager 导入书签\",\n      \"import_bookmarks_from_mymind_export\": \"从我的 mind 导出导入书签\",\n      \"import_bookmarks_from_instapaper_export\": \"从 Instapaper 导出内容导入书签\"\n    },\n    \"api_keys\": {\n      \"api_keys\": \"API密钥\",\n      \"new_api_key\": \"新API密钥\",\n      \"new_api_key_desc\": \"给您的API密钥一个唯一名称\",\n      \"key_success\": \"密钥创建成功\",\n      \"key_success_please_copy\": \"请复制密钥并妥善保存。一旦关闭对话框，您将无法再次访问它。\",\n      \"regenerate_api_key\": \"重新生成 API 密钥\",\n      \"key_regenerated\": \"密钥已成功重新生成\",\n      \"key_regenerated_please_copy\": \"请复制新密钥并将其保存在安全的地方。旧密钥已被撤销，将不再起作用。\",\n      \"regenerate_warning\": \"你确定要重新生成 API 密钥“{{name}}”吗？\",\n      \"regenerate_confirmation\": \"这将撤销当前密钥并生成一个新密钥。任何使用当前密钥的应用程序都将停止工作。\"\n    },\n    \"broken_links\": {\n      \"broken_links\": \"失效链接\",\n      \"last_crawled_at\": \"最后抓取时间\",\n      \"crawling_status\": \"抓取状态\",\n      \"crawling_failed\": \"抓取失败\"\n    },\n    \"webhooks\": {\n      \"edit_webhook\": \"修改网络钩子\",\n      \"webhook_url\": \"网络钩子链接\",\n      \"webhooks\": \"网络钩子\",\n      \"description\": \"您可以用网络钩子在创建、更改或爬取书签时触发操作。\",\n      \"events\": {\n        \"created\": \"创造了\",\n        \"edited\": \"已修改\",\n        \"crawled\": \"已爬取\",\n        \"title\": \"事件\"\n      },\n      \"auth_token\": \"认证 Token\",\n      \"add_auth_token\": \"添加认证 Token\",\n      \"edit_auth_token\": \"修改认证 Token\",\n      \"create_webhook\": \"创建网络钩子\",\n      \"delete_webhook\": \"删除网络钩子\",\n      \"delete_webhook_confirmation\": \"是否确定要删除这个网络钩子？\"\n    },\n    \"manage_assets\": {\n      \"manage_assets\": \"管理资源\",\n      \"no_assets\": \"你还没有任何资源。\",\n      \"asset_type\": \"资源类型\",\n      \"bookmark_link\": \"书签链接\",\n      \"asset_link\": \"资源链接\",\n      \"delete_asset\": \"删除资源\",\n      \"delete_asset_confirmation\": \"确定要删除此资源吗？\"\n    },\n    \"rules\": {\n      \"if\": \"如果...\",\n      \"rules\": \"规则引擎\",\n      \"rule_name\": \"规则名称\",\n      \"description\": \"你可以使用规则在事件触发时触发操作。\",\n      \"ceate_rule\": \"创建规则\",\n      \"edit_rule\": \"编辑规则\",\n      \"save_rule\": \"保存规则\",\n      \"delete_rule\": \"删除规则\",\n      \"delete_rule_confirmation\": \"你确定要删除此规则吗？\",\n      \"whenever\": \"无论何时...\",\n      \"enter_rule_name\": \"输入规则名称\",\n      \"describe_what_this_rule_does\": \"描述此规则的作用\",\n      \"rule_has_been_created\": \"规则已创建！\",\n      \"rule_has_been_updated\": \"规则已更新！\",\n      \"rule_has_been_deleted\": \"规则已被删除！\",\n      \"no_rules_created_yet\": \"尚未创建任何规则\",\n      \"create_your_first_rule\": \"创建你的第一个规则来自动化你的工作流程\",\n      \"conditions_types\": {\n        \"always\": \"总是\",\n        \"url_contains\": \"URL 包含\",\n        \"imported_from_feed\": \"从订阅源导入\",\n        \"bookmark_type_is\": \"书签类型为\",\n        \"has_tag\": \"具有标签\",\n        \"is_favourited\": \"已收藏\",\n        \"is_archived\": \"已归档\",\n        \"and\": \"以下全部为真\",\n        \"or\": \"以下任一条件为真\",\n        \"url_does_not_contain\": \"URL 不包含\",\n        \"title_contains\": \"标题包含\",\n        \"title_does_not_contain\": \"标题不包含\"\n      },\n      \"actions_types\": {\n        \"add_tag\": \"添加标签\",\n        \"remove_tag\": \"删除标签\",\n        \"add_to_list\": \"添加到列表\",\n        \"remove_from_list\": \"从列表中删除\",\n        \"download_full_page_archive\": \"下载完整页面存档\",\n        \"favourite_bookmark\": \"收藏书签\",\n        \"archive_bookmark\": \"存档书签\"\n      },\n      \"events_types\": {\n        \"bookmark_added\": \"已添加书签\",\n        \"tag_added\": \"此标签已添加到书签\",\n        \"tag_removed\": \"从此书签中删除此标签\",\n        \"added_to_list\": \"书签已添加到此列表\",\n        \"removed_from_list\": \"书签已从此列表中删除\",\n        \"favourited\": \"已收藏书签\",\n        \"archived\": \"书签已存档\"\n      }\n    },\n    \"stats\": {\n      \"usage_statistics\": \"使用统计信息\",\n      \"insights_description\": \"深入了解你的书签习惯和收藏\",\n      \"failed_to_load\": \"加载统计信息失败\",\n      \"overview\": {\n        \"total_bookmarks\": \"书签总数\",\n        \"all_saved_items\": \"所有已保存项目\",\n        \"favorites\": \"收藏夹\",\n        \"starred_bookmarks\": \"已加星标的书签\",\n        \"archived\": \"已存档\",\n        \"archived_items\": \"已归档的项目\",\n        \"tags\": \"标签\",\n        \"unique_tags_created\": \"已创建唯一标签\",\n        \"lists\": \"列表\",\n        \"bookmark_collections\": \"书签集合\",\n        \"highlights\": \"高亮\",\n        \"text_highlights\": \"文本高亮\",\n        \"storage_used\": \"已用存储\",\n        \"total_asset_storage\": \"总资产存储\",\n        \"this_month\": \"本月\",\n        \"bookmarks_added\": \"已添加书签\"\n      },\n      \"bookmark_types\": {\n        \"title\": \"书签类型\",\n        \"links\": \"链接\",\n        \"text_notes\": \"文本笔记\",\n        \"assets\": \"资产\"\n      },\n      \"recent_activity\": {\n        \"title\": \"最近的活动\",\n        \"this_week\": \"本周\",\n        \"this_month\": \"本月\",\n        \"this_year\": \"今年\"\n      },\n      \"top_domains\": {\n        \"title\": \"热门域名\",\n        \"no_domains_found\": \"未找到域名\"\n      },\n      \"most_used_tags\": {\n        \"title\": \"最常用的标签\",\n        \"no_tags_found\": \"未找到标签\"\n      },\n      \"activity_patterns\": {\n        \"activity_by_hour\": \"按小时统计的活动\",\n        \"activity_by_day\": \"每日活动\"\n      },\n      \"storage_breakdown\": {\n        \"title\": \"存储细分\"\n      },\n      \"bookmark_sources\": {\n        \"title\": \"书签来源\",\n        \"empty\": \"没有可用的来源数据\"\n      }\n    },\n    \"subscription\": {\n      \"subscription\": \"订阅\",\n      \"manage_subscription\": \"管理您的订阅和账单信息\",\n      \"current_plan\": \"当前计划\",\n      \"billing_period\": \"结算周期\",\n      \"paid_plan\": \"付费计划\",\n      \"unlock_bigger_quota\": \"解锁更大的配额并支持该项目\",\n      \"subscribe_now\": \"立即订阅\",\n      \"manage_billing\": \"管理账单\",\n      \"subscription_canceled\": \"您的订阅已取消，将于 {{date}} 结束。您可以随时重新订阅。\",\n      \"usage_quotas\": \"使用情况和配额\",\n      \"track_usage\": \"跟踪您当前的使用量与计划限制\",\n      \"total_bookmarks_saved\": \"已保存的书签总数\",\n      \"assets_file_storage\": \"资产和文件存储\",\n      \"unlimited_usage\": \"无限制使用\",\n      \"quota_limit_reached\": \"已达到配额限制\",\n      \"approaching_quota_limit\": \"即将达到配额限制\",\n      \"loading_usage\": \"正在加载使用信息...\",\n      \"free\": \"免费\",\n      \"paid\": \"已付费\"\n    },\n    \"import_sessions\": {\n      \"title\": \"导入会话\",\n      \"description\": \"查看和管理你的批量导入会话。当你导入书签时，会自动创建会话。\",\n      \"load_error\": \"加载导入会话失败\",\n      \"no_sessions\": \"还没有导入会话\",\n      \"no_sessions_detail\": \"当你导入书签时，导入会话会自动出现在这里\",\n      \"created_at\": \"创建于 {{time}}\",\n      \"progress\": \"进度\",\n      \"status\": {\n        \"pending\": \"待定\",\n        \"in_progress\": \"正在进行\",\n        \"completed\": \"已完成\",\n        \"failed\": \"失败\",\n        \"processing\": \"正在处理\",\n        \"staging\": \"正在暂存\",\n        \"running\": \"正在运行\",\n        \"paused\": \"已暂停\"\n      },\n      \"badges\": {\n        \"pending\": \"{{count}} 待定\",\n        \"processing\": \"{{count}} 正在处理\",\n        \"completed\": \"{{count}} 已完成\",\n        \"failed\": \"{{count}} 失败\"\n      },\n      \"imported_to\": \"导入至：\",\n      \"view_list\": \"查看列表\",\n      \"delete_dialog_title\": \"删除导入会话\",\n      \"delete_dialog_description\": \"确定要删除 “{{name}}” 吗？此操作无法撤销。书签本身不会被删除。\",\n      \"delete_session\": \"删除会话\",\n      \"pause_session\": \"暂停\",\n      \"resume_session\": \"恢复\",\n      \"view_details\": \"查看详情\",\n      \"detail\": {\n        \"page_title\": \"查看导入会话详情\",\n        \"back_to_import\": \"返回导入\",\n        \"filter_all\": \"全部\",\n        \"filter_accepted\": \"已接受\",\n        \"filter_rejected\": \"已拒绝\",\n        \"filter_duplicates\": \"重复\",\n        \"filter_pending\": \"待定\",\n        \"table_title\": \"标题/URL\",\n        \"table_type\": \"类型\",\n        \"table_result\": \"结果\",\n        \"table_reason\": \"原因\",\n        \"table_bookmark\": \"书签\",\n        \"result_accepted\": \"已接受\",\n        \"result_rejected\": \"已拒绝\",\n        \"result_skipped_duplicate\": \"重复\",\n        \"result_pending\": \"待定\",\n        \"result_processing\": \"正在处理\",\n        \"no_results\": \"没有找到符合此过滤器的结果。\",\n        \"view_bookmark\": \"查看书签\",\n        \"load_more\": \"加载更多\",\n        \"no_title\": \"没有标题\"\n      }\n    },\n    \"backups\": {\n      \"backups\": \"备份\",\n      \"page_title\": \"备份\",\n      \"page_description\": \"自动创建和管理您的书签备份。备份经过压缩并安全存储。\",\n      \"configuration\": {\n        \"title\": \"备份配置\",\n        \"enable_automatic_backups\": \"启用自动备份\",\n        \"enable_automatic_backups_description\": \"自动创建您的书签备份\",\n        \"backup_frequency\": \"备份频率\",\n        \"backup_frequency_description\": \"应该多久创建一次备份\",\n        \"retention_period\": \"保留期限（天）\",\n        \"retention_period_description\": \"备份在删除前保留的天数\",\n        \"frequency\": {\n          \"daily\": \"每天\",\n          \"weekly\": \"每周\"\n        },\n        \"select_frequency\": \"选择频率\",\n        \"save_settings\": \"保存设置\"\n      },\n      \"list\": {\n        \"title\": \"您的备份\",\n        \"create_backup_now\": \"立即创建备份\",\n        \"no_backups\": \"您还没有任何备份。启用自动备份或手动创建一个。\",\n        \"table\": {\n          \"created_at\": \"创建于\",\n          \"bookmarks\": \"书签\",\n          \"size\": \"大小\",\n          \"status\": \"状态\",\n          \"actions\": \"操作\"\n        },\n        \"status\": {\n          \"success\": \"成功\",\n          \"failed\": \"失败\",\n          \"pending\": \"待定\"\n        },\n        \"actions\": {\n          \"download_backup\": \"下载备份\",\n          \"delete_backup\": \"删除备份\"\n        }\n      },\n      \"dialogs\": {\n        \"delete_backup_title\": \"删除备份？\",\n        \"delete_backup_description\": \"你确定要删除这个备份吗？这个操作没法撤销哦。\"\n      },\n      \"toasts\": {\n        \"backup_queued\": \"备份任务已加入队列！很快就会处理。\",\n        \"backup_deleted\": \"备份已删除！\"\n      }\n    }\n  },\n  \"admin\": {\n    \"admin_settings\": \"管理员设置\",\n    \"server_stats\": {\n      \"server_stats\": \"服务器统计\",\n      \"total_users\": \"总用户数\",\n      \"total_bookmarks\": \"总书签数\",\n      \"server_version\": \"服务器版本\"\n    },\n    \"background_jobs\": {\n      \"background_jobs\": \"后台任务\",\n      \"crawler_jobs\": \"爬虫任务\",\n      \"indexing_jobs\": \"索引任务\",\n      \"inference_jobs\": \"推理任务\",\n      \"tidy_assets_jobs\": \"整理资产任务\",\n      \"job\": \"任务\",\n      \"queued\": \"已排队\",\n      \"pending\": \"待处理\",\n      \"failed\": \"失败\",\n      \"feed_jobs\": \"RSS 订阅作业\",\n      \"video_jobs\": \"视频下载作业\",\n      \"webhook_jobs\": \"Webhook 作业\",\n      \"asset_preprocessing_jobs\": \"资源预处理作业\",\n      \"jobs\": {\n        \"crawler\": {\n          \"title\": \"爬虫任务\",\n          \"description\": \"从 URL 抓取网页内容并提取信息\"\n        },\n        \"inference\": {\n          \"title\": \"推理任务\",\n          \"description\": \"用人工智能给内容打标签和做总结\"\n        },\n        \"indexing\": {\n          \"title\": \"索引任务\",\n          \"description\": \"搜索索引更新\"\n        },\n        \"asset_preprocessing\": {\n          \"title\": \"资源预处理任务\",\n          \"description\": \"图片和文档预处理（截图、文本提取等）\"\n        },\n        \"tidy_assets\": {\n          \"title\": \"整理资源任务\",\n          \"description\": \"资源清理和存储优化\"\n        },\n        \"video\": {\n          \"title\": \"视频下载任务\",\n          \"description\": \"视频提取和下载\"\n        },\n        \"webhook\": {\n          \"title\": \"Webhook 任务\",\n          \"description\": \"外部 webhook 通知\"\n        },\n        \"feed\": {\n          \"title\": \"RSS 订阅任务\",\n          \"description\": \"RSS 订阅源处理和内容更新\"\n        },\n        \"admin_maintenance\": {\n          \"title\": \"管理员维护任务\",\n          \"description\": \"管理清理和资产维护\"\n        }\n      },\n      \"monitor_and_manage\": \"监控和管理后台任务队列和系统处理任务\",\n      \"active\": \"活跃\",\n      \"available_actions\": \"可用操作\",\n      \"status\": {\n        \"title\": \"了解任务状态\",\n        \"queued\": {\n          \"title\": \"已加入队列\",\n          \"description\": \"正在排队等待处理的任务。当资源可用时，它们将自动启动。\"\n        },\n        \"unprocessed\": {\n          \"title\": \"未处理\",\n          \"description\": \"尚未处理的书签。如果它们没有排队，它们很可能已经在排队等待处理，如果没有，你可能需要手动重新将它们加入队列。\"\n        },\n        \"failed\": {\n          \"title\": \"失败\",\n          \"description\": \"处理过程中遇到错误的书签。这些书签可能需要手动处理。\"\n        }\n      },\n      \"actions\": {\n        \"recrawl_failed_links_only\": \"仅重新抓取失败链接\",\n        \"recrawl_all_links\": \"重新抓取所有链接\",\n        \"without_inference\": \"不进行推断\",\n        \"regenerate_ai_tags_for_failed_bookmarks_only\": \"仅为失败的书签重新生成 AI 标签\",\n        \"regenerate_ai_tags_for_all_bookmarks\": \"为所有书签重新生成 AI 标签\",\n        \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"仅为失败的书签重新生成 AI 摘要\",\n        \"regenerate_ai_summaries_for_all_bookmarks\": \"为所有书签重新生成 AI 摘要\",\n        \"reindex_all_bookmarks\": \"重新索引所有书签\",\n        \"clean_assets\": \"清理悬空资产并重新同步元数据\",\n        \"reprocess_assets_fix_mode\": \"重新处理未处理的资产\",\n        \"migrate_large_link_html_content\": \"将大型内联 HTML 内容移动到 Assets\",\n        \"recrawl_pending_links_only\": \"仅重新抓取待处理的链接\",\n        \"regenerate_ai_tags_for_pending_bookmarks_only\": \"仅为待处理的书签重新生成 AI 标签\",\n        \"regenerate_ai_summaries_for_pending_bookmarks_only\": \"仅为待处理的书签重新生成 AI 摘要\"\n      }\n    },\n    \"actions\": {\n      \"recrawl_failed_links_only\": \"仅重新抓取失败链接\",\n      \"recrawl_all_links\": \"重新抓取所有链接\",\n      \"without_inference\": \"无推理\",\n      \"regenerate_ai_tags_for_failed_bookmarks_only\": \"仅为失败书签重新生成AI标签\",\n      \"regenerate_ai_tags_for_all_bookmarks\": \"为所有书签重新生成AI标签\",\n      \"reindex_all_bookmarks\": \"重新索引所有书签\",\n      \"compact_assets\": \"压缩资产\",\n      \"reprocess_assets_fix_mode\": \"重新处理资产(固定模式)\",\n      \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"仅为失败的书签重新生成 AI 摘要\",\n      \"regenerate_ai_summaries_for_all_bookmarks\": \"为所有书签重新生成 AI 摘要\"\n    },\n    \"users_list\": {\n      \"users_list\": \"用户列表\",\n      \"create_user\": \"创建用户\",\n      \"change_role\": \"更改角色\",\n      \"reset_password\": \"重置密码\",\n      \"delete_user\": \"删除用户\",\n      \"num_bookmarks\": \"书签数\",\n      \"asset_sizes\": \"资产大小\",\n      \"local_user\": \"本地用户\",\n      \"confirm_password\": \"确认密码\",\n      \"delete_user_confirm_description\": \"你确定要删除用户“{{name}}”吗？\",\n      \"unlimited\": \"无限制\"\n    },\n    \"service_connections\": {\n      \"title\": \"服务连接\",\n      \"description\": \"监测外部系统依赖的健康状况和连接性\",\n      \"search_engine\": \"搜索引擎\",\n      \"browser\": \"浏览器\",\n      \"queue_system\": \"队列系统\",\n      \"status\": {\n        \"not_configured\": \"未配置\",\n        \"connected\": \"已连接\",\n        \"disconnected\": \"已断开连接\"\n      }\n    },\n    \"admin_tools\": {\n      \"admin_tools\": \"管理工具\",\n      \"bookmark_debugger\": \"书签调试器\",\n      \"bookmark_id\": \"书签 ID\",\n      \"bookmark_id_placeholder\": \"输入书签 ID\",\n      \"lookup\": \"查找\",\n      \"debug_info\": \"调试信息\",\n      \"basic_info\": \"基本信息\",\n      \"status\": \"状态\",\n      \"content\": \"内容\",\n      \"html_preview\": \"HTML 预览（前 1000 个字符）\",\n      \"summary\": \"摘要\",\n      \"url\": \"URL\",\n      \"source_url\": \"源 URL\",\n      \"asset_type\": \"资源类型\",\n      \"file_name\": \"文件名\",\n      \"owner_user_id\": \"所有者用户 ID\",\n      \"tagging_status\": \"标记状态\",\n      \"summarization_status\": \"摘要状态\",\n      \"crawl_status\": \"抓取状态\",\n      \"crawl_status_code\": \"HTTP 状态代码\",\n      \"crawled_at\": \"抓取时间\",\n      \"recrawl\": \"重新抓取\",\n      \"reindex\": \"重新索引\",\n      \"retag\": \"重新标记\",\n      \"resummarize\": \"重新总结\",\n      \"bookmark_not_found\": \"找不到书签\",\n      \"action_success\": \"操作成功完成\",\n      \"action_failed\": \"操作失败\",\n      \"recrawl_queued\": \"重新抓取任务已进入队列\",\n      \"reindex_queued\": \"重新索引任务已进入队列\",\n      \"retag_queued\": \"重新标记任务已进入队列\",\n      \"resummarize_queued\": \"重新总结任务已在队列中\",\n      \"view\": \"查看\",\n      \"fetch_error\": \"获取书签时出错\"\n    }\n  },\n  \"options\": {\n    \"dark_mode\": \"暗模式\",\n    \"light_mode\": \"亮模式\",\n    \"apps_extensions\": \"应用与扩展\",\n    \"documentation\": \"文档\",\n    \"follow_us_on_x\": \"在 X 上关注我们\"\n  },\n  \"lists\": {\n    \"all_lists\": \"所有列表\",\n    \"favourites\": \"收藏\",\n    \"new_list\": \"新列表\",\n    \"new_nested_list\": \"新嵌套列表\",\n    \"merge_list\": \"合并列表\",\n    \"destination_list\": \"目标列表\",\n    \"delete_after_merge\": \"合并后删除源列表\",\n    \"no_destination\": \"未选中目标\",\n    \"no_parent\": \"没有父级\",\n    \"parent_list\": \"父级列表\",\n    \"list_type\": \"列表类型\",\n    \"manual_list\": \"手动列表\",\n    \"smart_list\": \"智能列表\",\n    \"search_query\": \"搜索查询\",\n    \"edit_list\": \"修改列表\",\n    \"search_query_help\": \"了解更多关于搜索查询语言。\",\n    \"description\": \"描述（可选）\",\n    \"rss\": {\n      \"title\": \"RSS订阅\",\n      \"description\": \"为此列表启用 RSS 订阅\",\n      \"feed_url\": \"RSS 订阅 URL\"\n    },\n    \"share_list\": \"分享列表\",\n    \"public_list\": {\n      \"title\": \"公共列表\",\n      \"description\": \"允许其他人查看此列表\",\n      \"share_link\": \"分享链接\"\n    },\n    \"delete_list\": {\n      \"title\": \"删除列表\",\n      \"description\": \"删除列表不会删除该列表中的任何书签。\",\n      \"delete_children\": \"删除子列表（递归）\",\n      \"delete_children_description\": \"如果未选中，所有直接的子列表都将成为根列表\"\n    },\n    \"shared\": \"共享\",\n    \"collaborators\": {\n      \"manage\": \"管理协作者\",\n      \"view\": \"查看协作者\",\n      \"collaborators\": \"协作者\",\n      \"add\": \"添加协作者\",\n      \"current\": \"当前协作者\",\n      \"enter_email\": \"输入电子邮件地址\",\n      \"please_enter_email\": \"请输入一个电子邮件地址\",\n      \"added_successfully\": \"已成功添加协作者\",\n      \"failed_to_add\": \"添加协作者失败\",\n      \"removed\": \"已移除协作者\",\n      \"failed_to_remove\": \"移除协作者失败\",\n      \"role_updated\": \"角色已更新\",\n      \"failed_to_update_role\": \"更新角色失败\",\n      \"viewer\": \"查看者\",\n      \"editor\": \"编辑者\",\n      \"owner\": \"拥有者\",\n      \"viewer_description\": \"可以在列表中查看书签\",\n      \"editor_description\": \"可以添加和移除书签\",\n      \"no_collaborators\": \"还没有协作者。添加一个开始协作吧！\",\n      \"no_collaborators_readonly\": \"此列表没有协作者。\",\n      \"people_with_access\": \"有权访问此列表的人员\",\n      \"add_or_remove\": \"添加或删除可以访问此列表的人员\",\n      \"invitation_sent\": \"邀请已成功发送\",\n      \"invitation_revoked\": \"邀请已撤销\",\n      \"failed_to_revoke\": \"撤销邀请失败\",\n      \"pending\": \"待定\",\n      \"revoke\": \"撤销\",\n      \"declined\": \"已拒绝\"\n    },\n    \"leave_list\": {\n      \"title\": \"离开列表\",\n      \"confirm_message\": \"您确定要离开 {{icon}} {{name}} 吗？\",\n      \"warning\": \"您将无法再查看或访问此列表中的书签。列表所有者可以在需要时重新添加您。\",\n      \"action\": \"离开列表\",\n      \"success\": \"你已经离开了“{{icon}} {{name}}”\"\n    },\n    \"invitations\": {\n      \"pending\": \"待处理的邀请\",\n      \"description\": \"审核并响应列表协作邀请\",\n      \"invited_by\": \"邀请人\",\n      \"accept\": \"接受\",\n      \"decline\": \"拒绝\",\n      \"accepted\": \"邀请已接受\",\n      \"declined\": \"邀请已拒绝\",\n      \"failed_to_accept\": \"接受邀请失败\",\n      \"failed_to_decline\": \"拒绝邀请失败\"\n    },\n    \"shared_lists\": \"共享列表\"\n  },\n  \"tags\": {\n    \"all_tags\": \"所有标签\",\n    \"your_tags\": \"您的标签\",\n    \"your_tags_info\": \"至少一次由您附加的标签\",\n    \"ai_tags\": \"AI标签\",\n    \"ai_tags_info\": \"仅由AI自动附加的标签\",\n    \"unused_tags\": \"未使用标签\",\n    \"unused_tags_info\": \"未附加到任何书签的标签\",\n    \"delete_all_unused_tags\": \"删除所有未使用标签\",\n    \"drag_and_drop_merging\": \"拖放合并\",\n    \"drag_and_drop_merging_info\": \"拖放标签以相互合并\",\n    \"sort_by_name\": \"按名称排序\",\n    \"create_tag\": \"创建标签\",\n    \"create_tag_description\": \"创建一个新标签，不附加到任何书签\",\n    \"tag_name\": \"标签名称\",\n    \"enter_tag_name\": \"输入标签名称\",\n    \"sort_by_usage\": \"按使用情况排序\",\n    \"sort_by_relevance\": \"按相关性排序\",\n    \"no_custom_tags\": \"还没有自定义标签\",\n    \"no_ai_tags\": \"还没有 AI 标签呢\",\n    \"no_unused_tags\": \"你没有未使用的标签\",\n    \"no_unused_tags_match_your_search\": \"没有未使用的标签符合你的搜索\",\n    \"no_tags_match_your_search\": \"没有标签符合你的搜索内容\",\n    \"search_placeholder\": \"搜索标签...\",\n    \"search_or_create_placeholder\": \"搜索或创建标签...\"\n  },\n  \"preview\": {\n    \"view_original\": \"查看原文\",\n    \"cached_content\": \"缓存内容\",\n    \"reader_view\": \"阅读器视图\",\n    \"tabs\": {\n      \"content\": \"内容\",\n      \"details\": \"详情\"\n    },\n    \"archive_info\": \"如果存档需要 Javascript，则可能无法正确地以内联方式呈现。为了获得最佳效果，<1>请下载并在浏览器中打开它</1>。\",\n    \"fetch_error_title\": \"内容不可用\",\n    \"fetch_error_description\": \"我们没法获取此链接的内容。这个页面可能受保护、需要验证，或者暂时不可用。\",\n    \"crawling_in_progress\": \"正在获取页面内容……\",\n    \"continue_reading\": \"从上次结束的地方继续\",\n    \"continue_reading_percent\": \"从上次结束的地方继续（{{percent}}%）\",\n    \"continue_button\": \"继续\"\n  },\n  \"editor\": {\n    \"quickly_focus\": \"您可以按⌘ + E快速聚焦到此字段\",\n    \"multiple_urls_dialog_title\": \"将URL导入为单独的书签？\",\n    \"multiple_urls_dialog_desc\": \"输入包含多行上的多个URL。您要将它们作为单独的书签导入吗？\",\n    \"import_as_text\": \"作为文本书签导入\",\n    \"import_as_separate_bookmarks\": \"作为单独书签导入\",\n    \"placeholder\": \"粘贴链接或图像，写笔记或将图像拖放到此处…\",\n    \"new_item\": \"新项目\",\n    \"disabled_submissions\": \"提交已禁用\",\n    \"text_toolbar\": {\n      \"bold\": \"粗体\",\n      \"underline\": \"下划线\",\n      \"code\": \"代码\",\n      \"align_center\": \"居中对齐\",\n      \"align_right\": \"右对齐\",\n      \"markdown_shortcuts\": {\n        \"label\": \"Markdown快捷键\",\n        \"heading\": {\n          \"label\": \"标题\",\n          \"example\": \"# H1, ## H2, ### H3\"\n        },\n        \"bold\": {\n          \"label\": \"粗体\",\n          \"example\": \"**text** 或 CTRL+b\"\n        },\n        \"italic\": {\n          \"label\": \"斜体\",\n          \"example\": \"*Italic* 或 _Italic_ 或 CTRL+i\"\n        },\n        \"blockquote\": {\n          \"label\": \"块引用\",\n          \"example\": \"> 块引用\"\n        },\n        \"ordered_list\": {\n          \"example\": \"1. 列表项\",\n          \"label\": \"有序列表\"\n        },\n        \"unordered_list\": {\n          \"label\": \"无序列表\",\n          \"example\": \"- 列表项\"\n        },\n        \"inline_code\": {\n          \"label\": \"内联代码\",\n          \"example\": \"`代码`\"\n        },\n        \"block_code\": {\n          \"label\": \"代码块\",\n          \"example\": \"``` + 空格\"\n        }\n      },\n      \"italic\": \"斜体\",\n      \"highlight\": \"高亮\",\n      \"strikethrough\": \"删除线\",\n      \"undo\": \"取消\",\n      \"redo\": \"重做\",\n      \"align_left\": \"左对齐\"\n    },\n    \"placeholder_v2\": \"粘贴链接、写笔记或拖放图片……\"\n  },\n  \"toasts\": {\n    \"bookmarks\": {\n      \"updated\": \"书签已更新！\",\n      \"deleted\": \"书签已删除！\",\n      \"refetch\": \"重新获取已排队！\",\n      \"full_page_archive\": \"已触发完整页面归档创建\",\n      \"delete_from_list\": \"书签已从列表中删除\",\n      \"clipboard_copied\": \"链接已添加到您的剪贴板！\",\n      \"preserve_pdf\": \"已触发 PDF 保存\",\n      \"update_banner\": \"横幅已更新！\",\n      \"uploading_banner\": \"正在上传横幅...\"\n    },\n    \"lists\": {\n      \"created\": \"列表已创建！\",\n      \"updated\": \"列表已更新！\",\n      \"merged\": \"列表已合并！\",\n      \"deleted\": \"列表已删除！\"\n    },\n    \"tags\": {\n      \"created\": \"标签已创建！\",\n      \"failed_to_create\": \"创建标签失败\"\n    }\n  },\n  \"cleanups\": {\n    \"cleanups\": \"清理\",\n    \"duplicate_tags\": {\n      \"title\": \"重复标签\",\n      \"merge_all_suggestions\": \"合并所有建议？\"\n    }\n  },\n  \"search\": {\n    \"type_is_not\": \"类型不是\",\n    \"is_favorited\": \"已收藏\",\n    \"is_archived\": \"已存档\",\n    \"is_not_archived\": \"未存档\",\n    \"has_any_tag\": \"有任何标签\",\n    \"has_no_tags\": \"没有标签\",\n    \"is_in_any_list\": \"在任何列表里\",\n    \"is_not_in_any_list\": \"不在任何列表里\",\n    \"created_on_or_after\": \"创建于或晚于\",\n    \"not_created_on_or_after\": \"创建时间不晚于\",\n    \"is_in_list\": \"在列表里\",\n    \"is_not_in_list\": \"不在列表里\",\n    \"has_tag\": \"有标签\",\n    \"and\": \"和\",\n    \"is_not_favorited\": \"未收藏\",\n    \"created_on_or_before\": \"创建于或早于\",\n    \"not_created_on_or_before\": \"创建时间不早于\",\n    \"url_contains\": \"链接包含\",\n    \"url_does_not_contain\": \"链接不包含\",\n    \"does_not_have_tag\": \"没有标签\",\n    \"type_is\": \"类型是\",\n    \"or\": \"或\",\n    \"full_text_search\": \"全文搜索\",\n    \"is_from_feed\": \"来自 RSS 订阅\",\n    \"is_not_from_feed\": \"不是来自 RSS 订阅\",\n    \"month_s_ago\": \" {months} 个月前\",\n    \"year_s_ago\": \" {years} 年前\",\n    \"created_within\": \"创建于\",\n    \"created_earlier_than\": \"创建时间早于\",\n    \"day_s\": \" {days} 天\",\n    \"week_s\": \" {weeks} 周\",\n    \"month_s\": \" {months} 个月\",\n    \"year_s\": \" {years} 年\",\n    \"day_s_ago\": \" {days} 天前\",\n    \"week_s_ago\": \" {weeks} 周前\",\n    \"history\": \"最近搜索\",\n    \"title_contains\": \"标题包含\",\n    \"title_does_not_contain\": \"标题不包含\",\n    \"is_broken_link\": \"有损坏的链接\",\n    \"tags\": \"标签\",\n    \"no_suggestions\": \"没有建议\",\n    \"filters\": \"筛选器\",\n    \"is_not_broken_link\": \"有可用的链接\",\n    \"lists\": \"列表\",\n    \"feeds\": \"订阅\",\n    \"is_from_source\": \"来源是\",\n    \"is_not_from_source\": \"来源不是\"\n  },\n  \"dialogs\": {\n    \"bookmarks\": {\n      \"delete_confirmation_title\": \"删除书签？\",\n      \"delete_confirmation_description\": \"是否确定要删除此标签？\"\n    }\n  },\n  \"highlights\": {\n    \"no_highlights\": \"你目前没有高亮的地方。\"\n  },\n  \"banners\": {\n    \"no_bookmarks\": {\n      \"title\": \"还没有书签\",\n      \"description\": \"保存你感兴趣的文章、链接和页面，以便稍后快速访问。\"\n    }\n  },\n  \"bookmark_editor\": {\n    \"title\": \"编辑书签\",\n    \"subtitle\": \"更改书签详细信息。完成后点击保存。\",\n    \"author\": \"作者\",\n    \"publisher\": \"发布者\",\n    \"date_published\": \"发布日期\",\n    \"pick_a_date\": \"选择一个日期\",\n    \"save_changes\": \"保存更改\",\n    \"extracted_content\": \"提取的内容\"\n  },\n  \"view_options\": {\n    \"title\": \"查看选项\",\n    \"layout\": \"布局\",\n    \"columns\": \"列\",\n    \"display_options\": \"显示选项\",\n    \"show_note_previews\": \"显示备注\",\n    \"show_tags\": \"显示标签\",\n    \"show_title\": \"显示标题\",\n    \"image_options\": \"图像选项\",\n    \"image_fit_cover\": \"封面（填充）\",\n    \"image_fit_contain\": \"包含（适应）\"\n  },\n  \"version\": {\n    \"new_release_available\": \"有新的版本说明啦\",\n    \"whats_new_title\": \"v{{version}} 有啥新玩意儿？\",\n    \"release_notes_description\": \"这是从 GitHub 版本说明里搞来的最新更新。\",\n    \"loading_release_notes\": \"加载版本说明中……\",\n    \"unable_to_load_release_notes\": \"现在没法加载版本说明。稍后再试试呗。\",\n    \"no_release_notes\": \"这个版本没发布版本说明。\",\n    \"release_notes_synced\": \"版本说明是从 GitHub 同步过来的。\",\n    \"view_on_github\": \"在 GitHub 上瞅瞅\"\n  },\n  \"wrapped\": {\n    \"title\": \"你的 {{year}} 年终报告\",\n    \"subtitle\": \"Karakeep 年度回顾\",\n    \"banner\": {\n      \"title\": \"你的 2025 年度报告已准备就绪！\",\n      \"description\": \"查看你一年来的书签\",\n      \"view_now\": \"立即查看\"\n    },\n    \"button\": \"2025 年度报告\",\n    \"loading\": \"正在加载你的年度报告...\",\n    \"failed_to_load\": \"加载你的年度报告统计信息失败\",\n    \"sections\": {\n      \"total_saves\": {\n        \"prefix\": \"你保存了\",\n        \"suffix\": \"今年的条目\",\n        \"suffix_singular\": \"今年的条目\"\n      },\n      \"first_bookmark\": {\n        \"title\": \"你的旅程开始了\",\n        \"description\": \"{{year}}年的首次保存：\"\n      },\n      \"top_domains\": \"你最牛的站点\",\n      \"top_tags\": \"你最常用的标签\",\n      \"monthly_activity\": \"你今年的保存情况\",\n      \"most_active_day\": \"你最活跃的一天\",\n      \"peak_times\": {\n        \"title\": \"你在什么时候保存\",\n        \"peak_hour\": \"高峰时段\",\n        \"peak_day\": \"高峰日\"\n      },\n      \"how_you_save\": \"你怎样保存\",\n      \"what_you_saved\": \"你保存了什么\",\n      \"summary\": {\n        \"favorites\": \"收藏夹\",\n        \"tags_created\": \"创建的标签\",\n        \"highlights\": \"亮点\"\n      },\n      \"types\": {\n        \"links\": \"链接\",\n        \"notes\": \"笔记\",\n        \"assets\": \"资产\"\n      }\n    },\n    \"footer\": \"用 Karakeep 制作\",\n    \"share\": \"分享\",\n    \"download\": \"下载\",\n    \"close\": \"关闭\",\n    \"generating\": \"正在生成...\"\n  }\n}\n"
  },
  {
    "path": "apps/web/lib/i18n/locales/zhtw/translation.json",
    "content": "{\n  \"common\": {\n    \"url\": \"網址\",\n    \"name\": \"名稱\",\n    \"email\": \"電子郵件\",\n    \"password\": \"密碼\",\n    \"action\": \"操作\",\n    \"actions\": \"操作\",\n    \"created_at\": \"建立於\",\n    \"key\": \"金鑰\",\n    \"role\": \"角色\",\n    \"roles\": {\n      \"user\": \"使用者\",\n      \"admin\": \"管理員\"\n    },\n    \"something_went_wrong\": \"發生錯誤\",\n    \"experimental\": \"實驗性功能\",\n    \"search\": \"搜尋\",\n    \"tags\": \"標籤\",\n    \"note\": \"筆記\",\n    \"attachments\": \"附件\",\n    \"screenshot\": \"螢幕截圖\",\n    \"video\": \"影片\",\n    \"archive\": \"封存\",\n    \"home\": \"首頁\",\n    \"highlights\": \"標記重點\",\n    \"source\": \"來源\",\n    \"bookmark_types\": {\n      \"title\": \"書籤類型\",\n      \"link\": \"連結\",\n      \"text\": \"文字\",\n      \"media\": \"媒體\"\n    },\n    \"type\": \"類型\",\n    \"size\": \"大小\",\n    \"updated_at\": \"更新於\",\n    \"title\": \"標題\",\n    \"description\": \"描述\",\n    \"summary\": \"摘要\",\n    \"quota\": \"配額\",\n    \"bookmarks\": \"書籤\",\n    \"storage\": \"儲存空間\",\n    \"pdf\": \"已封存的 PDF\",\n    \"default\": \"預設\",\n    \"id\": \"ID\",\n    \"last_used\": \"上次使用\"\n  },\n  \"layouts\": {\n    \"masonry\": \"瀑布式\",\n    \"grid\": \"網格\",\n    \"list\": \"清單\",\n    \"compact\": \"緊湊\"\n  },\n  \"actions\": {\n    \"change_layout\": \"變更版面配置\",\n    \"archive\": \"封存\",\n    \"unarchive\": \"取消封存\",\n    \"favorite\": \"加入最愛\",\n    \"unfavorite\": \"移除最愛\",\n    \"delete\": \"刪除\",\n    \"refresh\": \"重新整理\",\n    \"download_full_page_archive\": \"下載完整網頁封存\",\n    \"edit_tags\": \"編輯標籤\",\n    \"add_to_list\": \"新增至清單\",\n    \"select_all\": \"全選\",\n    \"unselect_all\": \"取消全選\",\n    \"copy_link\": \"複製連結\",\n    \"close_bulk_edit\": \"關閉批次編輯\",\n    \"bulk_edit\": \"批次編輯\",\n    \"manage_lists\": \"管理清單\",\n    \"remove_from_list\": \"從清單移除\",\n    \"save\": \"儲存\",\n    \"add\": \"新增\",\n    \"edit\": \"編輯\",\n    \"create\": \"建立\",\n    \"fetch_now\": \"立即抓取\",\n    \"summarize_with_ai\": \"使用 AI 摘要\",\n    \"edit_title\": \"編輯標題\",\n    \"sign_out\": \"登出\",\n    \"close\": \"關閉\",\n    \"merge\": \"合併\",\n    \"cancel\": \"取消\",\n    \"apply_all\": \"全部套用\",\n    \"ignore\": \"忽略\",\n    \"sort\": {\n      \"title\": \"排序\",\n      \"newest_first\": \"最新在前\",\n      \"oldest_first\": \"最舊在前\",\n      \"relevant_first\": \"最相關優先\"\n    },\n    \"recrawl\": \"重新抓取\",\n    \"open_editor\": \"開啟編輯器\",\n    \"toggle_show_archived\": \"顯示已封存\",\n    \"confirm\": \"確認\",\n    \"regenerate\": \"重新產生\",\n    \"load_more\": \"載入更多\",\n    \"edit_notes\": \"編輯註解\",\n    \"preserve_as_pdf\": \"儲存為 PDF\",\n    \"offline_copies\": \"離線副本\",\n    \"preserve_offline_archive\": \"保留離線封存檔\",\n    \"download_full_page_archive_file\": \"下載封存檔\",\n    \"download_pdf_file\": \"下載 PDF 檔案\",\n    \"remove\": \"移除\",\n    \"more\": \"更多\",\n    \"replace_banner\": \"更換橫幅\",\n    \"add_banner\": \"新增橫幅\",\n    \"download\": \"下載\"\n  },\n  \"settings\": {\n    \"back_to_app\": \"返回應用程式\",\n    \"user_settings\": \"使用者設定\",\n    \"info\": {\n      \"user_info\": \"使用者資訊\",\n      \"basic_details\": \"基本資料\",\n      \"change_password\": \"變更密碼\",\n      \"current_password\": \"目前密碼\",\n      \"new_password\": \"新密碼\",\n      \"confirm_new_password\": \"確認新密碼\",\n      \"options\": \"選項\",\n      \"interface_lang\": \"介面語言\",\n      \"user_settings\": {\n        \"archive_display_behaviour\": {\n          \"hide\": \"在標籤和列表中隱藏已封存的書籤\",\n          \"title\": \"已封存的書籤\",\n          \"show\": \"在標籤和列表中顯示已封存的書籤\"\n        },\n        \"user_settings_updated\": \"使用者設定已更新！\",\n        \"bookmark_click_action\": {\n          \"title\": \"書籤點擊動作\",\n          \"open_external_url\": \"開啟原始網址\",\n          \"open_bookmark_details\": \"開啟書籤詳細資訊\"\n        }\n      },\n      \"reader_settings\": {\n        \"local_overrides_title\": \"裝置專用設定已啟動\",\n        \"using_default\": \"使用用戶端預設值\",\n        \"clear_override_hint\": \"清除裝置覆寫以使用全域設定（{{value}}）\",\n        \"font_size\": \"字型大小\",\n        \"font_family\": \"字型\",\n        \"preview_inline\": \"（預覽）\",\n        \"tooltip_preview\": \"未儲存的預覽變更\",\n        \"save_to_all_devices\": \"所有裝置\",\n        \"tooltip_local\": \"裝置設定與全域不同\",\n        \"reset_preview\": \"重設預覽\",\n        \"mono\": \"等寬字體\",\n        \"line_height\": \"行高\",\n        \"tooltip_default\": \"閱讀設定\",\n        \"title\": \"閱讀器設定\",\n        \"serif\": \"襯線體\",\n        \"preview\": \"預覽\",\n        \"not_set\": \"未設定\",\n        \"clear_local_overrides\": \"清除裝置設定\",\n        \"preview_text\": \"敏捷的棕色狐狸跳過懶惰的狗。您的閱讀器檢視文字會像這樣顯示。\",\n        \"local_overrides_cleared\": \"裝置專用設定已清除\",\n        \"local_overrides_description\": \"此裝置具有與全域預設值不同的閱讀器設定：\",\n        \"clear_defaults\": \"清除所有預設值\",\n        \"description\": \"設定閱讀器檢視的預設文字設定。這些設定會在您的所有裝置之間同步。\",\n        \"defaults_cleared\": \"閱讀器預設值已清除\",\n        \"save_hint\": \"僅儲存此裝置的設定，或跨所有裝置同步\",\n        \"save_as_default\": \"儲存為預設值\",\n        \"save_to_device\": \"此裝置\",\n        \"sans\": \"無襯線體\",\n        \"tooltip_preview_and_local\": \"未儲存的預覽變更數目；裝置設定與全域不同\",\n        \"adjust_hint\": \"調整以上設定以預覽變更\"\n      },\n      \"avatar\": {\n        \"upload\": \"上傳頭像\",\n        \"change\": \"變更頭像\",\n        \"remove_confirm_title\": \"要移除頭像嗎？\",\n        \"updated\": \"頭像已更新\",\n        \"removed\": \"頭像已移除\",\n        \"description\": \"上傳一張正方形圖片做為您的頭像。\",\n        \"remove_confirm_description\": \"這會清除您目前的個人資料相片。\",\n        \"title\": \"個人資料相片\",\n        \"remove\": \"移除頭像\"\n      }\n    },\n    \"ai\": {\n      \"ai_settings\": \"AI 設定\",\n      \"tagging_rules\": \"標籤規則\",\n      \"tagging_rule_description\": \"您在此處新增的提示詞將作為規則提供給模型用於產生標籤。您可以在提示詞預覽區域檢視最終提示詞。\",\n      \"prompt_preview\": \"提示詞預覽\",\n      \"text_prompt\": \"文字提示詞\",\n      \"images_prompt\": \"圖片提示詞\",\n      \"all_tagging\": \"所有標籤\",\n      \"text_tagging\": \"文字標籤\",\n      \"image_tagging\": \"圖片標籤\",\n      \"summarization\": \"摘要\",\n      \"summarization_prompt\": \"摘要提示詞\",\n      \"tag_style\": \"標籤樣式\",\n      \"auto_summarization_description\": \"使用 AI 自動為你的書籤產生摘要。\",\n      \"auto_tagging\": \"自動標記\",\n      \"titlecase_spaces\": \"首字大寫，含空格\",\n      \"lowercase_underscores\": \"小寫，含底線\",\n      \"inference_language\": \"推論語言\",\n      \"titlecase_hyphens\": \"首字大寫，含連字號\",\n      \"lowercase_hyphens\": \"小寫，含連字號\",\n      \"lowercase_spaces\": \"小寫，含空格\",\n      \"inference_language_description\": \"選擇 AI 產生的標籤和摘要的語言。\",\n      \"tag_style_description\": \"選擇自動產生的標籤應如何格式化。\",\n      \"auto_tagging_description\": \"使用 AI 自動為你的書籤產生標籤。\",\n      \"camelCase\": \"駝峰式大小寫\",\n      \"auto_summarization\": \"自動摘要\",\n      \"no_preference\": \"無偏好\",\n      \"curated_tags\": \"精選標籤\",\n      \"curated_tags_description\": \"您可以選擇性地將 AI 標記限制為僅使用此列表中的標籤。未選擇任何標籤時，AI 會自由產生標籤。\",\n      \"curated_tags_updated\": \"精選標籤已成功更新！\",\n      \"curated_tags_update_failed\": \"無法更新精選標籤\"\n    },\n    \"feeds\": {\n      \"rss_subscriptions\": \"RSS 訂閱\",\n      \"add_a_subscription\": \"新增訂閱\",\n      \"feed_enabled\": \"已啟用 RSS 摘要\",\n      \"feed_disabled\": \"已停用 RSS 摘要\"\n    },\n    \"import\": {\n      \"import_export\": \"匯入／匯出\",\n      \"import_export_bookmarks\": \"匯入／匯出書籤\",\n      \"import_bookmarks_from_html_file\": \"從 HTML 檔案匯入書籤\",\n      \"import_bookmarks_from_pocket_export\": \"從 Pocket 匯出檔案匯入書籤\",\n      \"import_bookmarks_from_matter_export\": \"從 Matter 匯出檔案匯入書籤\",\n      \"import_bookmarks_from_omnivore_export\": \"從 Omnivore 匯出檔案匯入書籤\",\n      \"import_bookmarks_from_karakeep_export\": \"從 Karakeep 匯出檔案匯入書籤\",\n      \"export_links_and_notes\": \"匯出連結和筆記\",\n      \"imported_bookmarks\": \"已匯入的書籤\",\n      \"import_bookmarks_from_linkwarden_export\": \"從 Linkwarden 匯出檔案匯入書籤\",\n      \"import_bookmarks_from_tab_session_manager_export\": \"從 Tab Session Manager 匯入書籤\",\n      \"import_bookmarks_from_mymind_export\": \"從 mymind 匯入書籤匯出檔案\",\n      \"import_bookmarks_from_instapaper_export\": \"從 Instapaper 匯出檔案匯入書籤\"\n    },\n    \"api_keys\": {\n      \"api_keys\": \"API 金鑰\",\n      \"new_api_key\": \"新增 API 金鑰\",\n      \"new_api_key_desc\": \"為您的 API 金鑰指定一個獨一無二的名稱\",\n      \"key_success\": \"金鑰建立成功\",\n      \"key_success_please_copy\": \"請複製金鑰並儲存至安全的位置。關閉對話框後，您將無法再次存取此金鑰。\",\n      \"regenerate_api_key\": \"重新產生 API 金鑰\",\n      \"key_regenerated\": \"金鑰已成功重新產生\",\n      \"key_regenerated_please_copy\": \"請複製新的金鑰並妥善存放。舊的金鑰已被撤銷，將無法再使用。\",\n      \"regenerate_warning\": \"確定要重新產生 API 金鑰「{{name}}」嗎？\",\n      \"regenerate_confirmation\": \"這會撤銷目前的金鑰並產生新的金鑰。使用目前金鑰的任何應用程式都將停止運作。\"\n    },\n    \"webhooks\": {\n      \"webhooks\": \"Webhook\",\n      \"events\": {\n        \"title\": \"事件\",\n        \"created\": \"已建立\",\n        \"edited\": \"已編輯\",\n        \"crawled\": \"已抓取\"\n      },\n      \"delete_webhook_confirmation\": \"您確定要刪除這個 Webhook 嗎？\",\n      \"edit_webhook\": \"編輯 Webhook\",\n      \"webhook_url\": \"Webhook 網址\",\n      \"description\": \"您可以使用 Webhook 在建立、變更或抓取書籤時觸發操作。\",\n      \"auth_token\": \"驗證權杖\",\n      \"add_auth_token\": \"新增驗證權杖\",\n      \"edit_auth_token\": \"編輯驗證權杖\",\n      \"create_webhook\": \"建立 Webhook\",\n      \"delete_webhook\": \"刪除 Webhook\"\n    },\n    \"broken_links\": {\n      \"broken_links\": \"失效連結\",\n      \"last_crawled_at\": \"最後抓取時間\",\n      \"crawling_status\": \"抓取狀態\",\n      \"crawling_failed\": \"抓取失敗\"\n    },\n    \"manage_assets\": {\n      \"manage_assets\": \"管理資產\",\n      \"no_assets\": \"你還沒有任何資產。\",\n      \"asset_type\": \"資產類型\",\n      \"bookmark_link\": \"書籤連結\",\n      \"asset_link\": \"資產連結\",\n      \"delete_asset\": \"刪除資產\",\n      \"delete_asset_confirmation\": \"你確定要刪除這個資產嗎？\"\n    },\n    \"rules\": {\n      \"conditions_types\": {\n        \"has_tag\": \"有標籤\",\n        \"is_favourited\": \"已設為最愛\",\n        \"always\": \"總是\",\n        \"url_contains\": \"網址包含\",\n        \"imported_from_feed\": \"從 Feed 匯入\",\n        \"bookmark_type_is\": \"書籤類型為\",\n        \"is_archived\": \"已封存\",\n        \"and\": \"以下全部為真\",\n        \"or\": \"以下任一項為真\",\n        \"url_does_not_contain\": \"URL 不包含\",\n        \"title_contains\": \"標題包含\",\n        \"title_does_not_contain\": \"標題不包含\"\n      },\n      \"rules\": \"規則引擎\",\n      \"rule_name\": \"規則名稱\",\n      \"description\": \"您可以使用規則在事件觸發時觸發動作。\",\n      \"ceate_rule\": \"建立規則\",\n      \"edit_rule\": \"編輯規則\",\n      \"save_rule\": \"儲存規則\",\n      \"delete_rule\": \"刪除規則\",\n      \"delete_rule_confirmation\": \"你確定要刪除此規則嗎？\",\n      \"whenever\": \"無論何時...\",\n      \"if\": \"如果...\",\n      \"enter_rule_name\": \"輸入規則名稱\",\n      \"describe_what_this_rule_does\": \"描述此規則的作用\",\n      \"rule_has_been_created\": \"規則已建立！\",\n      \"rule_has_been_updated\": \"規則已更新！\",\n      \"rule_has_been_deleted\": \"規則已刪除！\",\n      \"no_rules_created_yet\": \"尚未建立任何規則\",\n      \"create_your_first_rule\": \"建立您的第一個規則以自動化您的工作流程\",\n      \"actions_types\": {\n        \"add_tag\": \"新增標籤\",\n        \"remove_tag\": \"移除標籤\",\n        \"add_to_list\": \"新增至清單\",\n        \"remove_from_list\": \"從清單中移除\",\n        \"download_full_page_archive\": \"下載完整頁面封存\",\n        \"favourite_bookmark\": \"我的最愛書籤\",\n        \"archive_bookmark\": \"封存書籤\"\n      },\n      \"events_types\": {\n        \"bookmark_added\": \"已新增書籤\",\n        \"tag_added\": \"此標籤已新增至書籤\",\n        \"tag_removed\": \"已從書籤中移除此標籤\",\n        \"added_to_list\": \"已將書籤新增至此清單\",\n        \"removed_from_list\": \"已從此清單中移除書籤\",\n        \"favourited\": \"已將書籤設為我的最愛\",\n        \"archived\": \"已封存書籤\"\n      }\n    },\n    \"stats\": {\n      \"usage_statistics\": \"使用量統計\",\n      \"insights_description\": \"深入瞭解你的書籤習慣和收藏\",\n      \"failed_to_load\": \"載入統計資料失敗\",\n      \"overview\": {\n        \"total_bookmarks\": \"書籤總數\",\n        \"all_saved_items\": \"所有已儲存項目\",\n        \"favorites\": \"我的最愛\",\n        \"starred_bookmarks\": \"已加星號的書籤\",\n        \"archived\": \"已封存\",\n        \"archived_items\": \"已封存的項目\",\n        \"tags\": \"標籤\",\n        \"unique_tags_created\": \"已建立的唯一標籤\",\n        \"lists\": \"清單\",\n        \"bookmark_collections\": \"書籤收藏\",\n        \"highlights\": \"醒目提示\",\n        \"text_highlights\": \"文字醒目提示\",\n        \"storage_used\": \"已使用的儲存空間\",\n        \"total_asset_storage\": \"資產儲存總量\",\n        \"this_month\": \"本月\",\n        \"bookmarks_added\": \"已新增書籤\"\n      },\n      \"bookmark_types\": {\n        \"title\": \"書籤類型\",\n        \"links\": \"連結\",\n        \"text_notes\": \"文字筆記\",\n        \"assets\": \"資產\"\n      },\n      \"recent_activity\": {\n        \"title\": \"近期活動\",\n        \"this_week\": \"本週\",\n        \"this_month\": \"本月\",\n        \"this_year\": \"今年\"\n      },\n      \"top_domains\": {\n        \"title\": \"熱門網域\",\n        \"no_domains_found\": \"找不到網域\"\n      },\n      \"most_used_tags\": {\n        \"title\": \"最常使用的標籤\",\n        \"no_tags_found\": \"找不到標籤\"\n      },\n      \"activity_patterns\": {\n        \"activity_by_hour\": \"依小時區分的活動\",\n        \"activity_by_day\": \"每日活動\"\n      },\n      \"storage_breakdown\": {\n        \"title\": \"儲存空間明細\"\n      },\n      \"bookmark_sources\": {\n        \"title\": \"書籤來源\",\n        \"empty\": \"沒有可用的來源資料\"\n      }\n    },\n    \"subscription\": {\n      \"subscription\": \"訂閱\",\n      \"manage_subscription\": \"管理你的訂閱和帳單資訊\",\n      \"current_plan\": \"目前方案\",\n      \"billing_period\": \"帳單週期\",\n      \"paid_plan\": \"付費方案\",\n      \"unlock_bigger_quota\": \"解鎖更大的配額並支持這個專案\",\n      \"subscribe_now\": \"立即訂閱\",\n      \"manage_billing\": \"管理帳單\",\n      \"subscription_canceled\": \"你的訂閱已取消，將於 {{date}} 結束。你可以隨時重新訂閱。\",\n      \"usage_quotas\": \"使用量和配額\",\n      \"track_usage\": \"追蹤你目前的使用量，看看有沒有超過方案限制\",\n      \"total_bookmarks_saved\": \"已儲存的書籤總數\",\n      \"assets_file_storage\": \"資產和檔案儲存空間\",\n      \"unlimited_usage\": \"無限制使用\",\n      \"quota_limit_reached\": \"已達到配額上限\",\n      \"approaching_quota_limit\": \"快要達到配額上限了\",\n      \"loading_usage\": \"正在載入使用量資訊...\",\n      \"free\": \"免費\",\n      \"paid\": \"已付費\"\n    },\n    \"import_sessions\": {\n      \"title\": \"匯入工作階段\",\n      \"description\": \"檢視和管理你的大量匯入工作階段。當你匯入書籤時，會自動建立工作階段。\",\n      \"load_error\": \"載入匯入工作階段失敗\",\n      \"no_sessions\": \"還沒有匯入工作階段\",\n      \"no_sessions_detail\": \"當您匯入書籤時，匯入工作階段會自動出現在這裡\",\n      \"created_at\": \"已於 {{time}} 建立\",\n      \"progress\": \"進度\",\n      \"status\": {\n        \"pending\": \"等待中\",\n        \"in_progress\": \"進行中\",\n        \"completed\": \"已完成\",\n        \"failed\": \"失敗\",\n        \"processing\": \"處理中\",\n        \"staging\": \"預備\",\n        \"running\": \"執行中\",\n        \"paused\": \"已暫停\"\n      },\n      \"badges\": {\n        \"pending\": \"{{count}} 等待中\",\n        \"processing\": \"{{count}} 處理中\",\n        \"completed\": \"{{count}} 已完成\",\n        \"failed\": \"{{count}} 失敗\"\n      },\n      \"imported_to\": \"匯入至：\",\n      \"view_list\": \"檢視清單\",\n      \"delete_dialog_title\": \"刪除匯入工作階段\",\n      \"delete_dialog_description\": \"確定要刪除「{{name}}」嗎？此動作無法復原。書籤本身不會被刪除。\",\n      \"delete_session\": \"刪除工作階段\",\n      \"pause_session\": \"暫停\",\n      \"resume_session\": \"繼續\",\n      \"view_details\": \"檢視詳細資料\",\n      \"detail\": {\n        \"page_title\": \"匯入工作階段詳細資料\",\n        \"back_to_import\": \"返回匯入\",\n        \"filter_all\": \"全部\",\n        \"filter_accepted\": \"已接受\",\n        \"filter_rejected\": \"已拒絕\",\n        \"filter_duplicates\": \"重複\",\n        \"filter_pending\": \"待處理\",\n        \"table_title\": \"標題／網址\",\n        \"table_type\": \"類型\",\n        \"table_result\": \"結果\",\n        \"table_reason\": \"原因\",\n        \"table_bookmark\": \"書籤\",\n        \"result_accepted\": \"已接受\",\n        \"result_rejected\": \"已拒絕\",\n        \"result_skipped_duplicate\": \"重複\",\n        \"result_pending\": \"待處理\",\n        \"result_processing\": \"處理中\",\n        \"no_results\": \"這個篩選器沒有找到任何結果。\",\n        \"view_bookmark\": \"檢視書籤\",\n        \"load_more\": \"載入更多\",\n        \"no_title\": \"無標題\"\n      }\n    },\n    \"backups\": {\n      \"backups\": \"備份\",\n      \"page_title\": \"備份\",\n      \"page_description\": \"自動建立和管理你的書籤備份。備份會經過壓縮並安全地儲存。\",\n      \"configuration\": {\n        \"title\": \"備份設定\",\n        \"enable_automatic_backups\": \"啟用自動備份\",\n        \"enable_automatic_backups_description\": \"自動建立你的書籤備份\",\n        \"backup_frequency\": \"備份頻率\",\n        \"backup_frequency_description\": \"應該多久建立一次備份\",\n        \"retention_period\": \"保留期限（天）\",\n        \"retention_period_description\": \"備份在刪除前應保留多少天\",\n        \"frequency\": {\n          \"daily\": \"每日\",\n          \"weekly\": \"每週\"\n        },\n        \"select_frequency\": \"選取頻率\",\n        \"save_settings\": \"儲存設定\"\n      },\n      \"list\": {\n        \"title\": \"你的備份\",\n        \"create_backup_now\": \"立即建立備份\",\n        \"no_backups\": \"你還沒有任何備份。啟用自動備份或手動建立一個。\",\n        \"table\": {\n          \"created_at\": \"建立於\",\n          \"bookmarks\": \"書籤\",\n          \"size\": \"大小\",\n          \"status\": \"狀態\",\n          \"actions\": \"動作\"\n        },\n        \"status\": {\n          \"success\": \"成功\",\n          \"failed\": \"失敗\",\n          \"pending\": \"等待中\"\n        },\n        \"actions\": {\n          \"download_backup\": \"下載備份\",\n          \"delete_backup\": \"刪除備份\"\n        }\n      },\n      \"dialogs\": {\n        \"delete_backup_title\": \"刪除備份？\",\n        \"delete_backup_description\": \"你確定要刪除這個備份嗎？這個動作無法還原。\"\n      },\n      \"toasts\": {\n        \"backup_queued\": \"備份工作已排入佇列！它會很快處理。\",\n        \"backup_deleted\": \"備份已刪除！\"\n      }\n    }\n  },\n  \"admin\": {\n    \"admin_settings\": \"管理員設定\",\n    \"server_stats\": {\n      \"server_stats\": \"伺服器統計資訊\",\n      \"total_users\": \"使用者總數\",\n      \"total_bookmarks\": \"書籤總數\",\n      \"server_version\": \"伺服器版本\"\n    },\n    \"background_jobs\": {\n      \"background_jobs\": \"背景作業\",\n      \"crawler_jobs\": \"網頁抓取作業\",\n      \"indexing_jobs\": \"索引作業\",\n      \"inference_jobs\": \"推論作業\",\n      \"tidy_assets_jobs\": \"資源整理作業\",\n      \"job\": \"作業\",\n      \"queued\": \"已排入佇列\",\n      \"pending\": \"等待中\",\n      \"failed\": \"失敗\",\n      \"video_jobs\": \"影片下載工作\",\n      \"feed_jobs\": \"RSS 訂閱工作\",\n      \"webhook_jobs\": \"Webhook 工作\",\n      \"asset_preprocessing_jobs\": \"資產預處理工作\",\n      \"jobs\": {\n        \"crawler\": {\n          \"title\": \"爬蟲程式任務\",\n          \"description\": \"從網址提取網頁內容並進行網頁爬蟲\"\n        },\n        \"inference\": {\n          \"title\": \"推論任務\",\n          \"description\": \"AI驅動的內容標記和摘要\"\n        },\n        \"indexing\": {\n          \"title\": \"索引任務\",\n          \"description\": \"搜尋索引更新\"\n        },\n        \"asset_preprocessing\": {\n          \"title\": \"資產預處理任務\",\n          \"description\": \"圖片和文件預處理（螢幕截圖、文字提取等等）\"\n        },\n        \"tidy_assets\": {\n          \"title\": \"整理資產任務\",\n          \"description\": \"資產清理和儲存最佳化\"\n        },\n        \"video\": {\n          \"title\": \"影片下載任務\",\n          \"description\": \"影片提取和下載\"\n        },\n        \"webhook\": {\n          \"title\": \"Webhook 任務\",\n          \"description\": \"外部 Webhook 通知\"\n        },\n        \"feed\": {\n          \"title\": \"RSS Feed 任務\",\n          \"description\": \"RSS feed 處理和內容更新\"\n        },\n        \"admin_maintenance\": {\n          \"title\": \"管理員維護工作\",\n          \"description\": \"管理清理與資產維護\"\n        }\n      },\n      \"monitor_and_manage\": \"監控和管理背景任務佇列和系統處理任務\",\n      \"active\": \"啟用中\",\n      \"available_actions\": \"可用動作\",\n      \"status\": {\n        \"title\": \"了解任務狀態\",\n        \"queued\": {\n          \"title\": \"已排隊\",\n          \"description\": \"正在排隊等候處理的任務。當資源可用時，它們將自動啟動。\"\n        },\n        \"unprocessed\": {\n          \"title\": \"未處理\",\n          \"description\": \"尚未處理的書籤。它們很可能已經排隊等待處理，如果沒有，你可能需要手動重新排隊。\"\n        },\n        \"failed\": {\n          \"title\": \"失敗\",\n          \"description\": \"處理過程中遇到錯誤的書籤。這些可能需要手動處理。\"\n        }\n      },\n      \"actions\": {\n        \"recrawl_failed_links_only\": \"僅重新爬取失敗連結\",\n        \"recrawl_all_links\": \"重新爬取所有連結\",\n        \"without_inference\": \"不推論\",\n        \"regenerate_ai_tags_for_failed_bookmarks_only\": \"僅為失敗的書籤重新產生 AI 標籤\",\n        \"regenerate_ai_tags_for_all_bookmarks\": \"為所有書籤重新產生 AI 標籤\",\n        \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"僅為失敗的書籤重新產生 AI 摘要\",\n        \"regenerate_ai_summaries_for_all_bookmarks\": \"為所有書籤重新產生 AI 摘要\",\n        \"reindex_all_bookmarks\": \"重新索引所有書籤\",\n        \"clean_assets\": \"清除懸置資產並重新同步中繼資料\",\n        \"reprocess_assets_fix_mode\": \"重新處理未處理的資產\",\n        \"migrate_large_link_html_content\": \"將大型內嵌 HTML 內容移至資產\",\n        \"recrawl_pending_links_only\": \"僅重新檢索待處理的連結\",\n        \"regenerate_ai_tags_for_pending_bookmarks_only\": \"僅針對待處理的書籤重新產生 AI 標籤\",\n        \"regenerate_ai_summaries_for_pending_bookmarks_only\": \"僅針對待處理的書籤重新產生 AI 摘要\"\n      }\n    },\n    \"actions\": {\n      \"recrawl_failed_links_only\": \"僅重新抓取失敗的連結\",\n      \"recrawl_all_links\": \"重新抓取所有連結\",\n      \"without_inference\": \"不進行推論\",\n      \"regenerate_ai_tags_for_failed_bookmarks_only\": \"僅重新產生失敗書籤的 AI 標籤\",\n      \"regenerate_ai_tags_for_all_bookmarks\": \"重新產生所有書籤的 AI 標籤\",\n      \"reindex_all_bookmarks\": \"重新索引所有書籤\",\n      \"compact_assets\": \"壓縮資源\",\n      \"reprocess_assets_fix_mode\": \"重新處理資源(固定模式)\",\n      \"regenerate_ai_summaries_for_all_bookmarks\": \"為所有書籤重新產生 AI 摘要\",\n      \"regenerate_ai_summaries_for_failed_bookmarks_only\": \"僅為失敗的書籤重新產生 AI 摘要\"\n    },\n    \"users_list\": {\n      \"users_list\": \"使用者清單\",\n      \"create_user\": \"建立使用者\",\n      \"change_role\": \"變更角色\",\n      \"reset_password\": \"重設密碼\",\n      \"delete_user\": \"刪除使用者\",\n      \"num_bookmarks\": \"書籤數量\",\n      \"asset_sizes\": \"資源大小\",\n      \"local_user\": \"本機使用者\",\n      \"confirm_password\": \"確認密碼\",\n      \"delete_user_confirm_description\": \"你確定要刪除使用者「{{name}}」嗎？\",\n      \"unlimited\": \"無限制\"\n    },\n    \"service_connections\": {\n      \"title\": \"服務連線\",\n      \"description\": \"監控外部系統依賴的健康狀態和連線能力\",\n      \"search_engine\": \"搜尋引擎\",\n      \"browser\": \"瀏覽器\",\n      \"queue_system\": \"佇列系統\",\n      \"status\": {\n        \"not_configured\": \"未配置\",\n        \"connected\": \"已連線\",\n        \"disconnected\": \"已中斷連線\"\n      }\n    },\n    \"admin_tools\": {\n      \"admin_tools\": \"管理員工具\",\n      \"bookmark_debugger\": \"書籤偵錯工具\",\n      \"bookmark_id\": \"書籤 ID\",\n      \"bookmark_id_placeholder\": \"輸入書籤 ID\",\n      \"lookup\": \"查詢\",\n      \"debug_info\": \"偵錯資訊\",\n      \"basic_info\": \"基本資訊\",\n      \"status\": \"狀態\",\n      \"content\": \"內容\",\n      \"html_preview\": \"HTML 預覽（前 1000 個字元）\",\n      \"summary\": \"摘要\",\n      \"url\": \"網址\",\n      \"source_url\": \"來源網址\",\n      \"asset_type\": \"資產類型\",\n      \"file_name\": \"檔案名稱\",\n      \"owner_user_id\": \"擁有者使用者 ID\",\n      \"tagging_status\": \"標籤狀態\",\n      \"summarization_status\": \"摘要狀態\",\n      \"crawl_status\": \"爬網狀態\",\n      \"crawl_status_code\": \"HTTP 狀態代碼\",\n      \"crawled_at\": \"已爬取時間\",\n      \"recrawl\": \"重新爬取\",\n      \"reindex\": \"重新索引\",\n      \"retag\": \"重新標記\",\n      \"resummarize\": \"重新摘要\",\n      \"bookmark_not_found\": \"找不到書籤\",\n      \"action_success\": \"動作已成功完成\",\n      \"action_failed\": \"動作失敗\",\n      \"recrawl_queued\": \"重新爬取作業已排入佇列\",\n      \"reindex_queued\": \"重新索引作業已排入佇列\",\n      \"retag_queued\": \"重新標記作業已排入佇列\",\n      \"resummarize_queued\": \"重新摘要作業已排入佇列\",\n      \"view\": \"檢視\",\n      \"fetch_error\": \"擷取書籤時發生錯誤\"\n    }\n  },\n  \"options\": {\n    \"dark_mode\": \"深色模式\",\n    \"light_mode\": \"淺色模式\",\n    \"apps_extensions\": \"應用程式和擴充功能\",\n    \"documentation\": \"文件\",\n    \"follow_us_on_x\": \"在 X 上追蹤我們\"\n  },\n  \"lists\": {\n    \"all_lists\": \"所有清單\",\n    \"favourites\": \"最愛\",\n    \"new_list\": \"新增清單\",\n    \"new_nested_list\": \"新增巢狀清單\",\n    \"search_query_help\": \"了解更多關於搜尋查詢語言的資訊。\",\n    \"edit_list\": \"編輯清單\",\n    \"no_parent\": \"無上層\",\n    \"list_type\": \"清單類型\",\n    \"manual_list\": \"手動清單\",\n    \"smart_list\": \"智慧清單\",\n    \"search_query\": \"搜尋查詢\",\n    \"parent_list\": \"上層清單\",\n    \"description\": \"描述（可選）\",\n    \"merge_list\": \"合併清單\",\n    \"destination_list\": \"目標清單\",\n    \"delete_after_merge\": \"合併後刪除原始清單\",\n    \"no_destination\": \"無目標\",\n    \"rss\": {\n      \"description\": \"為此清單啟用 RSS 訂閱\",\n      \"feed_url\": \"RSS 訂閱網址\",\n      \"title\": \"RSS 訂閱\"\n    },\n    \"share_list\": \"分享清單\",\n    \"public_list\": {\n      \"title\": \"公開清單\",\n      \"description\": \"允許其他人檢視此清單\",\n      \"share_link\": \"分享連結\"\n    },\n    \"delete_list\": {\n      \"title\": \"刪除清單\",\n      \"description\": \"刪除清單唔會刪除個清單入面嘅任何書籤。\",\n      \"delete_children\": \"刪除子清單（遞迴）\",\n      \"delete_children_description\": \"如果唔剔選，所有直接嘅子清單會變成根清單\"\n    },\n    \"shared\": \"已共用\",\n    \"collaborators\": {\n      \"manage\": \"管理協作者\",\n      \"view\": \"檢視協作者\",\n      \"collaborators\": \"協作者\",\n      \"add\": \"新增協作者\",\n      \"current\": \"目前的協作者\",\n      \"enter_email\": \"輸入電子郵件地址\",\n      \"please_enter_email\": \"請輸入電子郵件地址\",\n      \"added_successfully\": \"已成功新增協作者\",\n      \"failed_to_add\": \"新增協作者失敗\",\n      \"removed\": \"已移除協作者\",\n      \"failed_to_remove\": \"移除協作者失敗\",\n      \"role_updated\": \"已更新角色\",\n      \"failed_to_update_role\": \"更新角色失敗\",\n      \"viewer\": \"檢視者\",\n      \"editor\": \"編輯者\",\n      \"owner\": \"擁有者\",\n      \"viewer_description\": \"可以在清單中檢視書籤\",\n      \"editor_description\": \"可以新增和移除書籤\",\n      \"no_collaborators\": \"目前還沒有協作者。新增一位來開始協作吧！\",\n      \"no_collaborators_readonly\": \"沒有此清單的協作者。\",\n      \"people_with_access\": \"可以存取此清單的人員\",\n      \"add_or_remove\": \"新增或移除可存取此清單的人員\",\n      \"invitation_sent\": \"邀請已成功送出\",\n      \"invitation_revoked\": \"邀請已撤銷\",\n      \"failed_to_revoke\": \"撤銷邀請失敗\",\n      \"pending\": \"等待中\",\n      \"revoke\": \"撤銷\",\n      \"declined\": \"已拒絕\"\n    },\n    \"leave_list\": {\n      \"title\": \"離開清單\",\n      \"confirm_message\": \"您確定要離開 {{icon}} {{name}} 嗎？\",\n      \"warning\": \"您將無法再檢視或存取此清單中的書籤。清單擁有者可以隨時將您重新加入。\",\n      \"action\": \"離開清單\",\n      \"success\": \"您已離開「{{icon}} {{name}}」\"\n    },\n    \"invitations\": {\n      \"pending\": \"等待中的邀請\",\n      \"description\": \"檢閱及回覆清單協作邀請\",\n      \"invited_by\": \"邀請人\",\n      \"accept\": \"接受\",\n      \"decline\": \"拒絕\",\n      \"accepted\": \"邀請已接受\",\n      \"declined\": \"邀請已拒絕\",\n      \"failed_to_accept\": \"接受邀請失敗\",\n      \"failed_to_decline\": \"拒絕邀請失敗\"\n    },\n    \"shared_lists\": \"共用清單\"\n  },\n  \"tags\": {\n    \"all_tags\": \"所有標籤\",\n    \"your_tags\": \"您的標籤\",\n    \"your_tags_info\": \"您至少使用過一次的標籤\",\n    \"ai_tags\": \"AI 標籤\",\n    \"ai_tags_info\": \"僅由 AI 自動加入的標籤\",\n    \"unused_tags\": \"未使用的標籤\",\n    \"unused_tags_info\": \"未附加至任何書籤的標籤\",\n    \"delete_all_unused_tags\": \"刪除所有未使用的標籤\",\n    \"drag_and_drop_merging\": \"拖曳合併\",\n    \"drag_and_drop_merging_info\": \"將標籤拖曳到其他標籤上即可合併\",\n    \"sort_by_name\": \"依名稱排序\",\n    \"create_tag\": \"建立標籤\",\n    \"create_tag_description\": \"建立新標籤，但不要連接到任何書籤\",\n    \"tag_name\": \"標籤名稱\",\n    \"enter_tag_name\": \"輸入標籤名稱\",\n    \"sort_by_usage\": \"依照使用次數排序\",\n    \"sort_by_relevance\": \"依照關聯性排序\",\n    \"no_custom_tags\": \"還沒有自訂標籤\",\n    \"no_ai_tags\": \"還沒有 AI 標籤\",\n    \"no_unused_tags\": \"您沒有任何未使用的標籤\",\n    \"no_unused_tags_match_your_search\": \"沒有符合您搜尋的未使用標籤\",\n    \"no_tags_match_your_search\": \"沒有符合您搜尋的標籤\",\n    \"search_placeholder\": \"搜尋標籤...\",\n    \"search_or_create_placeholder\": \"搜尋或建立標籤...\"\n  },\n  \"preview\": {\n    \"view_original\": \"檢視原始內容\",\n    \"cached_content\": \"快取內容\",\n    \"reader_view\": \"閱讀器檢視\",\n    \"tabs\": {\n      \"content\": \"內容\",\n      \"details\": \"詳細資訊\"\n    },\n    \"archive_info\": \"如果封存檔需要 Javascript，可能無法正確地內嵌呈現。為了獲得最佳效果，<1>請下載並在瀏覽器中開啟</1>。\",\n    \"fetch_error_title\": \"內容無法使用\",\n    \"fetch_error_description\": \"我們無法取得此連結的內容。此頁面可能受到保護、需要驗證或暫時無法使用。\",\n    \"crawling_in_progress\": \"正在抓取頁面內容…\",\n    \"continue_reading\": \"從上次中斷的地方繼續\",\n    \"continue_reading_percent\": \"從上次中斷的地方繼續（{{percent}}%）\",\n    \"continue_button\": \"繼續\"\n  },\n  \"editor\": {\n    \"quickly_focus\": \"您可以按下 ⌘ + E 快速聚焦此欄位\",\n    \"multiple_urls_dialog_title\": \"要將多個網址匯入為個別的書籤嗎？\",\n    \"multiple_urls_dialog_desc\": \"輸入內容包含分別位於不同行數的多個網址。您是否要將它們匯入為個別的書籤？\",\n    \"import_as_text\": \"匯入為文字書籤\",\n    \"import_as_separate_bookmarks\": \"匯入為個別書籤\",\n    \"placeholder\": \"貼上連結、圖片、撰寫筆記，或將圖片拖曳至此處…\",\n    \"new_item\": \"新增項目\",\n    \"disabled_submissions\": \"提交功能已停用\",\n    \"text_toolbar\": {\n      \"align_center\": \"置中對齊\",\n      \"align_right\": \"靠右對齊\",\n      \"markdown_shortcuts\": {\n        \"label\": \"Markdown 快捷鍵\",\n        \"bold\": {\n          \"example\": \"**text** 或 CTRL+b\",\n          \"label\": \"粗體\"\n        },\n        \"italic\": {\n          \"label\": \"斜體\",\n          \"example\": \"*Italic* 或 _Italic_ 或 CTRL+i\"\n        },\n        \"ordered_list\": {\n          \"label\": \"有序清單\",\n          \"example\": \"1. 清單項目\"\n        },\n        \"inline_code\": {\n          \"example\": \"`程式碼`\",\n          \"label\": \"行內程式碼\"\n        },\n        \"block_code\": {\n          \"label\": \"程式碼區塊\",\n          \"example\": \"``` + 空白鍵\"\n        },\n        \"blockquote\": {\n          \"example\": \"> 引用區塊\",\n          \"label\": \"引用區塊\"\n        },\n        \"unordered_list\": {\n          \"label\": \"無序清單\",\n          \"example\": \"- 清單項目\"\n        },\n        \"heading\": {\n          \"label\": \"標題\",\n          \"example\": \"# H1, ## H2, ### H3\"\n        }\n      },\n      \"undo\": \"復原\",\n      \"redo\": \"重做\",\n      \"bold\": \"粗體\",\n      \"italic\": \"斜體\",\n      \"underline\": \"底線\",\n      \"strikethrough\": \"刪除線\",\n      \"code\": \"程式碼\",\n      \"highlight\": \"醒目標示\",\n      \"align_left\": \"靠左對齊\"\n    },\n    \"placeholder_v2\": \"貼上連結、寫筆記或放入圖片…\"\n  },\n  \"toasts\": {\n    \"bookmarks\": {\n      \"updated\": \"書籤已更新！\",\n      \"deleted\": \"書籤已刪除！\",\n      \"refetch\": \"已將重新抓取加入佇列！\",\n      \"full_page_archive\": \"已觸發完整網頁封存建立\",\n      \"delete_from_list\": \"已從清單中移除書籤\",\n      \"clipboard_copied\": \"連結已複製到剪貼簿！\",\n      \"preserve_pdf\": \"已觸發 PDF 儲存\",\n      \"update_banner\": \"橫幅已更新！\",\n      \"uploading_banner\": \"正在上傳橫幅...\"\n    },\n    \"lists\": {\n      \"created\": \"清單已建立！\",\n      \"updated\": \"清單已更新！\",\n      \"merged\": \"清單已合併！\",\n      \"deleted\": \"清單已刪除！\"\n    },\n    \"tags\": {\n      \"created\": \"已建立標籤！\",\n      \"failed_to_create\": \"建立標籤失敗\"\n    }\n  },\n  \"cleanups\": {\n    \"cleanups\": \"清理\",\n    \"duplicate_tags\": {\n      \"title\": \"重複的標籤\",\n      \"merge_all_suggestions\": \"要合併所有建議嗎？\"\n    }\n  },\n  \"search\": {\n    \"is_not_favorited\": \"未加入最愛\",\n    \"is_archived\": \"已封存\",\n    \"is_not_archived\": \"未封存\",\n    \"has_any_tag\": \"有任何標籤\",\n    \"does_not_have_tag\": \"無此標籤\",\n    \"type_is_not\": \"類型不為\",\n    \"is_favorited\": \"已加入最愛\",\n    \"has_no_tags\": \"無標籤\",\n    \"is_in_any_list\": \"在任何清單中\",\n    \"is_not_in_any_list\": \"不在任何清單中\",\n    \"created_on_or_after\": \"建立於此日期或之後\",\n    \"not_created_on_or_after\": \"未建立於此日期或之後\",\n    \"full_text_search\": \"全文搜尋\",\n    \"and\": \"且\",\n    \"or\": \"或\",\n    \"created_on_or_before\": \"建立於此日期或之前\",\n    \"not_created_on_or_before\": \"未建立於此日期或之前\",\n    \"url_contains\": \"網址包含\",\n    \"url_does_not_contain\": \"網址不包含\",\n    \"has_tag\": \"有標籤\",\n    \"is_in_list\": \"在清單中\",\n    \"is_not_in_list\": \"不在清單中\",\n    \"type_is\": \"類型為\",\n    \"is_from_feed\": \"來自 RSS 訂閱\",\n    \"is_not_from_feed\": \"不是來自 RSS 訂閱\",\n    \"created_within\": \"建立於\",\n    \"created_earlier_than\": \"建立於早於\",\n    \"day_s\": \" 幾天\",\n    \"week_s\": \" 幾週\",\n    \"month_s\": \" 幾個月\",\n    \"year_s\": \" 幾年\",\n    \"day_s_ago\": \" 幾天前\",\n    \"week_s_ago\": \" 幾週前\",\n    \"month_s_ago\": \" 幾個月前\",\n    \"year_s_ago\": \" 幾年前\",\n    \"history\": \"近期搜尋\",\n    \"title_contains\": \"標題包含\",\n    \"title_does_not_contain\": \"標題不包含\",\n    \"is_broken_link\": \"連結已損毀\",\n    \"tags\": \"標籤\",\n    \"no_suggestions\": \"沒有任何建議\",\n    \"filters\": \"篩選器\",\n    \"is_not_broken_link\": \"擁有可用的連結\",\n    \"lists\": \"清單\",\n    \"feeds\": \"動態饋給\",\n    \"is_from_source\": \"來源是\",\n    \"is_not_from_source\": \"來源不是\"\n  },\n  \"dialogs\": {\n    \"bookmarks\": {\n      \"delete_confirmation_title\": \"刪除書籤？\",\n      \"delete_confirmation_description\": \"您確定要刪除這個書籤嗎？\"\n    }\n  },\n  \"highlights\": {\n    \"no_highlights\": \"您目前尚未有任何標記重點。\"\n  },\n  \"bookmark_editor\": {\n    \"date_published\": \"發佈日期\",\n    \"title\": \"編輯書籤\",\n    \"subtitle\": \"變更書籤詳細資訊。完成後點擊儲存。\",\n    \"author\": \"作者\",\n    \"publisher\": \"發佈者\",\n    \"pick_a_date\": \"選擇日期\",\n    \"save_changes\": \"儲存變更\",\n    \"extracted_content\": \"已擷取內容\"\n  },\n  \"banners\": {\n    \"no_bookmarks\": {\n      \"title\": \"還沒有書籤\",\n      \"description\": \"儲存有趣的的文章、連結和頁面，以便日後快速存取。\"\n    }\n  },\n  \"view_options\": {\n    \"title\": \"檢視選項\",\n    \"layout\": \"版面配置\",\n    \"columns\": \"欄位\",\n    \"display_options\": \"顯示選項\",\n    \"show_note_previews\": \"顯示註解\",\n    \"show_tags\": \"顯示標籤\",\n    \"show_title\": \"顯示標題\",\n    \"image_options\": \"圖片選項\",\n    \"image_fit_cover\": \"封面（填滿）\",\n    \"image_fit_contain\": \"包含（符合）\"\n  },\n  \"version\": {\n    \"new_release_available\": \"有新的版本更新說明啦\",\n    \"whats_new_title\": \"v{{version}} 有啥新東西？\",\n    \"release_notes_description\": \"以下是從 GitHub 版本更新說明抓取的最新更新。\",\n    \"loading_release_notes\": \"載入版本更新說明中…\",\n    \"unable_to_load_release_notes\": \"現在沒辦法載入版本更新說明啦。晚點再試試看。\",\n    \"no_release_notes\": \"這個版本沒有發佈版本更新說明。\",\n    \"release_notes_synced\": \"版本更新說明是從 GitHub 同步更新的。\",\n    \"view_on_github\": \"在 GitHub 上檢視\"\n  },\n  \"wrapped\": {\n    \"title\": \"您的 {{year}} 年度回顧\",\n    \"subtitle\": \"Karakeep 年度大事記\",\n    \"banner\": {\n      \"title\": \"您的 2025 年度回顧已準備就緒！\",\n      \"description\": \"看看你這年的書籤\",\n      \"view_now\": \"立即檢視\"\n    },\n    \"button\": \"2025 年度回顧\",\n    \"loading\": \"正在載入您的年度回顧...\",\n    \"failed_to_load\": \"載入您的年度回顧資料失敗\",\n    \"sections\": {\n      \"total_saves\": {\n        \"prefix\": \"您已儲存\",\n        \"suffix\": \"今年儲存的項目數\",\n        \"suffix_singular\": \"今年儲存的項目\"\n      },\n      \"first_bookmark\": {\n        \"title\": \"你的旅程開始了\",\n        \"description\": \"{{year}} 年首次儲存：\"\n      },\n      \"top_domains\": \"你的熱門網站\",\n      \"top_tags\": \"你的熱門標籤\",\n      \"monthly_activity\": \"你這一年儲存的內容\",\n      \"most_active_day\": \"你最活躍的一天\",\n      \"peak_times\": {\n        \"title\": \"你何時儲存\",\n        \"peak_hour\": \"尖峰時段\",\n        \"peak_day\": \"尖峰日\"\n      },\n      \"how_you_save\": \"你如何儲存\",\n      \"what_you_saved\": \"你儲存了什麼\",\n      \"summary\": {\n        \"favorites\": \"最愛\",\n        \"tags_created\": \"已建立的標籤\",\n        \"highlights\": \"精選\"\n      },\n      \"types\": {\n        \"links\": \"連結\",\n        \"notes\": \"筆記\",\n        \"assets\": \"資產\"\n      }\n    },\n    \"footer\": \"使用 Karakeep 製作\",\n    \"share\": \"分享\",\n    \"download\": \"下載\",\n    \"close\": \"關閉\",\n    \"generating\": \"正在產生...\"\n  }\n}\n"
  },
  {
    "path": "apps/web/lib/i18n/provider.tsx",
    "content": "import { i18n } from \"@/lib/i18n/client\";\nimport { I18nextProvider } from \"react-i18next\";\n\nconst CustomI18nextProvider = ({\n  lang,\n  children,\n}: {\n  lang: string;\n  children: React.ReactNode;\n}) => {\n  if (i18n.language !== lang) {\n    i18n.changeLanguage(lang);\n  }\n\n  return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;\n};\n\nexport default CustomI18nextProvider;\n"
  },
  {
    "path": "apps/web/lib/i18n/server.ts",
    "content": "import { getUserLocalSettings } from \"@/lib/userLocalSettings/userLocalSettings\";\nimport { createInstance, FlatNamespace, KeyPrefix } from \"i18next\";\nimport resourcesToBackend from \"i18next-resources-to-backend\";\nimport { FallbackNs } from \"react-i18next\";\nimport { initReactI18next } from \"react-i18next/initReactI18next\";\n\nimport { getOptions } from \"./settings\";\n\nconst initI18next = async (lng: string, ns: string | string[]) => {\n  const i18nInstance = createInstance();\n  await i18nInstance\n    .use(initReactI18next)\n    .use(\n      resourcesToBackend(\n        (language: string, namespace: string) =>\n          import(`./locales/${language}/${namespace}.json`),\n      ),\n    )\n    .init(getOptions(lng, ns?.toString()));\n  return i18nInstance;\n};\n\nexport async function useTranslation<\n  Ns extends FlatNamespace,\n  KPrefix extends KeyPrefix<FallbackNs<Ns>> = undefined,\n>(ns?: Ns, options: { keyPrefix?: KPrefix } = {}) {\n  const lng = (await getUserLocalSettings()).lang;\n  const i18nextInstance = await initI18next(\n    lng,\n    Array.isArray(ns) ? (ns as string[]) : (ns as string),\n  );\n  return {\n    t: i18nextInstance.getFixedT(lng, ns as FlatNamespace, options.keyPrefix),\n    i18n: i18nextInstance,\n  };\n}\n"
  },
  {
    "path": "apps/web/lib/i18n/settings.ts",
    "content": "import { supportedLangs } from \"@karakeep/shared/langs\";\n\nexport const fallbackLng = \"en\";\nexport const languages = supportedLangs;\nexport const defaultNS = \"translation\";\nexport const cookieName = \"i18next\";\n\nexport function getOptions(lng: string = fallbackLng, ns: string = defaultNS) {\n  return {\n    supportedLngs: languages,\n    fallbackLng,\n    lng,\n    fallbackNS: defaultNS,\n    defaultNS,\n    ns,\n  };\n}\n"
  },
  {
    "path": "apps/web/lib/providers.tsx",
    "content": "\"use client\";\n\nimport type { UserLocalSettings } from \"@/lib/userLocalSettings/types\";\nimport React, { useState } from \"react\";\nimport { ThemeProvider } from \"@/components/theme-provider\";\nimport { TooltipProvider } from \"@/components/ui/tooltip\";\nimport { Session, SessionProvider } from \"@/lib/auth/client\";\nimport { UserLocalSettingsCtx } from \"@/lib/userLocalSettings/bookmarksLayout\";\nimport { QueryClient, QueryClientProvider } from \"@tanstack/react-query\";\nimport { createTRPCClient, httpBatchLink, loggerLink } from \"@trpc/client\";\nimport superjson from \"superjson\";\n\nimport type { ClientConfig } from \"@karakeep/shared/config\";\nimport type { AppRouter } from \"@karakeep/trpc/routers/_app\";\nimport {\n  TRPC_MAX_URL_LENGTH_INTERNAL,\n  TRPCProvider,\n} from \"@karakeep/shared-react/trpc\";\n\nimport { ClientConfigCtx } from \"./clientConfig\";\nimport CustomI18nextProvider from \"./i18n/provider\";\n\nfunction makeQueryClient() {\n  return new QueryClient({\n    defaultOptions: {\n      queries: {\n        // With SSR, we usually want to set some default staleTime\n        // above 0 to avoid refetching immediately on the client\n        staleTime: 60 * 1000,\n      },\n    },\n  });\n}\n\nlet browserQueryClient: QueryClient | undefined = undefined;\n\nfunction getQueryClient() {\n  if (typeof window === \"undefined\") {\n    // Server: always make a new query client\n    return makeQueryClient();\n  } else {\n    // Browser: make a new query client if we don't already have one\n    // This is very important so we don't re-make a new client if React\n    // supsends during the initial render. This may not be needed if we\n    // have a suspense boundary BELOW the creation of the query client\n    if (!browserQueryClient) browserQueryClient = makeQueryClient();\n    return browserQueryClient;\n  }\n}\n\nexport default function Providers({\n  children,\n  session,\n  clientConfig,\n  userLocalSettings,\n}: {\n  children: React.ReactNode;\n  session: Session | null;\n  clientConfig: ClientConfig;\n  userLocalSettings: UserLocalSettings;\n}) {\n  const queryClient = getQueryClient();\n\n  const [trpcClient] = useState(() =>\n    createTRPCClient<AppRouter>({\n      links: [\n        loggerLink({\n          enabled: (op) =>\n            process.env.NODE_ENV === \"development\" ||\n            (op.direction === \"down\" && op.result instanceof Error),\n        }),\n        httpBatchLink({\n          // TODO: Change this to be a full URL exposed as a client side setting\n          url: `/api/trpc`,\n          maxURLLength: TRPC_MAX_URL_LENGTH_INTERNAL,\n          transformer: superjson,\n        }),\n      ],\n    }),\n  );\n\n  return (\n    <ClientConfigCtx.Provider value={clientConfig}>\n      <UserLocalSettingsCtx.Provider value={userLocalSettings}>\n        <SessionProvider session={session}>\n          <QueryClientProvider client={queryClient}>\n            <TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>\n              <CustomI18nextProvider lang={userLocalSettings.lang}>\n                <ThemeProvider\n                  attribute=\"class\"\n                  defaultTheme=\"system\"\n                  enableSystem\n                  disableTransitionOnChange\n                >\n                  <TooltipProvider delayDuration={0}>\n                    {children}\n                  </TooltipProvider>\n                </ThemeProvider>\n              </CustomI18nextProvider>\n            </TRPCProvider>\n          </QueryClientProvider>\n        </SessionProvider>\n      </UserLocalSettingsCtx.Provider>\n    </ClientConfigCtx.Provider>\n  );\n}\n"
  },
  {
    "path": "apps/web/lib/readerSettings.tsx",
    "content": "\"use client\";\n\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useMemo,\n  useState,\n} from \"react\";\n\nimport {\n  ReaderSettingsProvider as BaseReaderSettingsProvider,\n  useReaderSettingsContext,\n} from \"@karakeep/shared-react/hooks/reader-settings\";\nimport {\n  ReaderSettings,\n  ReaderSettingsPartial,\n} from \"@karakeep/shared/types/readers\";\n\nconst LOCAL_STORAGE_KEY = \"karakeep-reader-settings\";\n\nfunction getLocalOverridesFromStorage(): ReaderSettingsPartial {\n  if (typeof window === \"undefined\") return {};\n  try {\n    const stored = localStorage.getItem(LOCAL_STORAGE_KEY);\n    return stored ? JSON.parse(stored) : {};\n  } catch {\n    return {};\n  }\n}\n\nfunction saveLocalOverridesToStorage(overrides: ReaderSettingsPartial): void {\n  if (typeof window === \"undefined\") return;\n  localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(overrides));\n}\n\n// Session overrides context - web-specific feature for live preview\ninterface SessionOverridesContextValue {\n  sessionOverrides: ReaderSettingsPartial;\n  setSessionOverrides: React.Dispatch<\n    React.SetStateAction<ReaderSettingsPartial>\n  >;\n}\n\nconst SessionOverridesContext =\n  createContext<SessionOverridesContextValue | null>(null);\n\nexport function ReaderSettingsProvider({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  const [sessionOverrides, setSessionOverrides] =\n    useState<ReaderSettingsPartial>({});\n\n  const sessionValue = useMemo(\n    () => ({\n      sessionOverrides,\n      setSessionOverrides,\n    }),\n    [sessionOverrides],\n  );\n\n  // Memoize callbacks to prevent unnecessary re-renders\n  const getLocalOverrides = useCallback(getLocalOverridesFromStorage, []);\n  const saveLocalOverrides = useCallback(saveLocalOverridesToStorage, []);\n  const onClearSessionOverrides = useCallback(() => {\n    setSessionOverrides({});\n  }, []);\n\n  return (\n    <BaseReaderSettingsProvider\n      getLocalOverrides={getLocalOverrides}\n      saveLocalOverrides={saveLocalOverrides}\n      sessionOverrides={sessionOverrides}\n      onClearSessionOverrides={onClearSessionOverrides}\n    >\n      <SessionOverridesContext.Provider value={sessionValue}>\n        {children}\n      </SessionOverridesContext.Provider>\n    </BaseReaderSettingsProvider>\n  );\n}\n\nexport function useReaderSettings() {\n  const sessionContext = useContext(SessionOverridesContext);\n  if (!sessionContext) {\n    throw new Error(\n      \"useReaderSettings must be used within a ReaderSettingsProvider\",\n    );\n  }\n\n  const { sessionOverrides, setSessionOverrides } = sessionContext;\n  const baseSettings = useReaderSettingsContext();\n\n  // Update session override (live preview, not persisted)\n  const updateSession = useCallback(\n    (updates: ReaderSettingsPartial) => {\n      setSessionOverrides((prev) => ({ ...prev, ...updates }));\n    },\n    [setSessionOverrides],\n  );\n\n  // Clear all session overrides\n  const clearSession = useCallback(() => {\n    setSessionOverrides({});\n  }, [setSessionOverrides]);\n\n  // Save current settings to local storage (this device only)\n  const saveToDevice = useCallback(() => {\n    const newLocalOverrides = {\n      ...baseSettings.localOverrides,\n      ...sessionOverrides,\n    };\n    baseSettings.setLocalOverrides(newLocalOverrides);\n    saveLocalOverridesToStorage(newLocalOverrides);\n    setSessionOverrides({});\n  }, [baseSettings, sessionOverrides, setSessionOverrides]);\n\n  // Clear a single local override\n  const clearLocalOverride = useCallback(\n    (key: keyof ReaderSettings) => {\n      baseSettings.clearLocal(key);\n    },\n    [baseSettings],\n  );\n\n  // Check if there are unsaved session changes\n  const hasSessionChanges = Object.keys(sessionOverrides).length > 0;\n\n  return {\n    // Current effective settings (what should be displayed)\n    settings: baseSettings.settings,\n\n    // Raw values for UI indicators\n    serverSettings: baseSettings.serverDefaults,\n    localOverrides: baseSettings.localOverrides,\n    sessionOverrides,\n\n    // State indicators\n    hasSessionChanges,\n    hasLocalOverrides: baseSettings.hasLocalOverrides,\n    isSaving: baseSettings.isSaving,\n\n    // Actions\n    updateSession,\n    clearSession,\n    saveToDevice,\n    clearLocalOverrides: baseSettings.clearAllLocal,\n    clearLocalOverride,\n    saveToServer: baseSettings.saveAsDefault,\n    updateServerSetting: baseSettings.saveAsDefault,\n    clearServerDefaults: baseSettings.clearAllDefaults,\n  };\n}\n"
  },
  {
    "path": "apps/web/lib/store/useInBookmarkGridStore.ts",
    "content": "import { create } from \"zustand\";\n\ninterface InBookmarkGridState {\n  inBookmarkGrid: boolean;\n  setInBookmarkGrid: (inBookmarkGrid: boolean) => void;\n}\n\nexport const useInBookmarkGridStore = create<InBookmarkGridState>((set) => ({\n  inBookmarkGrid: false,\n  setInBookmarkGrid: (inBookmarkGrid) => set({ inBookmarkGrid }),\n}));\n"
  },
  {
    "path": "apps/web/lib/store/useInSearchPageStore.ts",
    "content": "import { create } from \"zustand\";\n\ninterface InSearchPageState {\n  inSearchPage: boolean;\n  setInSearchPage: (inSearchPage: boolean) => void;\n}\n\nexport const useInSearchPageStore = create<InSearchPageState>((set) => ({\n  inSearchPage: false,\n  setInSearchPage: (inSearchPage) => {\n    set({ inSearchPage });\n  },\n}));\n"
  },
  {
    "path": "apps/web/lib/store/useSortOrderStore.ts",
    "content": "import { create } from \"zustand\";\n\nimport { ZSortOrder } from \"@karakeep/shared/types/bookmarks\";\n\ninterface SortOrderState {\n  sortOrder: ZSortOrder;\n  setSortOrder: (sortOrder: ZSortOrder) => void;\n}\n\nexport const useSortOrderStore = create<SortOrderState>((set) => ({\n  sortOrder: \"desc\", // default sort order\n  setSortOrder: (sortOrder) => set({ sortOrder }),\n}));\n"
  },
  {
    "path": "apps/web/lib/userLocalSettings/bookmarksLayout.tsx",
    "content": "\"use client\";\n\nimport type { z } from \"zod\";\nimport { createContext, useContext } from \"react\";\nimport { fallbackLng } from \"@/lib/i18n/settings\";\n\nimport type { BookmarksLayoutTypes, zUserLocalSettings } from \"./types\";\n\nconst defaultLayout: BookmarksLayoutTypes = \"masonry\";\n\nexport const UserLocalSettingsCtx = createContext<\n  z.infer<typeof zUserLocalSettings>\n>({\n  bookmarkGridLayout: defaultLayout,\n  lang: fallbackLng,\n  gridColumns: 3,\n  showNotes: false,\n  showTags: true,\n  showTitle: true,\n  imageFit: \"cover\",\n});\n\nfunction useUserLocalSettings() {\n  return useContext(UserLocalSettingsCtx);\n}\n\nexport function useBookmarkDisplaySettings() {\n  const settings = useUserLocalSettings();\n  return {\n    showNotes: settings.showNotes,\n    showTags: settings.showTags,\n    showTitle: settings.showTitle,\n    imageFit: settings.imageFit,\n  };\n}\n\nexport function useBookmarkLayout() {\n  const settings = useUserLocalSettings();\n  return settings.bookmarkGridLayout;\n}\n\nexport function useInterfaceLang() {\n  const settings = useUserLocalSettings();\n  return settings.lang;\n}\n\nexport function useGridColumns() {\n  const settings = useUserLocalSettings();\n  return settings.gridColumns;\n}\n\nexport function bookmarkLayoutSwitch<T>(\n  layout: BookmarksLayoutTypes,\n  data: Record<BookmarksLayoutTypes, T>,\n) {\n  return data[layout];\n}\n\nexport function useBookmarkLayoutSwitch<T>(\n  data: Record<BookmarksLayoutTypes, T>,\n) {\n  const layout = useBookmarkLayout();\n  return data[layout];\n}\n"
  },
  {
    "path": "apps/web/lib/userLocalSettings/types.ts",
    "content": "import { z } from \"zod\";\n\nexport const USER_LOCAL_SETTINGS_COOKIE_NAME = \"hoarder-user-local-settings\";\n\nconst zBookmarkGridLayout = z.enum([\"grid\", \"list\", \"masonry\", \"compact\"]);\nexport type BookmarksLayoutTypes = z.infer<typeof zBookmarkGridLayout>;\n\nexport const zUserLocalSettings = z.object({\n  bookmarkGridLayout: zBookmarkGridLayout.optional().default(\"masonry\"),\n  lang: z.string().optional().default(\"en\"),\n  gridColumns: z.number().min(1).max(6).optional().default(3),\n  showNotes: z.boolean().optional().default(false),\n  showTags: z.boolean().optional().default(true),\n  showTitle: z.boolean().optional().default(true),\n  imageFit: z.enum([\"cover\", \"contain\"]).optional().default(\"cover\"),\n});\n\nexport type UserLocalSettings = z.infer<typeof zUserLocalSettings>;\n\nexport function parseUserLocalSettings(str: string | undefined) {\n  try {\n    return zUserLocalSettings.parse(JSON.parse(str ?? \"{}\"));\n  } catch {\n    return undefined;\n  }\n}\n\nexport function defaultUserLocalSettings() {\n  return zUserLocalSettings.parse({});\n}\n"
  },
  {
    "path": "apps/web/lib/userLocalSettings/userLocalSettings.ts",
    "content": "\"use server\";\n\nimport { cookies } from \"next/headers\";\n\nimport type { BookmarksLayoutTypes, UserLocalSettings } from \"./types\";\nimport {\n  defaultUserLocalSettings,\n  parseUserLocalSettings,\n  USER_LOCAL_SETTINGS_COOKIE_NAME,\n} from \"./types\";\n\nexport async function getUserLocalSettings(): Promise<UserLocalSettings> {\n  const userSettings = (await cookies()).get(USER_LOCAL_SETTINGS_COOKIE_NAME);\n  return (\n    parseUserLocalSettings(userSettings?.value) ?? defaultUserLocalSettings()\n  );\n}\n\nasync function readModifyWrite(\n  modifier: (settings: UserLocalSettings) => Partial<UserLocalSettings>,\n) {\n  const userSettings = (await cookies()).get(USER_LOCAL_SETTINGS_COOKIE_NAME);\n  const parsed =\n    parseUserLocalSettings(userSettings?.value) ?? defaultUserLocalSettings();\n  const updated = { ...parsed, ...modifier(parsed) };\n  (await cookies()).set({\n    name: USER_LOCAL_SETTINGS_COOKIE_NAME,\n    value: JSON.stringify(updated),\n    maxAge: 34560000, // Chrome caps max age to 400 days\n    sameSite: \"lax\",\n  });\n}\n\nexport async function updateBookmarksLayout(layout: BookmarksLayoutTypes) {\n  await readModifyWrite(() => ({ bookmarkGridLayout: layout }));\n}\n\nexport async function updateInterfaceLang(lang: string) {\n  await readModifyWrite(() => ({ lang }));\n}\n\nexport async function updateGridColumns(gridColumns: number) {\n  await readModifyWrite(() => ({ gridColumns }));\n}\n\nexport async function updateShowNotes(showNotes: boolean) {\n  await readModifyWrite(() => ({ showNotes }));\n}\n\nexport async function updateShowTags(showTags: boolean) {\n  await readModifyWrite(() => ({ showTags }));\n}\n\nexport async function updateShowTitle(showTitle: boolean) {\n  await readModifyWrite(() => ({ showTitle }));\n}\n\nexport async function updateImageFit(imageFit: \"cover\" | \"contain\") {\n  await readModifyWrite(() => ({ imageFit }));\n}\n"
  },
  {
    "path": "apps/web/lib/userSettings.tsx",
    "content": "\"use client\";\n\nimport { createContext, useContext } from \"react\";\nimport { useQuery } from \"@tanstack/react-query\";\n\nimport { useTRPC } from \"@karakeep/shared-react/trpc\";\nimport { ZUserSettings } from \"@karakeep/shared/types/users\";\n\nexport const UserSettingsContext = createContext<ZUserSettings>({\n  bookmarkClickAction: \"open_original_link\",\n  archiveDisplayBehaviour: \"show\",\n  timezone: \"UTC\",\n  backupsEnabled: false,\n  backupsFrequency: \"daily\",\n  backupsRetentionDays: 7,\n  readerFontSize: null,\n  readerLineHeight: null,\n  readerFontFamily: null,\n  autoTaggingEnabled: null,\n  autoSummarizationEnabled: null,\n  tagStyle: \"as-generated\",\n  curatedTagIds: null,\n  inferredTagLang: null,\n});\n\nexport function UserSettingsContextProvider({\n  userSettings,\n  children,\n}: {\n  userSettings: ZUserSettings;\n  children: React.ReactNode;\n}) {\n  const api = useTRPC();\n  const { data } = useQuery(\n    api.users.settings.queryOptions(undefined, {\n      initialData: userSettings,\n    }),\n  );\n\n  return (\n    <UserSettingsContext.Provider value={data}>\n      {children}\n    </UserSettingsContext.Provider>\n  );\n}\n\nexport function useUserSettings() {\n  return useContext(UserSettingsContext);\n}\n"
  },
  {
    "path": "apps/web/lib/utils.ts",
    "content": "import type { ClassValue } from \"clsx\";\nimport { clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n\nexport type OS = \"macos\" | \"ios\" | \"windows\" | \"android\" | \"linux\" | null;\n\nexport function formatBytes(bytes: number, decimals = 2) {\n  if (bytes === 0) return \"0 Bytes\";\n\n  const k = 1024;\n  const dm = decimals < 0 ? 0 : decimals;\n  const sizes = [\"Bytes\", \"KB\", \"MB\", \"GB\", \"TB\", \"PB\", \"EB\", \"ZB\", \"YB\"];\n\n  const i = Math.floor(Math.log(bytes) / Math.log(k));\n\n  return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + \" \" + sizes[i];\n}\n\nexport function getOS() {\n  if (typeof window === \"undefined\") return;\n  const userAgent = window.navigator.userAgent.toLowerCase();\n  const macosPlatforms = /(macintosh|macintel|macppc|mac68k|macos)/i;\n  const windowsPlatforms = /(win32|win64|windows|wince)/i;\n  const iosPlatforms = /(iphone|ipad|ipod)/i;\n  let os: OS = null;\n\n  if (macosPlatforms.test(userAgent)) {\n    os = \"macos\";\n  } else if (iosPlatforms.test(userAgent)) {\n    os = \"ios\";\n  } else if (windowsPlatforms.test(userAgent)) {\n    os = \"windows\";\n  } else if (/android/.test(userAgent)) {\n    os = \"android\";\n  } else if (!os && /linux/.test(userAgent)) {\n    os = \"linux\";\n  }\n  return os;\n}\n\nexport function match<T extends string | number | symbol, U>(\n  val: T,\n  options: Record<T, U>,\n) {\n  return options[val];\n}\n\nexport function matchFunc<T extends string | number | symbol, U>(\n  val: T,\n  options: Record<T, () => U>,\n) {\n  return options[val]();\n}\n"
  },
  {
    "path": "apps/web/next-env.d.ts",
    "content": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\n\n// NOTE: This file should not be edited\n// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.\n"
  },
  {
    "path": "apps/web/next.config.mjs",
    "content": "import bundleAnalyzer from \"@next/bundle-analyzer\";\nimport pwa from \"next-pwa\";\n\nconst withBundleAnalyzer = bundleAnalyzer({\n  enabled: process.env.ANALYZE === \"true\",\n});\n\nconst withPWA = pwa({\n  dest: \"public\",\n  disable: process.env.NODE_ENV != \"production\",\n});\n\n/** @type {import('next').NextConfig} */\nconst nextConfig = withPWA({\n  output: \"standalone\",\n  webpack: (config) => {\n    config.module.rules.push({\n      test: /\\.svg$/,\n      use: [\"@svgr/webpack\"],\n    });\n    return config;\n  },\n  async headers() {\n    return [\n      {\n        // Routes this applies to\n        source: \"/api/(.*)\",\n        // Headers\n        headers: [\n          // Allow for specific domains to have access or * for all\n          {\n            key: \"Access-Control-Allow-Origin\",\n            value: \"*\",\n          },\n          // Allows for specific methods accepted\n          {\n            key: \"Access-Control-Allow-Methods\",\n            value: \"GET, POST, PUT, PATCH, DELETE, OPTIONS\",\n          },\n          // Allows for specific headers accepted (These are a few standard ones)\n          {\n            key: \"Access-Control-Allow-Headers\",\n            value: \"Content-Type, Authorization\",\n          },\n          {\n            key: \"Access-Control-Allow-Credentials\",\n            value: \"true\",\n          },\n        ],\n      },\n    ];\n  },\n\n  // transpilePackages: [\"@karakeep/shared\", \"@karakeep/db\", \"@karakeep/trpc\"],\n\n  /** We already do linting and typechecking as separate tasks in CI */\n  eslint: { ignoreDuringBuilds: true },\n  typescript: { ignoreBuildErrors: true },\n});\n\nexport default withBundleAnalyzer(nextConfig);\n"
  },
  {
    "path": "apps/web/package.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/package.json\",\n  \"name\": \"@karakeep/web\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"clean\": \"git clean -xdf .next .turbo node_modules\",\n    \"build\": \"next build --experimental-build-mode compile\",\n    \"start\": \"next start\",\n    \"lint\": \"oxlint .\",\n    \"lint:fix\": \"oxlint . --fix\",\n    \"test\": \"vitest\",\n    \"typecheck\": \"tsc --noEmit\",\n    \"format\": \"oxfmt --check .\",\n    \"format:fix\": \"oxfmt .\"\n  },\n  \"dependencies\": {\n    \"@auth/drizzle-adapter\": \"~1.5.0\",\n    \"@emoji-mart/data\": \"^1.1.2\",\n    \"@emoji-mart/react\": \"^1.1.1\",\n    \"@hookform/resolvers\": \"^3.3.4\",\n    \"@karakeep/api\": \"workspace:^0.1.0\",\n    \"@karakeep/db\": \"workspace:^0.1.0\",\n    \"@karakeep/shared\": \"workspace:^0.1.0\",\n    \"@karakeep/shared-react\": \"workspace:^0.1.0\",\n    \"@karakeep/shared-server\": \"workspace:^0.1.0\",\n    \"@karakeep/trpc\": \"workspace:^0.1.0\",\n    \"@lexical/list\": \"^0.20.2\",\n    \"@lexical/markdown\": \"^0.20.2\",\n    \"@lexical/plain-text\": \"^0.20.2\",\n    \"@lexical/react\": \"^0.20.2\",\n    \"@lexical/rich-text\": \"^0.20.2\",\n    \"@marsidev/react-turnstile\": \"^1.3.1\",\n    \"@radix-ui/react-avatar\": \"^1.1.11\",\n    \"@radix-ui/react-collapsible\": \"^1.1.11\",\n    \"@radix-ui/react-dialog\": \"^1.1.14\",\n    \"@radix-ui/react-dropdown-menu\": \"^2.1.15\",\n    \"@radix-ui/react-label\": \"^2.1.7\",\n    \"@radix-ui/react-popover\": \"^1.1.14\",\n    \"@radix-ui/react-progress\": \"^1.1.7\",\n    \"@radix-ui/react-radio-group\": \"^1.3.8\",\n    \"@radix-ui/react-scroll-area\": \"^1.2.9\",\n    \"@radix-ui/react-select\": \"^2.2.5\",\n    \"@radix-ui/react-separator\": \"^1.1.7\",\n    \"@radix-ui/react-slider\": \"^1.3.5\",\n    \"@radix-ui/react-slot\": \"^1.2.4\",\n    \"@radix-ui/react-switch\": \"^1.2.5\",\n    \"@radix-ui/react-tabs\": \"^1.1.12\",\n    \"@radix-ui/react-toast\": \"^1.2.14\",\n    \"@radix-ui/react-toggle\": \"^1.1.9\",\n    \"@radix-ui/react-tooltip\": \"^1.2.7\",\n    \"@radix-ui/react-visually-hidden\": \"^1.2.3\",\n    \"@svgr/webpack\": \"^8.1.0\",\n    \"@tanstack/react-query\": \"5.90.2\",\n    \"@tanstack/react-query-devtools\": \"5.90.2\",\n    \"@trpc/client\": \"^11.9.0\",\n    \"@trpc/server\": \"^11.9.0\",\n    \"@trpc/tanstack-react-query\": \"^11.9.0\",\n    \"cheerio\": \"^1.0.0\",\n    \"class-variance-authority\": \"^0.7.0\",\n    \"clsx\": \"^2.1.0\",\n    \"cmdk\": \"^1.1.1\",\n    \"csv-parse\": \"^5.5.6\",\n    \"date-fns\": \"^3.6.0\",\n    \"drizzle-orm\": \"^0.44.2\",\n    \"fastest-levenshtein\": \"^1.0.16\",\n    \"i18next\": \"^23.16.5\",\n    \"i18next-resources-to-backend\": \"^1.2.1\",\n    \"lexical\": \"^0.20.2\",\n    \"lucide-react\": \"^0.501.0\",\n    \"modern-screenshot\": \"^4.6.7\",\n    \"next\": \"15.3.8\",\n    \"next-auth\": \"^4.24.11\",\n    \"next-i18next\": \"^15.3.1\",\n    \"next-pwa\": \"^5.6.0\",\n    \"next-themes\": \"^0.4.6\",\n    \"nuqs\": \"^2.4.3\",\n    \"react\": \"^19.2.1\",\n    \"react-day-picker\": \"^9.7.0\",\n    \"react-dom\": \"^19.2.1\",\n    \"react-draggable\": \"^4.5.0\",\n    \"react-dropzone\": \"^14.2.3\",\n    \"react-error-boundary\": \"^5.0.0\",\n    \"react-hook-form\": \"^7.57.0\",\n    \"react-hotkeys-hook\": \"^5.2.1\",\n    \"react-i18next\": \"^15.1.1\",\n    \"react-intersection-observer\": \"^9.13.1\",\n    \"react-markdown\": \"^9.0.1\",\n    \"react-masonry-css\": \"^1.0.16\",\n    \"react-select\": \"^5.10.1\",\n    \"react-syntax-highlighter\": \"^15.5.0\",\n    \"react-tweet\": \"^3.2.2\",\n    \"remark-breaks\": \"^4.0.0\",\n    \"remark-gfm\": \"^4.0.0\",\n    \"request-ip\": \"^3.3.0\",\n    \"sharp\": \"^0.33.3\",\n    \"sonner\": \"^2.0.7\",\n    \"superjson\": \"^2.2.1\",\n    \"tailwind-merge\": \"^2.2.1\",\n    \"zod\": \"^3.24.2\",\n    \"zustand\": \"^5.0.5\"\n  },\n  \"devDependencies\": {\n    \"@karakeep/tailwind-config\": \"workspace:^0.1.0\",\n    \"@karakeep/tsconfig\": \"workspace:^0.1.0\",\n    \"@next/bundle-analyzer\": \"15.3.8\",\n    \"@types/csv-parse\": \"^1.2.5\",\n    \"@types/emoji-mart\": \"^3.0.14\",\n    \"@types/react\": \"^19.2.14\",\n    \"@types/react-dom\": \"^19.1.6\",\n    \"@types/react-syntax-highlighter\": \"^15.5.13\",\n    \"@types/request-ip\": \"^0.0.41\",\n    \"autoprefixer\": \"^10.4.17\",\n    \"postcss\": \"^8.4.35\",\n    \"tailwindcss\": \"^3.4.1\",\n    \"vite-tsconfig-paths\": \"^4.3.1\",\n    \"vitest\": \"^3.2.4\"\n  }\n}\n"
  },
  {
    "path": "apps/web/postcss.config.cjs",
    "content": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n};\n"
  },
  {
    "path": "apps/web/public/manifest.json",
    "content": "{\n  \"name\": \"Karakeep\",\n  \"short_name\": \"Karakeep\",\n  \"icons\": [\n    {\n      \"src\": \"/icons/logo-16.png\",\n      \"sizes\": \"16x16\",\n      \"type\": \"image/png\",\n      \"purpose\": \"any maskable\"\n    },\n    {\n      \"src\": \"/icons/logo-48.png\",\n      \"sizes\": \"48x48\",\n      \"type\": \"image/png\"\n    },\n    {\n      \"src\": \"/icons/logo-128.png\",\n      \"sizes\": \"128x128\",\n      \"type\": \"image/png\"\n    }\n  ],\n  \"start_url\": \"/\",\n  \"display\": \"standalone\",\n  \"orientation\": \"portrait\"\n}\n"
  },
  {
    "path": "apps/web/server/api/client.ts",
    "content": "import { headers } from \"next/headers\";\nimport { getServerAuthSession } from \"@/server/auth\";\nimport requestIp from \"request-ip\";\n\nimport { db } from \"@karakeep/db\";\nimport { Context, createCallerFactory } from \"@karakeep/trpc\";\nimport { authenticateApiKey } from \"@karakeep/trpc/auth\";\nimport { appRouter } from \"@karakeep/trpc/routers/_app\";\n\nexport async function createContextFromRequest(req: Request) {\n  // TODO: This is a hack until we offer a proper REST API instead of the trpc based one.\n  // Check if the request has an Authorization token, if it does, assume that API key authentication is requested.\n  const ip = requestIp.getClientIp({\n    headers: Object.fromEntries(req.headers.entries()),\n  });\n  const authorizationHeader = req.headers.get(\"Authorization\");\n  if (authorizationHeader && authorizationHeader.startsWith(\"Bearer \")) {\n    const token = authorizationHeader.split(\" \")[1];\n    try {\n      const user = await authenticateApiKey(token, db);\n      return {\n        user,\n        db,\n        req: {\n          ip,\n        },\n      };\n    } catch {\n      // Fallthrough to cookie-based auth\n    }\n  }\n\n  return createContext(db, ip);\n}\n\nexport const createContext = async (\n  database?: typeof db,\n  ip?: string | null,\n): Promise<Context> => {\n  const session = await getServerAuthSession();\n  if (ip === undefined) {\n    const hdrs = await headers();\n    ip = requestIp.getClientIp({\n      headers: Object.fromEntries(hdrs.entries()),\n    });\n  }\n  return {\n    user: session?.user ?? null,\n    db: database ?? db,\n    req: {\n      ip,\n    },\n  };\n};\n\nconst createCaller = createCallerFactory(appRouter);\n\nexport const api = createCaller(createContext);\n\nexport const createTrcpClientFromCtx = createCaller;\n"
  },
  {
    "path": "apps/web/server/auth.ts",
    "content": "import { Adapter, AdapterUser } from \"@auth/core/adapters\";\nimport { DrizzleAdapter } from \"@auth/drizzle-adapter\";\nimport { count, eq } from \"drizzle-orm\";\nimport NextAuth, {\n  DefaultSession,\n  getServerSession,\n  NextAuthOptions,\n} from \"next-auth\";\nimport { Adapter as NextAuthAdapater } from \"next-auth/adapters\";\nimport CredentialsProvider from \"next-auth/providers/credentials\";\nimport { Provider } from \"next-auth/providers/index\";\n\nimport { db } from \"@karakeep/db\";\nimport {\n  accounts,\n  sessions,\n  users,\n  verificationTokens,\n} from \"@karakeep/db/schema\";\nimport serverConfig from \"@karakeep/shared/config\";\nimport { validatePassword } from \"@karakeep/trpc/auth\";\nimport { User } from \"@karakeep/trpc/models/users\";\n\ntype UserRole = \"admin\" | \"user\";\n\ndeclare module \"next-auth/jwt\" {\n  export interface JWT {\n    user: {\n      id: string;\n      role: UserRole;\n    } & DefaultSession[\"user\"];\n  }\n}\n\ndeclare module \"next-auth\" {\n  /**\n   * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context\n   */\n  export interface Session {\n    user: {\n      id: string;\n      role: UserRole;\n    } & DefaultSession[\"user\"];\n  }\n\n  export interface DefaultUser {\n    role: UserRole | null;\n  }\n}\n\n/**\n * Returns true if the user table is empty, which indicates that this user is going to be\n * the first one. This can be racy if multiple users are created at the same time, but\n * that should be fine.\n */\nasync function isFirstUser(): Promise<boolean> {\n  const [{ count: userCount }] = await db\n    .select({ count: count() })\n    .from(users);\n  return userCount == 0;\n}\n\n/**\n * Returns true if the user is an admin\n */\nasync function isAdmin(email: string): Promise<boolean> {\n  const res = await db.query.users.findFirst({\n    columns: { role: true },\n    where: eq(users.email, email),\n  });\n  return res?.role == \"admin\";\n}\n\nconst CustomProvider = (): Adapter => {\n  const adapter = DrizzleAdapter(db, {\n    usersTable: users,\n    accountsTable: accounts,\n    sessionsTable: sessions,\n    verificationTokensTable: verificationTokens,\n  });\n\n  return {\n    ...adapter,\n    createUser: async (user: Omit<AdapterUser, \"id\">) => {\n      return await User.createRaw(db, {\n        name: user.name ?? \"\",\n        email: user.email,\n        emailVerified: user.emailVerified,\n      });\n    },\n  };\n};\n\nconst providers: Provider[] = [\n  CredentialsProvider({\n    // The name to display on the sign in form (e.g. \"Sign in with...\")\n    name: \"Credentials\",\n    credentials: {\n      email: { label: \"Email\", type: \"email\", placeholder: \"Email\" },\n      password: { label: \"Password\", type: \"password\" },\n    },\n    async authorize(credentials) {\n      if (!credentials) {\n        return null;\n      }\n\n      try {\n        return await validatePassword(\n          credentials?.email,\n          credentials?.password,\n          db,\n        );\n      } catch {\n        return null;\n      }\n    },\n  }),\n];\n\nconst oauth = serverConfig.auth.oauth;\nif (oauth.wellKnownUrl) {\n  providers.push({\n    id: \"custom\",\n    name: oauth.name,\n    type: \"oauth\",\n    wellKnown: oauth.wellKnownUrl,\n    authorization: { params: { scope: oauth.scope } },\n    clientId: oauth.clientId,\n    clientSecret: oauth.clientSecret,\n    allowDangerousEmailAccountLinking: oauth.allowDangerousEmailAccountLinking,\n    checks: [\"pkce\", \"state\"],\n    httpOptions: {\n      timeout: oauth.timeout,\n    },\n    async profile(profile: Record<string, string>) {\n      const [admin, firstUser] = await Promise.all([\n        isAdmin(profile.email),\n        isFirstUser(),\n      ]);\n      return {\n        id: profile.sub,\n        name: profile.name || profile.email,\n        email: profile.email,\n        role: admin || firstUser ? \"admin\" : \"user\",\n      };\n    },\n  });\n}\n\nexport const authOptions: NextAuthOptions = {\n  // https://github.com/nextauthjs/next-auth/issues/9493\n  adapter: CustomProvider() as NextAuthAdapater,\n  providers: providers,\n  session: {\n    strategy: \"jwt\",\n  },\n  pages: {\n    signIn: \"/signin\",\n    signOut: \"/signin\",\n    error: \"/signin\",\n    newUser: \"/signin\",\n  },\n  callbacks: {\n    async signIn({ user: credUser, credentials, profile }) {\n      const email = credUser.email || profile?.email;\n      if (!email) {\n        throw new Error(\"Provider didn't provide an email during signin\");\n      }\n      const user = await db.query.users.findFirst({\n        columns: { emailVerified: true },\n        where: eq(users.email, email),\n      });\n\n      if (credentials) {\n        if (!user) {\n          throw new Error(\"Invalid credentials\");\n        }\n        if (\n          serverConfig.auth.emailVerificationRequired &&\n          !user.emailVerified\n        ) {\n          throw new Error(\"Please verify your email address before signing in\");\n        }\n        return true;\n      }\n\n      // If it's a new user and signups are disabled, fail the sign in\n      if (!user && serverConfig.auth.disableSignups) {\n        throw new Error(\"Signups are disabled in server config\");\n      }\n\n      // TODO: We're blindly trusting oauth providers to validate emails\n      // As such, oauth users can sign in even if email verification is enabled.\n      // We might want to change this in the future.\n\n      return true;\n    },\n    async jwt({ token, user }) {\n      if (user) {\n        token.user = {\n          id: user.id,\n          name: user.name,\n          email: user.email,\n          image: user.image,\n          role: user.role ?? \"user\",\n        };\n      }\n      return token;\n    },\n    async session({ session, token }) {\n      session.user = { ...token.user };\n      return session;\n    },\n  },\n};\n\nexport const authHandler = NextAuth(authOptions);\n\nexport const getServerAuthSession = () => getServerSession(authOptions);\n"
  },
  {
    "path": "apps/web/tailwind.config.ts",
    "content": "import type { Config } from \"tailwindcss\";\n\nimport web from \"@karakeep/tailwind-config/web\";\n\nconst config = {\n  content: [\n    ...web.content,\n    \"../../packages/shared-react/components/**/*.{ts,tsx}\",\n  ],\n  presets: [web],\n} satisfies Config;\n\nexport default config;\n"
  },
  {
    "path": "apps/web/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@karakeep/tsconfig/base.json\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"plugins\": [\n      {\n        \"name\": \"next\"\n      }\n    ],\n    \"paths\": {\n      \"@/*\": [\"./*\"]\n    },\n    \"tsBuildInfoFile\": \"node_modules/.cache/tsbuildinfo.json\"\n  },\n  \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\", \".next/types/**/*.ts\"],\n  \"exclude\": [\"node_modules\", \".next\"]\n}\n"
  },
  {
    "path": "apps/web/vitest.config.ts",
    "content": "/// <reference types=\"vitest\" />\n\nimport tsconfigPaths from \"vite-tsconfig-paths\";\nimport { defineConfig } from \"vitest/config\";\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  plugins: [tsconfigPaths()],\n  test: {\n    alias: {\n      \"@/*\": \"./*\",\n    },\n  },\n});\n"
  },
  {
    "path": "apps/workers/.oxlintrc.json",
    "content": "{\n  \"$schema\": \"../../node_modules/oxlint/configuration_schema.json\",\n  \"extends\": [\n    \"../../tooling/oxlint/oxlint-base.json\"\n  ],\n  \"env\": {\n    \"builtin\": true,\n    \"commonjs\": true\n  },\n  \"ignorePatterns\": [\n    \"**/*.config.js\",\n    \"**/*.config.cjs\",\n    \"**/.eslintrc.cjs\",\n    \"**/.next\",\n    \"**/dist\",\n    \"**/build\",\n    \"**/pnpm-lock.yaml\"\n  ]\n}\n"
  },
  {
    "path": "apps/workers/exit.ts",
    "content": "import logger from \"@karakeep/shared/logger\";\n\nexport const exitAbortController = new AbortController();\n\nexport const shutdownPromise = new Promise((resolve) => {\n  process.on(\"SIGTERM\", () => {\n    logger.info(\"Received SIGTERM, shutting down ...\");\n    exitAbortController.abort();\n    resolve(\"\");\n  });\n});\n"
  },
  {
    "path": "apps/workers/index.ts",
    "content": "import \"dotenv/config\";\n\nimport { buildServer } from \"server\";\n\nimport {\n  AdminMaintenanceQueue,\n  AssetPreprocessingQueue,\n  BackupQueue,\n  FeedQueue,\n  initTracing,\n  LinkCrawlerQueue,\n  loadAllPlugins,\n  LowPriorityCrawlerQueue,\n  OpenAIQueue,\n  prepareQueue,\n  RuleEngineQueue,\n  SearchIndexingQueue,\n  shutdownTracing,\n  startQueue,\n  VideoWorkerQueue,\n  WebhookQueue,\n} from \"@karakeep/shared-server\";\nimport serverConfig from \"@karakeep/shared/config\";\nimport logger from \"@karakeep/shared/logger\";\n\nimport { shutdownPromise } from \"./exit\";\nimport { AdminMaintenanceWorker } from \"./workers/adminMaintenanceWorker\";\nimport { AssetPreprocessingWorker } from \"./workers/assetPreprocessingWorker\";\nimport { BackupSchedulingWorker, BackupWorker } from \"./workers/backupWorker\";\nimport { CrawlerWorker } from \"./workers/crawlerWorker\";\nimport { FeedRefreshingWorker, FeedWorker } from \"./workers/feedWorker\";\nimport { ImportWorker } from \"./workers/importWorker\";\nimport { OpenAiWorker } from \"./workers/inference/inferenceWorker\";\nimport { RuleEngineWorker } from \"./workers/ruleEngineWorker\";\nimport { SearchIndexingWorker } from \"./workers/searchWorker\";\nimport { VideoWorker } from \"./workers/videoWorker\";\nimport { WebhookWorker } from \"./workers/webhookWorker\";\n\nconst workerBuilders = {\n  crawler: async () => {\n    await LinkCrawlerQueue.ensureInit();\n    return CrawlerWorker.build(LinkCrawlerQueue);\n  },\n  lowPriorityCrawler: async () => {\n    await LowPriorityCrawlerQueue.ensureInit();\n    return CrawlerWorker.build(LowPriorityCrawlerQueue);\n  },\n  inference: async () => {\n    await OpenAIQueue.ensureInit();\n    return OpenAiWorker.build();\n  },\n  search: async () => {\n    await SearchIndexingQueue.ensureInit();\n    return SearchIndexingWorker.build();\n  },\n  adminMaintenance: async () => {\n    await AdminMaintenanceQueue.ensureInit();\n    return AdminMaintenanceWorker.build();\n  },\n  video: async () => {\n    await VideoWorkerQueue.ensureInit();\n    return VideoWorker.build();\n  },\n  feed: async () => {\n    await FeedQueue.ensureInit();\n    return FeedWorker.build();\n  },\n  assetPreprocessing: async () => {\n    await AssetPreprocessingQueue.ensureInit();\n    return AssetPreprocessingWorker.build();\n  },\n  webhook: async () => {\n    await WebhookQueue.ensureInit();\n    return WebhookWorker.build();\n  },\n  ruleEngine: async () => {\n    await RuleEngineQueue.ensureInit();\n    return RuleEngineWorker.build();\n  },\n  backup: async () => {\n    await BackupQueue.ensureInit();\n    return BackupWorker.build();\n  },\n} as const;\n\ntype WorkerName = keyof typeof workerBuilders | \"import\";\nconst enabledWorkers = new Set(serverConfig.workers.enabledWorkers);\nconst disabledWorkers = new Set(serverConfig.workers.disabledWorkers);\n\nfunction isWorkerEnabled(name: WorkerName) {\n  if (enabledWorkers.size > 0 && !enabledWorkers.has(name)) {\n    return false;\n  }\n  if (disabledWorkers.has(name)) {\n    return false;\n  }\n  return true;\n}\n\nasync function main() {\n  await loadAllPlugins();\n  initTracing(\"workers\");\n  logger.info(`Workers version: ${serverConfig.serverVersion ?? \"not set\"}`);\n  await prepareQueue();\n\n  const httpServer = buildServer();\n\n  const workers = await Promise.all(\n    Object.entries(workerBuilders)\n      .filter(([name]) => isWorkerEnabled(name as WorkerName))\n      .map(async ([name, builder]) => ({\n        name: name as WorkerName,\n        worker: await builder(),\n      })),\n  );\n\n  await startQueue();\n\n  if (workers.some((w) => w.name === \"feed\")) {\n    FeedRefreshingWorker.start();\n  }\n\n  if (workers.some((w) => w.name === \"backup\")) {\n    BackupSchedulingWorker.start();\n  }\n\n  // Start import polling worker\n  let importWorker: ImportWorker | null = null;\n  let importWorkerPromise: Promise<void> | null = null;\n  if (isWorkerEnabled(\"import\")) {\n    importWorker = new ImportWorker();\n    importWorkerPromise = importWorker.start();\n  }\n\n  await Promise.any([\n    Promise.all([\n      ...workers.map(({ worker }) => worker.run()),\n      httpServer.serve(),\n      ...(importWorkerPromise ? [importWorkerPromise] : []),\n    ]),\n    shutdownPromise,\n  ]);\n\n  logger.info(\n    `Shutting down ${workers.map((w) => w.name).join(\", \")} workers ...`,\n  );\n\n  if (workers.some((w) => w.name === \"feed\")) {\n    FeedRefreshingWorker.stop();\n  }\n  if (workers.some((w) => w.name === \"backup\")) {\n    BackupSchedulingWorker.stop();\n  }\n  if (importWorker) {\n    importWorker.stop();\n  }\n  for (const { worker } of workers) {\n    worker.stop();\n  }\n  await httpServer.stop();\n  await shutdownTracing();\n  process.exit(0);\n}\n\nmain();\n"
  },
  {
    "path": "apps/workers/metascraper-plugins/metascraper-amazon-improved.ts",
    "content": "import type { Rules } from \"metascraper\";\n\n/**\n * Improved Amazon metascraper plugin that fixes image extraction.\n *\n * The default metascraper-amazon package uses `.a-dynamic-image` selector\n * which matches the FIRST element with that class. On amazon.com pages,\n * this is often the Prime logo instead of the product image.\n *\n * This plugin uses more specific selectors to target the actual product\n * image:\n * - #landingImage: The main product image ID\n * - #imgTagWrapperId img: Fallback container for product images\n * - #imageBlock img: Additional fallback for newer Amazon layouts\n *\n * By placing this plugin BEFORE metascraperAmazon() in the plugin chain,\n * we ensure the correct image is extracted while keeping all other Amazon\n * metadata (title, brand, description) from the original plugin.\n */\n\nconst REGEX_AMAZON_URL =\n  /https?:\\/\\/(.*amazon\\..*\\/.*|.*amzn\\..*\\/.*|.*a\\.co\\/.*)/i;\n\nconst test = ({ url }: { url: string }): boolean => REGEX_AMAZON_URL.test(url);\n\nconst metascraperAmazonImproved = () => {\n  const rules: Rules = {\n    pkgName: \"metascraper-amazon-improved\",\n    test,\n    image: ({ htmlDom }) => {\n      // Try the main product image ID first (most reliable)\n      // Prefer data-old-hires attribute for high-resolution images\n      const landingImageHires = htmlDom(\"#landingImage\").attr(\"data-old-hires\");\n      if (landingImageHires) {\n        return landingImageHires;\n      }\n\n      const landingImageSrc = htmlDom(\"#landingImage\").attr(\"src\");\n      if (landingImageSrc) {\n        return landingImageSrc;\n      }\n\n      // Fallback to image block container\n      const imgTagHires = htmlDom(\"#imgTagWrapperId img\").attr(\n        \"data-old-hires\",\n      );\n      if (imgTagHires) {\n        return imgTagHires;\n      }\n\n      const imgTagSrc = htmlDom(\"#imgTagWrapperId img\").attr(\"src\");\n      if (imgTagSrc) {\n        return imgTagSrc;\n      }\n\n      // Additional fallback for newer Amazon layouts\n      const imageBlockHires = htmlDom(\"#imageBlock img\")\n        .first()\n        .attr(\"data-old-hires\");\n      if (imageBlockHires) {\n        return imageBlockHires;\n      }\n\n      const imageBlockSrc = htmlDom(\"#imageBlock img\").first().attr(\"src\");\n      if (imageBlockSrc) {\n        return imageBlockSrc;\n      }\n\n      // Return undefined to allow next plugin to try\n      return undefined;\n    },\n  };\n\n  return rules;\n};\n\nexport default metascraperAmazonImproved;\n"
  },
  {
    "path": "apps/workers/metascraper-plugins/metascraper-reddit.ts",
    "content": "import type { CheerioAPI } from \"cheerio\";\nimport type { Rules, RulesOptions } from \"metascraper\";\nimport { decode as decodeHtmlEntities } from \"html-entities\";\nimport { fetchWithProxy } from \"network\";\nimport { z } from \"zod\";\n\nimport logger from \"@karakeep/shared/logger\";\n\n/**\n * This is a metascraper plugin to select a better\n * 'image' attribute for Reddit links, specifically\n * those sharing images. It will also extract the\n * Post Title for a Reddit post instead of use the\n * default.\n *\n * As of writing this, Reddit posts do not define\n * an open-graph image (og:image) attribute, so\n * metascraper resorts to looking for images in\n * the HTML DOM, and selects the first one.\n *\n * In Reddit posts, the first image is typically\n * the profile picture of the OP, which Karakeep\n * is using for the thumbnail.\n *\n * This metascraper plugin instead looks for images\n * with the domain i.redd.it, on which Reddit hosts\n * their preview images for posts. If this plugin\n * finds an i.redd.it image, it provides that for\n * the image metadata.\n *\n * If there is not a matching image, this plugin\n * will return 'undefined' and the next plugin\n * should continue to attempt to extract images.\n *\n * We also attempt to fetch the Reddit JSON response\n * (by appending '.json' to the URL) to grab the\n * title and preview images directly from the API.\n **/\n\nconst redditPreviewImageSchema = z.object({\n  source: z.object({ url: z.string().optional() }).optional(),\n  resolutions: z.array(z.object({ url: z.string().optional() })).optional(),\n});\n\nconst redditMediaMetadataItemSchema = z.object({\n  s: z.object({ u: z.string().optional() }).optional(),\n  p: z.array(z.object({ u: z.string().optional() })).optional(),\n});\n\nconst redditPostSchema = z.object({\n  title: z.string().optional(),\n  preview: z\n    .object({ images: z.array(redditPreviewImageSchema).optional() })\n    .optional(),\n  url_overridden_by_dest: z.string().optional(),\n  url: z.string().optional(),\n  thumbnail: z.string().optional(),\n  media_metadata: z.record(redditMediaMetadataItemSchema).optional(),\n  author: z.string().optional(),\n  created_utc: z.number().optional(),\n  selftext: z.string().nullish(),\n  selftext_html: z.string().nullish(),\n  subreddit_name_prefixed: z.string().optional(),\n});\n\ntype RedditPostData = z.infer<typeof redditPostSchema>;\n\nconst redditResponseSchema = z.array(\n  z.object({\n    data: z.object({\n      children: z.array(z.object({ data: redditPostSchema })).optional(),\n    }),\n  }),\n);\n\ninterface RedditFetchResult {\n  fetched: boolean;\n  post?: RedditPostData;\n}\n\nconst REDDIT_CACHE_TTL_MS = 60 * 1000; // 1 minute TTL to avoid stale data\n\ninterface RedditCacheEntry {\n  expiresAt: number;\n  promise: Promise<RedditFetchResult>;\n}\n\nconst redditJsonCache = new Map<string, RedditCacheEntry>();\n\nconst purgeExpiredCacheEntries = (now: number) => {\n  for (const [key, entry] of redditJsonCache.entries()) {\n    if (entry.expiresAt <= now) {\n      redditJsonCache.delete(key);\n    }\n  }\n};\n\nconst decodeRedditUrl = (url?: string): string | undefined => {\n  if (!url) {\n    return undefined;\n  }\n  const decoded = decodeHtmlEntities(url);\n  return decoded || undefined;\n};\n\nconst buildJsonUrl = (url: string): string => {\n  const urlObj = new URL(url);\n\n  if (!urlObj.pathname.endsWith(\".json\")) {\n    urlObj.pathname = urlObj.pathname.replace(/\\/?$/, \".json\");\n  }\n\n  return urlObj.toString();\n};\n\nconst extractImageFromMediaMetadata = (\n  media_metadata?: RedditPostData[\"media_metadata\"],\n): string | undefined => {\n  if (!media_metadata) {\n    return undefined;\n  }\n  const firstItem = Object.values(media_metadata)[0];\n  if (!firstItem) {\n    return undefined;\n  }\n\n  return (\n    decodeRedditUrl(firstItem.s?.u) ??\n    decodeRedditUrl(firstItem.p?.[0]?.u) ??\n    undefined\n  );\n};\n\nconst isRedditImageHost = (urlCandidate: string): boolean => {\n  try {\n    const hostname = new URL(urlCandidate).hostname;\n    return hostname.includes(\"redd.it\");\n  } catch {\n    return false;\n  }\n};\n\nconst extractImageFromPost = (post: RedditPostData): string | undefined => {\n  const previewImage = post.preview?.images?.[0];\n  const previewUrl =\n    decodeRedditUrl(previewImage?.source?.url) ??\n    decodeRedditUrl(previewImage?.resolutions?.[0]?.url);\n  if (previewUrl) {\n    return previewUrl;\n  }\n\n  const mediaUrl = extractImageFromMediaMetadata(post.media_metadata);\n  if (mediaUrl) {\n    return mediaUrl;\n  }\n\n  const directUrl =\n    decodeRedditUrl(post.url_overridden_by_dest) ??\n    decodeRedditUrl(post.url) ??\n    decodeRedditUrl(post.thumbnail);\n\n  if (directUrl && isRedditImageHost(directUrl)) {\n    return directUrl;\n  }\n\n  return undefined;\n};\n\nconst extractTitleFromPost = (post: RedditPostData): string | undefined =>\n  post.title?.trim() || undefined;\n\nconst extractAuthorFromPost = (post: RedditPostData): string | undefined =>\n  post.author?.trim() || undefined;\n\nconst extractDateFromPost = (post: RedditPostData): string | undefined => {\n  if (!post.created_utc) {\n    return undefined;\n  }\n  const date = new Date(post.created_utc * 1000);\n  return Number.isNaN(date.getTime()) ? undefined : date.toISOString();\n};\n\nconst extractPublisherFromPost = (post: RedditPostData): string | undefined =>\n  post.subreddit_name_prefixed?.trim() || \"Reddit\";\n\nconst REDDIT_LOGO_URL =\n  \"https://www.redditstatic.com/desktop2x/img/favicon/android-icon-192x192.png\";\n\nconst fallbackDomImage = ({ htmlDom }: { htmlDom: CheerioAPI }) => {\n  // 'preview' subdomain images are more likely to be what we're after\n  // but it could be in the 'i' subdomain.\n  // returns undefined if neither exists\n  const previewImages = htmlDom('img[src*=\"preview.redd.it\"]')\n    .map((_, el) => htmlDom(el).attr(\"src\"))\n    .get();\n  const iImages = htmlDom('img[src*=\"i.redd.it\"]')\n    .map((_, el) => htmlDom(el).attr(\"src\"))\n    .get();\n  return previewImages[0] || iImages[0];\n};\n\nconst fallbackDomTitle = ({ htmlDom }: { htmlDom: CheerioAPI }) => {\n  const title: string | undefined = htmlDom(\"shreddit-title[title]\")\n    .first()\n    .attr(\"title\");\n  const postTitle: string | undefined =\n    title ?? htmlDom(\"shreddit-post[post-title]\").first().attr(\"post-title\");\n  return postTitle ? postTitle.trim() : undefined;\n};\n\nconst fetchRedditPostData = async (url: string): Promise<RedditFetchResult> => {\n  const cached = redditJsonCache.get(url);\n  const now = Date.now();\n\n  purgeExpiredCacheEntries(now);\n\n  if (cached && cached.expiresAt > now) {\n    return cached.promise;\n  }\n\n  const promise = (async () => {\n    let jsonUrl: string;\n    try {\n      jsonUrl = buildJsonUrl(url);\n    } catch (error) {\n      logger.warn(\n        \"[MetascraperReddit] Failed to construct Reddit JSON URL\",\n        error,\n      );\n      return { fetched: false };\n    }\n\n    let response;\n    try {\n      response = await fetchWithProxy(jsonUrl, {\n        headers: { accept: \"application/json\" },\n      });\n    } catch (error) {\n      logger.warn(\n        `[MetascraperReddit] Failed to fetch Reddit JSON for ${jsonUrl}`,\n        error,\n      );\n      return { fetched: false };\n    }\n\n    if (response.status === 403) {\n      // API forbidden; fall back to DOM scraping.\n      return { fetched: false };\n    }\n\n    if (!response.ok) {\n      logger.warn(\n        `[MetascraperReddit] Reddit JSON request failed for ${jsonUrl} with status ${response.status}`,\n      );\n      return { fetched: false };\n    }\n\n    let payload: unknown;\n    try {\n      payload = await response.json();\n    } catch (error) {\n      logger.warn(\n        `[MetascraperReddit] Failed to parse Reddit JSON for ${jsonUrl}`,\n        error,\n      );\n      return { fetched: false };\n    }\n\n    const parsed = redditResponseSchema.safeParse(payload);\n    if (!parsed.success) {\n      logger.warn(\n        \"[MetascraperReddit] Reddit JSON schema validation failed\",\n        parsed.error,\n      );\n      return { fetched: false };\n    }\n\n    const firstListingWithChildren = parsed.data.find(\n      (listing) => (listing.data.children?.length ?? 0) > 0,\n    );\n\n    return {\n      fetched: true,\n      post: firstListingWithChildren?.data.children?.[0]?.data,\n    };\n  })();\n\n  redditJsonCache.set(url, {\n    promise,\n    expiresAt: now + REDDIT_CACHE_TTL_MS,\n  });\n\n  return promise;\n};\n\nconst domainFromUrl = (url: string): string => {\n  /**\n   * First-party metascraper plugins import metascraper-helpers,\n   * which exposes a parseUrl function from the tldtr package.\n   * This function does similar to the 'domainWithoutSuffix'\n   * field from the tldtr package, without requiring any\n   * additional packages.\n   **/\n  try {\n    // Create a URL instance to parse the hostname\n    const hostname = new URL(url).hostname;\n    const parts = hostname.split(\".\");\n    // Return the part before the TLD (assuming at least two segments)\n    // For example, \"www.example.com\" -> [\"www\", \"example\", \"com\"]\n    if (parts.length >= 2) {\n      return parts[parts.length - 2];\n    }\n    return hostname;\n  } catch (error) {\n    logger.error(\n      \"[MetascraperReddit] Test>domainFromUrl received an invalid URL:\",\n      error,\n    );\n    return \"\";\n  }\n};\n\nconst test = ({ url }: { url: string }): boolean =>\n  domainFromUrl(url).toLowerCase() === \"reddit\";\n\nconst metascraperReddit = () => {\n  const rules: Rules = {\n    pkgName: \"metascraper-reddit\",\n    test,\n    image: (async ({ url, htmlDom }: { url: string; htmlDom: CheerioAPI }) => {\n      const result = await fetchRedditPostData(url);\n      if (result.post) {\n        const redditImage = extractImageFromPost(result.post);\n        if (redditImage) {\n          return redditImage;\n        }\n      }\n\n      // If we successfully fetched JSON but found no Reddit image,\n      // avoid falling back to random DOM images.\n      if (result.fetched) {\n        return undefined;\n      }\n\n      return fallbackDomImage({ htmlDom });\n    }) as unknown as RulesOptions,\n    title: (async ({ url, htmlDom }: { url: string; htmlDom: CheerioAPI }) => {\n      const result = await fetchRedditPostData(url);\n      if (result.post) {\n        const redditTitle = extractTitleFromPost(result.post);\n        if (redditTitle) {\n          return redditTitle;\n        }\n      }\n\n      return fallbackDomTitle({ htmlDom });\n    }) as unknown as RulesOptions,\n    author: (async ({ url }: { url: string }) => {\n      const result = await fetchRedditPostData(url);\n      if (result.post) {\n        return extractAuthorFromPost(result.post);\n      }\n      return undefined;\n    }) as unknown as RulesOptions,\n    datePublished: (async ({ url }: { url: string }) => {\n      const result = await fetchRedditPostData(url);\n      if (result.post) {\n        return extractDateFromPost(result.post);\n      }\n      return undefined;\n    }) as unknown as RulesOptions,\n    publisher: (async ({ url }: { url: string }) => {\n      const result = await fetchRedditPostData(url);\n      if (result.post) {\n        return extractPublisherFromPost(result.post);\n      }\n      return undefined;\n    }) as unknown as RulesOptions,\n    logo: (async ({ url }: { url: string }) => {\n      const result = await fetchRedditPostData(url);\n      if (result.post) {\n        return REDDIT_LOGO_URL;\n      }\n      return undefined;\n    }) as unknown as RulesOptions,\n    readableContentHtml: (async ({ url }: { url: string }) => {\n      const result = await fetchRedditPostData(url);\n      if (result.post) {\n        const decoded = decodeHtmlEntities(result.post.selftext_html ?? \"\");\n        // The post has no content, return the title\n        return (decoded || result.post.title) ?? null;\n      }\n      return undefined;\n    }) as unknown as RulesOptions,\n  };\n\n  return rules;\n};\n\nexport default metascraperReddit;\n"
  },
  {
    "path": "apps/workers/metrics.ts",
    "content": "import { prometheus } from \"@hono/prometheus\";\nimport { Counter, Histogram, Registry } from \"prom-client\";\n\nexport const registry = new Registry();\n\nexport const { printMetrics } = prometheus({\n  registry: registry,\n  prefix: \"karakeep_\",\n  collectDefaultMetrics: true,\n});\n\nexport const workerStatsCounter = new Counter({\n  name: \"karakeep_worker_stats\",\n  help: \"Stats for each worker\",\n  labelNames: [\"worker_name\", \"status\"],\n});\n\nexport const crawlerStatusCodeCounter = new Counter({\n  name: \"karakeep_crawler_status_codes_total\",\n  help: \"HTTP status codes encountered during crawling\",\n  labelNames: [\"status_code\"],\n});\n\nexport const bookmarkCrawlLatencyHistogram = new Histogram({\n  name: \"karakeep_bookmark_crawl_latency_seconds\",\n  help: \"Latency from bookmark creation to crawl completion (excludes recrawls and imports)\",\n  buckets: [\n    0.1, 0.25, 0.5, 1, 2.5, 5, 7.5, 10, 15, 20, 30, 45, 60, 90, 120, 180, 300,\n    600, 900, 1200,\n  ],\n});\n\nregistry.registerMetric(workerStatsCounter);\nregistry.registerMetric(crawlerStatusCodeCounter);\nregistry.registerMetric(bookmarkCrawlLatencyHistogram);\n"
  },
  {
    "path": "apps/workers/network.ts",
    "content": "import dns from \"node:dns/promises\";\nimport type { HeadersInit, RequestInit, Response } from \"node-fetch\";\nimport { HttpProxyAgent } from \"http-proxy-agent\";\nimport { HttpsProxyAgent } from \"https-proxy-agent\";\nimport ipaddr from \"ipaddr.js\";\nimport { LRUCache } from \"lru-cache\";\nimport fetch, { Headers } from \"node-fetch\";\n\nimport serverConfig from \"@karakeep/shared/config\";\nimport logger from \"@karakeep/shared/logger\";\n\nconst DISALLOWED_IP_RANGES = new Set([\n  // IPv4 ranges\n  \"unspecified\",\n  \"broadcast\",\n  \"multicast\",\n  \"linkLocal\",\n  \"loopback\",\n  \"private\",\n  \"reserved\",\n  \"carrierGradeNat\",\n  // IPv6 ranges\n  \"uniqueLocal\",\n  \"6to4\", // RFC 3056 - IPv6 transition mechanism\n  \"teredo\", // RFC 4380 - IPv6 tunneling\n  \"benchmarking\", // RFC 5180 - benchmarking addresses\n  \"deprecated\", // RFC 3879 - deprecated IPv6 addresses\n  \"discard\", // RFC 6666 - discard-only prefix\n]);\n\n// DNS cache with 5 minute TTL and max 1000 entries\nconst dnsCache = new LRUCache<string, string[]>({\n  max: 1000,\n  ttl: 5 * 60 * 1000, // 5 minutes in milliseconds\n});\n\nasync function resolveHostAddresses(hostname: string): Promise<string[]> {\n  const resolver = new dns.Resolver({\n    timeout: serverConfig.crawler.ipValidation.dnsResolverTimeoutSec * 1000,\n  });\n\n  const results = await Promise.allSettled([\n    resolver.resolve4(hostname),\n    resolver.resolve6(hostname),\n  ]);\n\n  const addresses: string[] = [];\n  const errors: string[] = [];\n\n  for (const result of results) {\n    if (result.status === \"fulfilled\") {\n      addresses.push(...result.value);\n    } else {\n      const reason = result.reason;\n      if (reason instanceof Error) {\n        errors.push(reason.message);\n      } else {\n        errors.push(String(reason));\n      }\n    }\n  }\n\n  if (addresses.length > 0) {\n    return addresses;\n  }\n\n  const errorMessage =\n    errors.length > 0\n      ? errors.join(\"; \")\n      : \"DNS lookup did not return any A or AAAA records\";\n  throw new Error(errorMessage);\n}\n\nfunction isAddressForbidden(address: string): boolean {\n  if (!ipaddr.isValid(address)) {\n    return true;\n  }\n  const parsed = ipaddr.parse(address);\n  if (\n    parsed.kind() === \"ipv6\" &&\n    (parsed as ipaddr.IPv6).isIPv4MappedAddress()\n  ) {\n    const mapped = (parsed as ipaddr.IPv6).toIPv4Address();\n    return DISALLOWED_IP_RANGES.has(mapped.range());\n  }\n  return DISALLOWED_IP_RANGES.has(parsed.range());\n}\n\nexport function getBookmarkDomain(url?: string | null): string | undefined {\n  if (!url) return undefined;\n  try {\n    return new URL(url).hostname;\n  } catch {\n    return undefined;\n  }\n}\n\nexport type UrlValidationResult =\n  | { ok: true; url: URL }\n  | { ok: false; reason: string };\n\nfunction hostnameMatchesAnyPattern(\n  hostname: string,\n  patterns: string[],\n): boolean {\n  function hostnameMatchesPattern(hostname: string, pattern: string): boolean {\n    if (pattern === \".\") {\n      return true;\n    }\n\n    return (\n      pattern === hostname ||\n      (pattern.startsWith(\".\") && hostname.endsWith(pattern)) ||\n      hostname.endsWith(\".\" + pattern)\n    );\n  }\n\n  for (const pattern of patterns) {\n    if (hostnameMatchesPattern(hostname, pattern)) {\n      return true;\n    }\n  }\n  return false;\n}\n\nfunction isHostnameAllowedForInternalAccess(hostname: string): boolean {\n  if (!serverConfig.allowedInternalHostnames) {\n    return false;\n  }\n  return hostnameMatchesAnyPattern(\n    hostname,\n    serverConfig.allowedInternalHostnames,\n  );\n}\n\nexport async function validateUrl(\n  urlCandidate: string,\n  runningInProxyContext: boolean,\n): Promise<UrlValidationResult> {\n  let parsedUrl: URL;\n  try {\n    parsedUrl = new URL(urlCandidate);\n  } catch (error) {\n    return {\n      ok: false,\n      reason: `Invalid URL \"${urlCandidate}\": ${\n        error instanceof Error ? error.message : String(error)\n      }`,\n    } as const;\n  }\n\n  if (parsedUrl.protocol !== \"http:\" && parsedUrl.protocol !== \"https:\") {\n    return {\n      ok: false,\n      reason: `Unsupported protocol for URL: ${parsedUrl.toString()}`,\n    } as const;\n  }\n\n  const hostname = parsedUrl.hostname;\n  if (!hostname) {\n    return {\n      ok: false,\n      reason: `URL ${parsedUrl.toString()} must include a hostname`,\n    } as const;\n  }\n\n  if (isHostnameAllowedForInternalAccess(hostname)) {\n    return { ok: true, url: parsedUrl } as const;\n  }\n\n  if (ipaddr.isValid(hostname)) {\n    if (isAddressForbidden(hostname)) {\n      return {\n        ok: false,\n        reason: `Refusing to access disallowed IP address ${hostname} (requested via ${parsedUrl.toString()}). You can use CRAWLER_ALLOWED_INTERNAL_HOSTNAMES to allowlist specific hostnames for internal access.`,\n      } as const;\n    }\n    return { ok: true, url: parsedUrl } as const;\n  }\n\n  if (runningInProxyContext) {\n    // If we're running in a proxy context, we must skip DNS resolution\n    // as the DNS resolution will be handled by the proxy\n    return { ok: true, url: parsedUrl } as const;\n  }\n\n  // Check cache first\n  let records = dnsCache.get(hostname);\n\n  if (!records) {\n    // Cache miss or expired - perform DNS resolution\n    try {\n      records = await resolveHostAddresses(hostname);\n      dnsCache.set(hostname, records);\n    } catch (error) {\n      return {\n        ok: false,\n        reason: `Failed to resolve hostname ${hostname}: ${\n          error instanceof Error ? error.message : String(error)\n        }`,\n      } as const;\n    }\n  }\n\n  if (!records || records.length === 0) {\n    return {\n      ok: false,\n      reason: `DNS lookup for ${hostname} did not return any addresses (requested via ${parsedUrl.toString()})`,\n    } as const;\n  }\n\n  for (const record of records) {\n    if (isAddressForbidden(record)) {\n      return {\n        ok: false,\n        reason: `Refusing to access disallowed resolved address ${record} for host ${hostname}`,\n      } as const;\n    }\n  }\n\n  return { ok: true, url: parsedUrl } as const;\n}\n\nexport function getRandomProxy(proxyList: string[]): string {\n  return proxyList[Math.floor(Math.random() * proxyList.length)].trim();\n}\n\nexport function matchesNoProxy(url: string, noProxy: string[]) {\n  try {\n    const urlObj = new URL(url);\n    const hostname = urlObj.hostname;\n    return hostnameMatchesAnyPattern(hostname, noProxy);\n  } catch (e) {\n    logger.error(`Failed to parse URL: ${url}: ${e}`);\n    return false;\n  }\n}\n\nexport function getProxyAgent(url: string) {\n  const { proxy } = serverConfig;\n\n  if (!proxy.httpProxy && !proxy.httpsProxy) {\n    return undefined;\n  }\n\n  const urlObj = new URL(url);\n  const protocol = urlObj.protocol;\n\n  // Check if URL should bypass proxy\n  if (proxy.noProxy && matchesNoProxy(url, proxy.noProxy)) {\n    return undefined;\n  }\n\n  if (protocol === \"https:\" && proxy.httpsProxy) {\n    const selectedProxy = getRandomProxy(proxy.httpsProxy);\n    return new HttpsProxyAgent(selectedProxy);\n  } else if (protocol === \"http:\" && proxy.httpProxy) {\n    const selectedProxy = getRandomProxy(proxy.httpProxy);\n    return new HttpProxyAgent(selectedProxy);\n  } else if (proxy.httpProxy) {\n    const selectedProxy = getRandomProxy(proxy.httpProxy);\n    return new HttpProxyAgent(selectedProxy);\n  }\n\n  return undefined;\n}\n\nfunction cloneHeaders(init?: HeadersInit): Headers {\n  const headers = new Headers();\n  if (!init) {\n    return headers;\n  }\n  if (init instanceof Headers) {\n    init.forEach((value, key) => {\n      headers.set(key, value);\n    });\n    return headers;\n  }\n\n  if (Array.isArray(init)) {\n    for (const [key, value] of init) {\n      headers.append(key, value);\n    }\n    return headers;\n  }\n\n  for (const [key, value] of Object.entries(init)) {\n    if (Array.isArray(value)) {\n      headers.set(key, value.join(\", \"));\n    } else if (value !== undefined) {\n      headers.set(key, value);\n    }\n  }\n\n  return headers;\n}\n\nfunction isRedirectResponse(response: Response): boolean {\n  return (\n    response.status === 301 ||\n    response.status === 302 ||\n    response.status === 303 ||\n    response.status === 307 ||\n    response.status === 308\n  );\n}\n\nexport type FetchWithProxyOptions = Omit<\n  RequestInit & {\n    maxRedirects?: number;\n  },\n  \"agent\"\n>;\n\ninterface PreparedFetchOptions {\n  maxRedirects: number;\n  baseHeaders: Headers;\n  method: string;\n  body?: RequestInit[\"body\"];\n  baseOptions: RequestInit;\n}\n\nexport function prepareFetchOptions(\n  options: FetchWithProxyOptions = {},\n): PreparedFetchOptions {\n  const {\n    maxRedirects = 5,\n    headers: initHeaders,\n    method: initMethod,\n    body: initBody,\n    redirect: _ignoredRedirect,\n    ...restOptions\n  } = options;\n\n  const baseOptions = restOptions as RequestInit;\n\n  return {\n    maxRedirects,\n    baseHeaders: cloneHeaders(initHeaders),\n    method: initMethod?.toUpperCase?.() ?? \"GET\",\n    body: initBody,\n    baseOptions,\n  };\n}\n\ninterface BuildFetchOptionsInput {\n  method: string;\n  body?: RequestInit[\"body\"];\n  headers: Headers;\n  agent?: RequestInit[\"agent\"];\n  baseOptions: RequestInit;\n}\n\nexport function buildFetchOptions({\n  method,\n  body,\n  headers,\n  agent,\n  baseOptions,\n}: BuildFetchOptionsInput): RequestInit {\n  return {\n    ...baseOptions,\n    method,\n    body,\n    headers,\n    agent,\n    redirect: \"manual\",\n  };\n}\n\nexport const fetchWithProxy = async (\n  url: string,\n  options: FetchWithProxyOptions = {},\n) => {\n  const {\n    maxRedirects,\n    baseHeaders,\n    method: preparedMethod,\n    body: preparedBody,\n    baseOptions,\n  } = prepareFetchOptions(options);\n\n  let redirectsRemaining = maxRedirects;\n  let currentUrl = url;\n  let currentMethod = preparedMethod;\n  let currentBody = preparedBody;\n\n  while (true) {\n    const agent = getProxyAgent(currentUrl);\n\n    const validation = await validateUrl(currentUrl, !!agent);\n    if (!validation.ok) {\n      throw new Error(validation.reason);\n    }\n    const requestUrl = validation.url;\n    currentUrl = requestUrl.toString();\n\n    const response = await fetch(\n      currentUrl,\n      buildFetchOptions({\n        method: currentMethod,\n        body: currentBody,\n        headers: baseHeaders,\n        agent,\n        baseOptions,\n      }),\n    );\n\n    if (!isRedirectResponse(response)) {\n      return response;\n    }\n\n    const locationHeader = response.headers.get(\"location\");\n    if (!locationHeader) {\n      return response;\n    }\n\n    if (redirectsRemaining <= 0) {\n      throw new Error(`Too many redirects while fetching ${url}`);\n    }\n\n    const nextUrl = new URL(locationHeader, currentUrl);\n\n    if (\n      response.status === 303 ||\n      ((response.status === 301 || response.status === 302) &&\n        currentMethod !== \"GET\" &&\n        currentMethod !== \"HEAD\")\n    ) {\n      currentMethod = \"GET\";\n      currentBody = undefined;\n      baseHeaders.delete(\"content-length\");\n    }\n\n    currentUrl = nextUrl.toString();\n    redirectsRemaining -= 1;\n  }\n};\n"
  },
  {
    "path": "apps/workers/package.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/package.json\",\n  \"name\": \"@karakeep/workers\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"@ghostery/adblocker-playwright\": \"^2.5.1\",\n    \"@hono/node-server\": \"^1.19.0\",\n    \"@hono/prometheus\": \"^1.0.2\",\n    \"@karakeep/db\": \"workspace:^0.1.0\",\n    \"@karakeep/shared\": \"workspace:^0.1.0\",\n    \"@karakeep/shared-server\": \"workspace:^0.1.0\",\n    \"@karakeep/trpc\": \"workspace:^0.1.0\",\n    \"@karakeep/tsconfig\": \"workspace:^0.1.0\",\n    \"@mozilla/readability\": \"^0.6.0\",\n    \"@tsconfig/node22\": \"^22.0.0\",\n    \"archiver\": \"^7.0.1\",\n    \"async-mutex\": \"^0.4.1\",\n    \"dompurify\": \"^3.2.4\",\n    \"dotenv\": \"^16.4.1\",\n    \"drizzle-orm\": \"^0.44.2\",\n    \"execa\": \"9.3.1\",\n    \"hono\": \"^4.10.6\",\n    \"html-entities\": \"^2.6.0\",\n    \"http-proxy-agent\": \"^7.0.2\",\n    \"https-proxy-agent\": \"^7.0.6\",\n    \"ipaddr.js\": \"^2.2.0\",\n    \"jsdom\": \"^24.0.0\",\n    \"liteque\": \"^0.7.0\",\n    \"lru-cache\": \"^11.2.2\",\n    \"metascraper\": \"^5.49.5\",\n    \"metascraper-amazon\": \"^5.49.5\",\n    \"metascraper-author\": \"^5.49.5\",\n    \"metascraper-date\": \"^5.49.5\",\n    \"metascraper-description\": \"^5.49.5\",\n    \"metascraper-image\": \"^5.49.5\",\n    \"metascraper-logo\": \"^5.49.5\",\n    \"metascraper-logo-favicon\": \"^5.49.5\",\n    \"metascraper-publisher\": \"^5.49.5\",\n    \"metascraper-readability\": \"^5.49.6\",\n    \"metascraper-title\": \"^5.49.5\",\n    \"metascraper-url\": \"^5.49.5\",\n    \"metascraper-x\": \"^5.49.5\",\n    \"metascraper-youtube\": \"^5.49.7\",\n    \"node-cron\": \"^3.0.3\",\n    \"node-fetch\": \"^3.3.2\",\n    \"pdf2json\": \"^3.1.5\",\n    \"pdf2pic\": \"^3.1.3\",\n    \"pdfjs-dist\": \"^4.2.67\",\n    \"playwright\": \"^1.58.2\",\n    \"playwright-extra\": \"^4.3.6\",\n    \"prom-client\": \"^15.1.3\",\n    \"puppeteer-extra-plugin-stealth\": \"^2.11.2\",\n    \"rss-parser\": \"^3.13.0\",\n    \"tesseract.js\": \"^7.0.0\",\n    \"tsx\": \"^4.8.1\",\n    \"typescript\": \"^5.9\",\n    \"zod\": \"^3.24.2\"\n  },\n  \"devDependencies\": {\n    \"@types/archiver\": \"^7.0.0\",\n    \"@types/jsdom\": \"^21.1.6\",\n    \"@types/node-cron\": \"^3.0.11\",\n    \"tsdown\": \"^0.12.9\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"scripts\": {\n    \"start\": \"tsx watch index.ts\",\n    \"start:prod\": \"tsx index.ts\",\n    \"build\": \"tsdown\",\n    \"build:watch\": \"tsdown --watch\",\n    \"lint\": \"oxlint .\",\n    \"lint:fix\": \"oxlint . --fix\",\n    \"format\": \"oxfmt --check .\",\n    \"format:fix\": \"oxfmt .\",\n    \"test\": \"vitest run\",\n    \"test:watch\": \"vitest\",\n    \"typecheck\": \"tsc --noEmit\"\n  }\n}\n"
  },
  {
    "path": "apps/workers/scripts/parseHtmlSubprocess.ts",
    "content": "import { Readability } from \"@mozilla/readability\";\nimport DOMPurify from \"dompurify\";\nimport { HttpProxyAgent } from \"http-proxy-agent\";\nimport { HttpsProxyAgent } from \"https-proxy-agent\";\nimport { JSDOM, VirtualConsole } from \"jsdom\";\nimport metascraper from \"metascraper\";\nimport metascraperAmazon from \"metascraper-amazon\";\nimport metascraperAuthor from \"metascraper-author\";\nimport metascraperDate from \"metascraper-date\";\nimport metascraperDescription from \"metascraper-description\";\nimport metascraperImage from \"metascraper-image\";\nimport metascraperLogo from \"metascraper-logo-favicon\";\nimport metascraperPublisher from \"metascraper-publisher\";\nimport metascraperTitle from \"metascraper-title\";\nimport metascraperUrl from \"metascraper-url\";\nimport metascraperX from \"metascraper-x\";\nimport metascraperYoutube from \"metascraper-youtube\";\nimport { getRandomProxy } from \"network\";\nimport winston from \"winston\";\n\nimport serverConfig from \"@karakeep/shared/config\";\nimport logger from \"@karakeep/shared/logger\";\n\nimport metascraperAmazonImproved from \"../metascraper-plugins/metascraper-amazon-improved\";\nimport metascraperReddit from \"../metascraper-plugins/metascraper-reddit\";\nimport {\n  parseSubprocessErrorSchema,\n  parseSubprocessInputSchema,\n  parseSubprocessOutputSchema,\n} from \"../workers/utils/parseHtmlSubprocessIpc\";\n\n// Redirect all log output to stderr so it doesn't interfere with the JSON protocol on stdout.\nlogger.clear();\nlogger.add(new winston.transports.Stream({ stream: process.stderr }));\n\nconst metascraperParser = metascraper([\n  metascraperDate({\n    dateModified: true,\n    datePublished: true,\n  }),\n  metascraperAmazonImproved(),\n  metascraperAmazon(),\n  metascraperYoutube({\n    gotOpts: {\n      agent: {\n        http: serverConfig.proxy.httpProxy\n          ? new HttpProxyAgent(getRandomProxy(serverConfig.proxy.httpProxy))\n          : undefined,\n        https: serverConfig.proxy.httpsProxy\n          ? new HttpsProxyAgent(getRandomProxy(serverConfig.proxy.httpsProxy))\n          : undefined,\n      },\n    },\n  }),\n  metascraperReddit(),\n  metascraperAuthor(),\n  metascraperPublisher(),\n  metascraperTitle(),\n  metascraperDescription(),\n  metascraperX(),\n  metascraperImage(),\n  metascraperLogo({\n    gotOpts: {\n      agent: {\n        http: serverConfig.proxy.httpProxy\n          ? new HttpProxyAgent(getRandomProxy(serverConfig.proxy.httpProxy))\n          : undefined,\n        https: serverConfig.proxy.httpsProxy\n          ? new HttpsProxyAgent(getRandomProxy(serverConfig.proxy.httpsProxy))\n          : undefined,\n      },\n    },\n  }),\n  metascraperUrl(),\n]);\n\n/**\n * Many sites use custom data-* attributes for lazy loading images instead of the\n * standard `src` attribute (e.g. WeChat's data-src, data-actualsrc, data-srv, or\n * the common data-original / data-lazy patterns used by jQuery plugins).\n *\n * Readability and DOMPurify both operate on the DOM, so images with no `src` are\n * either ignored or their placeholders are stripped.  We normalise these attributes\n * to `src` *before* passing the document to Readability so that the extracted\n * article retains the actual image URLs.\n */\nconst LAZY_SRC_ATTRS = [\n  \"data-src\",\n  \"data-actualsrc\",\n  \"data-srv\",\n  \"data-original\",\n  \"data-lazy\",\n  \"data-lazyload\",\n  \"data-img-src\",\n  \"data-url\",\n];\n\nfunction normalizeLazyLoadImages(document: Document): void {\n  const images = document.querySelectorAll(\"img\");\n  for (const img of images) {\n    // Only fill in src if it is absent or a known placeholder (blank / data:)\n    const currentSrc = img.getAttribute(\"src\") ?? \"\";\n    const needsSrc =\n      !currentSrc ||\n      currentSrc === \"#\" ||\n      currentSrc.startsWith(\"data:image/gif\") ||\n      currentSrc.startsWith(\"data:image/png\");\n\n    if (!needsSrc) {\n      continue;\n    }\n\n    for (const attr of LAZY_SRC_ATTRS) {\n      const value = img.getAttribute(attr);\n      if (value && value.trim() && !value.startsWith(\"data:\")) {\n        img.setAttribute(\"src\", value.trim());\n        break;\n      }\n    }\n  }\n}\n\nfunction extractReadableContent(\n  htmlContent: string,\n  url: string,\n): { content: string } | null {\n  const virtualConsole = new VirtualConsole();\n  const dom = new JSDOM(htmlContent, { url, virtualConsole });\n  try {\n    normalizeLazyLoadImages(dom.window.document);\n    const readableContent = new Readability(dom.window.document).parse();\n    if (!readableContent || typeof readableContent.content !== \"string\") {\n      return null;\n    }\n\n    const purifyWindow = new JSDOM(\"\").window;\n    try {\n      const purify = DOMPurify(purifyWindow);\n      const purifiedHTML = purify.sanitize(readableContent.content);\n      return { content: purifiedHTML };\n    } finally {\n      purifyWindow.close();\n    }\n  } finally {\n    dom.window.close();\n  }\n}\n\nasync function main() {\n  // Read all of stdin\n  const chunks: Buffer[] = [];\n  for await (const chunk of process.stdin) {\n    chunks.push(chunk);\n  }\n  const input = parseSubprocessInputSchema.parse(\n    JSON.parse(Buffer.concat(chunks).toString()),\n  );\n  const { htmlContent, url, jobId } = input;\n\n  logger.info(\n    `[Crawler][${jobId}] Will attempt to extract metadata from page ...`,\n  );\n\n  // Run metascraper\n  const meta = await metascraperParser({\n    url,\n    html: htmlContent,\n    validateUrl: false,\n  });\n\n  logger.info(`[Crawler][${jobId}] Done extracting metadata from the page.`);\n\n  // Conditionally run readability (skip if metascraper already provided readable content, e.g. Reddit plugin)\n  let readableContent: { content: string } | null = null;\n  if (meta.readableContentHtml) {\n    // Sanitize plugin-provided HTML through DOMPurify (the extractReadableContent\n    // path already does this, but the direct-content path was missing it).\n    const purifyWindow = new JSDOM(\"\").window;\n    try {\n      const purify = DOMPurify(purifyWindow);\n      const purifiedHTML = purify.sanitize(meta.readableContentHtml);\n      readableContent = { content: purifiedHTML };\n    } finally {\n      purifyWindow.close();\n    }\n  }\n\n  if (!readableContent) {\n    logger.info(\n      `[Crawler][${jobId}] Will attempt to extract readable content ...`,\n    );\n    readableContent = extractReadableContent(\n      meta.contentHtml ?? htmlContent,\n      url,\n    );\n    logger.info(`[Crawler][${jobId}] Done extracting readable content.`);\n  }\n\n  const output = parseSubprocessOutputSchema.parse({\n    metadata: meta,\n    readableContent,\n  });\n\n  // Write the result as JSON to stdout\n  process.stdout.write(JSON.stringify(output));\n}\n\nmain().catch(async (err: unknown) => {\n  const errorOutput = parseSubprocessErrorSchema.parse({\n    error: err instanceof Error ? err.message : String(err),\n    stack: err instanceof Error ? err.stack : undefined,\n  });\n\n  const json = JSON.stringify(errorOutput);\n  if (!process.stdout.write(json)) {\n    await new Promise<void>((resolve) => process.stdout.once(\"drain\", resolve));\n  }\n\n  process.exitCode = 1;\n});\n"
  },
  {
    "path": "apps/workers/server.ts",
    "content": "import { serve } from \"@hono/node-server\";\nimport { Hono } from \"hono\";\nimport { bearerAuth } from \"hono/bearer-auth\";\n\nimport serverConfig from \"@karakeep/shared/config\";\nimport logger from \"@karakeep/shared/logger\";\n\nimport { printMetrics } from \"./metrics\";\n\nconst app = new Hono()\n  .get(\"/health\", (c) =>\n    c.json({ status: \"ok\", timestamp: new Date().toISOString() }),\n  )\n  .get(\n    \"/metrics\",\n    bearerAuth({ token: serverConfig.prometheus.metricsToken }),\n    printMetrics,\n  );\n\nexport function buildServer() {\n  const server = serve(\n    {\n      fetch: app.fetch,\n      port: serverConfig.workers.port,\n      hostname: serverConfig.workers.host,\n    },\n    (info) => {\n      logger.info(`Listening on http://${info.address}:${info.port}`);\n    },\n  );\n  return {\n    _server: server,\n    stop: () =>\n      new Promise<void>((resolve, reject) => {\n        server.close((err) => {\n          if (err) {\n            reject(err);\n          } else {\n            resolve();\n          }\n        });\n      }),\n    serve: () =>\n      new Promise<void>((resolve, reject) => {\n        server.on(\"error\", reject);\n        server.on(\"close\", () => resolve());\n      }),\n  };\n}\n\nexport default app;\n"
  },
  {
    "path": "apps/workers/trpc.ts",
    "content": "import { createCallerFactory } from \"@karakeep/trpc\";\nimport { buildImpersonatingAuthedContext as buildAuthedContext } from \"@karakeep/trpc/lib/impersonate\";\nimport { appRouter } from \"@karakeep/trpc/routers/_app\";\n\nexport const buildImpersonatingAuthedContext = buildAuthedContext;\n\n/**\n * This is only safe to use in the context of a worker.\n */\nexport async function buildImpersonatingTRPCClient(userId: string) {\n  const createCaller = createCallerFactory(appRouter);\n\n  return createCaller(await buildImpersonatingAuthedContext(userId));\n}\n"
  },
  {
    "path": "apps/workers/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@karakeep/tsconfig/node.json\",\n  \"include\": [\"**/*.ts\"],\n  \"exclude\": [\"node_modules\"],\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"tsBuildInfoFile\": \"node_modules/.cache/tsbuildinfo.json\"\n  }\n}\n"
  },
  {
    "path": "apps/workers/tsdown.config.ts",
    "content": "import { defineConfig } from \"tsdown\";\n\nexport default defineConfig({\n  entry: [\"index.ts\", \"scripts/parseHtmlSubprocess.ts\"],\n  format: [\"esm\"],\n  target: \"node22\",\n  outDir: \"dist\",\n  clean: true,\n  minify: false,\n  sourcemap: true,\n  platform: \"node\",\n  shims: true,\n  external: [\n    // Keep native binaries external (transitive deps of bundled workspace packages)\n    \"better-sqlite3\",\n    \"liteque\",\n  ],\n  noExternal: [\n    // Bundle workspace packages (since they're not published to npm)\n    /^@karakeep\\//,\n  ],\n});\n"
  },
  {
    "path": "apps/workers/utils.ts",
    "content": "export function withTimeout<T, Ret>(\n  func: (param: T) => Promise<Ret>,\n  timeoutSec: number,\n) {\n  return async (param: T): Promise<Ret> => {\n    return await Promise.race([\n      func(param),\n      new Promise<Ret>((_resolve, reject) =>\n        setTimeout(\n          () => reject(new Error(`Timed-out after ${timeoutSec} secs`)),\n          timeoutSec * 1000,\n        ),\n      ),\n    ]);\n  };\n}\n"
  },
  {
    "path": "apps/workers/workerTracing.ts",
    "content": "import type { DequeuedJob } from \"@karakeep/shared/queueing\";\nimport { getTracer, withSpan } from \"@karakeep/shared-server\";\n\nconst tracer = getTracer(\"@karakeep/workers\");\n\ntype WorkerRunFn<TData, TResult = void> = (\n  job: DequeuedJob<TData>,\n) => Promise<TResult>;\n\n/**\n * Wraps a worker run function with OpenTelemetry tracing.\n * Creates a span for each job execution and automatically handles error recording.\n *\n * @param name - The name of the span (e.g., \"feedWorker.run\", \"crawlerWorker.run\")\n * @param fn - The worker run function to wrap\n * @returns A wrapped function that executes within a traced span\n *\n * @example\n * ```ts\n * const run = withWorkerTracing(\"feedWorker.run\", async (job) => {\n *   // Your worker logic here\n * });\n * ```\n */\nexport function withWorkerTracing<TData, TResult = void>(\n  name: string,\n  fn: WorkerRunFn<TData, TResult>,\n): WorkerRunFn<TData, TResult> {\n  return async (job: DequeuedJob<TData>): Promise<TResult> => {\n    return await withSpan(\n      tracer,\n      name,\n      {\n        attributes: {\n          \"job.id\": job.id,\n          \"job.priority\": job.priority,\n          \"job.runNumber\": job.runNumber,\n        },\n      },\n      () => fn(job),\n    );\n  };\n}\n"
  },
  {
    "path": "apps/workers/workerUtils.ts",
    "content": "import { eq } from \"drizzle-orm\";\n\nimport { db, KarakeepDBTransaction } from \"@karakeep/db\";\nimport { assets, AssetTypes, bookmarks } from \"@karakeep/db/schema\";\n\ntype DBAssetType = typeof assets.$inferInsert;\nexport async function updateAsset(\n  oldAssetId: string | undefined,\n  newAsset: DBAssetType,\n  txn: KarakeepDBTransaction,\n) {\n  if (oldAssetId) {\n    await txn.delete(assets).where(eq(assets.id, oldAssetId));\n  }\n\n  await txn.insert(assets).values(newAsset);\n}\n\nexport async function getBookmarkDetails(bookmarkId: string) {\n  const bookmark = await db.query.bookmarks.findFirst({\n    where: eq(bookmarks.id, bookmarkId),\n    with: {\n      link: true,\n      assets: true,\n    },\n  });\n\n  if (!bookmark || !bookmark.link) {\n    throw new Error(\"The bookmark either doesn't exist or is not a link\");\n  }\n  return {\n    url: bookmark.link.url,\n    userId: bookmark.userId,\n    createdAt: bookmark.createdAt,\n    crawledAt: bookmark.link.crawledAt,\n    screenshotAssetId: bookmark.assets.find(\n      (a) => a.assetType == AssetTypes.LINK_SCREENSHOT,\n    )?.id,\n    pdfAssetId: bookmark.assets.find((a) => a.assetType == AssetTypes.LINK_PDF)\n      ?.id,\n    imageAssetId: bookmark.assets.find(\n      (a) => a.assetType == AssetTypes.LINK_BANNER_IMAGE,\n    )?.id,\n    fullPageArchiveAssetId: bookmark.assets.find(\n      (a) => a.assetType == AssetTypes.LINK_FULL_PAGE_ARCHIVE,\n    )?.id,\n    videoAssetId: bookmark.assets.find(\n      (a) => a.assetType == AssetTypes.LINK_VIDEO,\n    )?.id,\n    precrawledArchiveAssetId: bookmark.assets\n      .filter((a) => a.assetType == AssetTypes.LINK_PRECRAWLED_ARCHIVE)\n      .at(-1)?.id,\n    contentAssetId: bookmark.assets.find(\n      (a) => a.assetType == AssetTypes.LINK_HTML_CONTENT,\n    )?.id,\n  };\n}\n"
  },
  {
    "path": "apps/workers/workers/adminMaintenance/tasks/migrateLinkHtmlContent.ts",
    "content": "import { and, asc, eq, gt, isNotNull, isNull, sql } from \"drizzle-orm\";\n\nimport type { ZAdminMaintenanceMigrateLargeLinkHtmlTask } from \"@karakeep/shared-server\";\nimport type { DequeuedJob } from \"@karakeep/shared/queueing\";\nimport { db } from \"@karakeep/db\";\nimport { AssetTypes, bookmarkLinks, bookmarks } from \"@karakeep/db/schema\";\nimport { QuotaService } from \"@karakeep/shared-server\";\nimport {\n  ASSET_TYPES,\n  deleteAsset,\n  newAssetId,\n  saveAsset,\n} from \"@karakeep/shared/assetdb\";\nimport serverConfig from \"@karakeep/shared/config\";\nimport logger from \"@karakeep/shared/logger\";\nimport { tryCatch } from \"@karakeep/shared/tryCatch\";\n\nimport { updateAsset } from \"../../../workerUtils\";\n\nconst BATCH_SIZE = 25;\n\ninterface BookmarkHtmlRow {\n  bookmarkId: string;\n  userId: string;\n  htmlContent: string;\n}\n\nasync function getBookmarksWithLargeInlineHtml(limit: number, cursor?: string) {\n  const rows = await db\n    .select({\n      bookmarkId: bookmarkLinks.id,\n      userId: bookmarks.userId,\n      htmlContent: bookmarkLinks.htmlContent,\n    })\n    .from(bookmarkLinks)\n    .innerJoin(bookmarks, eq(bookmarkLinks.id, bookmarks.id))\n    .where(\n      cursor\n        ? and(\n            gt(bookmarkLinks.id, cursor),\n            isNotNull(bookmarkLinks.htmlContent),\n            isNull(bookmarkLinks.contentAssetId),\n            sql`length(CAST(${bookmarkLinks.htmlContent} AS BLOB)) > ${serverConfig.crawler.htmlContentSizeThreshold}`,\n          )\n        : and(\n            isNotNull(bookmarkLinks.htmlContent),\n            isNull(bookmarkLinks.contentAssetId),\n            sql`length(CAST(${bookmarkLinks.htmlContent} AS BLOB)) > ${serverConfig.crawler.htmlContentSizeThreshold}`,\n          ),\n    )\n    .orderBy(asc(bookmarkLinks.id))\n    .limit(limit);\n\n  return rows.filter((row): row is BookmarkHtmlRow => row.htmlContent !== null);\n}\n\nasync function migrateBookmarkHtml(\n  bookmark: BookmarkHtmlRow,\n  jobId: string,\n): Promise<boolean> {\n  const { bookmarkId, userId, htmlContent } = bookmark;\n\n  const contentSize = Buffer.byteLength(htmlContent, \"utf8\");\n\n  if (contentSize <= serverConfig.crawler.htmlContentSizeThreshold) {\n    logger.debug(\n      `[adminMaintenance:migrate_large_link_html][${jobId}] Bookmark ${bookmarkId} inline HTML (${contentSize} bytes) below threshold, skipping`,\n    );\n    return false;\n  }\n\n  const { data: quotaApproved, error: quotaError } = await tryCatch(\n    QuotaService.checkStorageQuota(db, userId, contentSize),\n  );\n\n  if (quotaError || !quotaApproved) {\n    logger.warn(\n      `[adminMaintenance:migrate_large_link_html][${jobId}] Skipping bookmark ${bookmarkId} due to storage quota error: ${quotaError?.message}`,\n    );\n    return false;\n  }\n\n  const contentBuffer = Buffer.from(htmlContent, \"utf8\");\n  const assetId = newAssetId();\n  const { error: saveError } = await tryCatch(\n    saveAsset({\n      userId,\n      assetId,\n      asset: contentBuffer,\n      metadata: { contentType: ASSET_TYPES.TEXT_HTML, fileName: null },\n      quotaApproved,\n    }),\n  );\n\n  if (saveError) {\n    logger.error(\n      `[adminMaintenance:migrate_large_link_html][${jobId}] Failed to persist HTML for bookmark ${bookmarkId} as asset: ${saveError}`,\n    );\n    await deleteAsset({ userId, assetId }).catch(() => {\n      /* ignore */\n    });\n    return false;\n  }\n\n  try {\n    await db.transaction(async (txn) => {\n      const res = await txn\n        .update(bookmarkLinks)\n        .set({ htmlContent: null, contentAssetId: assetId })\n        .where(\n          and(\n            eq(bookmarkLinks.id, bookmarkId),\n            isNull(bookmarkLinks.contentAssetId),\n          ),\n        );\n\n      if (res.changes === 0) {\n        throw new Error(\"Failed to update bookmark\");\n      }\n\n      await updateAsset(\n        undefined,\n        {\n          id: assetId,\n          bookmarkId,\n          userId,\n          assetType: AssetTypes.LINK_HTML_CONTENT,\n          contentType: ASSET_TYPES.TEXT_HTML,\n          size: contentSize,\n          fileName: null,\n        },\n        txn,\n      );\n    });\n  } catch (error) {\n    await deleteAsset({ userId, assetId }).catch(() => {\n      /* ignore */\n    });\n    logger.error(\n      `[adminMaintenance:migrate_large_link_html][${jobId}] Failed to update bookmark ${bookmarkId} after storing asset: ${error}`,\n    );\n    return false;\n  }\n\n  logger.info(\n    `[adminMaintenance:migrate_large_link_html][${jobId}] Migrated inline HTML (${contentSize} bytes) for bookmark ${bookmarkId} to asset ${assetId}`,\n  );\n\n  return true;\n}\n\nexport async function runMigrateLargeLinkHtmlTask(\n  job: DequeuedJob<ZAdminMaintenanceMigrateLargeLinkHtmlTask>,\n): Promise<void> {\n  const jobId = job.id;\n  let migratedCount = 0;\n  let cursor: string | undefined;\n\n  while (true) {\n    if (job.abortSignal.aborted) {\n      logger.warn(\n        `[adminMaintenance:migrate_large_link_html][${jobId}] Aborted`,\n      );\n      break;\n    }\n    const bookmarksToMigrate = await getBookmarksWithLargeInlineHtml(\n      BATCH_SIZE,\n      cursor,\n    );\n\n    if (bookmarksToMigrate.length === 0) {\n      break;\n    }\n\n    for (const bookmark of bookmarksToMigrate) {\n      try {\n        const migrated = await migrateBookmarkHtml(bookmark, jobId);\n        if (migrated) {\n          migratedCount += 1;\n        }\n      } catch (error) {\n        logger.error(\n          `[adminMaintenance:migrate_large_link_html][${jobId}] Unexpected error migrating bookmark ${bookmark.bookmarkId}: ${error}`,\n        );\n      }\n    }\n\n    cursor = bookmarksToMigrate[bookmarksToMigrate.length - 1]?.bookmarkId;\n  }\n\n  logger.info(\n    `[adminMaintenance:migrate_large_link_html][${jobId}] Completed migration. Total bookmarks migrated: ${migratedCount}`,\n  );\n}\n"
  },
  {
    "path": "apps/workers/workers/adminMaintenance/tasks/tidyAssets.ts",
    "content": "import { eq } from \"drizzle-orm\";\n\nimport { db } from \"@karakeep/db\";\nimport { assets } from \"@karakeep/db/schema\";\nimport {\n  ZAdminMaintenanceTidyAssetsTask,\n  ZTidyAssetsRequest,\n  zTidyAssetsRequestSchema,\n} from \"@karakeep/shared-server\";\nimport { deleteAsset, getAllAssets } from \"@karakeep/shared/assetdb\";\nimport logger from \"@karakeep/shared/logger\";\nimport { DequeuedJob } from \"@karakeep/shared/queueing\";\n\nasync function handleAsset(\n  asset: {\n    assetId: string;\n    userId: string;\n    size: number;\n    contentType: string;\n    fileName?: string | null;\n  },\n  request: ZTidyAssetsRequest,\n  jobId: string,\n) {\n  const dbRow = await db.query.assets.findFirst({\n    where: eq(assets.id, asset.assetId),\n  });\n  if (!dbRow) {\n    if (request.cleanDanglingAssets) {\n      await deleteAsset({ userId: asset.userId, assetId: asset.assetId });\n      logger.info(\n        `[adminMaintenance:tidy_assets][${jobId}] Asset ${asset.assetId} not found in the database. Deleting it.`,\n      );\n    } else {\n      logger.warn(\n        `[adminMaintenance:tidy_assets][${jobId}] Asset ${asset.assetId} not found in the database. Not deleting it because cleanDanglingAssets is false.`,\n      );\n    }\n    return;\n  }\n\n  if (request.syncAssetMetadata) {\n    await db\n      .update(assets)\n      .set({\n        contentType: asset.contentType,\n        fileName: asset.fileName,\n        size: asset.size,\n      })\n      .where(eq(assets.id, asset.assetId));\n    logger.info(\n      `[adminMaintenance:tidy_assets][${jobId}] Updated metadata for asset ${asset.assetId}`,\n    );\n  }\n}\n\nexport async function runTidyAssetsTask(\n  job: DequeuedJob<ZAdminMaintenanceTidyAssetsTask>,\n  task: ZAdminMaintenanceTidyAssetsTask,\n) {\n  const jobId = job.id;\n  const parseResult = zTidyAssetsRequestSchema.safeParse(task.args);\n  if (!parseResult.success) {\n    throw new Error(\n      `[adminMaintenance:tidy_assets][${jobId}] Got malformed tidy asset args: ${parseResult.error.toString()}`,\n    );\n  }\n\n  for await (const asset of getAllAssets()) {\n    if (job.abortSignal.aborted) {\n      logger.warn(`[adminMaintenance:tidy_assets][${jobId}] Aborted`);\n      break;\n    }\n    try {\n      await handleAsset(asset, parseResult.data, jobId);\n    } catch (error) {\n      logger.error(\n        `[adminMaintenance:tidy_assets][${jobId}] Failed to tidy asset ${asset.assetId}: ${error}`,\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "apps/workers/workers/adminMaintenanceWorker.ts",
    "content": "import { workerStatsCounter } from \"metrics\";\nimport { withWorkerTracing } from \"workerTracing\";\n\nimport {\n  AdminMaintenanceQueue,\n  ZAdminMaintenanceMigrateLargeLinkHtmlTask,\n  ZAdminMaintenanceTask,\n  zAdminMaintenanceTaskSchema,\n  ZAdminMaintenanceTidyAssetsTask,\n} from \"@karakeep/shared-server\";\nimport logger from \"@karakeep/shared/logger\";\nimport { DequeuedJob, getQueueClient } from \"@karakeep/shared/queueing\";\n\nimport { runMigrateLargeLinkHtmlTask } from \"./adminMaintenance/tasks/migrateLinkHtmlContent\";\nimport { runTidyAssetsTask } from \"./adminMaintenance/tasks/tidyAssets\";\n\nexport class AdminMaintenanceWorker {\n  static async build() {\n    logger.info(\"Starting admin maintenance worker ...\");\n    const worker =\n      (await getQueueClient())!.createRunner<ZAdminMaintenanceTask>(\n        AdminMaintenanceQueue,\n        {\n          run: withWorkerTracing(\n            \"adminMaintenanceWorker.run\",\n            runAdminMaintenance,\n          ),\n          onComplete: (job) => {\n            workerStatsCounter\n              .labels(`adminMaintenance:${job.data.type}`, \"completed\")\n              .inc();\n            logger.info(\n              `[adminMaintenance:${job.data.type}][${job.id}] Completed successfully`,\n            );\n            return Promise.resolve();\n          },\n          onError: (job) => {\n            workerStatsCounter\n              .labels(`adminMaintenance:${job.data?.type}`, \"failed\")\n              .inc();\n            if (job.numRetriesLeft == 0) {\n              workerStatsCounter\n                .labels(\n                  `adminMaintenance:${job.data?.type}`,\n                  \"failed_permanent\",\n                )\n                .inc();\n            }\n            logger.error(\n              `[adminMaintenance:${job.data?.type}][${job.id}] Job failed: ${job.error}\\n${job.error.stack}`,\n            );\n            return Promise.resolve();\n          },\n        },\n        {\n          concurrency: 1,\n          pollIntervalMs: 1000,\n          timeoutSecs: 600,\n        },\n      );\n\n    return worker;\n  }\n}\n\nasync function runAdminMaintenance(job: DequeuedJob<ZAdminMaintenanceTask>) {\n  const jobId = job.id;\n  const parsed = zAdminMaintenanceTaskSchema.safeParse(job.data);\n  if (!parsed.success) {\n    throw new Error(\n      `[adminMaintenance][${jobId}] Got malformed job request: ${parsed.error.toString()}`,\n    );\n  }\n\n  const task = parsed.data;\n\n  switch (task.type) {\n    case \"tidy_assets\":\n      return runTidyAssetsTask(\n        job as DequeuedJob<ZAdminMaintenanceTidyAssetsTask>,\n        task,\n      );\n    case \"migrate_large_link_html\":\n      return runMigrateLargeLinkHtmlTask(\n        job as DequeuedJob<ZAdminMaintenanceMigrateLargeLinkHtmlTask>,\n      );\n    default: {\n      const exhaustiveCheck: never = task;\n      throw new Error(\n        `[adminMaintenance][${jobId}] No handler registered for task ${(exhaustiveCheck as ZAdminMaintenanceTask).type}`,\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "apps/workers/workers/assetPreprocessingWorker.ts",
    "content": "import os from \"os\";\nimport { eq } from \"drizzle-orm\";\nimport { workerStatsCounter } from \"metrics\";\nimport PDFParser from \"pdf2json\";\nimport { fromBuffer } from \"pdf2pic\";\nimport { createWorker } from \"tesseract.js\";\nimport { withWorkerTracing } from \"workerTracing\";\n\nimport type { AssetPreprocessingRequest } from \"@karakeep/shared-server\";\nimport { db } from \"@karakeep/db\";\nimport {\n  assets,\n  AssetTypes,\n  bookmarkAssets,\n  bookmarks,\n} from \"@karakeep/db/schema\";\nimport {\n  AssetPreprocessingQueue,\n  OpenAIQueue,\n  QuotaService,\n  StorageQuotaError,\n  triggerSearchReindex,\n} from \"@karakeep/shared-server\";\nimport { newAssetId, readAsset, saveAsset } from \"@karakeep/shared/assetdb\";\nimport serverConfig from \"@karakeep/shared/config\";\nimport { InferenceClientFactory } from \"@karakeep/shared/inference\";\nimport logger from \"@karakeep/shared/logger\";\nimport { buildOCRPrompt } from \"@karakeep/shared/prompts\";\nimport {\n  DequeuedJob,\n  EnqueueOptions,\n  getQueueClient,\n} from \"@karakeep/shared/queueing\";\n\nexport class AssetPreprocessingWorker {\n  static async build() {\n    logger.info(\"Starting asset preprocessing worker ...\");\n    const worker =\n      (await getQueueClient())!.createRunner<AssetPreprocessingRequest>(\n        AssetPreprocessingQueue,\n        {\n          run: withWorkerTracing(\"assetPreprocessingWorker.run\", run),\n          onComplete: async (job) => {\n            workerStatsCounter.labels(\"assetPreprocessing\", \"completed\").inc();\n            const jobId = job.id;\n            logger.info(\n              `[assetPreprocessing][${jobId}] Completed successfully`,\n            );\n            return Promise.resolve();\n          },\n          onError: async (job) => {\n            workerStatsCounter.labels(\"assetPreProcessing\", \"failed\").inc();\n            if (job.numRetriesLeft == 0) {\n              workerStatsCounter\n                .labels(\"assetPreProcessing\", \"failed_permanent\")\n                .inc();\n            }\n            const jobId = job.id;\n            logger.error(\n              `[assetPreprocessing][${jobId}] Asset preprocessing failed: ${job.error}\\n${job.error.stack}`,\n            );\n            return Promise.resolve();\n          },\n        },\n        {\n          concurrency: serverConfig.assetPreprocessing.numWorkers,\n          pollIntervalMs: 1000,\n          timeoutSecs: serverConfig.assetPreprocessing.jobTimeoutSec,\n        },\n      );\n\n    return worker;\n  }\n}\n\nasync function readImageText(buffer: Buffer) {\n  if (serverConfig.ocr.langs.length == 1 && serverConfig.ocr.langs[0] == \"\") {\n    return null;\n  }\n  const worker = await createWorker(serverConfig.ocr.langs, undefined, {\n    cachePath: serverConfig.ocr.cacheDir ?? os.tmpdir(),\n  });\n  try {\n    const ret = await worker.recognize(buffer);\n    if (ret.data.confidence <= serverConfig.ocr.confidenceThreshold) {\n      return null;\n    }\n    return ret.data.text;\n  } finally {\n    await worker.terminate();\n  }\n}\n\nasync function readImageTextWithLLM(\n  buffer: Buffer,\n  contentType: string,\n): Promise<string | null> {\n  const inferenceClient = InferenceClientFactory.build();\n  if (!inferenceClient) {\n    logger.warn(\n      \"[assetPreprocessing] LLM OCR is enabled but no inference client is configured. Falling back to Tesseract.\",\n    );\n    return readImageText(buffer);\n  }\n\n  const base64 = buffer.toString(\"base64\");\n  const prompt = buildOCRPrompt();\n\n  const response = await inferenceClient.inferFromImage(\n    prompt,\n    contentType,\n    base64,\n    { schema: null },\n  );\n\n  const extractedText = response.response.trim();\n  if (!extractedText) {\n    return null;\n  }\n\n  return extractedText;\n}\n\nasync function readPDFText(buffer: Buffer): Promise<{\n  text: string;\n  metadata: Record<string, object>;\n}> {\n  return new Promise((resolve, reject) => {\n    const pdfParser = new PDFParser(null, true);\n    pdfParser.on(\"pdfParser_dataError\", reject);\n    pdfParser.on(\"pdfParser_dataReady\", (pdfData) => {\n      resolve({\n        text: pdfParser.getRawTextContent(),\n        metadata: pdfData.Meta,\n      });\n    });\n    pdfParser.parseBuffer(buffer);\n  });\n}\n\nexport async function extractAndSavePDFScreenshot(\n  jobId: string,\n  asset: Buffer,\n  bookmark: NonNullable<Awaited<ReturnType<typeof getBookmark>>>,\n  isFixMode: boolean,\n): Promise<boolean> {\n  {\n    const alreadyHasScreenshot =\n      bookmark.assets.find(\n        (r) => r.assetType === AssetTypes.ASSET_SCREENSHOT,\n      ) !== undefined;\n    if (alreadyHasScreenshot && isFixMode) {\n      logger.info(\n        `[assetPreprocessing][${jobId}] Skipping PDF screenshot generation as it's already been generated.`,\n      );\n      return false;\n    }\n  }\n  logger.info(\n    `[assetPreprocessing][${jobId}] Attempting to generate PDF screenshot for bookmarkId: ${bookmark.id}`,\n  );\n  try {\n    /**\n     * If you encountered any issues with this library, make sure you have ghostscript and graphicsmagick installed following this URL\n     * https://github.com/yakovmeister/pdf2image/blob/HEAD/docs/gm-installation.md\n     */\n    const screenshot = await fromBuffer(asset, {\n      density: 100,\n      quality: 100,\n      format: \"png\",\n      preserveAspectRatio: true,\n    })(1, { responseType: \"buffer\" });\n\n    if (!screenshot.buffer) {\n      logger.error(\n        `[assetPreprocessing][${jobId}] Failed to generate PDF screenshot`,\n      );\n      return false;\n    }\n\n    // Check storage quota before inserting\n    const quotaApproved = await QuotaService.checkStorageQuota(\n      db,\n      bookmark.userId,\n      screenshot.buffer.byteLength,\n    );\n\n    // Store the screenshot\n    const assetId = newAssetId();\n    const fileName = \"screenshot.png\";\n    const contentType = \"image/png\";\n    await saveAsset({\n      userId: bookmark.userId,\n      assetId,\n      asset: screenshot.buffer,\n      metadata: {\n        contentType,\n        fileName,\n      },\n      quotaApproved,\n    });\n\n    // Insert into database\n    await db.insert(assets).values({\n      id: assetId,\n      bookmarkId: bookmark.id,\n      userId: bookmark.userId,\n      assetType: AssetTypes.ASSET_SCREENSHOT,\n      contentType,\n      size: screenshot.buffer.byteLength,\n      fileName,\n    });\n\n    logger.info(\n      `[assetPreprocessing][${jobId}] Successfully saved PDF screenshot to database`,\n    );\n    return true;\n  } catch (error) {\n    if (error instanceof StorageQuotaError) {\n      logger.warn(\n        `[assetPreprocessing][${jobId}] Skipping PDF screenshot due to quota exceeded: ${error.message}`,\n      );\n      return true; // Return true to indicate the job completed successfully, just skipped the asset\n    }\n    logger.error(\n      `[assetPreprocessing][${jobId}] Failed to process PDF screenshot: ${error}`,\n    );\n    return false;\n  }\n}\n\nasync function extractAndSaveImageText(\n  jobId: string,\n  asset: Buffer,\n  contentType: string,\n  bookmark: NonNullable<Awaited<ReturnType<typeof getBookmark>>>,\n  isFixMode: boolean,\n): Promise<boolean> {\n  {\n    const alreadyHasText = !!bookmark.asset.content;\n    if (alreadyHasText && isFixMode) {\n      logger.info(\n        `[assetPreprocessing][${jobId}] Skipping image text extraction as it's already been extracted.`,\n      );\n      return false;\n    }\n  }\n  let imageText = null;\n\n  if (serverConfig.ocr.useLLM) {\n    logger.info(\n      `[assetPreprocessing][${jobId}] Attempting to extract text from image using LLM OCR.`,\n    );\n    try {\n      imageText = await readImageTextWithLLM(asset, contentType);\n    } catch (e) {\n      logger.error(\n        `[assetPreprocessing][${jobId}] Failed to read image text with LLM: ${e}`,\n      );\n    }\n  } else {\n    logger.info(\n      `[assetPreprocessing][${jobId}] Attempting to extract text from image using Tesseract.`,\n    );\n    try {\n      imageText = await readImageText(asset);\n    } catch (e) {\n      logger.error(\n        `[assetPreprocessing][${jobId}] Failed to read image text: ${e}`,\n      );\n    }\n  }\n\n  if (!imageText) {\n    return false;\n  }\n\n  logger.info(\n    `[assetPreprocessing][${jobId}] Extracted ${imageText.length} characters from image.`,\n  );\n  await db\n    .update(bookmarkAssets)\n    .set({\n      content: imageText,\n      metadata: null,\n    })\n    .where(eq(bookmarkAssets.id, bookmark.id));\n  return true;\n}\n\nasync function extractAndSavePDFText(\n  jobId: string,\n  asset: Buffer,\n  bookmark: NonNullable<Awaited<ReturnType<typeof getBookmark>>>,\n  isFixMode: boolean,\n): Promise<boolean> {\n  {\n    const alreadyHasText = !!bookmark.asset.content;\n    if (alreadyHasText && isFixMode) {\n      logger.info(\n        `[assetPreprocessing][${jobId}] Skipping PDF text extraction as it's already been extracted.`,\n      );\n      return false;\n    }\n  }\n  logger.info(\n    `[assetPreprocessing][${jobId}] Attempting to extract text from pdf.`,\n  );\n  const pdfParse = await readPDFText(asset);\n  if (!pdfParse?.text) {\n    throw new Error(\n      `[assetPreprocessing][${jobId}] PDF text is empty. Please make sure that the PDF includes text and not just images.`,\n    );\n  }\n  logger.info(\n    `[assetPreprocessing][${jobId}] Extracted ${pdfParse.text.length} characters from pdf.`,\n  );\n  await db\n    .update(bookmarkAssets)\n    .set({\n      content: pdfParse.text,\n      metadata: pdfParse.metadata ? JSON.stringify(pdfParse.metadata) : null,\n    })\n    .where(eq(bookmarkAssets.id, bookmark.id));\n  return true;\n}\n\nasync function getBookmark(bookmarkId: string) {\n  return db.query.bookmarks.findFirst({\n    where: eq(bookmarks.id, bookmarkId),\n    with: {\n      asset: true,\n      assets: true,\n    },\n  });\n}\n\nasync function run(req: DequeuedJob<AssetPreprocessingRequest>) {\n  const isFixMode = req.data.fixMode;\n  const jobId = req.id;\n  const bookmarkId = req.data.bookmarkId;\n\n  const bookmark = await db.query.bookmarks.findFirst({\n    where: eq(bookmarks.id, bookmarkId),\n    with: {\n      asset: true,\n      assets: true,\n    },\n  });\n\n  logger.info(\n    `[assetPreprocessing][${jobId}] Starting an asset preprocessing job for bookmark with id \"${bookmarkId}\"`,\n  );\n\n  if (!bookmark) {\n    throw new Error(`[assetPreprocessing][${jobId}] Bookmark not found`);\n  }\n\n  if (!bookmark.asset) {\n    throw new Error(\n      `[assetPreprocessing][${jobId}] Bookmark is not an asset (not an image or pdf)`,\n    );\n  }\n\n  const { asset, metadata } = await readAsset({\n    userId: bookmark.userId,\n    assetId: bookmark.asset.assetId,\n  });\n\n  if (!asset) {\n    throw new Error(\n      `[assetPreprocessing][${jobId}] AssetId ${bookmark.asset.assetId} for bookmark ${bookmarkId} not found`,\n    );\n  }\n\n  let anythingChanged = false;\n  switch (bookmark.asset.assetType) {\n    case \"image\": {\n      const extractedText = await extractAndSaveImageText(\n        jobId,\n        asset,\n        metadata.contentType,\n        bookmark,\n        isFixMode,\n      );\n      anythingChanged ||= extractedText;\n      break;\n    }\n    case \"pdf\": {\n      const extractedText = await extractAndSavePDFText(\n        jobId,\n        asset,\n        bookmark,\n        isFixMode,\n      );\n      const extractedScreenshot = await extractAndSavePDFScreenshot(\n        jobId,\n        asset,\n        bookmark,\n        isFixMode,\n      );\n      anythingChanged ||= extractedText || extractedScreenshot;\n      break;\n    }\n    default:\n      throw new Error(\n        `[assetPreprocessing][${jobId}] Unsupported bookmark type`,\n      );\n  }\n\n  // Propagate priority to child jobs\n  const enqueueOpts: EnqueueOptions = {\n    priority: req.priority,\n    groupId: bookmark.userId,\n  };\n  if (!isFixMode || anythingChanged) {\n    await OpenAIQueue.enqueue(\n      {\n        bookmarkId,\n        type: \"tag\",\n      },\n      enqueueOpts,\n    );\n    await OpenAIQueue.enqueue(\n      {\n        bookmarkId,\n        type: \"summarize\",\n      },\n      enqueueOpts,\n    );\n\n    // Update the search index\n    await triggerSearchReindex(bookmarkId, enqueueOpts);\n  }\n}\n"
  },
  {
    "path": "apps/workers/workers/backupWorker.ts",
    "content": "import { createHash } from \"node:crypto\";\nimport { createWriteStream } from \"node:fs\";\nimport { stat, unlink } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { createId } from \"@paralleldrive/cuid2\";\nimport archiver from \"archiver\";\nimport { and, eq, inArray } from \"drizzle-orm\";\nimport { workerStatsCounter } from \"metrics\";\nimport cron from \"node-cron\";\nimport { withWorkerTracing } from \"workerTracing\";\n\nimport type { ZBackupRequest } from \"@karakeep/shared-server\";\nimport { db } from \"@karakeep/db\";\nimport {\n  assets,\n  AssetTypes,\n  bookmarksInLists,\n  users,\n} from \"@karakeep/db/schema\";\nimport { BackupQueue, QuotaService } from \"@karakeep/shared-server\";\nimport { saveAssetFromFile } from \"@karakeep/shared/assetdb\";\nimport {\n  toExportFormat,\n  toExportListFormat,\n} from \"@karakeep/shared/import-export\";\nimport logger from \"@karakeep/shared/logger\";\nimport { DequeuedJob, getQueueClient } from \"@karakeep/shared/queueing\";\nimport { AuthedContext } from \"@karakeep/trpc\";\nimport { Backup } from \"@karakeep/trpc/models/backups\";\nimport { List } from \"@karakeep/trpc/models/lists\";\n\nimport { buildImpersonatingAuthedContext } from \"../trpc\";\nimport { fetchBookmarksInBatches } from \"./utils/fetchBookmarks\";\n\n// Run daily at midnight UTC\nexport const BackupSchedulingWorker = cron.schedule(\n  \"0 0 * * *\",\n  async () => {\n    logger.info(\"[backup] Scheduling daily backup jobs ...\");\n    try {\n      const usersWithBackups = await db.query.users.findMany({\n        columns: {\n          id: true,\n          backupsFrequency: true,\n        },\n        where: eq(users.backupsEnabled, true),\n      });\n\n      logger.info(\n        `[backup] Found ${usersWithBackups.length} users with backups enabled`,\n      );\n\n      const now = new Date();\n      const currentDay = now.toISOString().split(\"T\")[0]; // YYYY-MM-DD\n\n      for (const user of usersWithBackups) {\n        // Deterministically schedule backups throughout the day based on user ID\n        // This spreads the load across 24 hours\n        const hash = createHash(\"sha256\").update(user.id).digest(\"hex\");\n        const hashNum = parseInt(hash.substring(0, 8), 16);\n\n        // For daily: schedule within 24 hours\n        // For weekly: only schedule on the user's designated day of week\n        let shouldSchedule = false;\n        let delayMs = 0;\n\n        if (user.backupsFrequency === \"daily\") {\n          shouldSchedule = true;\n          // Spread across 24 hours (86400000 ms)\n          delayMs = hashNum % 86400000;\n        } else if (user.backupsFrequency === \"weekly\") {\n          // Use hash to determine day of week (0-6)\n          const userDayOfWeek = hashNum % 7;\n          const currentDayOfWeek = now.getDay();\n\n          if (userDayOfWeek === currentDayOfWeek) {\n            shouldSchedule = true;\n            // Spread across 24 hours\n            delayMs = hashNum % 86400000;\n          }\n        }\n\n        if (shouldSchedule) {\n          const idempotencyKey = `${user.id}-${currentDay}`;\n\n          await BackupQueue.enqueue(\n            {\n              userId: user.id,\n            },\n            {\n              delayMs,\n              idempotencyKey,\n            },\n          );\n\n          logger.info(\n            `[backup] Scheduled backup for user ${user.id} with delay ${Math.round(delayMs / 1000 / 60)} minutes`,\n          );\n        }\n      }\n\n      logger.info(\"[backup] Finished scheduling backup jobs\");\n    } catch (error) {\n      logger.error(`[backup] Error scheduling backup jobs: ${error}`);\n    }\n  },\n  {\n    runOnInit: false,\n    scheduled: false,\n  },\n);\n\nexport class BackupWorker {\n  static async build() {\n    logger.info(\"Starting backup worker ...\");\n    const worker = (await getQueueClient())!.createRunner<ZBackupRequest>(\n      BackupQueue,\n      {\n        run: withWorkerTracing(\"backupWorker.run\", run),\n        onComplete: async (job) => {\n          workerStatsCounter.labels(\"backup\", \"completed\").inc();\n          const jobId = job.id;\n          logger.info(`[backup][${jobId}] Completed successfully`);\n        },\n        onError: async (job) => {\n          workerStatsCounter.labels(\"backup\", \"failed\").inc();\n          if (job.numRetriesLeft == 0) {\n            workerStatsCounter.labels(\"backup\", \"failed_permanent\").inc();\n          }\n          const jobId = job.id;\n          logger.error(\n            `[backup][${jobId}] Backup job failed: ${job.error}\\n${job.error?.stack}`,\n          );\n\n          // Mark backup as failed\n          if (job.data?.backupId && job.data?.userId) {\n            try {\n              const authCtx = await buildImpersonatingAuthedContext(\n                job.data.userId,\n              );\n              const backup = await Backup.fromId(authCtx, job.data.backupId);\n              await backup.update({\n                status: \"failure\",\n                errorMessage: job.error?.message || \"Unknown error\",\n              });\n            } catch (err) {\n              logger.error(\n                `[backup][${jobId}] Failed to mark backup as failed: ${err}`,\n              );\n            }\n          }\n        },\n      },\n      {\n        concurrency: 2, // Process 2 backups at a time\n        pollIntervalMs: 5000,\n        timeoutSecs: 600, // 10 minutes timeout for large exports\n      },\n    );\n\n    return worker;\n  }\n}\n\nasync function run(req: DequeuedJob<ZBackupRequest>) {\n  const jobId = req.id;\n  const userId = req.data.userId;\n  const backupId = req.data.backupId;\n\n  logger.info(`[backup][${jobId}] Starting backup for user ${userId} ...`);\n\n  // Fetch user settings to check if backups are enabled and get retention\n  const user = await db.query.users.findFirst({\n    columns: {\n      id: true,\n      backupsRetentionDays: true,\n    },\n    where: eq(users.id, userId),\n  });\n\n  if (!user) {\n    logger.info(`[backup][${jobId}] User not found: ${userId}. Skipping.`);\n    return;\n  }\n\n  const timestamp = new Date().toISOString().replace(/[:.]/g, \"-\");\n  const tempJsonPath = join(\n    tmpdir(),\n    `karakeep-backup-${userId}-${timestamp}.json`,\n  );\n  const tempZipPath = join(\n    tmpdir(),\n    `karakeep-backup-${userId}-${timestamp}.zip`,\n  );\n\n  let backup: Backup | null = null;\n\n  try {\n    // Step 1: Stream bookmarks to JSON file\n    const ctx = await buildImpersonatingAuthedContext(userId);\n    const backupInstance = await (backupId\n      ? Backup.fromId(ctx, backupId)\n      : Backup.create(ctx));\n    backup = backupInstance;\n    // Ensure backupId is attached to job data so error handler can mark failure.\n    req.data.backupId = backupInstance.id;\n\n    const bookmarkCount = await streamBookmarksToJsonFile(\n      ctx,\n      tempJsonPath,\n      jobId,\n    );\n\n    logger.info(\n      `[backup][${jobId}] Streamed ${bookmarkCount} bookmarks to JSON file`,\n    );\n\n    // Step 2: Compress the JSON file as zip\n    logger.info(`[backup][${jobId}] Compressing JSON file as zip ...`);\n    await createZipArchiveFromFile(tempJsonPath, timestamp, tempZipPath);\n\n    const fileStats = await stat(tempZipPath);\n    const compressedSize = fileStats.size;\n    const jsonStats = await stat(tempJsonPath);\n\n    logger.info(\n      `[backup][${jobId}] Compressed ${jsonStats.size} bytes to ${compressedSize} bytes`,\n    );\n\n    // Step 3: Check quota and store as asset\n    const quotaApproval = await QuotaService.checkStorageQuota(\n      db,\n      userId,\n      compressedSize,\n    );\n    const assetId = createId();\n    const fileName = `karakeep-backup-${timestamp}.zip`;\n\n    // Step 4: Create asset record\n    await db.insert(assets).values({\n      id: assetId,\n      assetType: AssetTypes.BACKUP,\n      size: compressedSize,\n      contentType: \"application/zip\",\n      fileName: fileName,\n      bookmarkId: null,\n      userId: userId,\n    });\n    await saveAssetFromFile({\n      userId,\n      assetId,\n      assetPath: tempZipPath,\n      metadata: {\n        contentType: \"application/zip\",\n        fileName,\n      },\n      quotaApproved: quotaApproval,\n    });\n\n    // Step 5: Update backup record\n    await backupInstance.update({\n      size: compressedSize,\n      bookmarkCount: bookmarkCount,\n      status: \"success\",\n      assetId,\n    });\n\n    logger.info(\n      `[backup][${jobId}] Successfully created backup for user ${userId} with ${bookmarkCount} bookmarks (${compressedSize} bytes)`,\n    );\n\n    // Step 6: Clean up old backups based on retention\n    await cleanupOldBackups(ctx, user.backupsRetentionDays, jobId);\n  } catch (error) {\n    if (backup) {\n      try {\n        await backup.update({\n          status: \"failure\",\n          errorMessage:\n            error instanceof Error ? error.message : \"Unknown error\",\n        });\n      } catch (updateError) {\n        logger.error(\n          `[backup][${jobId}] Failed to mark backup ${backup.id} as failed: ${updateError}`,\n        );\n      }\n    }\n    throw error;\n  } finally {\n    // Final cleanup of temporary files\n    try {\n      await unlink(tempJsonPath);\n    } catch {\n      // Ignore errors during cleanup\n    }\n    try {\n      await unlink(tempZipPath);\n    } catch {\n      // Ignore errors during cleanup\n    }\n  }\n}\n\n/**\n * Streams bookmarks to a JSON file in batches to avoid loading everything into memory\n * @returns The total number of bookmarks written\n */\nasync function streamBookmarksToJsonFile(\n  ctx: AuthedContext,\n  outputPath: string,\n  jobId: string,\n): Promise<number> {\n  // Pre-fetch list definitions (small data set)\n  const allLists = await List.getAllOwned(ctx);\n  const exportedLists = allLists.map((l) =>\n    toExportListFormat(l.asZBookmarkList()),\n  );\n\n  const manualListIds = allLists\n    .filter((l) => l.asZBookmarkList().type === \"manual\")\n    .map((l) => l.id);\n\n  return new Promise((resolve, reject) => {\n    const writeStream = createWriteStream(outputPath, { encoding: \"utf-8\" });\n    let bookmarkCount = 0;\n    let isFirst = true;\n\n    writeStream.on(\"error\", reject);\n\n    // Start JSON structure with lists first, then bookmarks\n    writeStream.write('{\"lists\":');\n    writeStream.write(JSON.stringify(exportedLists));\n    writeStream.write(',\"bookmarks\":[');\n\n    (async () => {\n      try {\n        for await (const batch of fetchBookmarksInBatches(ctx, 1000)) {\n          // Fetch memberships for this batch only to keep memory bounded\n          const batchBookmarkIds = batch.map((b) => b.id);\n          const bookmarkListMap = new Map<string, string[]>();\n          if (manualListIds.length > 0 && batchBookmarkIds.length > 0) {\n            const memberships = await ctx.db\n              .select({\n                bookmarkId: bookmarksInLists.bookmarkId,\n                listId: bookmarksInLists.listId,\n              })\n              .from(bookmarksInLists)\n              .where(\n                and(\n                  inArray(bookmarksInLists.listId, manualListIds),\n                  inArray(bookmarksInLists.bookmarkId, batchBookmarkIds),\n                ),\n              );\n            for (const m of memberships) {\n              const existing = bookmarkListMap.get(m.bookmarkId) ?? [];\n              existing.push(m.listId);\n              bookmarkListMap.set(m.bookmarkId, existing);\n            }\n          }\n\n          for (const bookmark of batch) {\n            const exported = toExportFormat(\n              bookmark,\n              bookmarkListMap.get(bookmark.id) ?? [],\n            );\n            if (exported.content !== null) {\n              // Add comma separator for all items except the first\n              if (!isFirst) {\n                writeStream.write(\",\");\n              }\n              writeStream.write(JSON.stringify(exported));\n              isFirst = false;\n              bookmarkCount++;\n            }\n          }\n\n          // Log progress every batch\n          if (bookmarkCount % 1000 === 0) {\n            logger.info(\n              `[backup][${jobId}] Streamed ${bookmarkCount} bookmarks so far...`,\n            );\n          }\n        }\n\n        // Close JSON structure\n        writeStream.write(\"]}\");\n        writeStream.end();\n\n        writeStream.on(\"finish\", () => {\n          resolve(bookmarkCount);\n        });\n      } catch (error) {\n        writeStream.destroy();\n        reject(error);\n      }\n    })();\n  });\n}\n\n/**\n * Creates a zip archive from a JSON file (streaming from disk instead of memory)\n */\nasync function createZipArchiveFromFile(\n  jsonFilePath: string,\n  timestamp: string,\n  outputPath: string,\n): Promise<void> {\n  return new Promise((resolve, reject) => {\n    const archive = archiver(\"zip\", {\n      zlib: { level: 9 }, // Maximum compression\n    });\n\n    const output = createWriteStream(outputPath);\n\n    output.on(\"close\", () => {\n      resolve();\n    });\n\n    output.on(\"error\", reject);\n    archive.on(\"error\", reject);\n\n    // Pipe archive data to the file\n    archive.pipe(output);\n\n    // Add the JSON file to the zip (streaming from disk)\n    const jsonFileName = `karakeep-backup-${timestamp}.json`;\n    archive.file(jsonFilePath, { name: jsonFileName });\n\n    archive.finalize();\n  });\n}\n\n/**\n * Cleans up old backups based on retention policy\n */\nasync function cleanupOldBackups(\n  ctx: AuthedContext,\n  retentionDays: number,\n  jobId: string,\n) {\n  try {\n    logger.info(\n      `[backup][${jobId}] Cleaning up backups older than ${retentionDays} days for user ${ctx.user.id} ...`,\n    );\n\n    const oldBackups = await Backup.findOldBackups(ctx, retentionDays);\n\n    if (oldBackups.length === 0) {\n      return;\n    }\n\n    logger.info(\n      `[backup][${jobId}] Found ${oldBackups.length} old backups to delete for user ${ctx.user.id}`,\n    );\n\n    // Delete each backup using the model's delete method\n    for (const backup of oldBackups) {\n      try {\n        await backup.delete();\n        logger.info(\n          `[backup][${jobId}] Deleted backup ${backup.id} for user ${ctx.user.id}`,\n        );\n      } catch (error) {\n        logger.warn(\n          `[backup][${jobId}] Failed to delete backup ${backup.id}: ${error}`,\n        );\n      }\n    }\n\n    logger.info(\n      `[backup][${jobId}] Successfully cleaned up ${oldBackups.length} old backups for user ${ctx.user.id}`,\n    );\n  } catch (error) {\n    logger.error(\n      `[backup][${jobId}] Error cleaning up old backups for user ${ctx.user.id}: ${error}`,\n    );\n  }\n}\n"
  },
  {
    "path": "apps/workers/workers/crawlerWorker.ts",
    "content": "import * as dns from \"dns\";\nimport { promises as fs } from \"fs\";\nimport * as fsSync from \"fs\";\nimport * as path from \"node:path\";\nimport * as os from \"os\";\nimport { Transform } from \"stream\";\nimport { pipeline } from \"stream/promises\";\nimport { PlaywrightBlocker } from \"@ghostery/adblocker-playwright\";\nimport { Mutex } from \"async-mutex\";\nimport { and, eq } from \"drizzle-orm\";\nimport { execa } from \"execa\";\nimport { exitAbortController } from \"exit\";\nimport {\n  bookmarkCrawlLatencyHistogram,\n  crawlerStatusCodeCounter,\n  workerStatsCounter,\n} from \"metrics\";\nimport {\n  fetchWithProxy,\n  getBookmarkDomain,\n  getRandomProxy,\n  matchesNoProxy,\n  validateUrl,\n} from \"network\";\nimport {\n  Browser,\n  BrowserContext,\n  BrowserContextOptions,\n  Page,\n} from \"playwright\";\nimport { chromium } from \"playwright-extra\";\nimport StealthPlugin from \"puppeteer-extra-plugin-stealth\";\nimport { withWorkerTracing } from \"workerTracing\";\nimport { getBookmarkDetails, updateAsset } from \"workerUtils\";\nimport { z } from \"zod\";\n\nimport type { ZCrawlLinkRequest } from \"@karakeep/shared-server\";\nimport { db } from \"@karakeep/db\";\nimport {\n  assets,\n  AssetTypes,\n  bookmarkAssets,\n  bookmarkLinks,\n  bookmarks,\n  users,\n} from \"@karakeep/db/schema\";\nimport {\n  AssetPreprocessingQueue,\n  getTracer,\n  OpenAIQueue,\n  QuotaService,\n  setSpanAttributes,\n  triggerSearchReindex,\n  triggerWebhook,\n  VideoWorkerQueue,\n  withSpan,\n  zCrawlLinkRequestSchema,\n} from \"@karakeep/shared-server\";\nimport {\n  ASSET_TYPES,\n  getAssetSize,\n  IMAGE_ASSET_TYPES,\n  newAssetId,\n  readAsset,\n  saveAsset,\n  saveAssetFromFile,\n  silentDeleteAsset,\n  SUPPORTED_UPLOAD_ASSET_TYPES,\n} from \"@karakeep/shared/assetdb\";\nimport serverConfig from \"@karakeep/shared/config\";\nimport logger from \"@karakeep/shared/logger\";\nimport {\n  DequeuedJob,\n  DequeuedJobError,\n  EnqueueOptions,\n  getQueueClient,\n  Queue,\n  QueueRetryAfterError,\n} from \"@karakeep/shared/queueing\";\nimport { getRateLimitClient } from \"@karakeep/shared/ratelimiting\";\nimport { tryCatch } from \"@karakeep/shared/tryCatch\";\nimport { BookmarkTypes } from \"@karakeep/shared/types/bookmarks\";\n\nimport type {\n  ParseSubprocessError,\n  ParseSubprocessOutput,\n} from \"./utils/parseHtmlSubprocessIpc\";\nimport {\n  parseSubprocessErrorSchema,\n  parseSubprocessOutputSchema,\n} from \"./utils/parseHtmlSubprocessIpc\";\n\nconst tracer = getTracer(\"@karakeep/workers\");\n\nfunction abortPromise(signal: AbortSignal): Promise<never> {\n  if (signal.aborted) {\n    const p = Promise.reject(signal.reason ?? new Error(\"AbortError\"));\n    p.catch(() => {\n      /* empty */\n    }); // suppress unhandledRejection if not awaited\n    return p;\n  }\n\n  const p = new Promise<never>((_, reject) => {\n    signal.addEventListener(\n      \"abort\",\n      () => {\n        reject(signal.reason ?? new Error(\"AbortError\"));\n      },\n      { once: true },\n    );\n  });\n\n  p.catch(() => {\n    /* empty */\n  });\n  return p;\n}\n\n/**\n * Redact sensitive query parameters (e.g., tokens) from a URL for safe logging.\n */\nfunction redactUrlCredentials(url: string): string {\n  try {\n    const parsed = new URL(url);\n    for (const key of parsed.searchParams.keys()) {\n      parsed.searchParams.set(key, \"REDACTED\");\n    }\n    if (parsed.password) {\n      parsed.password = \"REDACTED\";\n    }\n    return parsed.toString();\n  } catch {\n    return url;\n  }\n}\n\n/**\n * Normalize a Content-Type header by stripping parameters (e.g., charset)\n * and lowercasing the media type, so comparisons against supported types work.\n */\nfunction normalizeContentType(header: string | null): string | null {\n  if (!header) {\n    return null;\n  }\n  return header.split(\";\", 1)[0]!.trim().toLowerCase();\n}\n\nfunction shouldRetryCrawlStatusCode(statusCode: number | null): boolean {\n  if (statusCode === null) {\n    return false;\n  }\n  return statusCode === 403 || statusCode === 429 || statusCode >= 500;\n}\n\ninterface Cookie {\n  name: string;\n  value: string;\n  domain?: string;\n  path?: string;\n  expires?: number;\n  httpOnly?: boolean;\n  secure?: boolean;\n  sameSite?: \"Strict\" | \"Lax\" | \"None\";\n}\n\nconst cookieSchema = z.object({\n  name: z.string(),\n  value: z.string(),\n  domain: z.string().optional(),\n  path: z.string().optional(),\n  expires: z.number().optional(),\n  httpOnly: z.boolean().optional(),\n  secure: z.boolean().optional(),\n  sameSite: z.enum([\"Strict\", \"Lax\", \"None\"]).optional(),\n});\n\nconst cookiesSchema = z.array(cookieSchema);\n\ninterface CrawlerRunResult {\n  status: \"completed\";\n}\n\nfunction getPlaywrightProxyConfig(): BrowserContextOptions[\"proxy\"] {\n  const { proxy } = serverConfig;\n\n  if (!proxy.httpProxy && !proxy.httpsProxy) {\n    return undefined;\n  }\n\n  // Use HTTPS proxy if available, otherwise fall back to HTTP proxy\n  const proxyList = proxy.httpsProxy || proxy.httpProxy;\n  if (!proxyList) {\n    // Unreachable, but TypeScript doesn't know that\n    return undefined;\n  }\n\n  const proxyUrl = getRandomProxy(proxyList);\n  const parsed = new URL(proxyUrl);\n\n  return {\n    server: proxyUrl,\n    username: parsed.username,\n    password: parsed.password,\n    bypass: proxy.noProxy?.join(\",\"),\n  };\n}\n\nlet globalBrowser: Browser | undefined;\nlet globalBlocker: PlaywrightBlocker | undefined;\n// Global variable to store parsed cookies\nlet globalCookies: Cookie[] = [];\n// Guards the interactions with the browser instance.\n// This is needed given that most of the browser APIs are async.\nconst browserMutex = new Mutex();\n\n// Tracks active browser contexts so we can reap leaked ones.\nconst activeContexts = new Map<\n  string,\n  { context: BrowserContext; createdAt: number }\n>();\n\nconst CONTEXT_CLOSE_TIMEOUT_MS = 10_000;\nconst PAGE_CLOSE_TIMEOUT_MS = 5_000;\n\n/**\n * Reaps browser contexts that have been open longer than the max job timeout.\n * This is a safety net for cases where context.close() hangs or is never called.\n */\nfunction startContextReaper() {\n  const maxContextAgeMs =\n    (serverConfig.crawler.jobTimeoutSec + 30) * 1000 +\n    60_000 * 5; /* 5 minutes buffer */\n  const intervalId = setInterval(() => {\n    try {\n      const now = Date.now();\n      for (const [id, entry] of activeContexts) {\n        if (now - entry.createdAt > maxContextAgeMs) {\n          logger.warn(\n            `[Crawler] Reaping stale browser context for job ${id} (age: ${Math.round((now - entry.createdAt) / 1000)}s)`,\n          );\n          void Promise.race([\n            entry.context\n              .close()\n              .then(() => true)\n              .catch((e: unknown) => {\n                logger.warn(\n                  `[Crawler] Failed to close stale context for job ${id}: ${e}`,\n                );\n                return true;\n              }),\n            new Promise<false>((r) =>\n              setTimeout(() => r(false), CONTEXT_CLOSE_TIMEOUT_MS),\n            ),\n          ]).then((contextClosed) => {\n            // Protect against deleting a newer context if the job id gets reused.\n            if (!contextClosed) {\n              logger.warn(\n                `[Crawler] Timed out closing stale context for job ${id} — keeping in active set for retry`,\n              );\n              return;\n            }\n            if (activeContexts.get(id) === entry) {\n              activeContexts.delete(id);\n            }\n          });\n        }\n      }\n    } catch (e) {\n      logger.error(\n        `[Crawler] caught an unexpected error while reaping stale browser contexts: ${e}`,\n      );\n    }\n  }, 60_000 * 5);\n  exitAbortController.signal.addEventListener(\n    \"abort\",\n    () => clearInterval(intervalId),\n    {\n      once: true,\n    },\n  );\n}\n\nasync function startBrowserInstance() {\n  if (serverConfig.crawler.browserWebSocketUrl) {\n    logger.info(\n      `[Crawler] Connecting to existing browser websocket address: ${redactUrlCredentials(serverConfig.crawler.browserWebSocketUrl)}`,\n    );\n    return await chromium.connect(serverConfig.crawler.browserWebSocketUrl, {\n      timeout: 5000,\n    });\n  } else if (serverConfig.crawler.browserWebUrl) {\n    logger.info(\n      `[Crawler] Connecting to existing browser instance: ${redactUrlCredentials(serverConfig.crawler.browserWebUrl)}`,\n    );\n\n    const webUrl = new URL(serverConfig.crawler.browserWebUrl);\n    const { address } = await dns.promises.lookup(webUrl.hostname);\n    webUrl.hostname = address;\n    logger.info(\n      `[Crawler] Successfully resolved IP address, new address: ${redactUrlCredentials(webUrl.toString())}`,\n    );\n\n    return await chromium.connectOverCDP(webUrl.toString(), {\n      timeout: 5000,\n    });\n  } else {\n    logger.info(`Running in browserless mode`);\n    return undefined;\n  }\n}\n\nasync function launchBrowser() {\n  globalBrowser = undefined;\n  await browserMutex.runExclusive(async () => {\n    const globalBrowserResult = await tryCatch(startBrowserInstance());\n    if (globalBrowserResult.error) {\n      logger.error(\n        `[Crawler] Failed to connect to the browser instance, will retry in 5 secs: ${globalBrowserResult.error.stack}`,\n      );\n      if (exitAbortController.signal.aborted) {\n        logger.info(\"[Crawler] We're shutting down so won't retry.\");\n        return;\n      }\n      setTimeout(() => {\n        launchBrowser();\n      }, 5000);\n      return;\n    }\n    globalBrowser = globalBrowserResult.data;\n    globalBrowser?.on(\"disconnected\", () => {\n      if (exitAbortController.signal.aborted) {\n        logger.info(\n          \"[Crawler] The Playwright browser got disconnected. But we're shutting down so won't restart it.\",\n        );\n        return;\n      }\n      logger.info(\n        \"[Crawler] The Playwright browser got disconnected. Will attempt to launch it again.\",\n      );\n      launchBrowser();\n    });\n  });\n}\n\nexport class CrawlerWorker {\n  private static initPromise: Promise<void> | null = null;\n\n  private static ensureInitialized() {\n    if (!CrawlerWorker.initPromise) {\n      CrawlerWorker.initPromise = (async () => {\n        chromium.use(StealthPlugin());\n        if (serverConfig.crawler.enableAdblocker) {\n          logger.info(\"[crawler] Loading adblocker ...\");\n          const globalBlockerResult = await tryCatch(\n            PlaywrightBlocker.fromPrebuiltFull(fetchWithProxy, {\n              path: path.join(os.tmpdir(), \"karakeep_adblocker.bin\"),\n              read: fs.readFile,\n              write: fs.writeFile,\n            }),\n          );\n          if (globalBlockerResult.error) {\n            logger.error(\n              `[crawler] Failed to load adblocker. Will not be blocking ads: ${globalBlockerResult.error}`,\n            );\n          } else {\n            globalBlocker = globalBlockerResult.data;\n          }\n        }\n        if (!serverConfig.crawler.browserConnectOnDemand) {\n          await launchBrowser();\n        } else {\n          logger.info(\n            \"[Crawler] Browser connect on demand is enabled, won't proactively start the browser instance\",\n          );\n        }\n        await loadCookiesFromFile();\n        startContextReaper();\n      })();\n    }\n    return CrawlerWorker.initPromise;\n  }\n\n  static async build(queue: Queue<ZCrawlLinkRequest>) {\n    await CrawlerWorker.ensureInitialized();\n\n    logger.info(\"Starting crawler worker ...\");\n    const worker = (await getQueueClient()).createRunner<\n      ZCrawlLinkRequest,\n      CrawlerRunResult\n    >(\n      queue,\n      {\n        run: withWorkerTracing(\"crawlerWorker.run\", (job) =>\n          runCrawler(job, queue.opts.defaultJobArgs.numRetries),\n        ),\n        onComplete: async (job: DequeuedJob<ZCrawlLinkRequest>) => {\n          workerStatsCounter.labels(\"crawler\", \"completed\").inc();\n          const jobId = job.id;\n          logger.info(`[Crawler][${jobId}] Completed successfully`);\n          const bookmarkId = job.data.bookmarkId;\n          if (bookmarkId) {\n            await db\n              .update(bookmarkLinks)\n              .set({\n                crawlStatus: \"success\",\n              })\n              .where(eq(bookmarkLinks.id, bookmarkId));\n          }\n        },\n        onError: async (job: DequeuedJobError<ZCrawlLinkRequest>) => {\n          workerStatsCounter.labels(\"crawler\", \"failed\").inc();\n          if (job.numRetriesLeft == 0) {\n            workerStatsCounter.labels(\"crawler\", \"failed_permanent\").inc();\n          }\n          const jobId = job.id;\n          logger.error(\n            `[Crawler][${jobId}] Crawling job failed: ${job.error}\\n${job.error.stack}`,\n          );\n          const bookmarkId = job.data?.bookmarkId;\n          if (bookmarkId && job.numRetriesLeft == 0) {\n            await db.transaction(async (tx) => {\n              await tx\n                .update(bookmarkLinks)\n                .set({\n                  crawlStatus: \"failure\",\n                })\n                .where(eq(bookmarkLinks.id, bookmarkId));\n              await tx\n                .update(bookmarks)\n                .set({\n                  taggingStatus: null,\n                })\n                .where(\n                  and(\n                    eq(bookmarks.id, bookmarkId),\n                    eq(bookmarks.taggingStatus, \"pending\"),\n                  ),\n                );\n              await tx\n                .update(bookmarks)\n                .set({\n                  summarizationStatus: null,\n                })\n                .where(\n                  and(\n                    eq(bookmarks.id, bookmarkId),\n                    eq(bookmarks.summarizationStatus, \"pending\"),\n                  ),\n                );\n            });\n          }\n        },\n      },\n      {\n        pollIntervalMs: 1000,\n        timeoutSecs: serverConfig.crawler.jobTimeoutSec,\n        concurrency: serverConfig.crawler.numWorkers,\n      },\n    );\n\n    return worker;\n  }\n}\n\nasync function loadCookiesFromFile(): Promise<void> {\n  try {\n    const path = serverConfig.crawler.browserCookiePath;\n    if (!path) {\n      logger.info(\n        \"[Crawler] Not defined in the server configuration BROWSER_COOKIE_PATH\",\n      );\n      return;\n    }\n    const data = await fs.readFile(path, \"utf8\");\n    const cookies = JSON.parse(data);\n    globalCookies = cookiesSchema.parse(cookies);\n  } catch (error) {\n    logger.error(\"Failed to read or parse cookies file:\", error);\n    if (error instanceof z.ZodError) {\n      logger.error(\"[Crawler] Invalid cookie file format:\", error.errors);\n    } else {\n      logger.error(\"[Crawler] Failed to read or parse cookies file:\", error);\n    }\n    throw error;\n  }\n}\n\ntype DBAssetType = typeof assets.$inferInsert;\n\nasync function browserlessCrawlPage(\n  jobId: string,\n  url: string,\n  abortSignal: AbortSignal,\n) {\n  return await withSpan(\n    tracer,\n    \"crawlerWorker.browserlessCrawlPage\",\n    {\n      attributes: {\n        \"bookmark.url\": url,\n        \"bookmark.domain\": getBookmarkDomain(url),\n        \"job.id\": jobId,\n      },\n    },\n    async () => {\n      logger.info(\n        `[Crawler][${jobId}] Running in browserless mode. Will do a plain http request to \"${url}\". Screenshots will be disabled.`,\n      );\n      const response = await fetchWithProxy(url, {\n        signal: AbortSignal.any([AbortSignal.timeout(5000), abortSignal]),\n      });\n      logger.info(\n        `[Crawler][${jobId}] Successfully fetched the content of \"${url}\". Status: ${response.status}, Size: ${response.size}`,\n      );\n      return {\n        htmlContent: await response.text(),\n        statusCode: response.status,\n        screenshot: undefined,\n        pdf: undefined,\n        url: response.url,\n      };\n    },\n  );\n}\n\nasync function crawlPage(\n  jobId: string,\n  url: string,\n  userId: string,\n  forceStorePdf: boolean,\n  abortSignal: AbortSignal,\n): Promise<{\n  htmlContent: string;\n  screenshot: Buffer | undefined;\n  pdf: Buffer | undefined;\n  statusCode: number;\n  url: string;\n}> {\n  return await withSpan(\n    tracer,\n    \"crawlerWorker.crawlPage\",\n    {\n      attributes: {\n        \"bookmark.url\": url,\n        \"bookmark.domain\": getBookmarkDomain(url),\n        \"job.id\": jobId,\n        \"user.id\": userId,\n        \"crawler.forceStorePdf\": forceStorePdf,\n      },\n    },\n    async () => {\n      const userData = await db.query.users.findFirst({\n        where: eq(users.id, userId),\n        columns: { browserCrawlingEnabled: true },\n      });\n      if (!userData) {\n        logger.error(`[Crawler][${jobId}] User ${userId} not found`);\n        throw new Error(`User ${userId} not found`);\n      }\n\n      const browserCrawlingEnabled = userData.browserCrawlingEnabled;\n\n      if (browserCrawlingEnabled !== null && !browserCrawlingEnabled) {\n        return browserlessCrawlPage(jobId, url, abortSignal);\n      }\n\n      let browser: Browser | undefined;\n      browser = await withSpan(\n        tracer,\n        \"crawlerWorker.crawlPage.getBrowserInstance\",\n        {\n          attributes: {\n            \"job.id\": jobId,\n          },\n        },\n        async () => {\n          if (serverConfig.crawler.browserConnectOnDemand) {\n            return startBrowserInstance();\n          }\n          return globalBrowser;\n        },\n      );\n      if (!browser) {\n        return browserlessCrawlPage(jobId, url, abortSignal);\n      }\n\n      const proxyConfig = getPlaywrightProxyConfig();\n      const isRunningInProxyContext =\n        proxyConfig !== undefined &&\n        !matchesNoProxy(url, proxyConfig.bypass?.split(\",\") ?? []);\n      const context = await withSpan(\n        tracer,\n        \"crawlerWorker.crawlPage.createContext\",\n        {\n          attributes: {\n            \"job.id\": jobId,\n          },\n        },\n        async () =>\n          browser.newContext({\n            viewport: { width: 1440, height: 900 },\n            userAgent:\n              \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36\",\n            proxy: proxyConfig,\n          }),\n      );\n\n      activeContexts.set(jobId, { context, createdAt: Date.now() });\n      let page: Page | undefined;\n      try {\n        if (globalCookies.length > 0) {\n          await context.addCookies(globalCookies);\n          logger.info(\n            `[Crawler][${jobId}] Cookies successfully loaded into browser context`,\n          );\n        }\n\n        page = await withSpan(\n          tracer,\n          \"crawlerWorker.crawlPage.setupPage\",\n          {\n            attributes: {\n              \"job.id\": jobId,\n            },\n          },\n          async () => {\n            // Create a new page in the context\n            const nextPage = await context.newPage();\n\n            // Apply ad blocking\n            if (globalBlocker) {\n              await globalBlocker.enableBlockingInPage(nextPage);\n            }\n\n            // Auto-dismiss JavaScript dialogs (alert, confirm, prompt)\n            // to prevent pages from hanging during crawl.\n            nextPage.on(\"dialog\", (dialog) => {\n              dialog.dismiss().catch(() => {\n                // Ignore errors — the dialog may have already been closed.\n              });\n            });\n\n            // Block audio/video resources and disallowed sub-requests\n            await nextPage.route(\"**/*\", async (route) => {\n              if (abortSignal.aborted) {\n                await route.abort(\"aborted\");\n                return;\n              }\n              const request = route.request();\n              const resourceType = request.resourceType();\n\n              // Block audio/video resources\n              if (\n                resourceType === \"media\" ||\n                request.headers()[\"content-type\"]?.includes(\"video/\") ||\n                request.headers()[\"content-type\"]?.includes(\"audio/\")\n              ) {\n                await route.abort(\"aborted\");\n                return;\n              }\n\n              const requestUrl = request.url();\n              const requestIsRunningInProxyContext =\n                proxyConfig !== undefined &&\n                !matchesNoProxy(\n                  requestUrl,\n                  proxyConfig.bypass?.split(\",\") ?? [],\n                );\n              if (\n                requestUrl.startsWith(\"http://\") ||\n                requestUrl.startsWith(\"https://\")\n              ) {\n                const validation = await validateUrl(\n                  requestUrl,\n                  requestIsRunningInProxyContext,\n                );\n                if (!validation.ok) {\n                  logger.warn(\n                    `[Crawler][${jobId}] Blocking sub-request to disallowed URL \"${requestUrl}\": ${validation.reason}`,\n                  );\n                  await route.abort(\"blockedbyclient\");\n                  return;\n                }\n              }\n\n              // Continue with other requests\n              await route.continue();\n            });\n\n            // On abort, immediately stop intercepting requests so that\n            // in-flight route handlers don't block page/context closure.\n            abortSignal.addEventListener(\n              \"abort\",\n              () => {\n                nextPage.unrouteAll({ behavior: \"ignoreErrors\" }).catch(() => {\n                  // Ignore errors — the page may already be closed.\n                });\n              },\n              { once: true },\n            );\n\n            return nextPage;\n          },\n        );\n\n        // page is guaranteed to be assigned here; alias to a const for\n        // TypeScript narrowing so the rest of the try block sees `Page`.\n        const activePage = page;\n\n        // Navigate to the target URL\n        const navigationValidation = await withSpan(\n          tracer,\n          \"crawlerWorker.crawlPage.validateNavigationTarget\",\n          {\n            attributes: {\n              \"job.id\": jobId,\n              \"bookmark.url\": url,\n              \"bookmark.domain\": getBookmarkDomain(url),\n            },\n          },\n          async () => validateUrl(url, isRunningInProxyContext),\n        );\n        if (!navigationValidation.ok) {\n          throw new Error(\n            `Disallowed navigation target \"${url}\": ${navigationValidation.reason}`,\n          );\n        }\n        const targetUrl = navigationValidation.url.toString();\n        logger.info(`[Crawler][${jobId}] Navigating to \"${targetUrl}\"`);\n        const response = await withSpan(\n          tracer,\n          \"crawlerWorker.crawlPage.navigate\",\n          {\n            attributes: {\n              \"job.id\": jobId,\n              \"bookmark.url\": targetUrl,\n              \"bookmark.domain\": getBookmarkDomain(targetUrl),\n            },\n          },\n          async () =>\n            Promise.race([\n              activePage.goto(targetUrl, {\n                timeout: serverConfig.crawler.navigateTimeoutSec * 1000,\n                waitUntil: \"domcontentloaded\",\n              }),\n              abortPromise(abortSignal).then(() => null),\n            ]),\n        );\n        setSpanAttributes({\n          \"crawler.statusCode\": response?.status() ?? 0,\n        });\n\n        logger.info(\n          `[Crawler][${jobId}] Successfully navigated to \"${targetUrl}\". Waiting for the page to load ...`,\n        );\n\n        // Wait until network is relatively idle or timeout after 5 seconds\n        await withSpan(\n          tracer,\n          \"crawlerWorker.crawlPage.waitForLoadState\",\n          {\n            attributes: {\n              \"job.id\": jobId,\n              \"bookmark.url\": targetUrl,\n              \"bookmark.domain\": getBookmarkDomain(targetUrl),\n            },\n          },\n          async () => {\n            await Promise.race([\n              activePage\n                .waitForLoadState(\"networkidle\", { timeout: 5000 })\n                .catch(() => ({})),\n              new Promise((resolve) => setTimeout(resolve, 5000)),\n              abortPromise(abortSignal),\n            ]);\n          },\n        );\n\n        abortSignal.throwIfAborted();\n\n        logger.info(\n          `[Crawler][${jobId}] Finished waiting for the page to load.`,\n        );\n\n        const [htmlContent, screenshot, pdf] = await withSpan(\n          tracer,\n          \"crawlerWorker.crawlPage.captureAssets\",\n          {\n            attributes: {\n              \"job.id\": jobId,\n            },\n          },\n          async () => {\n            const htmlPromise = withSpan(\n              tracer,\n              \"crawlerWorker.crawlPage.extractHtml\",\n              {\n                attributes: {\n                  \"job.id\": jobId,\n                },\n              },\n              async () => {\n                const content = await activePage.content();\n                abortSignal.throwIfAborted();\n                logger.info(\n                  `[Crawler][${jobId}] Successfully fetched the page content.`,\n                );\n                return content;\n              },\n            );\n\n            const screenshotPromise: Promise<Buffer | undefined> = serverConfig\n              .crawler.storeScreenshot\n              ? withSpan(\n                  tracer,\n                  \"crawlerWorker.crawlPage.captureScreenshot\",\n                  {\n                    attributes: {\n                      \"job.id\": jobId,\n                      \"asset.type\": \"image\",\n                    },\n                  },\n                  async () => {\n                    const { data: screenshotData, error: screenshotError } =\n                      await tryCatch(\n                        Promise.race<Buffer>([\n                          activePage.screenshot({\n                            // If you change this, you need to change the asset type in the store function.\n                            type: \"jpeg\",\n                            fullPage: serverConfig.crawler.fullPageScreenshot,\n                            quality: 80,\n                          }),\n                          new Promise((_, reject) =>\n                            setTimeout(\n                              () =>\n                                reject(\n                                  \"TIMED_OUT, consider increasing CRAWLER_SCREENSHOT_TIMEOUT_SEC\",\n                                ),\n                              serverConfig.crawler.screenshotTimeoutSec * 1000,\n                            ),\n                          ),\n                          abortPromise(abortSignal).then(() => Buffer.from(\"\")),\n                        ]),\n                      );\n                    abortSignal.throwIfAborted();\n                    if (screenshotError) {\n                      logger.warn(\n                        `[Crawler][${jobId}] Failed to capture the screenshot. Reason: ${screenshotError}`,\n                      );\n                      return undefined;\n                    }\n                    setSpanAttributes({\n                      \"asset.size\": screenshotData.byteLength,\n                    });\n                    logger.info(\n                      `[Crawler][${jobId}] Finished capturing page content and a screenshot. FullPageScreenshot: ${serverConfig.crawler.fullPageScreenshot}`,\n                    );\n                    return screenshotData;\n                  },\n                )\n              : Promise.resolve(undefined);\n\n            const pdfPromise: Promise<Buffer | undefined> =\n              serverConfig.crawler.storePdf || forceStorePdf\n                ? withSpan(\n                    tracer,\n                    \"crawlerWorker.crawlPage.capturePdf\",\n                    {\n                      attributes: {\n                        \"job.id\": jobId,\n                        \"asset.type\": \"pdf\",\n                      },\n                    },\n                    async () => {\n                      const { data: pdfData, error: pdfError } = await tryCatch(\n                        Promise.race<Buffer>([\n                          activePage.pdf({\n                            format: \"A4\",\n                            printBackground: true,\n                          }),\n                          new Promise((_, reject) =>\n                            setTimeout(\n                              () =>\n                                reject(\n                                  \"TIMED_OUT, consider increasing CRAWLER_SCREENSHOT_TIMEOUT_SEC\",\n                                ),\n                              serverConfig.crawler.screenshotTimeoutSec * 1000,\n                            ),\n                          ),\n                          abortPromise(abortSignal).then(() => Buffer.from(\"\")),\n                        ]),\n                      );\n                      abortSignal.throwIfAborted();\n                      if (pdfError) {\n                        logger.warn(\n                          `[Crawler][${jobId}] Failed to capture the PDF. Reason: ${pdfError}`,\n                        );\n                        return undefined;\n                      }\n                      setSpanAttributes({\n                        \"asset.size\": pdfData.byteLength,\n                      });\n                      logger.info(\n                        `[Crawler][${jobId}] Finished capturing page content as PDF`,\n                      );\n                      return pdfData;\n                    },\n                  )\n                : Promise.resolve(undefined);\n\n            const captureResults = await Promise.all([\n              htmlPromise,\n              screenshotPromise,\n              pdfPromise,\n            ] as const);\n            abortSignal.throwIfAborted();\n            return captureResults;\n          },\n        );\n\n        return {\n          htmlContent,\n          statusCode: response?.status() ?? 0,\n          screenshot,\n          pdf,\n          url: activePage.url(),\n        };\n      } finally {\n        await withSpan(\n          tracer,\n          \"crawlerWorker.crawlPage.cleanup\",\n          {\n            attributes: {\n              \"job.id\": jobId,\n              \"crawler.cleanup.hasPage\": !!page,\n            },\n          },\n          async () => {\n            // Explicitly close the page first (with timeout) to release resources\n            // even if context.close() later hangs.\n            if (page) {\n              const pageToClose = page;\n              const pageClosed = await withSpan(\n                tracer,\n                \"crawlerWorker.crawlPage.cleanup.closePage\",\n                { attributes: { \"job.id\": jobId } },\n                async () =>\n                  Promise.race([\n                    pageToClose\n                      .close()\n                      .then(() => true)\n                      .catch((e: unknown) => {\n                        logger.warn(\n                          `[Crawler][${jobId}] page.close() failed: ${e}`,\n                        );\n                        return true;\n                      }),\n                    new Promise<false>((r) =>\n                      setTimeout(() => r(false), PAGE_CLOSE_TIMEOUT_MS),\n                    ),\n                  ]),\n              );\n              setSpanAttributes({ \"crawler.cleanup.pageClosed\": pageClosed });\n              if (!pageClosed) {\n                logger.warn(`[Crawler][${jobId}] page.close() timed out`);\n              }\n            }\n\n            // Close the context (with timeout) to avoid hanging on in-flight ops.\n            // Only remove from tracking if close actually succeeded; otherwise\n            // the reaper will retry the close later.\n            const contextClosed = await withSpan(\n              tracer,\n              \"crawlerWorker.crawlPage.cleanup.closeContext\",\n              { attributes: { \"job.id\": jobId } },\n              async () =>\n                Promise.race([\n                  context\n                    .close()\n                    .then(() => true)\n                    .catch((e: unknown) => {\n                      logger.warn(\n                        `[Crawler][${jobId}] context.close() failed: ${e}`,\n                      );\n                      return true; // Error means it's likely already closed\n                    }),\n                  new Promise<false>((r) =>\n                    setTimeout(() => r(false), CONTEXT_CLOSE_TIMEOUT_MS),\n                  ),\n                ]),\n            );\n            setSpanAttributes({\n              \"crawler.cleanup.contextClosed\": contextClosed,\n            });\n\n            if (contextClosed) {\n              activeContexts.delete(jobId);\n            } else {\n              logger.warn(\n                `[Crawler][${jobId}] context.close() timed out — leaving in active set for reaper`,\n              );\n            }\n\n            // Only close the browser if it was created on demand\n            if (serverConfig.crawler.browserConnectOnDemand) {\n              await withSpan(\n                tracer,\n                \"crawlerWorker.crawlPage.cleanup.closeBrowser\",\n                { attributes: { \"job.id\": jobId } },\n                async () =>\n                  browser\n                    .close()\n                    .then(() => {\n                      activeContexts.delete(jobId);\n                    })\n                    .catch((e: unknown) => {\n                      logger.warn(\n                        `[Crawler][${jobId}] browser.close() failed: ${e}`,\n                      );\n                    }),\n              );\n            }\n          },\n        );\n      }\n    },\n  );\n}\n\nfunction getSubprocessScriptPath(): string {\n  const currentUrl = import.meta.url;\n  if (currentUrl.includes(\"/dist/\")) {\n    // Production: running from built output\n    return new URL(\"./scripts/parseHtmlSubprocess.js\", currentUrl).pathname;\n  }\n  // Dev mode: running via tsx\n  return new URL(\"../scripts/parseHtmlSubprocess.ts\", currentUrl).pathname;\n}\n\nfunction getSubprocessCommand(): { cmd: string; args: string[] } {\n  const scriptPath = getSubprocessScriptPath();\n  const maxOldSpaceSize = serverConfig.crawler.parserMemLimitMb;\n\n  if (scriptPath.endsWith(\".ts\")) {\n    // Dev mode: use tsx to run TypeScript directly\n    return {\n      cmd: \"tsx\",\n      args: [`--max-old-space-size=${maxOldSpaceSize}`, scriptPath],\n    };\n  }\n\n  return {\n    cmd: process.execPath,\n    args: [`--max-old-space-size=${maxOldSpaceSize}`, scriptPath],\n  };\n}\n\nasync function runParseSubprocess(\n  htmlContent: string,\n  url: string,\n  jobId: string,\n  abortSignal: AbortSignal,\n): Promise<{\n  metadata: ParseSubprocessOutput[\"metadata\"];\n  readableContent: { content: string } | null;\n}> {\n  return await withSpan(\n    tracer,\n    \"crawlerWorker.runParseSubprocess\",\n    {\n      attributes: {\n        \"bookmark.url\": url,\n        \"bookmark.domain\": getBookmarkDomain(url),\n        \"job.id\": jobId,\n      },\n    },\n    async () => {\n      logger.info(\n        `[Crawler][${jobId}] Spawning parse subprocess for \"${url}\" ...`,\n      );\n\n      const { cmd, args } = getSubprocessCommand();\n      const timeoutMs = serverConfig.crawler.parseTimeoutSec * 1000;\n\n      const result = await execa({\n        input: JSON.stringify({ htmlContent, url, jobId }),\n        cancelSignal: abortSignal,\n        timeout: timeoutMs,\n        reject: false,\n        stderr: \"inherit\",\n      })(cmd, args);\n\n      if (result.isCanceled) {\n        throw new Error(\n          `[Crawler][${jobId}] Parse subprocess was cancelled (job aborted)`,\n        );\n      }\n\n      if (result.exitCode !== 0) {\n        // Check for OOM: SIGKILL (137) from OS killer, SIGABRT from V8,\n        // or V8's \"heap out of memory\" fatal error message in stderr\n        const isOom =\n          result.exitCode === 137 ||\n          result.signal === \"SIGKILL\" ||\n          result.signal === \"SIGABRT\";\n        const reason = isOom\n          ? `OOM killed (exit code ${result.exitCode}). Consider increasing CRAWLER_PARSER_MEM_LIMIT_MB (currently ${serverConfig.crawler.parserMemLimitMb}MB).`\n          : `exited with code ${result.exitCode}${result.signal ? ` (signal: ${result.signal})` : \"\"}`;\n\n        // Try to parse structured error from stdout\n        if (result.stdout) {\n          let errorOutput: ParseSubprocessError | null = null;\n          try {\n            errorOutput = parseSubprocessErrorSchema.parse(\n              JSON.parse(result.stdout),\n            );\n          } catch {\n            // stdout wasn't valid JSON error, fall through\n          }\n\n          if (errorOutput?.error) {\n            throw new Error(\n              `[Crawler][${jobId}] Parse subprocess ${reason}: ${errorOutput.error}`,\n            );\n          }\n        }\n\n        throw new Error(`[Crawler][${jobId}] Parse subprocess ${reason}`);\n      }\n\n      if (!result.stdout) {\n        throw new Error(\n          `[Crawler][${jobId}] Parse subprocess produced no output`,\n        );\n      }\n\n      const output = parseSubprocessOutputSchema.parse(\n        JSON.parse(result.stdout),\n      );\n      logger.info(\n        `[Crawler][${jobId}] Parse subprocess completed successfully.`,\n      );\n\n      return {\n        metadata: output.metadata,\n        readableContent: output.readableContent,\n      };\n    },\n  );\n}\n\nasync function storeScreenshot(\n  screenshot: Buffer | undefined,\n  userId: string,\n  jobId: string,\n) {\n  return await withSpan(\n    tracer,\n    \"crawlerWorker.storeScreenshot\",\n    {\n      attributes: {\n        \"job.id\": jobId,\n        \"user.id\": userId,\n        \"asset.size\": screenshot?.byteLength ?? 0,\n      },\n    },\n    async () => {\n      if (!serverConfig.crawler.storeScreenshot) {\n        logger.info(\n          `[Crawler][${jobId}] Skipping storing the screenshot as per the config.`,\n        );\n        return null;\n      }\n      if (!screenshot) {\n        logger.info(\n          `[Crawler][${jobId}] Skipping storing the screenshot as it's empty.`,\n        );\n        return null;\n      }\n      const assetId = newAssetId();\n      const contentType = \"image/jpeg\";\n      const fileName = \"screenshot.jpeg\";\n\n      // Check storage quota before saving the screenshot\n      const { data: quotaApproved, error: quotaError } = await tryCatch(\n        QuotaService.checkStorageQuota(db, userId, screenshot.byteLength),\n      );\n\n      if (quotaError) {\n        logger.warn(\n          `[Crawler][${jobId}] Skipping screenshot storage due to quota exceeded: ${quotaError.message}`,\n        );\n        return null;\n      }\n\n      await saveAsset({\n        userId,\n        assetId,\n        metadata: { contentType, fileName },\n        asset: screenshot,\n        quotaApproved,\n      });\n      logger.info(\n        `[Crawler][${jobId}] Stored the screenshot as assetId: ${assetId} (${screenshot.byteLength} bytes)`,\n      );\n      return { assetId, contentType, fileName, size: screenshot.byteLength };\n    },\n  );\n}\n\nasync function storePdf(\n  pdf: Buffer | undefined,\n  userId: string,\n  jobId: string,\n) {\n  return await withSpan(\n    tracer,\n    \"crawlerWorker.storePdf\",\n    {\n      attributes: {\n        \"job.id\": jobId,\n        \"user.id\": userId,\n        \"asset.size\": pdf?.byteLength ?? 0,\n      },\n    },\n    async () => {\n      if (!pdf) {\n        logger.info(\n          `[Crawler][${jobId}] Skipping storing the PDF as it's empty.`,\n        );\n        return null;\n      }\n      const assetId = newAssetId();\n      const contentType = \"application/pdf\";\n      const fileName = \"page.pdf\";\n\n      // Check storage quota before saving the PDF\n      const { data: quotaApproved, error: quotaError } = await tryCatch(\n        QuotaService.checkStorageQuota(db, userId, pdf.byteLength),\n      );\n\n      if (quotaError) {\n        logger.warn(\n          `[Crawler][${jobId}] Skipping PDF storage due to quota exceeded: ${quotaError.message}`,\n        );\n        return null;\n      }\n\n      await saveAsset({\n        userId,\n        assetId,\n        metadata: { contentType, fileName },\n        asset: pdf,\n        quotaApproved,\n      });\n      logger.info(\n        `[Crawler][${jobId}] Stored the PDF as assetId: ${assetId} (${pdf.byteLength} bytes)`,\n      );\n      return { assetId, contentType, fileName, size: pdf.byteLength };\n    },\n  );\n}\n\nasync function downloadAndStoreFile(\n  url: string,\n  userId: string,\n  jobId: string,\n  fileType: string,\n  abortSignal: AbortSignal,\n) {\n  return await withSpan(\n    tracer,\n    \"crawlerWorker.downloadAndStoreFile\",\n    {\n      attributes: {\n        \"bookmark.url\": url,\n        \"bookmark.domain\": getBookmarkDomain(url),\n        \"job.id\": jobId,\n        \"user.id\": userId,\n        \"asset.type\": fileType,\n      },\n    },\n    async () => {\n      let assetPath: string | undefined;\n      try {\n        logger.info(\n          `[Crawler][${jobId}] Downloading ${fileType} from \"${url.length > 100 ? url.slice(0, 100) + \"...\" : url}\"`,\n        );\n        const response = await fetchWithProxy(url, {\n          signal: abortSignal,\n        });\n        if (!response.ok || response.body == null) {\n          throw new Error(`Failed to download ${fileType}: ${response.status}`);\n        }\n\n        const contentType = normalizeContentType(\n          response.headers.get(\"content-type\"),\n        );\n        if (!contentType) {\n          throw new Error(\"No content type in the response\");\n        }\n\n        const assetId = newAssetId();\n        assetPath = path.join(os.tmpdir(), assetId);\n\n        let bytesRead = 0;\n        const contentLengthEnforcer = new Transform({\n          transform(chunk, _, callback) {\n            bytesRead += chunk.length;\n\n            if (abortSignal.aborted) {\n              callback(new Error(\"AbortError\"));\n            } else if (bytesRead > serverConfig.maxAssetSizeMb * 1024 * 1024) {\n              callback(\n                new Error(\n                  `Content length exceeds maximum allowed size: ${serverConfig.maxAssetSizeMb}MB`,\n                ),\n              );\n            } else {\n              callback(null, chunk); // pass data along unchanged\n            }\n          },\n          flush(callback) {\n            callback();\n          },\n        });\n\n        await pipeline(\n          response.body,\n          contentLengthEnforcer,\n          fsSync.createWriteStream(assetPath),\n        );\n\n        // Check storage quota before saving the asset\n        const { data: quotaApproved, error: quotaError } = await tryCatch(\n          QuotaService.checkStorageQuota(db, userId, bytesRead),\n        );\n\n        if (quotaError) {\n          logger.warn(\n            `[Crawler][${jobId}] Skipping ${fileType} storage due to quota exceeded: ${quotaError.message}`,\n          );\n          return null;\n        }\n\n        await saveAssetFromFile({\n          userId,\n          assetId,\n          metadata: { contentType },\n          assetPath,\n          quotaApproved,\n        });\n\n        logger.info(\n          `[Crawler][${jobId}] Downloaded ${fileType} as assetId: ${assetId} (${bytesRead} bytes)`,\n        );\n\n        return { assetId, userId, contentType, size: bytesRead };\n      } catch (e) {\n        logger.error(\n          `[Crawler][${jobId}] Failed to download and store ${fileType}: ${e}`,\n        );\n        return null;\n      } finally {\n        if (assetPath) {\n          await tryCatch(fs.unlink(assetPath));\n        }\n      }\n    },\n  );\n}\n\nasync function downloadAndStoreImage(\n  url: string,\n  userId: string,\n  jobId: string,\n  abortSignal: AbortSignal,\n) {\n  if (!serverConfig.crawler.downloadBannerImage) {\n    logger.info(\n      `[Crawler][${jobId}] Skipping downloading the image as per the config.`,\n    );\n    return null;\n  }\n  return downloadAndStoreFile(url, userId, jobId, \"image\", abortSignal);\n}\n\nasync function archiveWebpage(\n  html: string,\n  url: string,\n  userId: string,\n  jobId: string,\n  abortSignal: AbortSignal,\n) {\n  return await withSpan(\n    tracer,\n    \"crawlerWorker.archiveWebpage\",\n    {\n      attributes: {\n        \"bookmark.url\": url,\n        \"bookmark.domain\": getBookmarkDomain(url),\n        \"job.id\": jobId,\n        \"user.id\": userId,\n      },\n    },\n    async () => {\n      logger.info(`[Crawler][${jobId}] Will attempt to archive page ...`);\n      const assetId = newAssetId();\n      const assetPath = path.join(os.tmpdir(), assetId);\n\n      let res = await execa({\n        input: html,\n        cancelSignal: abortSignal,\n        env: {\n          https_proxy: serverConfig.proxy.httpsProxy\n            ? getRandomProxy(serverConfig.proxy.httpsProxy)\n            : undefined,\n          http_proxy: serverConfig.proxy.httpProxy\n            ? getRandomProxy(serverConfig.proxy.httpProxy)\n            : undefined,\n          no_proxy: serverConfig.proxy.noProxy?.join(\",\"),\n        },\n      })(\"monolith\", [\"-\", \"-Ije\", \"-t\", \"5\", \"-b\", url, \"-o\", assetPath]);\n\n      if (res.isCanceled) {\n        logger.error(\n          `[Crawler][${jobId}] Canceled archiving the page as we hit global timeout.`,\n        );\n        await tryCatch(fs.unlink(assetPath));\n        return null;\n      }\n\n      if (res.exitCode !== 0) {\n        logger.error(\n          `[Crawler][${jobId}] Failed to archive the page as the command exited with code ${res.exitCode}`,\n        );\n        await tryCatch(fs.unlink(assetPath));\n        return null;\n      }\n\n      const contentType = \"text/html\";\n\n      // Get file size and check quota before saving\n      const stats = await fs.stat(assetPath);\n      const fileSize = stats.size;\n\n      const { data: quotaApproved, error: quotaError } = await tryCatch(\n        QuotaService.checkStorageQuota(db, userId, fileSize),\n      );\n\n      if (quotaError) {\n        logger.warn(\n          `[Crawler][${jobId}] Skipping page archive storage due to quota exceeded: ${quotaError.message}`,\n        );\n        await tryCatch(fs.unlink(assetPath));\n        return null;\n      }\n\n      await saveAssetFromFile({\n        userId,\n        assetId,\n        assetPath,\n        metadata: {\n          contentType,\n        },\n        quotaApproved,\n      });\n\n      logger.info(\n        `[Crawler][${jobId}] Done archiving the page as assetId: ${assetId}`,\n      );\n\n      return {\n        assetId,\n        contentType,\n        size: await getAssetSize({ userId, assetId }),\n      };\n    },\n  );\n}\n\nasync function getContentType(\n  url: string,\n  jobId: string,\n  abortSignal: AbortSignal,\n): Promise<string | null> {\n  return await withSpan(\n    tracer,\n    \"crawlerWorker.getContentType\",\n    {\n      attributes: {\n        \"bookmark.url\": url,\n        \"bookmark.domain\": getBookmarkDomain(url),\n        \"job.id\": jobId,\n      },\n    },\n    async () => {\n      try {\n        logger.info(\n          `[Crawler][${jobId}] Attempting to determine the content-type for the url ${url}`,\n        );\n        const response = await fetchWithProxy(url, {\n          method: \"GET\",\n          signal: AbortSignal.any([AbortSignal.timeout(5000), abortSignal]),\n        });\n        setSpanAttributes({\n          \"crawler.getContentType.statusCode\": response.status,\n        });\n        const rawContentType = response.headers.get(\"content-type\");\n        const contentType = normalizeContentType(rawContentType);\n        setSpanAttributes({\n          \"crawler.contentType\": contentType ?? undefined,\n        });\n        logger.info(\n          `[Crawler][${jobId}] Content-type for the url ${url} is \"${contentType}\"`,\n        );\n        return contentType;\n      } catch (e) {\n        logger.error(\n          `[Crawler][${jobId}] Failed to determine the content-type for the url ${url}: ${e}`,\n        );\n        return null;\n      }\n    },\n  );\n}\n\n/**\n * Downloads the asset from the URL and transforms the linkBookmark to an assetBookmark\n * @param url the url the user provided\n * @param assetType the type of the asset we're downloading\n * @param userId the id of the user\n * @param jobId the id of the job for logging\n * @param bookmarkId the id of the bookmark\n */\nasync function handleAsAssetBookmark(\n  url: string,\n  assetType: \"image\" | \"pdf\",\n  userId: string,\n  jobId: string,\n  bookmarkId: string,\n  abortSignal: AbortSignal,\n) {\n  return await withSpan(\n    tracer,\n    \"crawlerWorker.handleAsAssetBookmark\",\n    {\n      attributes: {\n        \"bookmark.url\": url,\n        \"bookmark.domain\": getBookmarkDomain(url),\n        \"job.id\": jobId,\n        \"user.id\": userId,\n        \"bookmark.id\": bookmarkId,\n        \"asset.type\": assetType,\n      },\n    },\n    async () => {\n      const downloaded = await downloadAndStoreFile(\n        url,\n        userId,\n        jobId,\n        assetType,\n        abortSignal,\n      );\n      if (!downloaded) {\n        return;\n      }\n      const fileName = path.basename(new URL(url).pathname);\n      await db.transaction(async (trx) => {\n        await updateAsset(\n          undefined,\n          {\n            id: downloaded.assetId,\n            bookmarkId,\n            userId,\n            assetType: AssetTypes.BOOKMARK_ASSET,\n            contentType: downloaded.contentType,\n            size: downloaded.size,\n            fileName,\n          },\n          trx,\n        );\n        await trx.insert(bookmarkAssets).values({\n          id: bookmarkId,\n          assetType,\n          assetId: downloaded.assetId,\n          content: null,\n          fileName,\n          sourceUrl: url,\n        });\n        // Switch the type of the bookmark from LINK to ASSET\n        await trx\n          .update(bookmarks)\n          .set({ type: BookmarkTypes.ASSET })\n          .where(eq(bookmarks.id, bookmarkId));\n        await trx.delete(bookmarkLinks).where(eq(bookmarkLinks.id, bookmarkId));\n      });\n      await AssetPreprocessingQueue.enqueue(\n        {\n          bookmarkId,\n          fixMode: false,\n        },\n        {\n          groupId: userId,\n        },\n      );\n    },\n  );\n}\n\ntype StoreHtmlResult =\n  | { result: \"stored\"; assetId: string; size: number }\n  | { result: \"store_inline\" }\n  | { result: \"not_stored\" };\n\nasync function storeHtmlContent(\n  htmlContent: string | undefined,\n  userId: string,\n  jobId: string,\n): Promise<StoreHtmlResult> {\n  return await withSpan(\n    tracer,\n    \"crawlerWorker.storeHtmlContent\",\n    {\n      attributes: {\n        \"job.id\": jobId,\n        \"user.id\": userId,\n        \"bookmark.content.size\": htmlContent\n          ? Buffer.byteLength(htmlContent, \"utf8\")\n          : 0,\n      },\n    },\n    async () => {\n      if (!htmlContent) {\n        return { result: \"not_stored\" };\n      }\n\n      const contentSize = Buffer.byteLength(htmlContent, \"utf8\");\n\n      // Only store in assets if content is >= 50KB\n      if (contentSize < serverConfig.crawler.htmlContentSizeThreshold) {\n        logger.info(\n          `[Crawler][${jobId}] HTML content size (${contentSize} bytes) is below threshold, storing inline`,\n        );\n        return { result: \"store_inline\" };\n      }\n\n      const { data: quotaApproved, error: quotaError } = await tryCatch(\n        QuotaService.checkStorageQuota(db, userId, contentSize),\n      );\n      if (quotaError) {\n        logger.warn(\n          `[Crawler][${jobId}] Skipping HTML content storage due to quota exceeded: ${quotaError.message}`,\n        );\n        return { result: \"not_stored\" };\n      }\n\n      const assetId = newAssetId();\n\n      const { error: saveError } = await tryCatch(\n        saveAsset({\n          userId,\n          assetId,\n          asset: Buffer.from(htmlContent, \"utf8\"),\n          metadata: {\n            contentType: ASSET_TYPES.TEXT_HTML,\n            fileName: null,\n          },\n          quotaApproved,\n        }),\n      );\n      if (saveError) {\n        logger.error(\n          `[Crawler][${jobId}] Failed to store HTML content as asset: ${saveError}`,\n        );\n        throw saveError;\n      }\n\n      logger.info(\n        `[Crawler][${jobId}] Stored large HTML content (${contentSize} bytes) as asset: ${assetId}`,\n      );\n\n      return {\n        result: \"stored\",\n        assetId,\n        size: contentSize,\n      };\n    },\n  );\n}\n\nasync function crawlAndParseUrl(\n  url: string,\n  userId: string,\n  jobId: string,\n  bookmarkId: string,\n  oldScreenshotAssetId: string | undefined,\n  oldPdfAssetId: string | undefined,\n  oldImageAssetId: string | undefined,\n  oldFullPageArchiveAssetId: string | undefined,\n  oldContentAssetId: string | undefined,\n  precrawledArchiveAssetId: string | undefined,\n  archiveFullPage: boolean,\n  forceStorePdf: boolean,\n  numRetriesLeft: number,\n  abortSignal: AbortSignal,\n) {\n  return await withSpan(\n    tracer,\n    \"crawlerWorker.crawlAndParseUrl\",\n    {\n      attributes: {\n        \"bookmark.url\": url,\n        \"bookmark.domain\": getBookmarkDomain(url),\n        \"job.id\": jobId,\n        \"user.id\": userId,\n        \"bookmark.id\": bookmarkId,\n        \"crawler.archiveFullPage\": archiveFullPage,\n        \"crawler.forceStorePdf\": forceStorePdf,\n        \"crawler.hasPrecrawledArchive\": !!precrawledArchiveAssetId,\n      },\n    },\n    async () => {\n      let result: {\n        htmlContent: string;\n        screenshot: Buffer | undefined;\n        pdf: Buffer | undefined;\n        statusCode: number | null;\n        url: string;\n      };\n\n      if (precrawledArchiveAssetId) {\n        logger.info(\n          `[Crawler][${jobId}] The page has been precrawled. Will use the precrawled archive instead.`,\n        );\n        const asset = await readAsset({\n          userId,\n          assetId: precrawledArchiveAssetId,\n        });\n        result = {\n          htmlContent: asset.asset.toString(),\n          screenshot: undefined,\n          pdf: undefined,\n          statusCode: 200,\n          url,\n        };\n      } else {\n        result = await crawlPage(\n          jobId,\n          url,\n          userId,\n          forceStorePdf,\n          abortSignal,\n        );\n      }\n      abortSignal.throwIfAborted();\n\n      const {\n        htmlContent,\n        screenshot,\n        pdf,\n        statusCode,\n        url: browserUrl,\n      } = result;\n\n      // Track status code in Prometheus\n      if (statusCode !== null) {\n        crawlerStatusCodeCounter.labels(statusCode.toString()).inc();\n        setSpanAttributes({\n          \"crawler.statusCode\": statusCode,\n        });\n      }\n\n      if (shouldRetryCrawlStatusCode(statusCode)) {\n        if (numRetriesLeft > 0) {\n          throw new Error(\n            `[Crawler][${jobId}] Received status code ${statusCode}. Will retry crawl. Retries left: ${numRetriesLeft}`,\n          );\n        }\n        logger.info(\n          `[Crawler][${jobId}] Received status code ${statusCode} on latest retry attempt. Proceeding without retry.`,\n        );\n      }\n\n      const { metadata: meta, readableContent: parsedReadableContent } =\n        await runParseSubprocess(htmlContent, browserUrl, jobId, abortSignal);\n      abortSignal.throwIfAborted();\n\n      const parseDate = (date: string | null | undefined) => {\n        if (!date) {\n          return null;\n        }\n        try {\n          return new Date(date);\n        } catch {\n          return null;\n        }\n      };\n\n      // Phase 1: Write metadata immediately for fast user feedback.\n      // Content and asset storage happen later and can be slow (banner\n      // image download, screenshot/pdf upload, etc.).\n      await db\n        .update(bookmarkLinks)\n        .set({\n          title: meta.title,\n          description: meta.description,\n          // Don't store data URIs as they're not valid URLs and are usually quite large\n          imageUrl: meta.image?.startsWith(\"data:\") ? null : meta.image,\n          favicon: meta.logo,\n          crawlStatusCode: statusCode,\n          author: meta.author,\n          publisher: meta.publisher,\n          datePublished: parseDate(meta.datePublished),\n          dateModified: parseDate(meta.dateModified),\n        })\n        .where(eq(bookmarkLinks.id, bookmarkId));\n\n      let readableContent = parsedReadableContent;\n\n      const screenshotAssetInfo = await Promise.race([\n        storeScreenshot(screenshot, userId, jobId),\n        abortPromise(abortSignal),\n      ]);\n      abortSignal.throwIfAborted();\n\n      const pdfAssetInfo = await Promise.race([\n        storePdf(pdf, userId, jobId),\n        abortPromise(abortSignal),\n      ]);\n      abortSignal.throwIfAborted();\n\n      const htmlContentAssetInfo = await storeHtmlContent(\n        readableContent?.content,\n        userId,\n        jobId,\n      );\n      abortSignal.throwIfAborted();\n      let imageAssetInfo: DBAssetType | null = null;\n      if (meta.image) {\n        const downloaded = await downloadAndStoreImage(\n          meta.image,\n          userId,\n          jobId,\n          abortSignal,\n        );\n        if (downloaded) {\n          imageAssetInfo = {\n            id: downloaded.assetId,\n            bookmarkId,\n            userId,\n            assetType: AssetTypes.LINK_BANNER_IMAGE,\n            contentType: downloaded.contentType,\n            size: downloaded.size,\n          };\n        }\n      }\n      abortSignal.throwIfAborted();\n\n      // Phase 2: Write content and asset references.\n      // TODO(important): Restrict the size of content to store\n      const assetDeletionTasks: Promise<void>[] = [];\n      const inlineHtmlContent =\n        htmlContentAssetInfo.result === \"store_inline\"\n          ? (readableContent?.content ?? null)\n          : null;\n      readableContent = null;\n      await db.transaction(async (txn) => {\n        await txn\n          .update(bookmarkLinks)\n          .set({\n            crawledAt: new Date(),\n            htmlContent: inlineHtmlContent,\n            contentAssetId:\n              htmlContentAssetInfo.result === \"stored\"\n                ? htmlContentAssetInfo.assetId\n                : null,\n          })\n          .where(eq(bookmarkLinks.id, bookmarkId));\n\n        if (screenshotAssetInfo) {\n          await updateAsset(\n            oldScreenshotAssetId,\n            {\n              id: screenshotAssetInfo.assetId,\n              bookmarkId,\n              userId,\n              assetType: AssetTypes.LINK_SCREENSHOT,\n              contentType: screenshotAssetInfo.contentType,\n              size: screenshotAssetInfo.size,\n              fileName: screenshotAssetInfo.fileName,\n            },\n            txn,\n          );\n          assetDeletionTasks.push(\n            silentDeleteAsset(userId, oldScreenshotAssetId),\n          );\n        }\n        if (pdfAssetInfo) {\n          await updateAsset(\n            oldPdfAssetId,\n            {\n              id: pdfAssetInfo.assetId,\n              bookmarkId,\n              userId,\n              assetType: AssetTypes.LINK_PDF,\n              contentType: pdfAssetInfo.contentType,\n              size: pdfAssetInfo.size,\n              fileName: pdfAssetInfo.fileName,\n            },\n            txn,\n          );\n          assetDeletionTasks.push(silentDeleteAsset(userId, oldPdfAssetId));\n        }\n        if (imageAssetInfo) {\n          await updateAsset(oldImageAssetId, imageAssetInfo, txn);\n          assetDeletionTasks.push(silentDeleteAsset(userId, oldImageAssetId));\n        }\n        if (htmlContentAssetInfo.result === \"stored\") {\n          await updateAsset(\n            oldContentAssetId,\n            {\n              id: htmlContentAssetInfo.assetId,\n              bookmarkId,\n              userId,\n              assetType: AssetTypes.LINK_HTML_CONTENT,\n              contentType: ASSET_TYPES.TEXT_HTML,\n              size: htmlContentAssetInfo.size,\n              fileName: null,\n            },\n            txn,\n          );\n          assetDeletionTasks.push(silentDeleteAsset(userId, oldContentAssetId));\n        } else if (oldContentAssetId) {\n          // Unlink the old content asset\n          await txn.delete(assets).where(eq(assets.id, oldContentAssetId));\n          assetDeletionTasks.push(silentDeleteAsset(userId, oldContentAssetId));\n        }\n      });\n\n      // Delete the old assets if any\n      await Promise.all(assetDeletionTasks);\n\n      return async () => {\n        if (\n          !precrawledArchiveAssetId &&\n          (serverConfig.crawler.fullPageArchive || archiveFullPage)\n        ) {\n          const archiveResult = await archiveWebpage(\n            htmlContent,\n            browserUrl,\n            userId,\n            jobId,\n            abortSignal,\n          );\n\n          if (archiveResult) {\n            const {\n              assetId: fullPageArchiveAssetId,\n              size,\n              contentType,\n            } = archiveResult;\n\n            await db.transaction(async (txn) => {\n              await updateAsset(\n                oldFullPageArchiveAssetId,\n                {\n                  id: fullPageArchiveAssetId,\n                  bookmarkId,\n                  userId,\n                  assetType: AssetTypes.LINK_FULL_PAGE_ARCHIVE,\n                  contentType,\n                  size,\n                  fileName: null,\n                },\n                txn,\n              );\n            });\n            if (oldFullPageArchiveAssetId) {\n              await silentDeleteAsset(userId, oldFullPageArchiveAssetId);\n            }\n          }\n        }\n      };\n    },\n  );\n}\n\n/**\n * Checks if the domain should be rate limited and throws QueueRetryAfterError if needed.\n * @throws {QueueRetryAfterError} if the domain is rate limited\n */\nasync function checkDomainRateLimit(url: string, jobId: string): Promise<void> {\n  return await withSpan(\n    tracer,\n    \"crawlerWorker.checkDomainRateLimit\",\n    {\n      attributes: {\n        \"bookmark.url\": url,\n        \"bookmark.domain\": getBookmarkDomain(url),\n        \"job.id\": jobId,\n      },\n    },\n    async () => {\n      const crawlerDomainRateLimitConfig =\n        serverConfig.crawler.domainRatelimiting;\n      if (!crawlerDomainRateLimitConfig) {\n        return;\n      }\n\n      const rateLimitClient = await getRateLimitClient();\n      if (!rateLimitClient) {\n        return;\n      }\n\n      const hostname = new URL(url).hostname;\n      const rateLimitResult = await rateLimitClient.checkRateLimit(\n        {\n          name: \"domain-ratelimit\",\n          maxRequests: crawlerDomainRateLimitConfig.maxRequests,\n          windowMs: crawlerDomainRateLimitConfig.windowMs,\n        },\n        hostname,\n      );\n\n      if (!rateLimitResult.allowed) {\n        const resetInSeconds = rateLimitResult.resetInSeconds;\n        // Add jitter to prevent thundering herd: +40% random variation\n        const jitterFactor = 1.0 + Math.random() * 0.4; // Random value between 1.0 and 1.4\n        const delayMs = Math.floor(resetInSeconds * 1000 * jitterFactor);\n        logger.info(\n          `[Crawler][${jobId}] Domain \"${hostname}\" is rate limited. Will retry in ${(delayMs / 1000).toFixed(2)} seconds (with jitter).`,\n        );\n        throw new QueueRetryAfterError(\n          `Domain \"${hostname}\" is rate limited`,\n          delayMs,\n        );\n      }\n    },\n  );\n}\n\nasync function runCrawler(\n  job: DequeuedJob<ZCrawlLinkRequest>,\n  maxRetries: number,\n): Promise<CrawlerRunResult> {\n  const jobId = `${job.id}:${job.runNumber}`;\n  const numRetriesLeft = Math.max(maxRetries - job.runNumber, 0);\n\n  const request = zCrawlLinkRequestSchema.safeParse(job.data);\n  if (!request.success) {\n    logger.error(\n      `[Crawler][${jobId}] Got malformed job request: ${request.error.toString()}`,\n    );\n    return { status: \"completed\" };\n  }\n\n  const { bookmarkId, archiveFullPage, storePdf } = request.data;\n  const {\n    url,\n    userId,\n    createdAt,\n    crawledAt,\n    screenshotAssetId: oldScreenshotAssetId,\n    pdfAssetId: oldPdfAssetId,\n    imageAssetId: oldImageAssetId,\n    fullPageArchiveAssetId: oldFullPageArchiveAssetId,\n    contentAssetId: oldContentAssetId,\n    precrawledArchiveAssetId,\n  } = await getBookmarkDetails(bookmarkId);\n\n  await checkDomainRateLimit(url, jobId);\n\n  logger.info(\n    `[Crawler][${jobId}] Will crawl \"${url}\" for link with id \"${bookmarkId}\"`,\n  );\n\n  const contentType = await getContentType(url, jobId, job.abortSignal);\n  job.abortSignal.throwIfAborted();\n\n  // Link bookmarks get transformed into asset bookmarks if they point to a supported asset instead of a webpage\n  const isPdf = contentType === ASSET_TYPES.APPLICATION_PDF;\n\n  if (isPdf) {\n    await handleAsAssetBookmark(\n      url,\n      \"pdf\",\n      userId,\n      jobId,\n      bookmarkId,\n      job.abortSignal,\n    );\n  } else if (\n    contentType &&\n    IMAGE_ASSET_TYPES.has(contentType) &&\n    SUPPORTED_UPLOAD_ASSET_TYPES.has(contentType)\n  ) {\n    await handleAsAssetBookmark(\n      url,\n      \"image\",\n      userId,\n      jobId,\n      bookmarkId,\n      job.abortSignal,\n    );\n  } else {\n    const archivalLogic = await crawlAndParseUrl(\n      url,\n      userId,\n      jobId,\n      bookmarkId,\n      oldScreenshotAssetId,\n      oldPdfAssetId,\n      oldImageAssetId,\n      oldFullPageArchiveAssetId,\n      oldContentAssetId,\n      precrawledArchiveAssetId,\n      archiveFullPage,\n      storePdf ?? false,\n      numRetriesLeft,\n      job.abortSignal,\n    );\n\n    // Propagate priority to child jobs\n    const enqueueOpts: EnqueueOptions = {\n      priority: job.priority,\n      groupId: userId,\n    };\n\n    // Enqueue openai job (if not set, assume it's true for backward compatibility)\n    if (job.data.runInference !== false) {\n      await OpenAIQueue.enqueue(\n        {\n          bookmarkId,\n          type: \"tag\",\n        },\n        enqueueOpts,\n      );\n      await OpenAIQueue.enqueue(\n        {\n          bookmarkId,\n          type: \"summarize\",\n        },\n        enqueueOpts,\n      );\n    }\n\n    // Update the search index\n    await triggerSearchReindex(bookmarkId, enqueueOpts);\n\n    if (serverConfig.crawler.downloadVideo) {\n      // Trigger a potential download of a video from the URL\n      await VideoWorkerQueue.enqueue(\n        {\n          bookmarkId,\n          url,\n        },\n        enqueueOpts,\n      );\n    }\n\n    // Trigger a webhook\n    await triggerWebhook(bookmarkId, \"crawled\", undefined, enqueueOpts);\n\n    // Do the archival as a separate last step as it has the potential for failure\n    await archivalLogic();\n  }\n\n  // Record the latency from bookmark creation to crawl completion.\n  // Only for first-time, high-priority crawls (excludes recrawls and imports).\n  if (crawledAt === null && job.priority === 0) {\n    const latencySeconds = (Date.now() - createdAt.getTime()) / 1000;\n    bookmarkCrawlLatencyHistogram.observe(latencySeconds);\n  }\n\n  return { status: \"completed\" };\n}\n"
  },
  {
    "path": "apps/workers/workers/feedWorker.ts",
    "content": "import { and, eq, inArray } from \"drizzle-orm\";\nimport { workerStatsCounter } from \"metrics\";\nimport { fetchWithProxy } from \"network\";\nimport cron from \"node-cron\";\nimport { buildImpersonatingTRPCClient } from \"trpc\";\nimport { withWorkerTracing } from \"workerTracing\";\n\nimport type { ZFeedRequestSchema } from \"@karakeep/shared-server\";\nimport { db } from \"@karakeep/db\";\nimport { rssFeedImportsTable, rssFeedsTable } from \"@karakeep/db/schema\";\nimport { FeedQueue, QuotaService } from \"@karakeep/shared-server\";\nimport logger from \"@karakeep/shared/logger\";\nimport { DequeuedJob, getQueueClient } from \"@karakeep/shared/queueing\";\nimport { BookmarkTypes } from \"@karakeep/shared/types/bookmarks\";\n\nimport { parseFeedItems } from \"./utils/feedParser\";\n\n/**\n * Deterministically maps a feed ID to a minute offset within the hour (0-59).\n * This ensures feeds are spread evenly across the hour based on their ID.\n */\nfunction getFeedMinuteOffset(feedId: string): number {\n  // Simple hash function: sum character codes\n  let hash = 0;\n  for (let i = 0; i < feedId.length; i++) {\n    hash = (hash << 5) - hash + feedId.charCodeAt(i);\n    hash = hash & hash; // Convert to 32-bit integer\n  }\n  // Return a minute offset between 0 and 59\n  return Math.abs(hash) % 60;\n}\n\nexport const FeedRefreshingWorker = cron.schedule(\n  \"0 * * * *\",\n  () => {\n    logger.info(\"[feed] Scheduling feed refreshing jobs ...\");\n    db.query.rssFeedsTable\n      .findMany({\n        columns: {\n          id: true,\n          userId: true,\n        },\n        where: eq(rssFeedsTable.enabled, true),\n      })\n      .then((feeds) => {\n        const currentHour = new Date();\n        currentHour.setMinutes(0, 0, 0);\n        const hourlyWindow = currentHour.toISOString();\n        const now = new Date();\n        const currentMinute = now.getMinutes();\n\n        for (const feed of feeds) {\n          const idempotencyKey = `${feed.id}-${hourlyWindow}`;\n          const targetMinute = getFeedMinuteOffset(feed.id);\n\n          // Calculate delay: if target minute has passed, schedule for next hour\n          let delayMinutes = targetMinute - currentMinute;\n          if (delayMinutes < 0) {\n            delayMinutes += 60;\n          }\n          const delayMs = delayMinutes * 60 * 1000;\n\n          logger.debug(\n            `[feed] Scheduling feed ${feed.id} at minute ${targetMinute} (delay: ${delayMinutes} minutes)`,\n          );\n\n          FeedQueue.enqueue(\n            {\n              feedId: feed.id,\n            },\n            {\n              idempotencyKey,\n              groupId: feed.userId,\n              delayMs,\n            },\n          );\n        }\n      });\n  },\n  {\n    runOnInit: false,\n    scheduled: false,\n  },\n);\n\nexport class FeedWorker {\n  static async build() {\n    logger.info(\"Starting feed worker ...\");\n    const worker = (await getQueueClient())!.createRunner<ZFeedRequestSchema>(\n      FeedQueue,\n      {\n        run: withWorkerTracing(\"feedWorker.run\", run),\n        onComplete: async (job) => {\n          workerStatsCounter.labels(\"feed\", \"completed\").inc();\n          const jobId = job.id;\n          logger.info(`[feed][${jobId}] Completed successfully`);\n          await db\n            .update(rssFeedsTable)\n            .set({ lastFetchedStatus: \"success\", lastFetchedAt: new Date() })\n            .where(eq(rssFeedsTable.id, job.data?.feedId));\n        },\n        onError: async (job) => {\n          workerStatsCounter.labels(\"feed\", \"failed\").inc();\n          if (job.numRetriesLeft == 0) {\n            workerStatsCounter.labels(\"feed\", \"failed_permanent\").inc();\n          }\n          const jobId = job.id;\n          logger.error(\n            `[feed][${jobId}] Feed fetch job failed: ${job.error}\\n${job.error.stack}`,\n          );\n          if (job.data) {\n            await db\n              .update(rssFeedsTable)\n              .set({ lastFetchedStatus: \"failure\", lastFetchedAt: new Date() })\n              .where(eq(rssFeedsTable.id, job.data?.feedId));\n          }\n        },\n      },\n      {\n        concurrency: 1,\n        pollIntervalMs: 1000,\n        timeoutSecs: 30,\n      },\n    );\n\n    return worker;\n  }\n}\n\nasync function run(req: DequeuedJob<ZFeedRequestSchema>) {\n  const jobId = req.id;\n  const feed = await db.query.rssFeedsTable.findFirst({\n    where: eq(rssFeedsTable.id, req.data.feedId),\n  });\n  if (!feed) {\n    throw new Error(\n      `[feed][${jobId}] Feed with id ${req.data.feedId} not found`,\n    );\n  }\n\n  // If the user doesn't have bookmark quota, don't bother with fetching the feed\n  {\n    const quotaResult = await QuotaService.canCreateBookmark(db, feed.userId);\n    if (!quotaResult.result) {\n      logger.debug(\n        `[feed][${jobId}] User ${feed.userId} doesn't have enough quota to create bookmarks. Skipping feed fetching.`,\n      );\n      return;\n    }\n  }\n\n  logger.info(\n    `[feed][${jobId}] Starting fetching feed \"${feed.name}\" (${feed.id}) ...`,\n  );\n\n  const response = await fetchWithProxy(feed.url, {\n    signal: AbortSignal.timeout(5000),\n    headers: {\n      \"User-Agent\":\n        \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36\",\n      Accept: \"application/rss+xml, application/xml;q=0.9, text/xml;q=0.8\",\n    },\n  });\n  if (response.status !== 200) {\n    throw new Error(\n      `[feed][${jobId}] Feed \"${feed.name}\" (${feed.id}) returned a non-success status: ${response.status}.`,\n    );\n  }\n  const contentType = response.headers.get(\"content-type\");\n  if (!contentType || !contentType.includes(\"xml\")) {\n    throw new Error(\n      `[feed][${jobId}] Feed \"${feed.name}\" (${feed.id}) is not a valid RSS feed`,\n    );\n  }\n  const xmlData = await response.text();\n\n  logger.info(\n    `[feed][${jobId}] Successfully fetched feed \"${feed.name}\" (${feed.id}) ...`,\n  );\n\n  const feedItems = await parseFeedItems(xmlData);\n\n  logger.info(\n    `[feed][${jobId}] Found ${feedItems.length} entries in feed \"${feed.name}\" (${feed.id}) ...`,\n  );\n\n  if (feedItems.length === 0) {\n    logger.info(`[feed][${jobId}] No entries found.`);\n    return;\n  }\n\n  const exitingEntries = await db.query.rssFeedImportsTable.findMany({\n    where: and(\n      eq(rssFeedImportsTable.rssFeedId, feed.id),\n      inArray(\n        rssFeedImportsTable.entryId,\n        feedItems.map((item) => item.guid).filter((id): id is string => !!id),\n      ),\n    ),\n  });\n\n  const newEntries = feedItems.filter(\n    (item) =>\n      !exitingEntries.some((entry) => entry.entryId === item.guid) &&\n      item.link &&\n      item.guid,\n  );\n\n  if (newEntries.length === 0) {\n    logger.info(\n      `[feed][${jobId}] No new entries found in feed \"${feed.name}\" (${feed.id}).`,\n    );\n    return;\n  }\n\n  logger.info(\n    `[feed][${jobId}] Found ${newEntries.length} new entries in feed \"${feed.name}\" (${feed.id}) ...`,\n  );\n\n  const trpcClient = await buildImpersonatingTRPCClient(feed.userId);\n\n  const createdBookmarks = await Promise.allSettled(\n    newEntries.map((item) =>\n      trpcClient.bookmarks.createBookmark({\n        type: BookmarkTypes.LINK,\n        url: item.link!,\n        title: item.title,\n        source: \"rss\",\n      }),\n    ),\n  );\n\n  // If importTags is enabled, attach categories as tags to the created bookmarks\n  if (feed.importTags) {\n    await Promise.allSettled(\n      newEntries.map(async (item, idx) => {\n        const bookmark = createdBookmarks[idx];\n        if (\n          bookmark.status === \"fulfilled\" &&\n          item.categories &&\n          item.categories.length > 0\n        ) {\n          try {\n            await trpcClient.bookmarks.updateTags({\n              bookmarkId: bookmark.value.id,\n              attach: item.categories.map((tagName) => ({ tagName })),\n              detach: [],\n            });\n          } catch (error) {\n            logger.warn(\n              `[feed][${jobId}] Failed to attach tags to bookmark ${bookmark.value.id}: ${error}`,\n            );\n          }\n        }\n      }),\n    );\n  }\n\n  // It's ok if this is not transactional as the bookmarks will get linked in the next iteration.\n  await db\n    .insert(rssFeedImportsTable)\n    .values(\n      newEntries.map((item, idx) => {\n        const b = createdBookmarks[idx];\n        return {\n          entryId: item.guid!,\n          bookmarkId: b.status === \"fulfilled\" ? b.value.id : null,\n          rssFeedId: feed.id,\n        };\n      }),\n    )\n    .onConflictDoNothing();\n\n  logger.info(\n    `[feed][${jobId}] Successfully imported ${newEntries.length} new enteries from feed \"${feed.name}\" (${feed.id}).`,\n  );\n\n  return Promise.resolve();\n}\n"
  },
  {
    "path": "apps/workers/workers/importWorker.ts",
    "content": "import { TRPCError } from \"@trpc/server\";\nimport {\n  and,\n  count,\n  eq,\n  gt,\n  inArray,\n  isNotNull,\n  isNull,\n  lt,\n  or,\n} from \"drizzle-orm\";\nimport { Counter, Gauge, Histogram } from \"prom-client\";\nimport { buildImpersonatingTRPCClient } from \"trpc\";\n\nimport { db } from \"@karakeep/db\";\nimport {\n  bookmarkLinks,\n  bookmarks,\n  importSessions,\n  importStagingBookmarks,\n} from \"@karakeep/db/schema\";\nimport logger, { throttledLogger } from \"@karakeep/shared/logger\";\nimport {\n  BookmarkTypes,\n  MAX_BOOKMARK_TITLE_LENGTH,\n} from \"@karakeep/shared/types/bookmarks\";\n\nimport { registry } from \"../metrics\";\n\n// Prometheus metrics\nconst importStagingProcessedCounter = new Counter({\n  name: \"karakeep_import_staging_processed_total\",\n  help: \"Total number of staged items processed\",\n  labelNames: [\"result\"],\n  registers: [registry],\n});\n\nconst importStagingStaleResetCounter = new Counter({\n  name: \"karakeep_import_staging_stale_reset_total\",\n  help: \"Total number of stale processing items reset to pending\",\n  registers: [registry],\n});\n\nconst importStagingInFlightGauge = new Gauge({\n  name: \"karakeep_import_staging_in_flight\",\n  help: \"Current number of in-flight items (processing + recently completed)\",\n  registers: [registry],\n});\n\nconst importSessionsGauge = new Gauge({\n  name: \"karakeep_import_sessions_active\",\n  help: \"Number of active import sessions by status\",\n  labelNames: [\"status\"],\n  registers: [registry],\n});\n\nconst importStagingPendingGauge = new Gauge({\n  name: \"karakeep_import_staging_pending_total\",\n  help: \"Total number of pending items in staging table\",\n  registers: [registry],\n});\n\nconst importBatchDurationHistogram = new Histogram({\n  name: \"karakeep_import_batch_duration_seconds\",\n  help: \"Time taken to process a batch of staged items\",\n  buckets: [0.1, 0.5, 1, 2, 5, 10, 30],\n  registers: [registry],\n});\n\nconst backpressureLogger = throttledLogger(60_000);\n\nfunction sleep(ms: number): Promise<void> {\n  return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Extract a safe, user-facing error message from an error.\n * Avoids leaking internal details like database errors, stack traces, or file paths.\n */\nfunction getSafeErrorMessage(error: unknown): string {\n  // TRPCError client errors are designed to be user-facing\n  if (error instanceof TRPCError && error.code !== \"INTERNAL_SERVER_ERROR\") {\n    return error.message;\n  }\n\n  // Known safe validation errors thrown within the import worker\n  if (error instanceof Error) {\n    const safeMessages = [\n      \"URL is required for link bookmarks\",\n      \"Content is required for text bookmarks\",\n    ];\n    if (safeMessages.includes(error.message)) {\n      return error.message;\n    }\n  }\n\n  return \"An unexpected error occurred while processing the bookmark\";\n}\n\nexport class ImportWorker {\n  private running = false;\n  private pollIntervalMs = 5000;\n\n  // Backpressure settings\n  private maxInFlight = 50;\n  private batchSize = 10;\n  private staleThresholdMs = 60 * 60 * 1000; // 1 hour\n\n  async start() {\n    this.running = true;\n    let iterationCount = 0;\n\n    logger.info(\"[import] Starting import polling worker\");\n\n    while (this.running) {\n      try {\n        // Periodically reset stale processing items (every 60 iterations ~= 1 min)\n        if (iterationCount % 60 === 0) {\n          await this.resetStaleProcessingItems();\n        }\n        iterationCount++;\n\n        // Check if any processing items have completed downstream work\n        await this.checkAndCompleteProcessingItems();\n\n        const processed = await this.processBatch();\n        if (processed === 0) {\n          await this.checkAndCompleteIdleSessions();\n          await this.updateGauges();\n          // Nothing to do, wait before polling again\n          await sleep(this.pollIntervalMs);\n        } else {\n          await this.updateGauges();\n        }\n      } catch (error) {\n        logger.error(`[import] Error in polling loop: ${error}`);\n        await sleep(this.pollIntervalMs);\n      }\n    }\n  }\n\n  stop() {\n    logger.info(\"[import] Stopping import polling worker\");\n    this.running = false;\n  }\n\n  private async processBatch(): Promise<number> {\n    const countPendingItems = await this.countPendingItems();\n    importStagingPendingGauge.set(countPendingItems);\n    if (countPendingItems === 0) {\n      // Nothing to do, wait before polling again\n      return 0;\n    }\n\n    // 1. Check backpressure - inflight items + queue sizes\n    const availableCapacity = await this.getAvailableCapacity();\n\n    if (availableCapacity <= 0) {\n      // At capacity, wait before trying again\n      backpressureLogger(\n        \"info\",\n        `[import] Pending import items: ${countPendingItems}, but current capacity is ${availableCapacity}. Will wait until capacity is available.`,\n      );\n      return 0;\n    }\n\n    logger.debug(\n      `[import] ${countPendingItems} pending items, available capacity: ${availableCapacity}`,\n    );\n\n    // 2. Get candidate IDs with fair scheduling across users\n    const batchLimit = Math.min(this.batchSize, availableCapacity);\n    const candidateIds = await this.getNextBatchFairly(batchLimit);\n\n    if (candidateIds.length === 0) return 0;\n\n    // 3. Atomically claim rows - only rows still pending will be claimed\n    // This prevents race conditions where multiple workers select the same rows\n    const batch = await db\n      .update(importStagingBookmarks)\n      .set({ status: \"processing\", processingStartedAt: new Date() })\n      .where(\n        and(\n          eq(importStagingBookmarks.status, \"pending\"),\n          inArray(importStagingBookmarks.id, candidateIds),\n        ),\n      )\n      .returning();\n\n    // If no rows were claimed (another worker got them first), skip processing\n    if (batch.length === 0) return 0;\n\n    const batchTimer = importBatchDurationHistogram.startTimer();\n\n    // 4. Mark session(s) as running (using claimed rows, not candidates)\n    const sessionIds = [...new Set(batch.map((b) => b.importSessionId))];\n    logger.info(\n      `[import] Claimed batch of ${batch.length} items from ${sessionIds.length} session(s): [${sessionIds.join(\", \")}]`,\n    );\n    await db\n      .update(importSessions)\n      .set({ status: \"running\" })\n      .where(\n        and(\n          inArray(importSessions.id, sessionIds),\n          eq(importSessions.status, \"pending\"),\n        ),\n      );\n\n    // 5. Process in parallel\n    const results = await Promise.allSettled(\n      batch.map((staged) => this.processOneBookmark(staged)),\n    );\n\n    const outcomes: Record<string, number> = {};\n    for (const r of results) {\n      const key = r.status === \"fulfilled\" ? r.value : \"error\";\n      outcomes[key] = (outcomes[key] ?? 0) + 1;\n    }\n    logger.debug(\n      `[import] Batch results: ${Object.entries(outcomes)\n        .map(([k, v]) => `${k}=${v}`)\n        .join(\", \")}`,\n    );\n\n    // 6. Check if any sessions are now complete\n    await this.checkAndCompleteEmptySessions(sessionIds);\n\n    batchTimer(); // Record batch duration\n\n    return batch.length;\n  }\n\n  private async updateGauges() {\n    // Update active sessions gauge by status\n    const sessions = await db\n      .select({\n        status: importSessions.status,\n        count: count(),\n      })\n      .from(importSessions)\n      .where(\n        inArray(importSessions.status, [\n          \"staging\",\n          \"pending\",\n          \"running\",\n          \"paused\",\n        ]),\n      )\n      .groupBy(importSessions.status);\n\n    // Reset all status gauges to 0 first\n    for (const status of [\"staging\", \"pending\", \"running\", \"paused\"]) {\n      importSessionsGauge.set({ status }, 0);\n    }\n\n    // Set actual values\n    for (const s of sessions) {\n      importSessionsGauge.set({ status: s.status }, s.count);\n    }\n  }\n\n  private async checkAndCompleteIdleSessions() {\n    const sessions = await db\n      .select({ id: importSessions.id })\n      .from(importSessions)\n      .where(inArray(importSessions.status, [\"pending\", \"running\"]));\n\n    const sessionIds = sessions.map((session) => session.id);\n    if (sessionIds.length === 0) {\n      return;\n    }\n\n    await this.checkAndCompleteEmptySessions(sessionIds);\n  }\n\n  private async countPendingItems(): Promise<number> {\n    const res = await db\n      .select({ count: count() })\n      .from(importStagingBookmarks)\n      .innerJoin(\n        importSessions,\n        eq(importStagingBookmarks.importSessionId, importSessions.id),\n      )\n      .where(\n        and(\n          eq(importStagingBookmarks.status, \"pending\"),\n          inArray(importSessions.status, [\"pending\", \"running\"]),\n        ),\n      );\n    return res[0]?.count ?? 0;\n  }\n\n  private async getNextBatchFairly(limit: number): Promise<string[]> {\n    // Query pending item IDs from active sessions, ordered by:\n    // 1. User's last-served timestamp (fairness)\n    // 2. Staging item creation time (FIFO within user)\n    // Returns only IDs - actual rows will be fetched atomically during claim\n    const results = await db\n      .select({\n        id: importStagingBookmarks.id,\n      })\n      .from(importStagingBookmarks)\n      .innerJoin(\n        importSessions,\n        eq(importStagingBookmarks.importSessionId, importSessions.id),\n      )\n      .where(\n        and(\n          eq(importStagingBookmarks.status, \"pending\"),\n          inArray(importSessions.status, [\"pending\", \"running\"]),\n        ),\n      )\n      .orderBy(importSessions.lastProcessedAt, importStagingBookmarks.createdAt)\n      .limit(limit);\n\n    return results.map((r) => r.id);\n  }\n\n  private async attachBookmarkToLists(\n    caller: Awaited<ReturnType<typeof buildImpersonatingTRPCClient>>,\n    session: typeof importSessions.$inferSelect,\n    staged: typeof importStagingBookmarks.$inferSelect,\n    bookmarkId: string,\n  ): Promise<void> {\n    const listIds = new Set<string>();\n\n    if (session.rootListId) {\n      listIds.add(session.rootListId);\n    }\n\n    if (staged.listIds && staged.listIds.length > 0) {\n      for (const listId of staged.listIds) {\n        listIds.add(listId);\n      }\n    }\n\n    for (const listId of listIds) {\n      try {\n        await caller.lists.addToList({ listId, bookmarkId });\n      } catch (error) {\n        logger.warn(\n          `[import] Failed to add bookmark ${bookmarkId} to list ${listId}: ${error}`,\n        );\n      }\n    }\n  }\n\n  private async processOneBookmark(\n    staged: typeof importStagingBookmarks.$inferSelect,\n  ): Promise<string> {\n    const session = await db.query.importSessions.findFirst({\n      where: eq(importSessions.id, staged.importSessionId),\n    });\n\n    if (!session || session.status === \"paused\") {\n      // Session paused mid-batch, reset item to pending\n      await db\n        .update(importStagingBookmarks)\n        .set({ status: \"pending\" })\n        .where(eq(importStagingBookmarks.id, staged.id));\n      return \"reset\";\n    }\n\n    try {\n      // Use existing tRPC mutation via internal caller\n      // Note: Duplicate detection is handled by createBookmark itself\n      const caller = await buildImpersonatingTRPCClient(session.userId);\n\n      // Build the request based on bookmark type\n      type CreateBookmarkInput = Parameters<\n        typeof caller.bookmarks.createBookmark\n      >[0];\n\n      const normalizedTitle = staged.title\n        ?.trim()\n        .substring(0, MAX_BOOKMARK_TITLE_LENGTH);\n\n      const baseRequest = {\n        title: normalizedTitle || undefined,\n        note: staged.note ?? undefined,\n        createdAt: staged.sourceAddedAt ?? undefined,\n        crawlPriority: \"low\" as const,\n      };\n\n      let bookmarkRequest: CreateBookmarkInput;\n\n      if (staged.type === \"link\") {\n        if (!staged.url) {\n          throw new Error(\"URL is required for link bookmarks\");\n        }\n        bookmarkRequest = {\n          ...baseRequest,\n          type: BookmarkTypes.LINK,\n          url: staged.url,\n        };\n      } else if (staged.type === \"text\") {\n        if (!staged.content) {\n          throw new Error(\"Content is required for text bookmarks\");\n        }\n        bookmarkRequest = {\n          ...baseRequest,\n          type: BookmarkTypes.TEXT,\n          text: staged.content,\n        };\n      } else {\n        // asset type - skip for now as it needs special handling\n        await db\n          .update(importStagingBookmarks)\n          .set({\n            status: \"failed\",\n            result: \"rejected\",\n            resultReason: \"Asset bookmarks not yet supported\",\n            completedAt: new Date(),\n          })\n          .where(eq(importStagingBookmarks.id, staged.id));\n        await this.updateSessionLastProcessedAt(staged.importSessionId);\n        return \"unsupported\";\n      }\n\n      const result = await caller.bookmarks.createBookmark(bookmarkRequest);\n\n      // Apply tags via existing mutation (for both new and duplicate bookmarks)\n      if (staged.tags && staged.tags.length > 0) {\n        await caller.bookmarks.updateTags({\n          bookmarkId: result.id,\n          attach: staged.tags.map((t) => ({ tagName: t })),\n          detach: [],\n        });\n      }\n\n      // Handle duplicate case (createBookmark returns alreadyExists: true)\n      if (result.alreadyExists) {\n        await db\n          .update(importStagingBookmarks)\n          .set({\n            status: \"completed\",\n            result: \"skipped_duplicate\",\n            resultReason: \"URL already exists\",\n            resultBookmarkId: result.id,\n            completedAt: new Date(),\n          })\n          .where(eq(importStagingBookmarks.id, staged.id));\n\n        importStagingProcessedCounter.inc({ result: \"skipped_duplicate\" });\n        await this.attachBookmarkToLists(caller, session, staged, result.id);\n        await this.updateSessionLastProcessedAt(staged.importSessionId);\n        return \"duplicate\";\n      }\n\n      // Mark as accepted but keep in \"processing\" until crawl/tag is done\n      // The item will be moved to \"completed\" by checkAndCompleteProcessingItems()\n      await db\n        .update(importStagingBookmarks)\n        .set({\n          result: \"accepted\",\n          resultBookmarkId: result.id,\n        })\n        .where(eq(importStagingBookmarks.id, staged.id));\n\n      await this.attachBookmarkToLists(caller, session, staged, result.id);\n\n      await this.updateSessionLastProcessedAt(staged.importSessionId);\n      return \"accepted\";\n    } catch (error) {\n      logger.error(\n        `[import] Error processing staged item ${staged.id}: ${error}`,\n      );\n      await db\n        .update(importStagingBookmarks)\n        .set({\n          status: \"failed\",\n          result: \"rejected\",\n          resultReason: getSafeErrorMessage(error),\n          completedAt: new Date(),\n        })\n        .where(eq(importStagingBookmarks.id, staged.id));\n\n      importStagingProcessedCounter.inc({ result: \"rejected\" });\n      await this.updateSessionLastProcessedAt(staged.importSessionId);\n      return \"failed\";\n    }\n  }\n\n  private async updateSessionLastProcessedAt(sessionId: string) {\n    await db\n      .update(importSessions)\n      .set({ lastProcessedAt: new Date() })\n      .where(eq(importSessions.id, sessionId));\n  }\n\n  private async checkAndCompleteEmptySessions(sessionIds: string[]) {\n    for (const sessionId of sessionIds) {\n      const remaining = await db\n        .select({ count: count() })\n        .from(importStagingBookmarks)\n        .where(\n          and(\n            eq(importStagingBookmarks.importSessionId, sessionId),\n            inArray(importStagingBookmarks.status, [\"pending\", \"processing\"]),\n          ),\n        );\n\n      if (remaining[0]?.count === 0) {\n        logger.info(\n          `[import] Session ${sessionId} completed, all items processed`,\n        );\n        await db\n          .update(importSessions)\n          .set({ status: \"completed\" })\n          .where(eq(importSessions.id, sessionId));\n      }\n    }\n  }\n\n  /**\n   * Check processing items that have a bookmark created and mark them as completed\n   * once downstream processing (crawling/tagging) is done.\n   */\n  private async checkAndCompleteProcessingItems(): Promise<number> {\n    // Find processing items where:\n    // - A bookmark was created (resultBookmarkId is set)\n    // - Downstream processing is complete (crawl/tag not pending)\n    const completedItems = await db\n      .select({\n        id: importStagingBookmarks.id,\n        importSessionId: importStagingBookmarks.importSessionId,\n        crawlStatus: bookmarkLinks.crawlStatus,\n        taggingStatus: bookmarks.taggingStatus,\n      })\n      .from(importStagingBookmarks)\n      .leftJoin(\n        bookmarks,\n        eq(bookmarks.id, importStagingBookmarks.resultBookmarkId),\n      )\n      .leftJoin(\n        bookmarkLinks,\n        eq(bookmarkLinks.id, importStagingBookmarks.resultBookmarkId),\n      )\n      .where(\n        and(\n          eq(importStagingBookmarks.status, \"processing\"),\n          isNotNull(importStagingBookmarks.resultBookmarkId),\n          // Crawl is done (not pending) - either success, failure, or null (not a link)\n          or(\n            isNull(bookmarkLinks.crawlStatus),\n            eq(bookmarkLinks.crawlStatus, \"success\"),\n            eq(bookmarkLinks.crawlStatus, \"failure\"),\n          ),\n          // Tagging is done (not pending) - either success, failure, or null\n          or(\n            isNull(bookmarks.taggingStatus),\n            eq(bookmarks.taggingStatus, \"success\"),\n            eq(bookmarks.taggingStatus, \"failure\"),\n          ),\n        ),\n      );\n\n    if (completedItems.length === 0) {\n      return 0;\n    }\n\n    const succeededItems = completedItems.filter(\n      (i) => i.crawlStatus !== \"failure\" && i.taggingStatus !== \"failure\",\n    );\n    const failedItems = completedItems.filter(\n      (i) => i.crawlStatus === \"failure\" || i.taggingStatus === \"failure\",\n    );\n\n    logger.debug(\n      `[import] ${completedItems.length} item(s) finished downstream processing (${succeededItems.length} succeeded, ${failedItems.length} failed)`,\n    );\n\n    // Mark succeeded items as completed\n    if (succeededItems.length > 0) {\n      await db\n        .update(importStagingBookmarks)\n        .set({\n          status: \"completed\",\n          completedAt: new Date(),\n        })\n        .where(\n          inArray(\n            importStagingBookmarks.id,\n            succeededItems.map((i) => i.id),\n          ),\n        );\n\n      importStagingProcessedCounter.inc(\n        { result: \"accepted\" },\n        succeededItems.length,\n      );\n    }\n\n    // Mark failed items as failed\n    if (failedItems.length > 0) {\n      for (const item of failedItems) {\n        const reason =\n          item.crawlStatus === \"failure\" ? \"Crawl failed\" : \"Tagging failed\";\n        await db\n          .update(importStagingBookmarks)\n          .set({\n            status: \"failed\",\n            result: \"rejected\",\n            resultReason: reason,\n            completedAt: new Date(),\n          })\n          .where(eq(importStagingBookmarks.id, item.id));\n      }\n\n      importStagingProcessedCounter.inc(\n        { result: \"rejected\" },\n        failedItems.length,\n      );\n    }\n\n    // Check if any sessions are now complete\n    const sessionIds = [\n      ...new Set(completedItems.map((i) => i.importSessionId)),\n    ];\n    await this.checkAndCompleteEmptySessions(sessionIds);\n\n    return completedItems.length;\n  }\n\n  /**\n   * Backpressure: Calculate available capacity based on number of items currently processing.\n   */\n  private async getAvailableCapacity(): Promise<number> {\n    const processingCount = await db\n      .select({ count: count() })\n      .from(importStagingBookmarks)\n      .where(\n        and(\n          eq(importStagingBookmarks.status, \"processing\"),\n          gt(\n            importStagingBookmarks.processingStartedAt,\n            new Date(Date.now() - this.staleThresholdMs),\n          ),\n        ),\n      );\n\n    const inFlight = processingCount[0]?.count ?? 0;\n    importStagingInFlightGauge.set(inFlight);\n\n    return this.maxInFlight - inFlight;\n  }\n\n  /**\n   * Reset stale \"processing\" items back to \"pending\" so they can be retried.\n   * Called periodically to handle crashed workers or stuck items.\n   *\n   * Only resets items that don't have a resultBookmarkId - those with a bookmark\n   * are waiting for downstream processing (crawl/tag), not stale.\n   */\n  private async resetStaleProcessingItems(): Promise<number> {\n    const staleThreshold = new Date(Date.now() - this.staleThresholdMs);\n\n    const staleItems = await db\n      .select({ id: importStagingBookmarks.id })\n      .from(importStagingBookmarks)\n      .where(\n        and(\n          eq(importStagingBookmarks.status, \"processing\"),\n          lt(importStagingBookmarks.processingStartedAt, staleThreshold),\n          // Only reset items that haven't created a bookmark yet\n          // Items with a bookmark are waiting for downstream, not stale\n          isNull(importStagingBookmarks.resultBookmarkId),\n        ),\n      );\n\n    if (staleItems.length > 0) {\n      logger.warn(\n        `[import] Resetting ${staleItems.length} stale processing items`,\n      );\n\n      await db\n        .update(importStagingBookmarks)\n        .set({ status: \"pending\", processingStartedAt: null })\n        .where(\n          inArray(\n            importStagingBookmarks.id,\n            staleItems.map((i) => i.id),\n          ),\n        );\n\n      importStagingStaleResetCounter.inc(staleItems.length);\n      return staleItems.length;\n    }\n\n    return 0;\n  }\n}\n"
  },
  {
    "path": "apps/workers/workers/inference/inferenceWorker.ts",
    "content": "import { eq } from \"drizzle-orm\";\nimport { workerStatsCounter } from \"metrics\";\nimport { withWorkerTracing } from \"workerTracing\";\n\nimport type { ZOpenAIRequest } from \"@karakeep/shared-server\";\nimport { db } from \"@karakeep/db\";\nimport { bookmarks } from \"@karakeep/db/schema\";\nimport { OpenAIQueue, zOpenAIRequestSchema } from \"@karakeep/shared-server\";\nimport serverConfig from \"@karakeep/shared/config\";\nimport { InferenceClientFactory } from \"@karakeep/shared/inference\";\nimport logger from \"@karakeep/shared/logger\";\nimport { DequeuedJob, getQueueClient } from \"@karakeep/shared/queueing\";\n\nimport { runSummarization } from \"./summarize\";\nimport { runTagging } from \"./tagging\";\n\nasync function attemptMarkStatus(\n  jobData: object | undefined,\n  status: \"success\" | \"failure\",\n) {\n  if (!jobData) {\n    return;\n  }\n  try {\n    const request = zOpenAIRequestSchema.parse(jobData);\n    await db\n      .update(bookmarks)\n      .set({\n        ...(request.type === \"summarize\"\n          ? { summarizationStatus: status }\n          : {}),\n        ...(request.type === \"tag\" ? { taggingStatus: status } : {}),\n      })\n      .where(eq(bookmarks.id, request.bookmarkId));\n  } catch (e) {\n    logger.error(`Something went wrong when marking the tagging status: ${e}`);\n  }\n}\n\nexport class OpenAiWorker {\n  static async build() {\n    logger.info(\"Starting inference worker ...\");\n    const worker = (await getQueueClient())!.createRunner<ZOpenAIRequest>(\n      OpenAIQueue,\n      {\n        run: withWorkerTracing(\"inferenceWorker.run\", runOpenAI),\n        onComplete: async (job) => {\n          workerStatsCounter.labels(\"inference\", \"completed\").inc();\n          const jobId = job.id;\n          logger.info(`[inference][${jobId}] Completed successfully`);\n          await attemptMarkStatus(job.data, \"success\");\n        },\n        onError: async (job) => {\n          workerStatsCounter.labels(\"inference\", \"failed\").inc();\n          const jobId = job.id;\n          logger.error(\n            `[inference][${jobId}] inference job failed: ${job.error}\\n${job.error.stack}`,\n          );\n          if (job.numRetriesLeft == 0) {\n            workerStatsCounter.labels(\"inference\", \"failed_permanent\").inc();\n            await attemptMarkStatus(job?.data, \"failure\");\n          }\n        },\n      },\n      {\n        concurrency: serverConfig.inference.numWorkers,\n        pollIntervalMs: 1000,\n        timeoutSecs: serverConfig.inference.jobTimeoutSec,\n      },\n    );\n\n    return worker;\n  }\n}\n\nasync function runOpenAI(job: DequeuedJob<ZOpenAIRequest>) {\n  const jobId = job.id;\n\n  const inferenceClient = InferenceClientFactory.build();\n  if (!inferenceClient) {\n    logger.debug(\n      `[inference][${jobId}] No inference client configured, nothing to do now`,\n    );\n    return;\n  }\n\n  const request = zOpenAIRequestSchema.safeParse(job.data);\n  if (!request.success) {\n    throw new Error(\n      `[inference][${jobId}] Got malformed job request: ${request.error.toString()}`,\n    );\n  }\n\n  const { bookmarkId } = request.data;\n  switch (request.data.type) {\n    case \"summarize\":\n      await runSummarization(bookmarkId, job, inferenceClient);\n      break;\n    case \"tag\":\n      await runTagging(bookmarkId, job, inferenceClient);\n      break;\n    default:\n      throw new Error(`Unknown inference type: ${request.data.type}`);\n  }\n}\n"
  },
  {
    "path": "apps/workers/workers/inference/summarize.ts",
    "content": "import { and, eq } from \"drizzle-orm\";\nimport { getBookmarkDomain } from \"network\";\n\nimport { db } from \"@karakeep/db\";\nimport { bookmarks, customPrompts, users } from \"@karakeep/db/schema\";\nimport {\n  setSpanAttributes,\n  triggerSearchReindex,\n  ZOpenAIRequest,\n} from \"@karakeep/shared-server\";\nimport serverConfig from \"@karakeep/shared/config\";\nimport { InferenceClient } from \"@karakeep/shared/inference\";\nimport logger from \"@karakeep/shared/logger\";\nimport { buildSummaryPrompt } from \"@karakeep/shared/prompts.server\";\nimport { DequeuedJob } from \"@karakeep/shared/queueing\";\nimport { BookmarkTypes } from \"@karakeep/shared/types/bookmarks\";\nimport { Bookmark } from \"@karakeep/trpc/models/bookmarks\";\n\nasync function fetchBookmarkDetailsForSummary(bookmarkId: string) {\n  const bookmark = await db.query.bookmarks.findFirst({\n    where: eq(bookmarks.id, bookmarkId),\n    columns: { id: true, userId: true, type: true },\n    with: {\n      link: {\n        columns: {\n          title: true,\n          description: true,\n          htmlContent: true,\n          contentAssetId: true,\n          crawlStatusCode: true,\n          publisher: true,\n          author: true,\n          url: true,\n        },\n      },\n      // If assets (like PDFs with extracted text) should be summarized, extend here\n    },\n  });\n\n  if (!bookmark) {\n    throw new Error(`Bookmark with id ${bookmarkId} not found`);\n  }\n  return bookmark;\n}\n\nexport async function runSummarization(\n  bookmarkId: string,\n  job: DequeuedJob<ZOpenAIRequest>,\n  inferenceClient: InferenceClient,\n) {\n  if (!serverConfig.inference.enableAutoSummarization) {\n    logger.debug(\n      `[inference][${job.id}] Skipping summarization job for bookmark with id \"${bookmarkId}\" because it's disabled in the config.`,\n    );\n    return;\n  }\n  const jobId = job.id;\n\n  logger.info(\n    `[inference][${jobId}] Starting a summary job for bookmark with id \"${bookmarkId}\"`,\n  );\n\n  const bookmarkData = await fetchBookmarkDetailsForSummary(bookmarkId);\n\n  // Check user-level preference\n  const userSettings = await db.query.users.findFirst({\n    where: eq(users.id, bookmarkData.userId),\n    columns: {\n      autoSummarizationEnabled: true,\n      inferredTagLang: true,\n    },\n  });\n\n  setSpanAttributes({\n    \"user.id\": bookmarkData.userId,\n    \"bookmark.id\": bookmarkData.id,\n    \"bookmark.url\": bookmarkData.link?.url,\n    \"bookmark.domain\": getBookmarkDomain(bookmarkData.link?.url),\n    \"bookmark.content.type\": bookmarkData.type,\n    \"crawler.statusCode\": bookmarkData.link?.crawlStatusCode ?? undefined,\n    \"inference.type\": \"summarization\",\n    \"inference.model\": serverConfig.inference.textModel,\n  });\n\n  if (userSettings?.autoSummarizationEnabled === false) {\n    logger.debug(\n      `[inference][${jobId}] Skipping summarization job for bookmark with id \"${bookmarkId}\" because user has disabled auto-summarization.`,\n    );\n    return;\n  }\n\n  let textToSummarize = \"\";\n  if (bookmarkData.type === BookmarkTypes.LINK && bookmarkData.link) {\n    const link = bookmarkData.link;\n\n    // Extract plain text content from HTML for summarization\n    let content =\n      (await Bookmark.getBookmarkPlainTextContent(link, bookmarkData.userId)) ??\n      \"\";\n\n    if (!link.description && !content) {\n      // No content to infer from; skip summarization\n      logger.info(\n        `[inference] No content found for link \"${bookmarkId}\". Skipping summary.`,\n      );\n      return;\n    }\n\n    textToSummarize = `\nTitle: ${link.title ?? \"\"}\nDescription: ${link.description ?? \"\"}\nContent: ${content}\nPublisher: ${link.publisher ?? \"\"}\nAuthor: ${link.author ?? \"\"}\nURL: ${link.url ?? \"\"}\n`;\n  } else {\n    logger.warn(\n      `[inference][${jobId}] Bookmark ${bookmarkId} (type: ${bookmarkData.type}) is not a LINK or TEXT type with content, or content is missing. Skipping summary.`,\n    );\n    return;\n  }\n\n  if (!textToSummarize.trim()) {\n    logger.info(\n      `[inference][${jobId}] No content to summarize for bookmark ${bookmarkId}.`,\n    );\n    return;\n  }\n\n  const prompts = await db.query.customPrompts.findMany({\n    where: and(\n      eq(customPrompts.userId, bookmarkData.userId),\n      eq(customPrompts.appliesTo, \"summary\"),\n    ),\n    columns: {\n      text: true,\n    },\n  });\n\n  setSpanAttributes({\n    \"inference.prompt.customCount\": prompts.length,\n  });\n\n  const summaryPrompt = await buildSummaryPrompt(\n    userSettings?.inferredTagLang ?? serverConfig.inference.inferredTagLang,\n    prompts.map((p) => p.text),\n    textToSummarize,\n    serverConfig.inference.contextLength,\n  );\n\n  setSpanAttributes({\n    \"inference.prompt.size\": Buffer.byteLength(summaryPrompt, \"utf8\"),\n  });\n\n  const summaryResult = await inferenceClient.inferFromText(summaryPrompt, {\n    schema: null, // Summaries are typically free-form text\n    abortSignal: job.abortSignal,\n  });\n\n  if (!summaryResult.response) {\n    throw new Error(\n      `[inference][${jobId}] Failed to summarize bookmark ${bookmarkId}, empty response from inference client.`,\n    );\n  }\n\n  setSpanAttributes({\n    \"inference.summary.size\": Buffer.byteLength(summaryResult.response, \"utf8\"),\n    \"inference.totalTokens\": summaryResult.totalTokens,\n  });\n\n  logger.info(\n    `[inference][${jobId}] Generated summary for bookmark \"${bookmarkId}\" using ${summaryResult.totalTokens} tokens.`,\n  );\n\n  await db\n    .update(bookmarks)\n    .set({\n      summary: summaryResult.response,\n      modifiedAt: new Date(),\n    })\n    .where(eq(bookmarks.id, bookmarkId));\n\n  await triggerSearchReindex(bookmarkId, {\n    priority: job.priority,\n    groupId: bookmarkData.userId,\n  });\n}\n"
  },
  {
    "path": "apps/workers/workers/inference/tagging.ts",
    "content": "import { and, eq, inArray } from \"drizzle-orm\";\nimport { getBookmarkDomain } from \"network\";\nimport { buildImpersonatingTRPCClient } from \"trpc\";\nimport { z } from \"zod\";\n\nimport type { ZOpenAIRequest } from \"@karakeep/shared-server\";\nimport type {\n  InferenceClient,\n  InferenceResponse,\n} from \"@karakeep/shared/inference\";\nimport type { ZTagStyle } from \"@karakeep/shared/types/users\";\nimport { db } from \"@karakeep/db\";\nimport {\n  bookmarks,\n  bookmarkTags,\n  customPrompts,\n  tagsOnBookmarks,\n  users,\n} from \"@karakeep/db/schema\";\nimport {\n  setSpanAttributes,\n  triggerRuleEngineOnEvent,\n  triggerSearchReindex,\n  triggerWebhook,\n} from \"@karakeep/shared-server\";\nimport { ASSET_TYPES, readAsset } from \"@karakeep/shared/assetdb\";\nimport serverConfig from \"@karakeep/shared/config\";\nimport logger from \"@karakeep/shared/logger\";\nimport { buildImagePrompt } from \"@karakeep/shared/prompts\";\nimport { buildTextPrompt } from \"@karakeep/shared/prompts.server\";\nimport { DequeuedJob, EnqueueOptions } from \"@karakeep/shared/queueing\";\nimport { Bookmark } from \"@karakeep/trpc/models/bookmarks\";\n\nconst openAIResponseSchema = z.object({\n  tags: z.array(z.string()),\n});\n\nfunction parseJsonFromLLMResponse(response: string): unknown {\n  const trimmedResponse = response.trim();\n\n  // Try parsing the response as-is first\n  try {\n    return JSON.parse(trimmedResponse);\n  } catch {\n    // If that fails, try to extract JSON from markdown code blocks\n    const jsonBlockRegex = /```(?:json)?\\s*(\\{[\\s\\S]*?\\})\\s*```/i;\n    const match = trimmedResponse.match(jsonBlockRegex);\n\n    if (match) {\n      try {\n        return JSON.parse(match[1]);\n      } catch {\n        // Fall through to other extraction methods\n      }\n    }\n\n    // Try to find JSON object boundaries in the text\n    const jsonObjectRegex = /\\{[\\s\\S]*\\}/;\n    const objectMatch = trimmedResponse.match(jsonObjectRegex);\n\n    if (objectMatch) {\n      try {\n        return JSON.parse(objectMatch[0]);\n      } catch {\n        // Fall through to final attempt\n      }\n    }\n\n    // Last resort: try to parse the original response again to get the original error\n    return JSON.parse(trimmedResponse);\n  }\n}\n\nfunction tagNormalizer() {\n  // This function needs to be in sync with the generated normalizedName column in bookmarkTags\n  function normalizeTag(tag: string) {\n    return tag.toLowerCase().replace(/[ \\-_]/g, \"\");\n  }\n\n  return {\n    normalizeTag,\n  };\n}\nasync function buildPrompt(\n  bookmark: NonNullable<Awaited<ReturnType<typeof fetchBookmark>>>,\n  tagStyle: ZTagStyle,\n  inferredTagLang: string,\n  curatedTags?: string[],\n): Promise<string | null> {\n  const prompts = await fetchCustomPrompts(bookmark.userId, \"text\");\n  if (bookmark.link) {\n    let content =\n      (await Bookmark.getBookmarkPlainTextContent(\n        bookmark.link,\n        bookmark.userId,\n      )) ?? \"\";\n\n    if (!bookmark.link.description && !content) {\n      // No content to infer from; signal skip to avoid marking job as failed\n      logger.info(\n        `[inference] No content found for link \"${bookmark.id}\". Skipping tagging.`,\n      );\n      return null;\n    }\n    return await buildTextPrompt(\n      inferredTagLang,\n      prompts,\n      `URL: ${bookmark.link.url}\nTitle: ${bookmark.link.title ?? \"\"}\nDescription: ${bookmark.link.description ?? \"\"}\nContent: ${content ?? \"\"}`,\n      serverConfig.inference.contextLength,\n      tagStyle,\n      curatedTags,\n    );\n  }\n\n  if (bookmark.text) {\n    return await buildTextPrompt(\n      inferredTagLang,\n      prompts,\n      bookmark.text.text ?? \"\",\n      serverConfig.inference.contextLength,\n      tagStyle,\n      curatedTags,\n    );\n  }\n\n  throw new Error(\"Unknown bookmark type\");\n}\n\nasync function inferTagsFromImage(\n  jobId: string,\n  bookmark: NonNullable<Awaited<ReturnType<typeof fetchBookmark>>>,\n  inferenceClient: InferenceClient,\n  abortSignal: AbortSignal,\n  tagStyle: ZTagStyle,\n  inferredTagLang: string,\n  curatedTags?: string[],\n): Promise<InferenceResponse | null> {\n  const { asset, metadata } = await readAsset({\n    userId: bookmark.userId,\n    assetId: bookmark.asset.assetId,\n  });\n\n  if (!asset) {\n    throw new Error(\n      `[inference][${jobId}] AssetId ${bookmark.asset.assetId} for bookmark ${bookmark.id} not found`,\n    );\n  }\n  if (metadata.contentType === ASSET_TYPES.IMAGE_GIF) {\n    logger.info(\n      `[inference][${jobId}] Skipping inference for bookmark with id \"${bookmark.id}\" because it's a GIF.`,\n    );\n    return null;\n  }\n\n  const base64 = asset.toString(\"base64\");\n  setSpanAttributes({\n    \"inference.model\": serverConfig.inference.imageModel,\n  });\n  return inferenceClient.inferFromImage(\n    buildImagePrompt(\n      inferredTagLang,\n      await fetchCustomPrompts(bookmark.userId, \"images\"),\n      tagStyle,\n      curatedTags,\n    ),\n    metadata.contentType,\n    base64,\n    { schema: openAIResponseSchema, abortSignal },\n  );\n}\n\nasync function fetchCustomPrompts(\n  userId: string,\n  appliesTo: \"text\" | \"images\",\n) {\n  const prompts = await db.query.customPrompts.findMany({\n    where: and(\n      eq(customPrompts.userId, userId),\n      inArray(customPrompts.appliesTo, [\"all_tagging\", appliesTo]),\n    ),\n    columns: {\n      text: true,\n    },\n  });\n\n  setSpanAttributes({\n    \"inference.prompt.customCount\": prompts.length,\n  });\n\n  let promptTexts = prompts.map((p) => p.text);\n  if (containsTagsPlaceholder(prompts)) {\n    promptTexts = await replaceTagsPlaceholders(promptTexts, userId);\n  }\n\n  return promptTexts;\n}\n\nasync function replaceTagsPlaceholders(\n  prompts: string[],\n  userId: string,\n): Promise<string[]> {\n  const api = await buildImpersonatingTRPCClient(userId);\n  const tags = (await api.tags.list({})).tags;\n  const tagsString = `[${tags.map((tag) => tag.name).join(\", \")}]`;\n  const aiTagsString = `[${tags\n    .filter((tag) => tag.numBookmarksByAttachedType.human ?? true)\n    .map((tag) => tag.name)\n    .join(\", \")}]`;\n  const userTagsString = `[${tags\n    .filter((tag) => tag.numBookmarksByAttachedType.human ?? false)\n    .map((tag) => tag.name)\n    .join(\", \")}]`;\n\n  return prompts.map((p) =>\n    p\n      .replaceAll(\"$tags\", tagsString)\n      .replaceAll(\"$aiTags\", aiTagsString)\n      .replaceAll(\"$userTags\", userTagsString),\n  );\n}\n\nfunction containsTagsPlaceholder(prompts: { text: string }[]): boolean {\n  return (\n    prompts.filter(\n      (p) =>\n        p.text.includes(\"$tags\") ||\n        p.text.includes(\"$aiTags\") ||\n        p.text.includes(\"$userTags\"),\n    ).length > 0\n  );\n}\n\nasync function inferTagsFromPDF(\n  _jobId: string,\n  bookmark: NonNullable<Awaited<ReturnType<typeof fetchBookmark>>>,\n  inferenceClient: InferenceClient,\n  abortSignal: AbortSignal,\n  tagStyle: ZTagStyle,\n  inferredTagLang: string,\n  curatedTags?: string[],\n) {\n  const prompt = await buildTextPrompt(\n    inferredTagLang,\n    await fetchCustomPrompts(bookmark.userId, \"text\"),\n    `Content: ${bookmark.asset.content}`,\n    serverConfig.inference.contextLength,\n    tagStyle,\n    curatedTags,\n  );\n  setSpanAttributes({\n    \"inference.model\": serverConfig.inference.textModel,\n  });\n  setSpanAttributes({\n    \"inference.prompt.size\": Buffer.byteLength(prompt, \"utf8\"),\n  });\n  return inferenceClient.inferFromText(prompt, {\n    schema: openAIResponseSchema,\n    abortSignal,\n  });\n}\n\nasync function inferTagsFromText(\n  bookmark: NonNullable<Awaited<ReturnType<typeof fetchBookmark>>>,\n  inferenceClient: InferenceClient,\n  abortSignal: AbortSignal,\n  tagStyle: ZTagStyle,\n  inferredTagLang: string,\n  curatedTags?: string[],\n) {\n  const prompt = await buildPrompt(\n    bookmark,\n    tagStyle,\n    inferredTagLang,\n    curatedTags,\n  );\n  if (!prompt) {\n    return null;\n  }\n  setSpanAttributes({\n    \"inference.model\": serverConfig.inference.textModel,\n  });\n  setSpanAttributes({\n    \"inference.prompt.size\": Buffer.byteLength(prompt, \"utf8\"),\n  });\n  return await inferenceClient.inferFromText(prompt, {\n    schema: openAIResponseSchema,\n    abortSignal,\n  });\n}\n\nasync function inferTags(\n  jobId: string,\n  bookmark: NonNullable<Awaited<ReturnType<typeof fetchBookmark>>>,\n  inferenceClient: InferenceClient,\n  abortSignal: AbortSignal,\n  tagStyle: ZTagStyle,\n  inferredTagLang: string,\n  curatedTags?: string[],\n) {\n  setSpanAttributes({\n    \"user.id\": bookmark.userId,\n    \"bookmark.id\": bookmark.id,\n    \"bookmark.url\": bookmark.link?.url,\n    \"bookmark.domain\": getBookmarkDomain(bookmark.link?.url),\n    \"bookmark.content.type\": bookmark.type,\n    \"crawler.statusCode\": bookmark.link?.crawlStatusCode ?? undefined,\n    \"inference.tagging.style\": tagStyle,\n    \"inference.lang\": inferredTagLang,\n    \"inference.type\": \"tagging\",\n  });\n\n  let response: InferenceResponse | null;\n  if (bookmark.link || bookmark.text) {\n    response = await inferTagsFromText(\n      bookmark,\n      inferenceClient,\n      abortSignal,\n      tagStyle,\n      inferredTagLang,\n      curatedTags,\n    );\n  } else if (bookmark.asset) {\n    switch (bookmark.asset.assetType) {\n      case \"image\":\n        response = await inferTagsFromImage(\n          jobId,\n          bookmark,\n          inferenceClient,\n          abortSignal,\n          tagStyle,\n          inferredTagLang,\n          curatedTags,\n        );\n        break;\n      case \"pdf\":\n        response = await inferTagsFromPDF(\n          jobId,\n          bookmark,\n          inferenceClient,\n          abortSignal,\n          tagStyle,\n          inferredTagLang,\n          curatedTags,\n        );\n        break;\n      default:\n        throw new Error(`[inference][${jobId}] Unsupported bookmark type`);\n    }\n  } else {\n    throw new Error(`[inference][${jobId}] Unsupported bookmark type`);\n  }\n\n  if (!response) {\n    // Skipped due to missing content or prompt; propagate skip\n    return null;\n  }\n\n  try {\n    let tags = openAIResponseSchema.parse(\n      parseJsonFromLLMResponse(response.response),\n    ).tags;\n    logger.info(\n      `[inference][${jobId}] Inferring tag for bookmark \"${bookmark.id}\" used ${response.totalTokens} tokens and inferred: ${tags}`,\n    );\n\n    // Sometimes the tags contain the hashtag symbol, let's strip them out if they do.\n    // Additionally, trim the tags to prevent whitespaces at the beginning/the end of the tag.\n    tags = tags.map((t) => {\n      let tag = t;\n      if (tag.startsWith(\"#\")) {\n        tag = t.slice(1);\n      }\n      return tag.trim();\n    });\n    setSpanAttributes({\n      \"inference.tagging.numGeneratedTags\": tags.length,\n      \"inference.totalTokens\": response.totalTokens,\n    });\n\n    return tags;\n  } catch (e) {\n    const responseSneak = response.response.substring(0, 20);\n    throw new Error(\n      `[inference][${jobId}] The model ignored our prompt and didn't respond with the expected JSON: ${JSON.stringify(e)}. Here's a sneak peak from the response: ${responseSneak}`,\n    );\n  }\n}\n\nasync function connectTags(\n  bookmarkId: string,\n  inferredTags: string[],\n  userId: string,\n) {\n  if (inferredTags.length == 0) {\n    return;\n  }\n\n  const res = await db.transaction(async (tx) => {\n    // Attempt to match exiting tags with the new ones\n    const { matchedTagIds, notFoundTagNames } = await (async () => {\n      const { normalizeTag } = tagNormalizer();\n      const normalizedInferredTags = inferredTags.map((t) => ({\n        originalTag: t,\n        normalizedTag: normalizeTag(t),\n      }));\n\n      const matchedTags = await tx.query.bookmarkTags.findMany({\n        where: and(\n          eq(bookmarkTags.userId, userId),\n          inArray(\n            bookmarkTags.normalizedName,\n            normalizedInferredTags.map((t) => t.normalizedTag),\n          ),\n        ),\n      });\n\n      const matchedTagIds = matchedTags.map((r) => r.id);\n      const notFoundTagNames = normalizedInferredTags\n        .filter(\n          (t) =>\n            !matchedTags.some(\n              (mt) => normalizeTag(mt.name) === t.normalizedTag,\n            ),\n        )\n        .map((t) => t.originalTag);\n\n      return { matchedTagIds, notFoundTagNames };\n    })();\n\n    // Create tags that didn't exist previously\n    let newTagIds: string[] = [];\n    if (notFoundTagNames.length > 0) {\n      newTagIds = (\n        await tx\n          .insert(bookmarkTags)\n          .values(\n            notFoundTagNames.map((t) => ({\n              name: t,\n              userId,\n            })),\n          )\n          .onConflictDoNothing()\n          .returning()\n      ).map((t) => t.id);\n    }\n\n    // Delete old AI tags\n    const detachedTags = await tx\n      .delete(tagsOnBookmarks)\n      .where(\n        and(\n          eq(tagsOnBookmarks.attachedBy, \"ai\"),\n          eq(tagsOnBookmarks.bookmarkId, bookmarkId),\n        ),\n      )\n      .returning();\n\n    const allTagIds = new Set([...matchedTagIds, ...newTagIds]);\n\n    // Attach new ones\n    let attachedTags: { tagId: string; bookmarkId: string }[] = [];\n    if (allTagIds.size > 0) {\n      attachedTags = await tx\n        .insert(tagsOnBookmarks)\n        .values(\n          [...allTagIds].map((tagId) => ({\n            tagId,\n            bookmarkId,\n            attachedBy: \"ai\" as const,\n          })),\n        )\n        .onConflictDoNothing()\n        .returning();\n    }\n\n    return { detachedTags, attachedTags };\n  });\n\n  await triggerRuleEngineOnEvent(bookmarkId, [\n    ...res.detachedTags.map((t) => ({\n      type: \"tagRemoved\" as const,\n      tagId: t.tagId,\n    })),\n    ...res.attachedTags.map((t) => ({\n      type: \"tagAdded\" as const,\n      tagId: t.tagId,\n    })),\n  ]);\n}\n\nasync function fetchBookmark(linkId: string) {\n  return await db.query.bookmarks.findFirst({\n    where: eq(bookmarks.id, linkId),\n    with: {\n      link: true,\n      text: true,\n      asset: true,\n    },\n  });\n}\n\nexport async function runTagging(\n  bookmarkId: string,\n  job: DequeuedJob<ZOpenAIRequest>,\n  inferenceClient: InferenceClient,\n) {\n  if (!serverConfig.inference.enableAutoTagging) {\n    logger.debug(\n      `[inference][${job.id}] Skipping tagging job for bookmark with id \"${bookmarkId}\" because it's disabled in the config.`,\n    );\n    return;\n  }\n  const jobId = job.id;\n  const bookmark = await fetchBookmark(bookmarkId);\n  if (!bookmark) {\n    throw new Error(\n      `[inference][${jobId}] bookmark with id ${bookmarkId} was not found`,\n    );\n  }\n\n  // Check user-level preference\n  const userSettings = await db.query.users.findFirst({\n    where: eq(users.id, bookmark.userId),\n    columns: {\n      autoTaggingEnabled: true,\n      tagStyle: true,\n      curatedTagIds: true,\n      inferredTagLang: true,\n    },\n  });\n\n  if (userSettings?.autoTaggingEnabled === false) {\n    logger.debug(\n      `[inference][${jobId}] Skipping tagging job for bookmark with id \"${bookmarkId}\" because user has disabled auto-tagging.`,\n    );\n    return;\n  }\n\n  // Resolve curated tag names if configured\n  let curatedTagNames: string[] | undefined;\n  if (userSettings?.curatedTagIds && userSettings.curatedTagIds.length > 0) {\n    const tags = await db.query.bookmarkTags.findMany({\n      where: and(\n        eq(bookmarkTags.userId, bookmark.userId),\n        inArray(bookmarkTags.id, userSettings.curatedTagIds),\n      ),\n      columns: { name: true },\n    });\n    curatedTagNames = tags.map((t) => t.name);\n  }\n\n  logger.info(\n    `[inference][${jobId}] Starting an inference job for bookmark with id \"${bookmark.id}\"`,\n  );\n\n  const tags = await inferTags(\n    jobId,\n    bookmark,\n    inferenceClient,\n    job.abortSignal,\n    userSettings?.tagStyle ?? \"as-generated\",\n    userSettings?.inferredTagLang ?? serverConfig.inference.inferredTagLang,\n    curatedTagNames,\n  );\n\n  if (tags === null) {\n    logger.info(\n      `[inference][${jobId}] Skipping tagging for bookmark \"${bookmark.id}\" due to missing content.`,\n    );\n    return;\n  }\n\n  await connectTags(bookmarkId, tags, bookmark.userId);\n\n  // Propagate priority to child jobs\n  const enqueueOpts: EnqueueOptions = {\n    priority: job.priority,\n    groupId: bookmark.userId,\n  };\n\n  // Trigger a webhook\n  await triggerWebhook(bookmarkId, \"ai tagged\", undefined, enqueueOpts);\n\n  // Update the search index\n  await triggerSearchReindex(bookmarkId, enqueueOpts);\n}\n"
  },
  {
    "path": "apps/workers/workers/ruleEngineWorker.ts",
    "content": "import { eq } from \"drizzle-orm\";\nimport { workerStatsCounter } from \"metrics\";\nimport { buildImpersonatingAuthedContext } from \"trpc\";\nimport { withWorkerTracing } from \"workerTracing\";\n\nimport type { ZRuleEngineRequest } from \"@karakeep/shared-server\";\nimport { db } from \"@karakeep/db\";\nimport { bookmarks } from \"@karakeep/db/schema\";\nimport {\n  RuleEngineQueue,\n  zRuleEngineRequestSchema,\n} from \"@karakeep/shared-server\";\nimport serverConfig from \"@karakeep/shared/config\";\nimport logger from \"@karakeep/shared/logger\";\nimport { DequeuedJob, getQueueClient } from \"@karakeep/shared/queueing\";\nimport { RuleEngine } from \"@karakeep/trpc/lib/ruleEngine\";\n\nexport class RuleEngineWorker {\n  static async build() {\n    logger.info(\"Starting rule engine worker ...\");\n    const worker = (await getQueueClient())!.createRunner<ZRuleEngineRequest>(\n      RuleEngineQueue,\n      {\n        run: withWorkerTracing(\"ruleEngineWorker.run\", runRuleEngine),\n        onComplete: (job) => {\n          workerStatsCounter.labels(\"ruleEngine\", \"completed\").inc();\n          const jobId = job.id;\n          logger.info(`[ruleEngine][${jobId}] Completed successfully`);\n          return Promise.resolve();\n        },\n        onError: (job) => {\n          workerStatsCounter.labels(\"ruleEngine\", \"failed\").inc();\n          if (job.numRetriesLeft == 0) {\n            workerStatsCounter.labels(\"ruleEngine\", \"failed_permanent\").inc();\n          }\n          const jobId = job.id;\n          logger.error(\n            `[ruleEngine][${jobId}] rule engine job failed: ${job.error}\\n${job.error.stack}`,\n          );\n          return Promise.resolve();\n        },\n      },\n      {\n        concurrency: serverConfig.ruleEngine.numWorkers,\n        pollIntervalMs: 1000,\n        timeoutSecs: 10,\n        validator: zRuleEngineRequestSchema,\n      },\n    );\n\n    return worker;\n  }\n}\n\nasync function getBookmarkUserId(bookmarkId: string) {\n  return await db.query.bookmarks.findFirst({\n    where: eq(bookmarks.id, bookmarkId),\n    columns: {\n      userId: true,\n    },\n  });\n}\n\nasync function runRuleEngine(job: DequeuedJob<ZRuleEngineRequest>) {\n  const jobId = job.id;\n  const { bookmarkId, events } = job.data;\n\n  const bookmark = await getBookmarkUserId(bookmarkId);\n  if (!bookmark) {\n    logger.info(\n      `[ruleEngine][${jobId}] bookmark with id ${bookmarkId} was not found, skipping`,\n    );\n    return;\n  }\n  const userId = bookmark.userId;\n  const authedCtx = await buildImpersonatingAuthedContext(userId);\n\n  const ruleEngine = await RuleEngine.forBookmark(authedCtx, bookmarkId);\n  if (!ruleEngine) {\n    logger.info(\n      `[ruleEngine][${jobId}] bookmark with id ${bookmarkId} was not found during rule evaluation, skipping`,\n    );\n    return;\n  }\n\n  const results = (\n    await Promise.all(events.map((event) => ruleEngine.onEvent(event)))\n  ).flat();\n\n  if (results.length == 0) {\n    return;\n  }\n\n  const message = results\n    .map((result) => `${result.ruleId}, (${result.type}): ${result.message}`)\n    .join(\"\\n\");\n\n  logger.info(\n    `[ruleEngine][${jobId}] Rule engine job for bookmark ${bookmarkId} completed with results: ${message}`,\n  );\n}\n"
  },
  {
    "path": "apps/workers/workers/searchWorker.ts",
    "content": "import { eq } from \"drizzle-orm\";\nimport { workerStatsCounter } from \"metrics\";\nimport { withWorkerTracing } from \"workerTracing\";\n\nimport type { ZSearchIndexingRequest } from \"@karakeep/shared-server\";\nimport { db } from \"@karakeep/db\";\nimport { bookmarks } from \"@karakeep/db/schema\";\nimport {\n  SearchIndexingQueue,\n  zSearchIndexingRequestSchema,\n} from \"@karakeep/shared-server\";\nimport serverConfig from \"@karakeep/shared/config\";\nimport logger from \"@karakeep/shared/logger\";\nimport { DequeuedJob, getQueueClient } from \"@karakeep/shared/queueing\";\nimport {\n  BookmarkSearchDocument,\n  getSearchClient,\n  SearchIndexClient,\n} from \"@karakeep/shared/search\";\nimport { Bookmark } from \"@karakeep/trpc/models/bookmarks\";\n\nexport class SearchIndexingWorker {\n  static async build() {\n    logger.info(\"Starting search indexing worker ...\");\n    const worker =\n      (await getQueueClient())!.createRunner<ZSearchIndexingRequest>(\n        SearchIndexingQueue,\n        {\n          run: withWorkerTracing(\"searchWorker.run\", runSearchIndexing),\n          onComplete: (job) => {\n            workerStatsCounter.labels(\"search\", \"completed\").inc();\n            const jobId = job.id;\n            logger.info(`[search][${jobId}] Completed successfully`);\n            return Promise.resolve();\n          },\n          onError: (job) => {\n            workerStatsCounter.labels(\"search\", \"failed\").inc();\n            if (job.numRetriesLeft == 0) {\n              workerStatsCounter.labels(\"search\", \"failed_permanent\").inc();\n            }\n            const jobId = job.id;\n            logger.error(\n              `[search][${jobId}] search job failed: ${job.error}\\n${job.error.stack}`,\n            );\n            return Promise.resolve();\n          },\n        },\n        {\n          concurrency: serverConfig.search.numWorkers,\n          pollIntervalMs: 1000,\n          timeoutSecs: serverConfig.search.jobTimeoutSec,\n        },\n      );\n\n    return worker;\n  }\n}\n\nasync function runIndex(\n  searchClient: SearchIndexClient,\n  bookmarkId: string,\n  batch: boolean,\n) {\n  const bookmark = await db.query.bookmarks.findFirst({\n    where: eq(bookmarks.id, bookmarkId),\n    with: {\n      link: true,\n      text: true,\n      asset: true,\n      tagsOnBookmarks: {\n        with: {\n          tag: true,\n        },\n      },\n    },\n  });\n\n  if (!bookmark) {\n    throw new Error(`Bookmark ${bookmarkId} not found`);\n  }\n\n  const document: BookmarkSearchDocument = {\n    id: bookmark.id,\n    userId: bookmark.userId,\n    ...(bookmark.link\n      ? {\n          url: bookmark.link.url,\n          linkTitle: bookmark.link.title,\n          description: bookmark.link.description,\n          content: await Bookmark.getBookmarkPlainTextContent(\n            bookmark.link,\n            bookmark.userId,\n          ),\n          publisher: bookmark.link.publisher,\n          author: bookmark.link.author,\n          datePublished: bookmark.link.datePublished,\n          dateModified: bookmark.link.dateModified,\n        }\n      : {}),\n    ...(bookmark.asset\n      ? {\n          content: bookmark.asset.content,\n          metadata: bookmark.asset.metadata,\n        }\n      : {}),\n    ...(bookmark.text ? { content: bookmark.text.text } : {}),\n    note: bookmark.note,\n    summary: bookmark.summary,\n    title: bookmark.title,\n    createdAt: bookmark.createdAt.toISOString(),\n    tags: bookmark.tagsOnBookmarks.map((t) => t.tag.name),\n  };\n\n  await searchClient.addDocuments([document], { batch });\n}\n\nasync function runDelete(\n  searchClient: SearchIndexClient,\n  bookmarkId: string,\n  batch: boolean,\n) {\n  await searchClient.deleteDocuments([bookmarkId], { batch });\n}\n\nasync function runSearchIndexing(job: DequeuedJob<ZSearchIndexingRequest>) {\n  const jobId = job.id;\n\n  const request = zSearchIndexingRequestSchema.safeParse(job.data);\n  if (!request.success) {\n    throw new Error(\n      `[search][${jobId}] Got malformed job request: ${request.error.toString()}`,\n    );\n  }\n\n  const searchClient = await getSearchClient();\n  if (!searchClient) {\n    logger.debug(\n      `[search][${jobId}] Search is not configured, nothing to do now`,\n    );\n    return;\n  }\n\n  const bookmarkId = request.data.bookmarkId;\n  // Disable batching on retries (runNumber > 0) for improved reliability\n  const batch = job.runNumber === 0;\n\n  logger.info(\n    `[search][${jobId}] Attempting to index bookmark with id ${bookmarkId} (run ${job.runNumber}, batch=${batch}) ...`,\n  );\n\n  switch (request.data.type) {\n    case \"index\": {\n      await runIndex(searchClient, bookmarkId, batch);\n      break;\n    }\n    case \"delete\": {\n      await runDelete(searchClient, bookmarkId, batch);\n      break;\n    }\n  }\n}\n"
  },
  {
    "path": "apps/workers/workers/utils/__fixtures__/twz-feed.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<rss\n  xmlns:content=\"http://purl.org/rss/1.0/modules/content/\"\n  xmlns:dc=\"http://purl.org/dc/elements/1.1/\"\n  version=\"2.0\"\n>\n  <channel>\n    <title>The War Zone</title>\n    <link>https://www.twz.com</link>\n    <description><![CDATA[A strong offense for the world of defense.]]></description>\n    <item>\n      <title><![CDATA[Test TWZ article]]></title>\n      <description><![CDATA[<p>Test description.</p>]]></description>\n      <link>https://www.twz.com/sea/test-article</link>\n      <guid isPermaLink=\"false\">https://www.twz.com/?p=12345</guid>\n      <pubDate>Tue, 14 Jan 2026 10:00:00 -0500</pubDate>\n      <category domain=\"https://www.twz.com/category/sea\">Sea</category>\n      <category domain=\"https://www.twz.com/category/news-features\">News &amp; Features</category>\n      <content:encoded><![CDATA[<p>Test content.</p>]]></content:encoded>\n      <dc:creator><![CDATA[Test Author]]></dc:creator>\n    </item>\n  </channel>\n</rss>\n"
  },
  {
    "path": "apps/workers/workers/utils/feedParser.test.ts",
    "content": "import { readFile } from \"node:fs/promises\";\n\nimport { describe, expect, test } from \"vitest\";\n\nimport { parseFeedItems } from \"./feedParser\";\n\ndescribe(\"parseFeedItems\", () => {\n  test(\"parses TWZ-style RSS items without dropping them\", async () => {\n    const xmlData = await readFile(\n      new URL(\"./__fixtures__/twz-feed.xml\", import.meta.url),\n      \"utf8\",\n    );\n\n    const items = await parseFeedItems(xmlData);\n\n    expect(items).toHaveLength(1);\n    expect(items[0]).toMatchObject({\n      guid: \"https://www.twz.com/?p=12345\",\n      link: \"https://www.twz.com/sea/test-article\",\n      title: \"Test TWZ article\",\n      categories: [\"Sea\", \"News & Features\"],\n    });\n  });\n\n  test(\"falls back to guid when feeds do not provide an item id\", async () => {\n    const items = await parseFeedItems(`\n      <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n      <rss version=\"2.0\">\n        <channel>\n          <title>Test Feed</title>\n          <link>https://example.com</link>\n          <description>Test</description>\n          <item>\n            <guid isPermaLink=\"false\">guid-1</guid>\n            <link>https://example.com/post-1</link>\n            <title>Post 1</title>\n          </item>\n        </channel>\n      </rss>\n    `);\n\n    expect(items).toHaveLength(1);\n    expect(items[0]).toMatchObject({\n      guid: \"guid-1\",\n      link: \"https://example.com/post-1\",\n      title: \"Post 1\",\n    });\n    expect(items[0].id).toBeUndefined();\n  });\n});\n"
  },
  {
    "path": "apps/workers/workers/utils/feedParser.ts",
    "content": "import Parser from \"rss-parser\";\nimport { z } from \"zod\";\n\nconst parser = new Parser({\n  customFields: {\n    item: [\"id\"],\n  },\n});\n\nconst categorySchema = z\n  .union([z.string(), z.object({ _: z.string() })])\n  .transform((c) => (typeof c === \"string\" ? c : c._));\n\nconst optionalStringSchema = z.preprocess(\n  (value) => (typeof value === \"string\" ? value : undefined),\n  z.string().optional(),\n);\n\nconst feedItemSchema = z\n  .object({\n    id: optionalStringSchema,\n    link: z.string().optional(),\n    guid: z.string().optional(),\n    title: z.string().optional(),\n    categories: z.array(categorySchema).optional(),\n  })\n  .transform((item) => ({\n    ...item,\n    guid: item.guid ?? item.id ?? item.link,\n  }));\n\nexport type ParsedFeedItem = z.infer<typeof feedItemSchema>;\n\nexport async function parseFeedItems(\n  xmlData: string,\n): Promise<ParsedFeedItem[]> {\n  const unparsedFeedData = await parser.parseString(xmlData);\n\n  return unparsedFeedData.items\n    .map((item) => feedItemSchema.safeParse(item))\n    .flatMap((item) => (item.success ? [item.data] : []));\n}\n"
  },
  {
    "path": "apps/workers/workers/utils/fetchBookmarks.ts",
    "content": "import { asc, eq } from \"drizzle-orm\";\n\nimport type { ZBookmark } from \"@karakeep/shared/types/bookmarks\";\nimport type { ZCursor } from \"@karakeep/shared/types/pagination\";\nimport type { AuthedContext } from \"@karakeep/trpc\";\nimport { db } from \"@karakeep/db\";\nimport { bookmarks } from \"@karakeep/db/schema\";\nimport { BookmarkTypes } from \"@karakeep/shared/types/bookmarks\";\nimport { Bookmark } from \"@karakeep/trpc/models/bookmarks\";\n\n/**\n * Fetches all bookmarks for a user with all necessary relations for export\n * @deprecated Use fetchBookmarksInBatches for memory-efficient iteration\n */\nexport async function fetchAllBookmarksForUser(\n  dbInstance: typeof db,\n  userId: string,\n): Promise<ZBookmark[]> {\n  const allBookmarks = await dbInstance.query.bookmarks.findMany({\n    where: eq(bookmarks.userId, userId),\n    with: {\n      tagsOnBookmarks: {\n        with: {\n          tag: true,\n        },\n      },\n      link: true,\n      text: true,\n      asset: true,\n      assets: true,\n    },\n    orderBy: [asc(bookmarks.createdAt)],\n  });\n\n  // Transform to ZBookmark format\n  return allBookmarks.map((bookmark) => {\n    let content: ZBookmark[\"content\"] | null = null;\n\n    switch (bookmark.type) {\n      case BookmarkTypes.LINK:\n        if (bookmark.link) {\n          content = {\n            type: BookmarkTypes.LINK,\n            url: bookmark.link.url,\n            title: bookmark.link.title || undefined,\n            description: bookmark.link.description || undefined,\n            imageUrl: bookmark.link.imageUrl || undefined,\n            favicon: bookmark.link.favicon || undefined,\n          };\n        }\n        break;\n      case BookmarkTypes.TEXT:\n        if (bookmark.text) {\n          content = {\n            type: BookmarkTypes.TEXT,\n            text: bookmark.text.text || \"\",\n          };\n        }\n        break;\n      case BookmarkTypes.ASSET:\n        if (bookmark.asset) {\n          content = {\n            type: BookmarkTypes.ASSET,\n            assetType: bookmark.asset.assetType,\n            assetId: bookmark.asset.assetId,\n          };\n        }\n        break;\n    }\n\n    return {\n      id: bookmark.id,\n      title: bookmark.title || null,\n      createdAt: bookmark.createdAt,\n      archived: bookmark.archived,\n      favourited: bookmark.favourited,\n      taggingStatus: bookmark.taggingStatus || \"pending\",\n      note: bookmark.note || null,\n      summary: bookmark.summary || null,\n      content,\n      tags: bookmark.tagsOnBookmarks.map((t) => ({\n        id: t.tag.id,\n        name: t.tag.name,\n        attachedBy: t.attachedBy,\n      })),\n      assets: bookmark.assets.map((a) => ({\n        id: a.id,\n        assetType: a.assetType,\n      })),\n    } as ZBookmark;\n  });\n}\n\n/**\n * Fetches bookmarks in batches using cursor-based pagination from the Bookmark model\n * This is memory-efficient for large datasets as it only loads one batch at a time\n */\nexport async function* fetchBookmarksInBatches(\n  ctx: AuthedContext,\n  batchSize = 1000,\n): AsyncGenerator<ZBookmark[], number, undefined> {\n  let cursor: ZCursor | null = null;\n  let totalFetched = 0;\n\n  while (true) {\n    const result = await Bookmark.loadMulti(ctx, {\n      limit: batchSize,\n      cursor: cursor,\n      sortOrder: \"asc\",\n      includeContent: false, // We don't need full content for export\n    });\n\n    if (result.bookmarks.length === 0) {\n      break;\n    }\n\n    // Convert Bookmark instances to ZBookmark\n    const batch = result.bookmarks.map((b) => b.asZBookmark());\n    yield batch;\n\n    totalFetched += batch.length;\n    cursor = result.nextCursor;\n\n    // If there's no next cursor, we've reached the end\n    if (!cursor) {\n      break;\n    }\n  }\n\n  return totalFetched;\n}\n"
  },
  {
    "path": "apps/workers/workers/utils/parseHtmlSubprocessIpc.ts",
    "content": "import { z } from \"zod\";\n\nexport const parseSubprocessInputSchema = z.object({\n  htmlContent: z.string(),\n  url: z.string(),\n  jobId: z.string(),\n});\n\nexport const parseSubprocessMetadataSchema = z\n  .object({\n    title: z.string().nullish(),\n    description: z.string().nullish(),\n    image: z.string().nullish(),\n    logo: z.string().nullish(),\n    author: z.string().nullish(),\n    publisher: z.string().nullish(),\n    datePublished: z.string().nullish(),\n    dateModified: z.string().nullish(),\n  })\n  .passthrough();\n\nexport const parseSubprocessOutputSchema = z.object({\n  metadata: parseSubprocessMetadataSchema,\n  readableContent: z.object({ content: z.string() }).nullable(),\n});\n\nexport const parseSubprocessErrorSchema = z.object({\n  error: z.string(),\n  stack: z.string().optional(),\n});\n\nexport type ParseSubprocessInput = z.infer<typeof parseSubprocessInputSchema>;\nexport type ParseSubprocessOutput = z.infer<typeof parseSubprocessOutputSchema>;\nexport type ParseSubprocessError = z.infer<typeof parseSubprocessErrorSchema>;\n"
  },
  {
    "path": "apps/workers/workers/videoWorker.ts",
    "content": "import fs from \"fs\";\nimport * as os from \"os\";\nimport path from \"path\";\nimport { execa } from \"execa\";\nimport { workerStatsCounter } from \"metrics\";\nimport { getProxyAgent, validateUrl } from \"network\";\nimport { withWorkerTracing } from \"workerTracing\";\n\nimport { db } from \"@karakeep/db\";\nimport { AssetTypes } from \"@karakeep/db/schema\";\nimport {\n  QuotaService,\n  StorageQuotaError,\n  VideoWorkerQueue,\n  ZVideoRequest,\n  zvideoRequestSchema,\n} from \"@karakeep/shared-server\";\nimport {\n  ASSET_TYPES,\n  newAssetId,\n  saveAssetFromFile,\n  silentDeleteAsset,\n} from \"@karakeep/shared/assetdb\";\nimport serverConfig from \"@karakeep/shared/config\";\nimport logger from \"@karakeep/shared/logger\";\nimport { DequeuedJob, getQueueClient } from \"@karakeep/shared/queueing\";\n\nimport { getBookmarkDetails, updateAsset } from \"../workerUtils\";\n\nconst TMP_FOLDER = path.join(os.tmpdir(), \"video_downloads\");\n\nexport class VideoWorker {\n  static async build() {\n    logger.info(\"Starting video worker ...\");\n\n    return (await getQueueClient())!.createRunner<ZVideoRequest>(\n      VideoWorkerQueue,\n      {\n        run: withWorkerTracing(\"videoWorker.run\", runWorker),\n        onComplete: async (job) => {\n          workerStatsCounter.labels(\"video\", \"completed\").inc();\n          const jobId = job.id;\n          logger.info(\n            `[VideoCrawler][${jobId}] Video Download Completed successfully`,\n          );\n          return Promise.resolve();\n        },\n        onError: async (job) => {\n          workerStatsCounter.labels(\"video\", \"failed\").inc();\n          if (job.numRetriesLeft == 0) {\n            workerStatsCounter.labels(\"video\", \"failed_permanent\").inc();\n          }\n          const jobId = job.id;\n          logger.error(\n            `[VideoCrawler][${jobId}] Video Download job failed: ${job.error}`,\n          );\n          return Promise.resolve();\n        },\n      },\n      {\n        pollIntervalMs: 1000,\n        timeoutSecs: serverConfig.crawler.downloadVideoTimeout,\n        concurrency: 1,\n        validator: zvideoRequestSchema,\n      },\n    );\n  }\n}\n\nfunction prepareYtDlpArguments(\n  url: string,\n  proxy: string | undefined,\n  assetPath: string,\n) {\n  const ytDlpArguments = [url];\n  if (serverConfig.crawler.maxVideoDownloadSize > 0) {\n    ytDlpArguments.push(\n      \"-f\",\n      `best[filesize<${serverConfig.crawler.maxVideoDownloadSize}M]`,\n    );\n  }\n\n  ytDlpArguments.push(...serverConfig.crawler.ytDlpArguments);\n  ytDlpArguments.push(\"-o\", assetPath);\n  ytDlpArguments.push(\"--no-playlist\");\n  if (proxy) {\n    ytDlpArguments.push(\"--proxy\", proxy);\n  }\n  return ytDlpArguments;\n}\n\nasync function runWorker(job: DequeuedJob<ZVideoRequest>) {\n  const jobId = job.id;\n  const { bookmarkId } = job.data;\n\n  const {\n    url,\n    userId,\n    videoAssetId: oldVideoAssetId,\n  } = await getBookmarkDetails(bookmarkId);\n\n  if (!serverConfig.crawler.downloadVideo) {\n    logger.info(\n      `[VideoCrawler][${jobId}] Skipping video download from \"${url}\", because it is disabled in the config.`,\n    );\n    return;\n  }\n\n  const proxy = getProxyAgent(url);\n  const validation = await validateUrl(url, !!proxy);\n  if (!validation.ok) {\n    logger.warn(\n      `[VideoCrawler][${jobId}] Skipping video download to disallowed URL \"${url}\": ${validation.reason}`,\n    );\n    return;\n  }\n  const normalizedUrl = validation.url.toString();\n\n  const videoAssetId = newAssetId();\n  let assetPath = `${TMP_FOLDER}/${videoAssetId}`;\n  await fs.promises.mkdir(TMP_FOLDER, { recursive: true });\n\n  const ytDlpArguments = prepareYtDlpArguments(\n    normalizedUrl,\n    proxy?.proxy.toString(),\n    assetPath,\n  );\n\n  try {\n    logger.info(\n      `[VideoCrawler][${jobId}] Attempting to download a file from \"${normalizedUrl}\" to \"${assetPath}\" using the following arguments: \"${ytDlpArguments}\"`,\n    );\n\n    await execa(\"yt-dlp\", ytDlpArguments, {\n      cancelSignal: job.abortSignal,\n    });\n    const downloadPath = await findAssetFile(videoAssetId);\n    if (!downloadPath) {\n      logger.info(\n        \"[VideoCrawler][${jobId}] yt-dlp didn't download anything. Skipping ...\",\n      );\n      return;\n    }\n    assetPath = downloadPath;\n  } catch (e) {\n    const err = e as Error;\n    if (\n      err.message.includes(\"ERROR: Unsupported URL:\") ||\n      err.message.includes(\"No media found\")\n    ) {\n      logger.info(\n        `[VideoCrawler][${jobId}] Skipping video download from \"${normalizedUrl}\", because it's not one of the supported yt-dlp URLs`,\n      );\n      return;\n    }\n    const genericError = `[VideoCrawler][${jobId}] Failed to download a file from \"${normalizedUrl}\" to \"${assetPath}\"`;\n    if (\"stderr\" in err) {\n      logger.error(`${genericError}: ${err.stderr}`);\n    } else {\n      logger.error(genericError);\n    }\n    await deleteLeftOverAssetFile(jobId, videoAssetId);\n    return;\n  }\n\n  logger.info(\n    `[VideoCrawler][${jobId}] Finished downloading a file from \"${normalizedUrl}\" to \"${assetPath}\"`,\n  );\n\n  // Get file size and check quota before saving\n  const stats = await fs.promises.stat(assetPath);\n  const fileSize = stats.size;\n\n  try {\n    const quotaApproved = await QuotaService.checkStorageQuota(\n      db,\n      userId,\n      fileSize,\n    );\n\n    await saveAssetFromFile({\n      userId,\n      assetId: videoAssetId,\n      assetPath,\n      metadata: { contentType: ASSET_TYPES.VIDEO_MP4 },\n      quotaApproved,\n    });\n\n    await db.transaction(async (txn) => {\n      await updateAsset(\n        oldVideoAssetId,\n        {\n          id: videoAssetId,\n          bookmarkId,\n          userId,\n          assetType: AssetTypes.LINK_VIDEO,\n          contentType: ASSET_TYPES.VIDEO_MP4,\n          size: fileSize,\n        },\n        txn,\n      );\n    });\n    await silentDeleteAsset(userId, oldVideoAssetId);\n\n    logger.info(\n      `[VideoCrawler][${jobId}] Finished downloading video from \"${normalizedUrl}\" and adding it to the database`,\n    );\n  } catch (error) {\n    if (error instanceof StorageQuotaError) {\n      logger.warn(\n        `[VideoCrawler][${jobId}] Skipping video storage due to quota exceeded: ${error.message}`,\n      );\n      await deleteLeftOverAssetFile(jobId, videoAssetId);\n      return;\n    }\n    throw error;\n  }\n}\n\n/**\n * Deletes leftover assets in case the download fails\n *\n * @param jobId the id of the job\n * @param assetId the id of the asset to delete\n */\nasync function deleteLeftOverAssetFile(\n  jobId: string,\n  assetId: string,\n): Promise<void> {\n  let assetFile;\n  try {\n    assetFile = await findAssetFile(assetId);\n  } catch {\n    // ignore exception, no asset file was found\n    return;\n  }\n  if (!assetFile) {\n    return;\n  }\n  logger.info(\n    `[VideoCrawler][${jobId}] Deleting leftover video asset \"${assetFile}\".`,\n  );\n  try {\n    await fs.promises.rm(assetFile);\n  } catch {\n    logger.error(\n      `[VideoCrawler][${jobId}] Failed deleting leftover video asset \"${assetFile}\".`,\n    );\n  }\n}\n\n/**\n * yt-dlp automatically adds a file ending to the passed in filename --> we have to search it again in the folder\n *\n * @param assetId the id of the asset to search\n * @returns the path to the downloaded asset\n */\nasync function findAssetFile(assetId: string): Promise<string | null> {\n  const files = await fs.promises.readdir(TMP_FOLDER);\n  for (const file of files) {\n    if (file.startsWith(assetId)) {\n      return path.join(TMP_FOLDER, file);\n    }\n  }\n  return null;\n}\n"
  },
  {
    "path": "apps/workers/workers/webhookWorker.ts",
    "content": "import { eq } from \"drizzle-orm\";\nimport { workerStatsCounter } from \"metrics\";\nimport { fetchWithProxy } from \"network\";\nimport { withWorkerTracing } from \"workerTracing\";\n\nimport { db } from \"@karakeep/db\";\nimport { bookmarks, webhooksTable } from \"@karakeep/db/schema\";\nimport {\n  WebhookQueue,\n  ZWebhookRequest,\n  zWebhookRequestSchema,\n} from \"@karakeep/shared-server\";\nimport serverConfig from \"@karakeep/shared/config\";\nimport logger from \"@karakeep/shared/logger\";\nimport { DequeuedJob, getQueueClient } from \"@karakeep/shared/queueing\";\n\nexport class WebhookWorker {\n  static async build() {\n    logger.info(\"Starting webhook worker ...\");\n    const worker = (await getQueueClient())!.createRunner<ZWebhookRequest>(\n      WebhookQueue,\n      {\n        run: withWorkerTracing(\"webhookWorker.run\", runWebhook),\n        onComplete: async (job) => {\n          workerStatsCounter.labels(\"webhook\", \"completed\").inc();\n          const jobId = job.id;\n          logger.info(`[webhook][${jobId}] Completed successfully`);\n          return Promise.resolve();\n        },\n        onError: async (job) => {\n          workerStatsCounter.labels(\"webhook\", \"failed\").inc();\n          if (job.numRetriesLeft == 0) {\n            workerStatsCounter.labels(\"webhook\", \"failed_permanent\").inc();\n          }\n          const jobId = job.id;\n          logger.error(\n            `[webhook][${jobId}] webhook job failed: ${job.error}\\n${job.error.stack}`,\n          );\n          return Promise.resolve();\n        },\n      },\n      {\n        concurrency: serverConfig.webhook.numWorkers,\n        pollIntervalMs: 1000,\n        timeoutSecs:\n          serverConfig.webhook.timeoutSec *\n            (serverConfig.webhook.retryTimes + 1) +\n          1, //consider retry times, and timeout and add 1 second for other stuff\n        validator: zWebhookRequestSchema,\n      },\n    );\n\n    return worker;\n  }\n}\n\nasync function fetchBookmark(bookmarkId: string) {\n  return await db.query.bookmarks.findFirst({\n    where: eq(bookmarks.id, bookmarkId),\n    with: {\n      link: {\n        columns: {\n          url: true,\n        },\n      },\n    },\n  });\n}\n\nasync function fetchUserWebhooks(userId: string) {\n  return await db.query.webhooksTable.findMany({\n    where: eq(webhooksTable.userId, userId),\n  });\n}\n\nasync function runWebhook(job: DequeuedJob<ZWebhookRequest>) {\n  const jobId = job.id;\n  const webhookTimeoutSec = serverConfig.webhook.timeoutSec;\n\n  const { bookmarkId } = job.data;\n  const bookmark = await fetchBookmark(bookmarkId);\n\n  const userId = job.data.userId ?? bookmark?.userId;\n  if (!userId) {\n    logger.error(\n      `[webhook][${jobId}] Failed to find user for bookmark with id ${bookmarkId}. Skipping webhook`,\n    );\n    return;\n  }\n\n  const webhooks = await fetchUserWebhooks(userId);\n\n  logger.info(\n    `[webhook][${jobId}] Starting a webhook job for bookmark with id \"${bookmarkId} for operation \"${job.data.operation}\"`,\n  );\n\n  await Promise.allSettled(\n    webhooks\n      .filter((w) => w.events.includes(job.data.operation))\n      .map(async (webhook) => {\n        const url = webhook.url;\n        const webhookToken = webhook.token;\n        const maxRetries = serverConfig.webhook.retryTimes;\n        let attempt = 0;\n        let success = false;\n\n        while (attempt < maxRetries && !success) {\n          try {\n            const response = await fetchWithProxy(url, {\n              method: \"POST\",\n              headers: {\n                \"Content-Type\": \"application/json\",\n                ...(webhookToken\n                  ? {\n                      Authorization: `Bearer ${webhookToken}`,\n                    }\n                  : {}),\n              },\n              body: JSON.stringify({\n                jobId,\n                bookmarkId,\n                userId,\n                url: bookmark?.link ? bookmark.link.url : undefined,\n                type: bookmark?.type,\n                operation: job.data.operation,\n              }),\n              signal: AbortSignal.timeout(webhookTimeoutSec * 1000),\n            });\n\n            if (!response.ok) {\n              logger.error(\n                `Webhook call to ${url} failed with status: ${response.status}`,\n              );\n            } else {\n              logger.info(\n                `[webhook][${jobId}] Webhook to ${url} call succeeded`,\n              );\n              success = true;\n            }\n          } catch (error) {\n            logger.error(\n              `[webhook][${jobId}] Webhook to ${url} call failed: ${error}`,\n            );\n          }\n          attempt++;\n          if (!success && attempt < maxRetries) {\n            logger.info(\n              `[webhook][${jobId}] Retrying webhook call to ${url}, attempt ${attempt + 1}`,\n            );\n          }\n        }\n      }),\n  );\n}\n"
  },
  {
    "path": "charts/README.md",
    "content": "The helm charts got moved to a separate repo: https://github.com/karakeep-app/helm-charts\n"
  },
  {
    "path": "docker/Dockerfile",
    "content": "################# Monolith Builder ##############\nFROM rust:1-bookworm AS monolith_builder\n\nRUN apt-get update \\\n    && apt-get install --no-install-recommends -y \\\n        libssl-dev \\\n        pkg-config \\\n    && rm -rf /var/lib/apt/lists/* \\\n    && cargo install monolith\n\n################# Base Builder ##############\nFROM node:24-slim AS base\n\nWORKDIR /app\nENV PNPM_HOME=\"/pnpm\"\nENV PATH=\"$PNPM_HOME:$PATH\"\n\n# https://github.com/karakeep-app/karakeep/issues/967\nRUN npm install -g corepack@0.31.0 && corepack enable\n\nRUN apt-get update \\\n    && apt-get install --no-install-recommends -y \\\n        make \\\n        g++ \\\n        python3 \\\n        python3-pip \\\n    && rm -rf /var/lib/apt/lists/*\n\n# Copy package files for dependency installation\nCOPY package.json pnpm-lock.yaml pnpm-workspace.yaml .npmrc ./\nCOPY apps/cli/package.json ./apps/cli/\nCOPY apps/mcp/package.json ./apps/mcp/\nCOPY apps/web/package.json ./apps/web/\nCOPY apps/workers/package.json ./apps/workers/\nCOPY packages/api/package.json ./packages/api/\nCOPY packages/db/package.json ./packages/db/\nCOPY packages/open-api/package.json ./packages/open-api/\nCOPY packages/plugins/package.json ./packages/plugins/\nCOPY packages/sdk/package.json ./packages/sdk/\nCOPY packages/shared-react/package.json ./packages/shared-react/\nCOPY packages/shared-server/package.json ./packages/shared-server/\nCOPY packages/shared/package.json ./packages/shared/\nCOPY packages/trpc/package.json ./packages/trpc/\nCOPY tooling/github/package.json ./tooling/github/\nCOPY tooling/oxlint/package.json ./tooling/oxlint/\nCOPY tooling/prettier/package.json ./tooling/prettier/\nCOPY tooling/tailwind/package.json ./tooling/tailwind/\nCOPY tooling/typescript/package.json ./tooling/typescript/\nCOPY ./patches ./patches\n\nENV NEXT_TELEMETRY_DISABLED 1\nENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1\nRUN pnpm install --frozen-lockfile\n\nCOPY . .\n\n# Build the db migration script\nRUN cd packages/db && \\\n    pnpm exec ncc build migrate.ts -o /db_migrations && \\\n    cp -R drizzle /db_migrations\n\n\n# Compile the web app\nRUN (cd apps/web && pnpm exec next build --experimental-build-mode compile)\n\n# Build the worker code\nRUN (cd apps/workers && pnpm build)\nRUN pnpm deploy --node-linker=isolated --filter @karakeep/workers --prod /prod/workers\n\n# Build the cli\nRUN (cd apps/cli && pnpm build)\n\n# Build the mcp server\nRUN (cd apps/mcp && pnpm build)\n\n################# The All-in-one builder ##############\n\nFROM node:24-slim AS aio_builder\nLABEL org.opencontainers.image.source=\"https://github.com/karakeep-app/karakeep\"\nWORKDIR /app\n\nARG SERVER_VERSION=nightly\nENV SERVER_VERSION=${SERVER_VERSION}\n\nENV PORT 3000\nENV HOSTNAME \"0.0.0.0\"\nEXPOSE 3000\n\n######################\n# Prepare s6-overlay\n######################\nARG S6_OVERLAY_VERSION=3.2.1.0\nARG TARGETARCH\n\n# Install wget and xz-utils needed for s6-overlay\nRUN apt-get update \\\n    && apt-get install --no-install-recommends -y \\\n        ca-certificates \\\n        wget \\\n        xz-utils \\\n    && rm -rf /var/lib/apt/lists/*\n\nADD https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz /tmp\nRUN tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz \\\n    && case ${TARGETARCH} in \\\n            \"amd64\")  S6_ARCH=x86_64   ;; \\\n            \"arm64\")  S6_ARCH=aarch64  ;; \\\n        esac \\\n    && echo https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz -O /tmp/s6-overlay-${S6_ARCH}.tar.xz \\\n    && wget https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-${S6_ARCH}.tar.xz -O /tmp/s6-overlay-${S6_ARCH}.tar.xz \\\n    && tar -C / -Jxpf /tmp/s6-overlay-${S6_ARCH}.tar.xz \\\n    && rm -f /tmp/s6-overlay-${S6_ARCH}.tar.xz\n\n# Copy the s6-overlay config\nCOPY --chmod=755 ./docker/root/etc/s6-overlay /etc/s6-overlay\n\n######################\n# Install runtime deps\n######################\nRUN apt-get update \\\n    && apt-get install --no-install-recommends -y \\\n        curl \\\n        graphicsmagick \\\n        ghostscript \\\n        libjemalloc2 \\\n    && rm -rf /var/lib/apt/lists/*\n\nARG TARGETARCH\nRUN case ${TARGETARCH} in \\\n        \"amd64\")  YTDLP_FILE=yt-dlp_linux ;; \\\n        \"arm64\")  YTDLP_FILE=yt-dlp_linux_aarch64 ;; \\\n        *) echo \"Unsupported arch: ${TARGETARCH}\" && exit 1 ;; \\\n    esac \\\n    && curl -fsSL https://github.com/yt-dlp/yt-dlp/releases/latest/download/${YTDLP_FILE} -o /usr/local/bin/yt-dlp \\\n    && chmod +x /usr/local/bin/yt-dlp\n\n# Copy monolith binary from builder\nCOPY --from=monolith_builder /usr/local/cargo/bin/monolith /usr/local/bin/monolith\n\n######################\n# Prepare the web app\n######################\n\nENV NODE_ENV production\nENV NEXT_TELEMETRY_DISABLED 1\nENV NEXTAUTH_URL_INTERNAL=http://localhost:${PORT}\n\n# Create symlink for jemalloc based on architecture\nARG TARGETARCH\nRUN if [ \"$TARGETARCH\" = \"arm64\" ]; then \\\n        ln -s /usr/lib/aarch64-linux-gnu/libjemalloc.so.2 /usr/local/lib/libjemalloc.so; \\\n    else \\\n        ln -s /usr/lib/x86_64-linux-gnu/libjemalloc.so.2 /usr/local/lib/libjemalloc.so; \\\n    fi\n\nCOPY --from=base --chown=node:node /app/apps/web/.next/standalone ./\nCOPY --from=base /app/apps/web/public ./apps/web/public\nCOPY --from=base /db_migrations /db_migrations\n\n# Set the correct permission for prerender cache\nRUN mkdir -p ./apps/web/.next && chown node:node ./apps/web/.next\n\n# Automatically leverage output traces to reduce image size\n# https://nextjs.org/docs/advanced-features/output-file-tracing\nCOPY --from=base --chown=node:node /app/apps/web/.next/static ./apps/web/.next/static\n\n######################\n# Prepare the workers app\n######################\nCOPY --from=base /prod/workers /app/apps/workers\n\nENTRYPOINT [\"/init\"]\n\n################# The AIO ##############\n\nFROM aio_builder AS aio\n\nRUN touch /etc/s6-overlay/s6-rc.d/user/contents.d/init-db-migration \\\n    /etc/s6-overlay/s6-rc.d/user/contents.d/svc-web \\\n    /etc/s6-overlay/s6-rc.d/user/contents.d/svc-workers\n\nHEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:3000/api/health || exit 1\n\n################# The web container ##############\n\nFROM aio_builder AS web\n\nRUN touch /etc/s6-overlay/s6-rc.d/user/contents.d/init-db-migration \\\n    /etc/s6-overlay/s6-rc.d/user/contents.d/svc-web\nENV USING_LEGACY_SEPARATE_CONTAINERS=true\n\nHEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:3000/api/health || exit 1\n\n################# The workers container ##############\n\nFROM aio_builder AS workers\n\n# In the current implemtation, the workers assume the migration\n# is done for them.\nRUN rm /etc/s6-overlay/s6-rc.d/svc-workers/dependencies.d/init-db-migration \\\n    && touch /etc/s6-overlay/s6-rc.d/user/contents.d/svc-workers\nENV USING_LEGACY_SEPARATE_CONTAINERS=true\n\n################# The cli ##############\n\nFROM node:24-slim AS cli\nLABEL org.opencontainers.image.source=\"https://github.com/karakeep-app/karakeep\"\nWORKDIR /app\n\nCOPY --from=base /app/apps/cli/dist/index.mjs apps/cli/index.mjs\n\nWORKDIR /app/apps/cli\n\nARG SERVER_VERSION=nightly\nENV SERVER_VERSION=${SERVER_VERSION}\n\nENTRYPOINT [\"node\", \"index.mjs\"]\n\n################# MCP server ##############\n\nFROM node:24-slim AS mcp\nLABEL org.opencontainers.image.source=\"https://github.com/karakeep-app/karakeep\"\nWORKDIR /app\n\nCOPY --from=base /app/apps/mcp/dist/index.js apps/mcp/index.js\n\nWORKDIR /app/apps/mcp\n\nENTRYPOINT [\"node\", \"index.js\"]\n"
  },
  {
    "path": "docker/Dockerfile.dev",
    "content": "FROM node:24-alpine\n\nWORKDIR /app\nENV PNPM_HOME=\"/pnpm\"\nENV PATH=\"$PNPM_HOME:$PATH\"\n\n# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.\nRUN apk add --no-cache libc6-compat make g++ py3-pip linux-headers git\n\nRUN corepack enable\n\nCOPY . .\nENV NEXT_TELEMETRY_DISABLED 1\nENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1\n"
  },
  {
    "path": "docker/docker-compose.build.yml",
    "content": "services:\n  web:\n    build:\n      dockerfile: docker/Dockerfile\n      context: ..\n      target: aio\n    restart: unless-stopped\n    volumes:\n      - data:/data\n    environment:\n      MEILI_ADDR: http://meilisearch:7700\n      BROWSER_WEB_URL: http://chrome:9222\n      DATA_DIR: /data\n  chrome:\n    image: gcr.io/zenika-hub/alpine-chrome:124\n    restart: unless-stopped\n    command:\n      - --no-sandbox\n      - --disable-gpu\n      - --disable-dev-shm-usage\n      - --remote-debugging-address=0.0.0.0\n      - --remote-debugging-port=9222\n      - --hide-scrollbars\n  meilisearch:\n    image: getmeili/meilisearch:v1.37.0\n    restart: unless-stopped\n    environment:\n      MEILI_NO_ANALYTICS: \"true\"\n    volumes:\n      - meilisearch:/meili_data\n\nvolumes:\n  meilisearch:\n  data:\n"
  },
  {
    "path": "docker/docker-compose.dev.yml",
    "content": "version: \"3.8\"\nservices:\n  web:\n    build:\n      dockerfile: Dockerfile.dev\n    volumes:\n      - data:/data\n      - ..:/app\n    ports:\n      - 3000:3000\n    env_file:\n      - .env  # Automatically load all variables from this file\n    command:\n      - pnpm\n      - web\n    working_dir: /app\n    depends_on:\n      prep:\n        condition: service_completed_successfully\n  chrome:\n    image: gcr.io/zenika-hub/alpine-chrome:124\n    restart: unless-stopped\n    ports:\n      - 9222:9222\n    command:\n      - --no-sandbox\n      - --disable-gpu\n      - --disable-dev-shm-usage\n      - --remote-debugging-address=0.0.0.0\n      - --remote-debugging-port=9222\n      - --hide-scrollbars\n  meilisearch:\n    image: getmeili/meilisearch:v1.37.0\n    volumes:\n      - meilisearch:/meili_data\n  workers:\n    build:\n      dockerfile: Dockerfile.dev\n    volumes:\n      - data:/data\n      - ..:/app\n    env_file:\n      - .env\n    working_dir: /app\n    environment:\n      MEILI_ADDR: http://meilisearch:7700\n      BROWSER_WEB_URL: http://chrome:9222\n      DATA_DIR: /data\n      # OPENAI_API_KEY: ...\n    command:\n      - pnpm\n      - workers\n    depends_on:\n      prep:\n        condition: service_completed_successfully\n  prep:\n    build:\n      dockerfile: Dockerfile.dev\n    env_file:\n      - .env\n    working_dir: /app\n    environment:\n      DATA_DIR: /data\n    volumes:\n      - data:/data\n      - ..:/app\n    command:\n     - /bin/sh\n     - -c\n     - \"pnpm install --frozen-lockfile && pnpm run db:migrate\"\n\nvolumes:\n  meilisearch:\n  data:\n"
  },
  {
    "path": "docker/docker-compose.yml",
    "content": "services:\n  web:\n    image: ghcr.io/karakeep-app/karakeep:${KARAKEEP_VERSION:-release}\n    restart: unless-stopped\n    volumes:\n      # By default, the data is stored in a docker volume called \"data\".\n      # If you want to mount a custom directory, change the volume mapping to:\n      # - /path/to/your/directory:/data\n      - data:/data\n    ports:\n      - 3000:3000\n    env_file:\n      - .env\n    environment:\n      MEILI_ADDR: http://meilisearch:7700\n      BROWSER_WEB_URL: http://chrome:9222\n      # OPENAI_API_KEY: ...\n\n      # You almost never want to change the value of the DATA_DIR variable.\n      # If you want to mount a custom directory, change the volume mapping above instead.\n      DATA_DIR: /data # DON'T CHANGE THIS\n  chrome:\n    image: gcr.io/zenika-hub/alpine-chrome:124\n    restart: unless-stopped\n    command:\n      - --no-sandbox\n      - --disable-gpu\n      - --disable-dev-shm-usage\n      - --remote-debugging-address=0.0.0.0\n      - --remote-debugging-port=9222\n      - --hide-scrollbars\n  meilisearch:\n    image: getmeili/meilisearch:v1.37.0\n    restart: unless-stopped\n    env_file:\n      - .env\n    environment:\n      MEILI_NO_ANALYTICS: \"true\"\n    volumes:\n      - meilisearch:/meili_data\n\nvolumes:\n  meilisearch:\n  data:\n"
  },
  {
    "path": "docker/root/etc/s6-overlay/s6-rc.d/init-db-migration/run",
    "content": "#!/command/with-contenv sh\n# shellcheck shell=bash\n\necho \"Running db migration script\";\ncd /db_migrations;\nif [ \"${USE_JEMALLOC:-false}\" = \"true\" ]; then\n    export LD_PRELOAD=/usr/local/lib/libjemalloc.so\nfi\nexec node index.js;\n"
  },
  {
    "path": "docker/root/etc/s6-overlay/s6-rc.d/init-db-migration/type",
    "content": "oneshot\n"
  },
  {
    "path": "docker/root/etc/s6-overlay/s6-rc.d/init-db-migration/up",
    "content": "/etc/s6-overlay/s6-rc.d/init-db-migration/run\n"
  },
  {
    "path": "docker/root/etc/s6-overlay/s6-rc.d/svc-web/dependencies.d/init-db-migration",
    "content": ""
  },
  {
    "path": "docker/root/etc/s6-overlay/s6-rc.d/svc-web/run",
    "content": "#!/command/with-contenv sh\n# shellcheck shell=bash\n\ncd /app/apps/web;\nif [ \"${USE_JEMALLOC:-false}\" = \"true\" ]; then\n    export LD_PRELOAD=/usr/local/lib/libjemalloc.so\nfi\nexec node server.js;\n"
  },
  {
    "path": "docker/root/etc/s6-overlay/s6-rc.d/svc-web/type",
    "content": "longrun\n"
  },
  {
    "path": "docker/root/etc/s6-overlay/s6-rc.d/svc-workers/dependencies.d/init-db-migration",
    "content": ""
  },
  {
    "path": "docker/root/etc/s6-overlay/s6-rc.d/svc-workers/run",
    "content": "#!/command/with-contenv sh\n# shellcheck shell=bash\n\ncd /app/apps/workers;\nif [ \"${USE_JEMALLOC:-false}\" = \"true\" ]; then\n    export LD_PRELOAD=/usr/local/lib/libjemalloc.so\nfi\nexec node dist/index.js\n"
  },
  {
    "path": "docker/root/etc/s6-overlay/s6-rc.d/svc-workers/type",
    "content": "longrun\n"
  },
  {
    "path": "docker/root/etc/s6-overlay/s6-rc.d/user/contents.d/.gitkeep",
    "content": ""
  },
  {
    "path": "docs/.gitignore",
    "content": "# Dependencies\n/node_modules\n\n# Production\n/build\n\n# Generated files\n.docusaurus\n.cache-loader\n\n# Misc\n.DS_Store\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n"
  },
  {
    "path": "docs/.oxlintrc.json",
    "content": "{\n  \"$schema\": \"../node_modules/oxlint/configuration_schema.json\",\n  \"extends\": [\n    \"../tooling/oxlint/oxlint-base.json\",\n    \"../tooling/oxlint/oxlint-nextjs.json\",\n    \"../tooling/oxlint/oxlint-react.json\"\n  ],\n  \"env\": {\n    \"builtin\": true,\n    \"commonjs\": true,\n    \"browser\": true,\n    \"es2022\": true,\n    \"node\": true\n  },\n  \"globals\": {\n    \"React\": \"writeable\"\n  },\n  \"settings\": {\n    \"react\": {\n      \"version\": \"19\"\n    }\n  },\n  \"ignorePatterns\": [\n    \"**/*.config.js\",\n    \"**/*.config.cjs\",\n    \"**/.eslintrc.cjs\",\n    \".next\",\n    \"dist\",\n    \"build\",\n    \"pnpm-lock.yaml\"\n  ]\n}\n"
  },
  {
    "path": "docs/README.md",
    "content": "# Website\n\nThis website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator.\n\n### Installation\n\n```\n$ yarn\n```\n\n### Local Development\n\n```\n$ yarn start\n```\n\nThis command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.\n\n### Build\n\n```\n$ yarn build\n```\n\nThis command generates static content into the `build` directory and can be served using any static contents hosting service.\n\n### Deployment\n\nUsing SSH:\n\n```\n$ USE_SSH=true yarn deploy\n```\n\nNot using SSH:\n\n```\n$ GIT_USER=<Your GitHub username> yarn deploy\n```\n\nIf you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.\n"
  },
  {
    "path": "docs/babel.config.js",
    "content": "module.exports = {\n  presets: [require.resolve('@docusaurus/core/lib/babel/preset')],\n};\n"
  },
  {
    "path": "docs/docs/01-getting-started/01-intro.md",
    "content": "---\nslug: /\n---\n\n# Introduction\n\nKarakeep (previously Hoarder) is an open source \"Bookmark Everything\" app that uses AI for automatically tagging the content you throw at it. The app is built with self-hosting as a first class citizen.\n\n![Screenshot](https://raw.githubusercontent.com/karakeep-app/karakeep/main/screenshots/homepage.png)\n\n\n## Features\n\n- 🔗 Bookmark links, take simple notes and store images and pdfs.\n- ⬇️ Automatic fetching for link titles, descriptions and images.\n- 📋 Sort your bookmarks into lists.\n- 👥 Collaborate with others on the same list.\n- 🔎 Full text search of all the content stored.\n- ✨ AI-based (aka chatgpt) automatic tagging and summarization. With supports for local models using ollama!\n- 🤖 Rule-based engine for customized management.\n- 🎆 OCR for extracting text from images.\n- 🔖 [Chrome plugin](https://chromewebstore.google.com/detail/karakeep/kgcjekpmcjjogibpjebkhaanilehneje) and [Firefox addon](https://addons.mozilla.org/en-US/firefox/addon/karakeep/) for quick bookmarking.\n- 📱 An [iOS app](https://apps.apple.com/us/app/karakeep-app/id6479258022), and an [Android app](https://play.google.com/store/apps/details?id=app.hoarder.hoardermobile&pcampaignid=web_share).\n- 📰 Auto hoarding from RSS feeds.\n- 🔌 REST API and multiple clients.\n- 🌐 Multi-language support.\n- 🖍️ Mark and store highlights from your hoarded content.\n- 🗄️ Full page archival (using [monolith](https://github.com/Y2Z/monolith)) to protect against link rot.\n- ▶️ Auto video archiving using [yt-dlp](https://github.com/yt-dlp/yt-dlp).\n- ☑️ Bulk actions support.\n- 🔐 SSO support.\n- 🌙 Dark mode support.\n- 💾 Self-hosting first.\n- ⬇️ Bookmark importers from Chrome, Pocket, Linkwarden, Omnivore, Tab Session Manager.\n- 🔄 Automatic sync with browser bookmarks via [floccus](https://floccus.org/).\n- [Planned] Offline reading on mobile, semantic search across bookmarks, ...\n\n**⚠️ This app is under heavy development.**\n\n\n## Demo\n\nYou can access the demo at [https://try.karakeep.app](https://try.karakeep.app). Login with the following creds:\n\n```\nemail: demo@karakeep.app\npassword: demodemo\n```\n\nThe demo is seeded with some content, but it's in read-only mode to prevent abuse.\n\n## About the name\n\nThe name Karakeep is inspired by the Arabic word \"كراكيب\" (karakeeb), a colloquial term commonly used to refer to miscellaneous clutter, odds and ends, or items that may seem disorganized but often hold personal value or hidden usefulness. It evokes the image of a messy drawer or forgotten box, full of stuff you can't quite throw away—because somehow, it matters (or more likely, because you're a hoarder!).\n"
  },
  {
    "path": "docs/docs/01-getting-started/02-screenshots.md",
    "content": "# Screenshots\n\n## Homepage\n\n![Homepage](/img/screenshots/homepage.png)\n\n## Homepage (Dark Mode)\n\n![Homepage](/img/screenshots/homepage-dark.png)\n\n## Tags\n\n![All Tags](/img/screenshots/all-tags.png)\n\n## Lists\n\n![All Lists](/img/screenshots/all-lists.png)\n\n## Bookmark Preview\n\n![Bookmark Preview](/img/screenshots/bookmark-preview.png)\n\n## Settings\n\n![Settings](/img/screenshots/settings.png)\n\n\n## Sharing\n\n<img src=\"/img/screenshots/share-sheet.png\" width=\"400px\"  />\n"
  },
  {
    "path": "docs/docs/01-getting-started/_category_.json",
    "content": "{\n  \"label\": \"Getting Started\",\n  \"position\": 1\n}\n"
  },
  {
    "path": "docs/docs/02-installation/01-docker.md",
    "content": "# Docker\n\n### Requirements\n\n- Docker\n- Docker Compose\n\n### 1. Create a new directory\n\nCreate a new directory to host the compose file and env variables.\n\nThis is where you’ll place the `docker-compose.yml` file from the next step and the environment variables.\n\nFor example you could make a new directory called \"karakeep-app\" with the following command:\n```\nmkdir karakeep-app\n```\n\n\n### 2. Download the compose file\n\nDownload the docker compose file provided [here](https://github.com/karakeep-app/karakeep/blob/main/docker/docker-compose.yml) directly into your new directory.\n\n```\nwget https://raw.githubusercontent.com/karakeep-app/karakeep/main/docker/docker-compose.yml\n```\n\n### 3. Populate the environment variables\n\nTo configure the app, create a `.env` file in the directory and add this minimal env file:\n\n```\nKARAKEEP_VERSION=release\nNEXTAUTH_SECRET=super_random_string\nMEILI_MASTER_KEY=another_random_string\nNEXTAUTH_URL=http://localhost:3000\n```\n\nYou **should** change the random strings. You can use `openssl rand -base64 36` in a seperate terminal window to generate the random strings. You should also change the `NEXTAUTH_URL` variable to point to your server address.\n\nUsing `KARAKEEP_VERSION=release` will pull the latest stable version. You might want to pin the version instead to control the upgrades (e.g. `KARAKEEP_VERSION=0.10.0`). Check the latest versions [here](https://github.com/karakeep-app/karakeep/pkgs/container/karakeep).\n\nPersistent storage and the wiring between the different services is already taken care of in the docker compose file.\n\nKeep in mind that every time you change the `.env` file, you'll need to re-run `docker compose up`.\n\nIf you want more config params, check the config documentation [here](../03-configuration/01-environment-variables.md).\n\n### 4. Setup OpenAI\n\nTo enable automatic tagging, you'll need to configure OpenAI. This is optional though but highly recommended.\n\n- Follow [OpenAI's help](https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key) to get an API key.\n- Add the OpenAI API key to the env file:\n\n```\nOPENAI_API_KEY=<key>\n```\n\nLearn more about the costs of using openai [here](../06-administration/03-openai.md).\n\nIf you want to use a different AI provider (e.g. Ollama for local inference), check out the [different AI providers](../03-configuration/02-different-ai-providers.md) guide.\n\n### 5. Start the service\n\nStart the service by running:\n\n```\ndocker compose up -d\n```\n\nThen visit `http://localhost:3000` and you should be greeted with the Sign In page.\n\n### [Optional] 6. Enable optional features\n\nCheck the [configuration docs](../03-configuration/01-environment-variables.md) for extra features to enable such as full page archival, full page screenshots, inference languages, etc.\n\n### [Optional] 7. Setup quick sharing extensions\n\nGo to the [quick sharing page](../04-using-karakeep/quick-sharing.md) to install the mobile apps and the browser extensions. Those will help you hoard things faster!\n\n## Updating\n\nUpdating Karakeep will depend on what you used for the `KARAKEEP_VERSION` env variable.\n\n- If you pinned the app to a specific version, bump the version and re-run `docker compose up -d`. This should pull the new version for you.\n- If you used `KARAKEEP_VERSION=release`, you'll need to force docker to pull the latest version by running `docker compose up --pull always -d`.\n\nNote that if you want to upgrade/migrate `Meilisearch` versions, refer to the [troubleshooting](../06-administration/05-troubleshooting.md) page.\n"
  },
  {
    "path": "docs/docs/02-installation/02-unraid.md",
    "content": "# Unraid\n\n## Docker Compose Manager Plugin (Recommended)\n\nYou can use [Docker Compose Manager](https://forums.unraid.net/topic/114415-plugin-docker-compose-manager/) plugin to deploy Karakeep using the official docker compose file provided [here](https://github.com/karakeep-app/karakeep/blob/main/docker/docker-compose.yml). After creating the stack, you'll need to setup some env variables similar to that from the docker compose installation docs [here](/installation/docker#3-populate-the-environment-variables).\n\n## Community Apps\n\n:::info\nThe community application template is maintained by the community.\n:::\n\nKarakeep can be installed on Unraid using the community application plugins. Karakeep is a multi-container service, and because unraid doesn't natively support that, you'll have to install the different pieces as separate applications and wire them manually together.\n\nHere's a high level overview of the services you'll need:\n\n- **Karakeep** ([Support post](https://forums.unraid.net/topic/165108-support-collectathon-karakeep/)): Karakeep's main web app.\n- **Browserless** ([Support post](https://forums.unraid.net/topic/130163-support-template-masterwishxbrowserless/)): The chrome headless service used for fetching the content. Karakeep's official docker compose doesn't use browserless, but it's currently the only headless chrome service available on unraid, so you'll have to use it.\n- **MeiliSearch** ([Support post](https://forums.unraid.net/topic/164847-support-collectathon-meilisearch/)): The search engine used by Karakeep. It's optional but highly recommended. If you don't have it set up, search will be disabled.\n"
  },
  {
    "path": "docs/docs/02-installation/03-archlinux.md",
    "content": "# Arch Linux\n\n## Installation\n\n> [Karakeep on AUR](https://aur.archlinux.org/packages/karakeep) is not maintained by the karakeep official.\n\n1. Install karakeep\n\n    ```shell\n    paru -S karakeep\n    ```\n\n2. (**Optional**) Install optional dependencies\n\n    ```shell\n    # karakeep-cli: karakeep cli tool\n    paru -S karakeep-cli\n\n    # ollama: for automatic tagging\n    sudo pacman -S ollama\n\n    # yt-dlp: for download video\n    sudo pacman -S yt-dlp\n    ```\n\n    You can use Open-AI instead of `ollama`. If you use `ollama`, you need to download the ollama model. Please refer to: [https://ollama.com/library](https://ollama.com/library).\n\n3. Set up\n\n    Environment variables can be set in `/etc/karakeep/karakeep.env` according to [configuration page](../03-configuration/01-environment-variables.md). **The environment variables that are not specified in `/etc/karakeep/karakeep.env` need to be added by yourself.**\n\n4. Enable service\n\n    ```shell\n    sudo systemctl enable --now karakeep.target\n    ```\n\n    Then visit `http://localhost:3000` and you should be greated with the sign in page.\n\n## Services and Ports\n\n`karakeep.target` include 3 services: `karakeep-web.service`, `karakeep-works.service`, `karakeep-browser.service`.\n\n- `karakeep-web.service`: Provide karakeep webui service, uses `3000` port by default.\n\n- `karakeep-workers.service`: Provide karakeep workers service, no port.\n\n- `karakeep-browser.service`: Provide browser headless service, uses `9222` port by default.\n\nNow `karakeep` depends on `meilisearch`, and `karakeep-workers.service` wants `meilisearch.service`, starting `karakeep.target` will start `meilisearch.service` at the same time.\n\n## How to Migrate from Hoarder to Karakeep\n\nThe PKGBUILD has been fully updated to replace all references to `hoarder` with `karakeep`. If you want to preserve your existing `hoarder` data during the upgrade, please follow the steps below:\n\n**1. Stop the old services**\n\n```shell\nsudo systemctl stop hoarder-web.service hoarder-worker.service hoarder-browser.service\nsudo systemctl disable --now hoarder.target\n```\n\n**2. Uninstall Hoarder**  \nAfter uninstalling, you can manually remove the old `hoarder` user and group if needed.\n```shell\nparu -R hoarder\n```\n\n**3. Rename the old data directory**\n```shell\nsudo mv /var/lib/hoarder /var/lib/karakeep\n```\n\n**4. Install Karakeep**\n```shell\nparu -S karakeep\n```\n\n**5. Fix ownership of the data directory**\n```shell\nsudo chown -R karakeep:karakeep /var/lib/karakeep\n```\n\n**6. Set Karakeep**  \nEdit `/etc/karakeep/karakeep.env` according to [configuration page](../03-configuration/01-environment-variables.md). **The environment variables that are not specified in `/etc/karakeep/karakeep.env` need to be added by yourself.**\n\nOr you can copy old hoarder env file to karakeep:\n```shell\nsudo cp -f /etc/hoarder/hoarder.env /etc/karakeep/karakeep.env\n```\n\n**7. Start Karakeep**\n```shell\nsudo systemctl enable --now karakeep.target\n```\n"
  },
  {
    "path": "docs/docs/02-installation/04-kubernetes.md",
    "content": "# Kubernetes\n\n### Requirements\n\n- A kubernetes cluster\n- kubectl\n- kustomize\n\n### 1. Get the deployment manifests\n\nYou can clone the repository and copy the `/kubernetes` directory into another directory of your choice.\n\n### 2. Populate the environment variables and secrets\n\nTo configure the app, copy the `.env_sample` to `.env` and change to your specific needs.\n\nYou should also change the `NEXTAUTH_URL` variable to point to your server address.\n\nUsing `KARAKEEP_VERSION=release` will pull the latest stable version. You might want to pin the version instead to control the upgrades (e.g. `KARAKEEP_VERSION=0.10.0`). Check the latest versions [here](https://github.com/karakeep-app/karakeep/pkgs/container/karakeep).\n\nTo see all available configuration options check the [documentation](../03-configuration/01-environment-variables.md).\n\nTo configure the neccessary secrets for the application copy the `.secrets_sample` file to `.secrets` and change the sample secrets to your generated secrets.\n\n> Note: You **should** change the random strings. You can use `openssl rand -base64 36` to generate the random strings.\n\n### 3. Setup OpenAI\n\nTo enable automatic tagging, you'll need to configure OpenAI. This is optional though but highly recommended.\n\n- Follow [OpenAI's help](https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key) to get an API key.\n- Add the OpenAI API key to the `.env` file:\n\n```\nOPENAI_API_KEY=<key>\n```\n\nLearn more about the costs of using openai [here](../06-administration/03-openai.md).\n\nIf you want to use a different AI provider (e.g. Ollama for local inference), check out the [different AI providers](../03-configuration/02-different-ai-providers.md) guide.\n\n### 4. Deploy the service\n\nDeploy the service by running:\n\n```\nmake deploy\n```\n\n### 5. Access the service\n\n#### via LoadBalancer IP\n\nBy default, these manifests expose the application as a LoadBalancer Service. You can run `kubectl get services` to identify the IP of the loadbalancer for your service.\n\nThen visit `http://<loadbalancer-ip>:3000` and you should be greated with the Sign In page.\n\n> Note: Depending on your setup you might want to expose the service via an Ingress, or have a different means to access it.\n\n#### Via Ingress\n\nIf you want to use an ingress, you can customize the sample ingress in the kubernetes folder and change the host to the DNS name of your choice.\n\nAfter that you have to configure the web service to the type ClusterIP so it is only reachable via the ingress.\n\nIf you have already deployed the service you can patch the web service to the type ClusterIP with the following command:\n\n` kubectl -n karakeep patch service web -p '{\"spec\":{\"type\":\"ClusterIP\"}}' `\n\nAfterwards you can apply the ingress and access the service via your chosen URL.\n\n#### Setting up HTTPS access to the Service\n\nTo access karakeep securely you can configure the ingress to use a preconfigured TLS certificate. This requires that you already have the needed files, namely your .crt and .key file, on hand.\n\nAfter you have deployed the karakeep manifests you can deploy your certificate for karakeep in the `karakeep` namespace with this example command. You can name the secret however you want. But be aware that the secret name in the ingress definition has to match the secret name.\n\n` $ kubectl --namespace karakeep create secret tls karakeep-web-tls --cert=/path/to/crt --key=/path/to/key `\n\nIf the secret is successfully created you can now configure the Ingress to use TLS via this changes to the spec:\n\n```` yaml\n spec:\n  tls:\n  - hosts:\n      - karakeep.example.com\n    secretName: karakeep-web-tls\n````\n\n> Note: Be aware that the hosts have to match between the tls spec and the HTTP spec.\n\n### [Optional] 6. Setup quick sharing extensions\n\nGo to the [quick sharing page](../04-using-karakeep/quick-sharing.md) to install the mobile apps and the browser extensions. Those will help you hoard things faster!\n\n## Updating\n\nEdit the `KARAKEEP_VERSION` variable in the `kustomization.yaml` file and run `make clean deploy`.\n\nIf you have chosen `release` as the image tag you can also destroy the web pod, since the deployment has an ImagePullPolicy set to always the pod always pulls the image from the registry, this way we can ensure that the newest release image is pulled.\n"
  },
  {
    "path": "docs/docs/02-installation/06-debuntu.md",
    "content": "# Debian 12/Ubuntu 24.04\n\n:::warning\nThis script is a stripped-down version of those found in the [Proxmox Community Scripts](https://github.com/community-scripts/ProxmoxVE) repo. It has been adapted to work on baremetal Debian 12 or Ubuntu 24.04 installs **only**. Any other use is not supported and you use this script at your own risk.\n:::\n\n### Requirements\n\n- **Debian 12** (Buster) or\n- **Ubuntu 24.04** (Noble Numbat)\n\nThe script will download and install all dependencies (except for Ollama), install Karakeep, do a basic configuration of Karakeep and Meilisearch (the search app used by Karakeep), and create and enable the systemd service files needed to run Karakeep on startup. Karakeep and Meilisearch are run in the context of their low-privilege user environments for more security.\n\nThe script functions as an update script in addition to an installer. See **[Updating](#updating)**.\n\n### 1. Download the script from the [Karakeep repository](https://github.com/karakeep-app/karakeep/blob/main/karakeep-linux.sh)\n\n```\nwget https://raw.githubusercontent.com/karakeep-app/karakeep/main/karakeep-linux.sh\n```\n\n### 2. Run the script\n\n> This script must be run as `root`, or as a user with `sudo` privileges.\n\n    If this is a fresh install, then run the installer by using the following command:\n\n    ```shell\n    bash karakeep-linux.sh install\n    ```\n\n### 3. Create an account/sign in\n\n    Then visit `http://localhost:3000` and you should be greated with the Sign In page.\n\n## Updating\n\n> This script must be run as `root`, or as a user with `sudo` privileges.\n\n    If Karakeep has previously been installed using this script, then run the updater like so:\n\n    ```shell\n     bash karakeep-linux.sh update\n    ```\n\n## Services and Ports\n\n`karakeep.target` includes 4 services: `meilisearch.service`, `karakeep-web.service`, `karakeep-workers.service`, `karakeep-browser.service`.\n\n- `meilisearch.service`: Provides full-text search, Karakeep Workers service connects to it, uses port `7700` by default.\n\n- `karakeep-web.service`: Provides the karakeep web service, uses `3000` port by default.\n\n- `karakeep-workers.service`: Provides the karakeep workers service, no port.\n\n- `karakeep-browser.service`: Provides the headless browser service, uses `9222` port by default.\n\n## Configuration, ENV file, database locations\n\nDuring installation, the script created a configuration file for `meilisearch`, an `ENV` file for Karakeep, and located config paths and database paths separate from the installation path of Karakeep, so as to allow for easier updating. Their names/locations are as follows:\n\n- `/etc/meilisearch.toml` - a basic configuration for meilisearch, that contains configs for the database location, disabling analytics, and using a master key, which prevents unauthorized connections.\n- `/var/lib/meilisearch` - Meilisearch DB location.\n- `/etc/karakeep/karakeep.env` - The Karakeep `ENV` file. Edit this file to configure Karakeep beyond the default. The web service and the workers service need to be restarted after editing this file:\n\n    ```shell\n    sudo systemctl restart karakeep-workers karakeep-web\n    ```\n\n- `/var/lib/karakeep` - The Karakeep database location. If you delete the contents of this folder you will lose all your data.\n\n## Still Running Hoarder?\n\nThere is a way to upgrade. Please see [Hoarder to Karakeep Migration](../06-administration/08-hoarder-to-karakeep-migration.md)\n"
  },
  {
    "path": "docs/docs/02-installation/07-minimal-install.md",
    "content": "# Minimal Installation\n\n:::warning\nUnless necessary, prefer the [full installation](/installation/docker) to leverage all the features of Karakeep. You'll be sacrificing a lot of functionality if you go with the minimal installation route.\n:::\n\nKarakeep's default installation has a dependency on Meilisearch for the full text search, Chrome for crawling and OpenAI/Ollama for AI tagging. You can however run Karakeep without those dependencies if you're willing to sacrifice those features.\n\n- If you run without meilisearch, the search functionality will be completely disabled.\n- If you run without chrome, crawling will still work, but you'll lose ability to take screenshots of websites and websites with javascript content won't get crawled correctly.\n- If you don't setup OpenAI/Ollama, AI tagging will be disabled.\n\nThose features are important for leveraging Karakeep's full potential, but if you're running in constrained environments, you can use the following minimal docker compose to skip all those dependencies:\n\n```yaml\nservices:\n  web:\n    image: ghcr.io/karakeep-app/karakeep:release\n    restart: unless-stopped\n    volumes:\n      - data:/data\n    ports:\n      - 3000:3000\n    environment:\n      DATA_DIR: /data\n      NEXTAUTH_SECRET: super_random_string\nvolumes:\n  data:\n```\n\nOr just with the following docker command:\n\n```base\ndocker run -d \\\n  --restart unless-stopped \\\n  -v data:/data \\\n  -p 3000:3000 \\\n  -e DATA_DIR=/data \\\n  -e NEXTAUTH_SECRET=super_random_string \\\n  ghcr.io/karakeep-app/karakeep:release\n```\n\n:::warning\nYou **MUST** change the `super_random_string` to a true random string which you can generate with `openssl rand -hex 32`.\n:::\n\nCheck the [configuration docs](../03-configuration/01-environment-variables.md) for extra features to enable such as full page archival, full page screenshots, inference languages, etc.\n"
  },
  {
    "path": "docs/docs/02-installation/08-truenas.md",
    "content": "# TrueNAS\n\nKarakeep is available directly from TrueNAS's app catalog ([link](https://apps.truenas.com/catalog/karakeep/)).\n"
  },
  {
    "path": "docs/docs/02-installation/09-cloud-hosting.md",
    "content": "# Karakeep Cloud\n\n:::tip\nIf you want to use Karakeep without running your own servers, the hosted cloud option is the fastest way to start.\n:::\n\n[Karakeep Cloud](https://cloud.karakeep.app) is the fully managed version of Karakeep operated by the core team. It handles hosting, updates, monitoring, and backups for you, so you can focus on saving content instead of maintaining infrastructure.\n\n### Get started\n\n1. Visit [cloud.karakeep.app](https://cloud.karakeep.app) and create an account.\n2. Follow the onboarding flow to create your workspace.\n3. Install the browser extension or mobile apps from the [quick sharing page](../04-using-karakeep/quick-sharing.md) and start saving links immediately.\n"
  },
  {
    "path": "docs/docs/02-installation/10-pikapods.md",
    "content": "# PikaPods\n\n:::info\nNote: PikaPods shares some of its revenue from hosting Karakeep with the maintainer of this project.\n:::\n\n[PikaPods](https://www.pikapods.com/) offers managed paid hosting for many open source apps, including Karakeep.\nServer administration, updates, migrations and backups are all taken care of, which makes it well suited\nfor less technical users. As of Nov 2024, running Karakeep there will cost you ~$3 per month.\n\n### Requirements\n\n- A _PikaPods_ account. Can be created for free [here](https://www.pikapods.com/register). You get an initial welcome credit of $5.\n\n### 1. Choose app\n\nChoose _Karakeep_ from their [list of apps](https://www.pikapods.com/apps) or use this [direct link](https://www.pikapods.com/pods?run=karakeep). This will either\nopen a new dialog to add a new _Karakeep_ pod or ask you to log in.\n\n### 2. Add settings\n\nThere are a few settings to configure in the dialog:\n\n- **Basics**: Give the pod a name and choose a region that's near you.\n- **Env Vars**: Here you can disable signups or set an OpenAI API key. All settings are optional.\n- **Resources**: The resources your _Karakeep_ pod can use. The defaults are fine, unless you have a very large collection.\n\n### 3. Start pod and add user\n\nAfter hitting _Add pod_ it will take about a minute for the app to fully start. After this you can visit\nthe pod's URL and add an initial user under _Sign Up_. After this you may want to disable further sign-ups\nby setting the pod's `DISABLE_SIGNUPS` _Env Var_ to `true`.\n"
  },
  {
    "path": "docs/docs/02-installation/_category_.json",
    "content": "{\n  \"label\": \"Installation\",\n  \"position\": 3\n}\n"
  },
  {
    "path": "docs/docs/03-configuration/01-environment-variables.md",
    "content": "# Configuration\n\nThe app is mainly configured by environment variables. All the used environment variables are listed in [packages/shared/config.ts](https://github.com/karakeep-app/karakeep/blob/main/packages/shared/config.ts). The most important ones are:\n\n| Name                                   | Required                              | Default         | Description                                                                                                                                                                                                                                                            |\n| -------------------------------------- | ------------------------------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| PORT                                   | No                                    | 3000            | The port on which the web server will listen. DON'T CHANGE THIS IF YOU'RE USING DOCKER, instead changed the docker bound external port.                                                                                                                                |\n| WORKERS_PORT                           | No                                    | 0 (Random Port) | The port on which the worker will export its prometheus metrics on `/metrics`. By default it's a random unused port. If you want to utilize those metrics, fix the port to a value (and export it in docker if you're using docker).                                   |\n| WORKERS_HOST                           | No                                    | 127.0.0.1       | Host to listen to for requests to WORKERS_PORT. You will need to set this if running in a container, since localhost will not be reachable from outside                                                                                                                |\n| WORKERS_ENABLED_WORKERS                | No                                    | Not set         | Comma separated list of worker names to enable. If set, only these workers will run. Valid values: crawler,inference,search,adminMaintenance,video,feed,assetPreprocessing,webhook,ruleEngine.                                                                         |\n| WORKERS_DISABLED_WORKERS               | No                                    | Not set         | Comma separated list of worker names to disable. Takes precedence over `WORKERS_ENABLED_WORKERS`.                                                                                                                                                                      |\n| LOG_LEVEL                              | No                                    | debug           | The application log level as defined in the [winston documentation](https://github.com/winstonjs/winston?tab=readme-ov-file#logging-levels). You may want to set this to `notice` or `warning` when running Karakeep in a production environment.                      |\n| DATA_DIR                               | Yes                                   | Not set         | The path for the persistent data directory. This is where the db lives. Assets are stored here by default unless `ASSETS_DIR` is set.                                                                                                                                  |\n| ASSETS_DIR                             | No                                    | Not set         | The path where crawled assets will be stored. If not set, defaults to `${DATA_DIR}/assets`.                                                                                                                                                                            |\n| NEXTAUTH_URL                           | Yes                                   | Not set         | Should point to the address of your server. The app will function without it, but will redirect you to wrong addresses on signout for example.                                                                                                                         |\n| NEXTAUTH_SECRET                        | Yes                                   | Not set         | Random string used to sign the JWT tokens. Generate one with `openssl rand -base64 36`.                                                                                                                                                                                |\n| MEILI_ADDR                             | No                                    | Not set         | The address of meilisearch. If not set, Search will be disabled. E.g. (`http://meilisearch:7700`)                                                                                                                                                                      |\n| MEILI_MASTER_KEY                       | Only in Prod and if search is enabled | Not set         | The master key configured for meilisearch. Not needed in development environment. Generate one with `openssl rand -base64 36 \\| tr -dc 'A-Za-z0-9'`                                                                                                                    |\n| MAX_ASSET_SIZE_MB                      | No                                    | 50              | Sets the maximum allowed asset size (in MB) to be uploaded                                                                                                                                                                                                             |\n| DISABLE_NEW_RELEASE_CHECK              | No                                    | false           | If set to true, latest release check will be disabled in the admin panel.                                                                                                                                                                                              |\n| RATE_LIMITING_ENABLED                  | No                                    | false           | If set to true, API rate limiting will be enabled.                                                                                                                                                                                                                     |\n| CRAWLER_DOMAIN_RATE_LIMIT_WINDOW_MS    | No                                    | Not set         | Time window in milliseconds for per-domain crawler rate limiting.                                                                                                                                                                                                      |\n| CRAWLER_DOMAIN_RATE_LIMIT_MAX_REQUESTS | No                                    | Not set         | Maximum crawler requests allowed per domain inside the configured window.                                                                                                                                                                                              |\n| DB_WAL_MODE                            | No                                    | false           | Enables WAL mode for the sqlite database. This should improve the performance of the database. There's no reason why you shouldn't set this to true unless you're running the db on a network attached drive. This will become the default at some time in the future. |\n| SEARCH_NUM_WORKERS                     | No                                    | 1               | Number of concurrent workers for search indexing tasks. Increase this if you have a high volume of content being indexed for search.                                                                                                                                   |\n| SEARCH_JOB_TIMEOUT_SEC                 | No                                    | 30              | How long to wait for a search indexing job to finish before timing out. Increase this if you have large bookmarks with extensive content that takes longer to index.                                                                                                   |\n| WEBHOOK_NUM_WORKERS                    | No                                    | 1               | Number of concurrent workers for webhook delivery. Increase this if you have multiple webhook endpoints or high webhook traffic.                                                                                                                                       |\n| ASSET_PREPROCESSING_NUM_WORKERS        | No                                    | 1               | Number of concurrent workers for asset preprocessing tasks (image processing, OCR, etc.). Increase this if you have many images or documents that need processing.                                                                                                     |\n| ASSET_PREPROCESSING_JOB_TIMEOUT_SEC    | No                                    | 60              | How long to wait for an asset preprocessing job to finish before timing out. Increase this if you have large images or PDFs that take longer to process.                                                                                                               |\n| RULE_ENGINE_NUM_WORKERS                | No                                    | 1               | Number of concurrent workers for rule engine processing. Increase this if you have complex automation rules that need to be processed quickly.                                                                                                                         |\n| MAX_RSS_FEEDS_PER_USER                 | No                                    | 1000            | The maximum number of RSS feeds a user can create.                                                                                                                                                                                                                     |\n| MAX_WEBHOOKS_PER_USER                  | No                                    | 100             | The maximum number of webhooks a user can create.                                                                                                                                                                                                                      |\n\n## Asset Storage\n\nKarakeep supports two storage backends for assets: local filesystem (default) and S3-compatible object storage. S3 storage is automatically detected when an S3 endpoint is passed.\n\n| Name                             | Required          | Default | Description                                                                                               |\n| -------------------------------- | ----------------- | ------- | --------------------------------------------------------------------------------------------------------- |\n| ASSET_STORE_S3_ENDPOINT          | No                | Not set | The S3 endpoint URL. Required for S3-compatible services like MinIO. **Setting this enables S3 storage**. |\n| ASSET_STORE_S3_REGION            | No                | Not set | The S3 region to use.                                                                                     |\n| ASSET_STORE_S3_BUCKET            | Yes when using S3 | Not set | The S3 bucket name where assets will be stored.                                                           |\n| ASSET_STORE_S3_ACCESS_KEY_ID     | Yes when using S3 | Not set | The S3 access key ID for authentication.                                                                  |\n| ASSET_STORE_S3_SECRET_ACCESS_KEY | Yes when using S3 | Not set | The S3 secret access key for authentication.                                                              |\n| ASSET_STORE_S3_FORCE_PATH_STYLE  | No                | false   | Whether to force path-style URLs for S3 requests. Set to true for MinIO and other S3-compatible services. |\n\n:::info\nWhen using S3 storage, make sure the bucket exists and the provided credentials have the necessary permissions to read, write, and delete objects in the bucket.\n:::\n\n:::warning\nSwitching between storage backends after data has been stored will require manual migration of existing assets. Plan your storage backend choice carefully before deploying.\n:::\n\n## Authentication / Signup\n\nBy default, Karakeep uses the database to store users, but it is possible to also use OAuth.\nThe flags need to be provided to the `web` container.\n\n:::info\nOnly OIDC compliant OAuth providers are supported! For information on how to set it up, consult the documentation of your provider.\n:::\n\n:::info\nWhen setting up OAuth, the allowed redirect URLs configured at the provider should be set to `<KARAKEEP_ADDRESS>/api/auth/callback/custom` where `<KARAKEEP_ADDRESS>` is the address you configured in `NEXTAUTH_URL` (for example: `https://try.karakeep.app/api/auth/callback/custom`).\n:::\n\n| Name                                        | Required | Default                | Description                                                                                                                                                                                           |\n| ------------------------------------------- | -------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| DISABLE_SIGNUPS                             | No       | false                  | If enabled, no new signups will be allowed and the signup button will be disabled in the UI                                                                                                           |\n| DISABLE_PASSWORD_AUTH                       | No       | false                  | If enabled, only signups and logins using OAuth are allowed and the signup button and login form for local accounts will be disabled in the UI                                                        |\n| EMAIL_VERIFICATION_REQUIRED                 | No       | false                  | Whether email verification is required during user signup. If enabled, users must verify their email address before they can use their account. If you enable this, you must configure SMTP settings. |\n| OAUTH_AUTO_REDIRECT                         | No       | false                  | If enabled and password authentication is disabled, automatically redirect to the OAuth provider instead of showing the login page. Useful when OAuth is the only authentication method available.     |\n| OAUTH_WELLKNOWN_URL                         | No       | Not set                | The \"wellknown Url\" for openid-configuration as provided by the OAuth provider                                                                                                                        |\n| OAUTH_CLIENT_SECRET                         | No       | Not set                | The \"Client Secret\" as provided by the OAuth provider                                                                                                                                                 |\n| OAUTH_CLIENT_ID                             | No       | Not set                | The \"Client ID\" as provided by the OAuth provider                                                                                                                                                     |\n| OAUTH_SCOPE                                 | No       | \"openid email profile\" | \"Full list of scopes to request (space delimited)\"                                                                                                                                                    |\n| OAUTH_PROVIDER_NAME                         | No       | \"Custom Provider\"      | The name of your provider. Will be shown on the signup page as \"Sign in with `<name>`\"                                                                                                                |\n| OAUTH_ALLOW_DANGEROUS_EMAIL_ACCOUNT_LINKING | No       | false                  | Whether existing accounts in karakeep stored in the database should automatically be linked with your OAuth account. Only enable it if you trust the OAuth provider!                                  |\n| OAUTH_TIMEOUT                               | No       | 3500                   | The wait time in milliseconds for the OAuth provider response. Increase this if you are having `outgoing request timed out` errors                                                                    |\n\nFor more information on `OAUTH_ALLOW_DANGEROUS_EMAIL_ACCOUNT_LINKING`, check the [next-auth.js documentation](https://next-auth.js.org/configuration/providers/oauth#allowdangerousemailaccountlinking-option).\n\n## Inference Configs (For automatic tagging)\n\nEither `OPENAI_API_KEY` or `OLLAMA_BASE_URL` need to be set for automatic tagging to be enabled. Otherwise, automatic tagging will be skipped.\n\n:::warning\n\n- The quality of the tags you'll get will depend on the quality of the model you choose.\n- You might want to tune the `INFERENCE_CONTEXT_LENGTH` as the default is quite small. The larger the value, the better the quality of the tags, but the more expensive the inference will be (money-wise on OpenAI and resource-wise on ollama).\n  :::\n\n| Name                                 | Required | Default                | Description                                                                                                                                                                                                                                                                                                                                                                           |\n| ------------------------------------ | -------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| OPENAI_API_KEY                       | No       | Not set                | The OpenAI key used for automatic tagging. More on that in [here](../06-administration/03-openai.md).                                                                                                                                                                                                                                                                                            |\n| OPENAI_BASE_URL                      | No       | Not set                | If you just want to use OpenAI you don't need to pass this variable. If, however, you want to use some other openai compatible API (e.g. azure openai service), set this to the url of the API.                                                                                                                                                                                       |\n| OPENAI_PROXY_URL                     | No       | Not set                | HTTP proxy server URL for OpenAI API requests (e.g., `http://proxy.example.com:8080`).                                                                                                                                                                                                                                                                                                |\n| OPENAI_SERVICE_TIER                  | No       | Not set                | Set to `auto`, `default`, or `flex`. Flex processing provides lower costs in exchange for slower response times and occasional resource unavailability. See [OpenAI Flex Processing](https://platform.openai.com/docs/guides/flex-processing) and [Chat Service Tier](https://platform.openai.com/docs/api-reference/chat/object#chat-object-service_tier) for more details.          |\n| OLLAMA_BASE_URL                      | No       | Not set                | If you want to use ollama for local inference, set the address of ollama API here.                                                                                                                                                                                                                                                                                                    |\n| OLLAMA_KEEP_ALIVE                    | No       | Not set                | Controls how long the model will stay loaded into memory following the request (examples: \"5m\" for 5 minutes, \"-1m\" to keep the model loaded indefinitely, \"0\" to unload the model instantly after processing is complete).                                                                                                                                                                                                                                                                                 |\n| INFERENCE_TEXT_MODEL                 | No       | gpt-4.1-mini           | The model to use for text inference. You'll need to change this to some other model if you're using ollama.                                                                                                                                                                                                                                                                           |\n| INFERENCE_IMAGE_MODEL                | No       | gpt-4o-mini            | The model to use for image inference. You'll need to change this to some other model if you're using ollama and that model needs to support vision APIs (e.g. llava).                                                                                                                                                                                                                 |\n| EMBEDDING_TEXT_MODEL                 | No       | text-embedding-3-small | The model to be used for generating embeddings for the text.                                                                                                                                                                                                                                                                                                                          |\n| INFERENCE_CONTEXT_LENGTH             | No       | 2048                   | The max number of tokens that we'll pass to the inference model. If your content is larger than this size, it'll be truncated to fit. The larger this value, the more of the content will be used in tag inference, but the more expensive the inference will be (money-wise on openAI and resource-wise on ollama). Check the model you're using for its max supported content size. |\n| INFERENCE_MAX_OUTPUT_TOKENS          | No       | 2048                   | The maximum number of tokens that the inference model is allowed to generate in its response. This controls the length of AI-generated content like tags and summaries. Increase this if you need longer responses, but be aware that higher values will increase costs (for OpenAI) and processing time.                                                                             |\n| INFERENCE_USE_MAX_COMPLETION_TOKENS  | No       | false                  | \\[OpenAI Only\\] Whether to use the newer `max_completion_tokens` parameter instead of the deprecated `max_tokens` parameter. Set to `true` if using GPT-5 or o-series models which require this. Will become the default in a future release.                                                                                                                                         |\n| INFERENCE_LANG                       | No       | english                | The language in which the tags will be generated.                                                                                                                                                                                                                                                                                                                                     |\n| INFERENCE_NUM_WORKERS                | No       | 1                      | Number of concurrent workers for AI inference tasks (tagging and summarization). Increase this if you have multiple AI inference requests and want to process them in parallel.                                                                                                                                                                                                       |\n| INFERENCE_ENABLE_AUTO_TAGGING        | No       | true                   | Whether automatic AI tagging is enabled or disabled.                                                                                                                                                                                                                                                                                                                                  |\n| INFERENCE_ENABLE_AUTO_SUMMARIZATION  | No       | false                  | Whether automatic AI summarization is enabled or disabled.                                                                                                                                                                                                                                                                                                                            |\n| INFERENCE_JOB_TIMEOUT_SEC            | No       | 30                     | How long to wait for the inference job to finish before timing out. If you're running ollama without powerful GPUs, you might want to increase the timeout a bit.                                                                                                                                                                                                                     |\n| INFERENCE_FETCH_TIMEOUT_SEC          | No       | 300                    | \\[Ollama Only\\] The timeout of the fetch request to the ollama server. If your inference requests take longer than the default 5mins, you might want to increase this timeout.                                                                                                                                                                                                        |\n| INFERENCE_SUPPORTS_STRUCTURED_OUTPUT | No       | Not set                | \\[DEPRECATED\\] Whether the inference model supports structured output or not. Use INFERENCE_OUTPUT_SCHEMA instead. Setting this to true translates to INFERENCE_OUTPUT_SCHEMA=structured, and to false translates to INFERENCE_OUTPUT_SCHEMA=plain.                                                                                                                                   |\n| INFERENCE_OUTPUT_SCHEMA              | No       | structured             | Possible values are \"structured\", \"json\", \"plain\". Structured is the preferred option, but if your model doesn't support it, you can use \"json\" if your model supports JSON mode, otherwise \"plain\" which should be supported by all the models but the model might not output the data in the correct format.                                                                        |\n\n:::info\n\n- You can append additional instructions to the prompt used for automatic tagging, in the `AI Settings` (in the `User Settings` screen)\n- You can use the placeholders `$tags`, `$aiTags`, `$userTags` in the prompt. These placeholders will be replaced with all tags, ai generated tags or human created tags when automatic tagging is performed (e.g. `[karakeep, computer, ai]`)\n  :::\n\n## Crawler Configs\n\n| Name                                     | Required | Default   | Description                                                                                                                                                                                                                                                                                                                                                                   |\n| ---------------------------------------- | -------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| CRAWLER_NUM_WORKERS                      | No       | 1         | Number of allowed concurrent crawling jobs. By default, we're only doing one crawling request at a time to avoid consuming a lot of resources.                                                                                                                                                                                                                                |\n| BROWSER_WEB_URL                          | No       | Not set   | The browser's http debugging address. The worker will talk to this endpoint to resolve the debugging console's websocket address. If you already have the websocket address, use `BROWSER_WEBSOCKET_URL` instead. If neither `BROWSER_WEB_URL` nor `BROWSER_WEBSOCKET_URL` are set, the worker will use plain http requests skipping screenshotting and javascript execution. |\n| BROWSER_WEBSOCKET_URL                    | No       | Not set   | The websocket address of browser's debugging console. If you want to use [browserless](https://browserless.io), use their websocket address here. If neither `BROWSER_WEB_URL` nor `BROWSER_WEBSOCKET_URL` are set, the worker will use plain http requests skipping screenshotting and javascript execution.                                                                 |\n| BROWSER_CONNECT_ONDEMAND                 | No       | false     | If set to false, the crawler will proactively connect to the browser instance and always maintain an active connection. If set to true, the browser will be launched on demand only whenever a crawling is requested. Set to true if you're using a service that provides you with browser instances on demand.                                                               |\n| CRAWLER_DOWNLOAD_BANNER_IMAGE            | No       | true      | Whether to cache the banner image used in the cards locally or fetch it each time directly from the website. Caching it consumes more storage space, but is more resilient against link rot and rate limits from websites.                                                                                                                                                    |\n| CRAWLER_STORE_SCREENSHOT                 | No       | true      | Whether to store a screenshot from the crawled website or not. Screenshots act as a fallback for when we fail to extract an image from a website. You can also view the stored screenshots for any link.                                                                                                                                                                      |\n| CRAWLER_FULL_PAGE_SCREENSHOT             | No       | false     | Whether to store a screenshot of the full page or not. Disabled by default, as it can lead to much higher disk usage. If disabled, the screenshot will only include the visible part of the page                                                                                                                                                                              |\n| CRAWLER_SCREENSHOT_TIMEOUT_SEC           | No       | 5         | How long to wait for the screenshot finish before timing out. If you are capturing full-page screenshots of long webpages, consider increasing this value.                                                                                                                                                                                                                    |\n| CRAWLER_STORE_PDF                        | No       | false     | Whether to store a PDF snapshot of the crawled page. Disabled by default, as it can lead to much higher disk usage. When enabled, a PDF version of each crawled page will be captured and stored as an asset, which can be viewed in the bookmark preview.                                                                                                                    |\n| CRAWLER_FULL_PAGE_ARCHIVE                | No       | false     | Whether to store a full local copy of the page or not. Disabled by default, as it can lead to much higher disk usage. If disabled, only the readable text of the page is archived.                                                                                                                                                                                            |\n| CRAWLER_JOB_TIMEOUT_SEC                  | No       | 60        | How long to wait for the crawler job to finish before timing out. If you have a slow internet connection or a low powered device, you might want to bump this up a bit                                                                                                                                                                                                        |\n| CRAWLER_NAVIGATE_TIMEOUT_SEC             | No       | 30        | How long to spend navigating to the page (along with its redirects). Increase this if you have a slow internet connection                                                                                                                                                                                                                                                     |\n| CRAWLER_PARSE_TIMEOUT_SEC                | No       | 60        | How long to wait for the HTML parsing subprocess (metadata extraction + readability) to finish before timing out.                                                                                                                                                                                                                                                              |\n| CRAWLER_PARSER_MEM_LIMIT_MB              | No       | 512       | Maximum heap size in MB for the HTML parsing subprocess. If the parser OOMs on large pages, increase this value. The subprocess is isolated so an OOM won't crash the main worker.                                                                                                                                                                                             |\n| CRAWLER_VIDEO_DOWNLOAD                   | No       | false     | Whether to download videos from the page or not (using yt-dlp)                                                                                                                                                                                                                                                                                                                |\n| CRAWLER_VIDEO_DOWNLOAD_MAX_SIZE          | No       | 50        | The maximum file size for the downloaded video. The quality will be chosen accordingly. Use -1 to disable the limit.                                                                                                                                                                                                                                                          |\n| CRAWLER_VIDEO_DOWNLOAD_TIMEOUT_SEC       | No       | 600       | How long to wait for the video download to finish                                                                                                                                                                                                                                                                                                                             |\n| CRAWLER_ENABLE_ADBLOCKER                 | No       | true      | Whether to enable an adblocker in the crawler or not. If you're facing troubles downloading the adblocking lists on worker startup, you can disable this.                                                                                                                                                                                                                     |\n| CRAWLER_YTDLP_ARGS                       | No       | []        | Include additional yt-dlp arguments to be passed at crawl time separated by %%: https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#general-options                                                                                                                                                                                                                           |\n| BROWSER_COOKIE_PATH                      | No       | Not set   | Path to a JSON file containing cookies to be loaded into the browser context. The file should be an array of cookie objects, each with name and value (required), and optional fields like domain, path, expires, httpOnly, secure, and sameSite (e.g., `[{\"name\": \"session\", \"value\": \"xxx\", \"domain\": \".example.com\"}`]).                                                   |\n| HTML_CONTENT_SIZE_INLINE_THRESHOLD_BYTES | No       | 5 \\* 1024 | The thresholds in bytes after which larger assets will be stored in the assetdb (folder/s3) instead of inline in the database.                                                                                                                                                                                                                                                |\n\n<details>\n\n  <summary>More info on BROWSER_COOKIE_PATH</summary>\n\nBROWSER_COOKIE_PATH specifies the path to a JSON file containing cookies to be loaded into the browser context for crawling.\n\nThe JSON file must be an array of cookie objects, each with:\n\n- name: The cookie name (required).\n- value: The cookie value (required).\n- Optional fields: domain, path, expires, httpOnly, secure, sameSite (values: \"Strict\", \"Lax\", or \"None\").\n\nExample JSON file:\n\n```json\n[\n  {\n    \"name\": \"session\",\n    \"value\": \"xxx\",\n    \"domain\": \".example.com\",\n    \"path\": \"/\",\n    \"expires\": 1735689600,\n    \"httpOnly\": true,\n    \"secure\": true,\n    \"sameSite\": \"Lax\"\n  }\n]\n```\n\n</details>\n\n## OCR Configs\n\nKarakeep uses [tesseract.js](https://github.com/naptha/tesseract.js) to extract text from images by default. Alternatively, you can use an LLM-based OCR by enabling the `OCR_USE_LLM` flag. LLM-based OCR uses the configured inference model (OpenAI or Ollama) to extract text from images, which can provide better results for complex images but requires a configured inference provider.\n\n| Name                     | Required | Default   | Description                                                                                                                                                                                                                               |\n| ------------------------ | -------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| OCR_CACHE_DIR            | No       | $TEMP_DIR | The dir where tesseract will download its models. By default, those models are not persisted and stored in the OS' temp dir.                                                                                                              |\n| OCR_LANGS                | No       | eng       | Comma separated list of the language codes that you want tesseract to support. You can find the language codes [here](https://tesseract-ocr.github.io/tessdoc/Data-Files-in-different-versions.html). Set to empty string to disable OCR. |\n| OCR_CONFIDENCE_THRESHOLD | No       | 50        | A number between 0 and 100 indicating the minimum acceptable confidence from tessaract. If tessaract's confidence is lower than this value, extracted text won't be stored.                                                               |\n| OCR_USE_LLM              | No       | false     | If set to true, uses the configured inference model (OpenAI or Ollama) for OCR instead of Tesseract. This can provide better results for complex images but requires a configured inference provider (`OPENAI_API_KEY` or `OLLAMA_BASE_URL`). Falls back to Tesseract if no inference provider is configured. |\n\n## Webhook Configs\n\nYou can use webhooks to trigger actions when bookmarks are created, changed or crawled.\n\n| Name                | Required | Default | Description                                       |\n| ------------------- | -------- | ------- | ------------------------------------------------- |\n| WEBHOOK_TIMEOUT_SEC | No       | 5       | The timeout for the webhook request in seconds.   |\n| WEBHOOK_RETRY_TIMES | No       | 3       | The number of times to retry the webhook request. |\n\n:::info\n\n- The WEBHOOK_TOKEN is used for authentication. It will appear in the Authorization header as Bearer token.\n  ```\n  Authorization: Bearer <WEBHOOK_TOKEN>\n  ```\n- The webhook will be triggered with the job id (used for idempotence), bookmark id, bookmark type, the user id, the url and the operation in JSON format in the body.\n\n  ```json\n  {\n    \"jobId\": \"123\",\n    \"type\": \"link\",\n    \"bookmarkId\": \"exampleBookmarkId\",\n    \"userId\": \"exampleUserId\",\n    \"url\": \"https://example.com\",\n    \"operation\": \"crawled\"\n  }\n  ```\n\n  :::\n\n## SMTP Configuration\n\nKarakeep can send emails for various purposes such as email verification during signup. Configure these settings to enable email functionality.\n\n| Name          | Required | Default | Description                                                                                     |\n| ------------- | -------- | ------- | ----------------------------------------------------------------------------------------------- |\n| SMTP_HOST     | No       | Not set | The SMTP server hostname or IP address. Required if you want to enable email functionality.     |\n| SMTP_PORT     | No       | 587     | The SMTP server port. Common values are 587 (STARTTLS), 465 (SSL/TLS), or 25 (unencrypted).     |\n| SMTP_SECURE   | No       | false   | Whether to use SSL/TLS encryption. Set to true for port 465, false for port 587 with STARTTLS.  |\n| SMTP_USER     | No       | Not set | The username for SMTP authentication. Usually your email address.                               |\n| SMTP_PASSWORD | No       | Not set | The password for SMTP authentication. For services like Gmail, use an app-specific password.    |\n| SMTP_FROM     | No       | Not set | The \"from\" email address that will appear in sent emails. This should be a valid email address. |\n\n## Proxy Configuration\n\nIf your Karakeep instance needs to connect through a proxy server, you can configure the following settings:\n\n| Name                               | Required | Default | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |\n| ---------------------------------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| CRAWLER_HTTP_PROXY                 | No       | Not set | HTTP proxy server URL for outgoing HTTP requests (e.g., `http://proxy.example.com:8080`). You can pass multiple comma separated proxies and the used one will be chosen at random. The proxy is used for crawling, RSS feed fetches and webhooks.                                                                                                                                                                                                                                                                           |\n| CRAWLER_HTTPS_PROXY                | No       | Not set | HTTPS proxy server URL for outgoing HTTPS requests (e.g., `http://proxy.example.com:8080`). You can pass multiple comma separated proxies and the used one will be chosen at random. The proxy is used for crawling, RSS feed fetches and webhooks.                                                                                                                                                                                                                                                                         |\n| CRAWLER_NO_PROXY                   | No       | Not set | Comma-separated list of hostnames/IPs that should bypass the proxy (e.g., `localhost,127.0.0.1,.local`)                                                                                                                                                                                                                                                                                                                                                                                                                     |\n| CRAWLER_ALLOWED_INTERNAL_HOSTNAMES | No       | Not set | By default, Karakeep blocks worker-initiated requests whose DNS resolves to private, loopback, link-local, or Tailscale CGNAT IP addresses. Use this to allowlist specific hostnames for internal access (e.g., `internal.company.com`, `app-name.local`). Supports domain wildcards by prefixing with a dot (e.g., `.internal.company.com`, `.<tailnet-name>.ts.net`). Passing `.` allowlists all domains. Note: Internal IP validation is bypassed when a proxy is configured for the URL as the local DNS resolver won't necessarily be the same as the one used by the proxy. |\n\n:::info\nThese proxy settings will be used by the crawler and other components that make outgoing HTTP requests.\n:::\n\n## Monitoring\n\nKarakeep supports distributed tracing via OpenTelemetry. When enabled, traces are collected for tRPC API calls, background worker operations, and other key workflows. Karakeep also exports prometheus-based metrics.\n\n| Name                        | Required | Default  | Description                                                                                                                                                                                                                                                                                                             |\n| --------------------------- | -------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| OTEL_TRACING_ENABLED        | No       | false    | Set to `true` to enable OpenTelemetry tracing. When disabled, all tracing operations are no-ops.                                                                                                                                                                                                                        |\n| OTEL_EXPORTER_OTLP_ENDPOINT | No       | Not set  | The OTLP HTTP endpoint to send traces to (e.g., `http://jaeger:4318/v1/traces` or `http://otel-collector:4318/v1/traces`). If not set, traces are logged to the console.                                                                                                                                                |\n| OTEL_SERVICE_NAME           | No       | karakeep | The service name that will appear in your tracing backend. The actual service name will include a suffix (e.g., `karakeep-api`, `karakeep-workers`).                                                                                                                                                                    |\n| OTEL_SAMPLE_RATE            | No       | 1.0      | The sampling rate for traces, between 0.0 and 1.0. A value of 1.0 means all traces are sampled, while 0.1 means only 10% of traces are sampled. Lower values reduce overhead and storage costs in production.                                                                                                           |\n| PROMETHEUS_AUTH_TOKEN       | No       | Random   | Enable a prometheus metrics endpoint at `/api/metrics`. This endpoint will require this token being passed in the Authorization header as a Bearer token. If not set, a new random token is generated everytime at startup. This cannot contain any special characters or you may encounter a 400 Bad Request response. |\n"
  },
  {
    "path": "docs/docs/03-configuration/02-different-ai-providers.md",
    "content": "# Configuring different AI Providers\n\nKarakeep uses LLM providers for AI tagging and summarization. We support OpenAI-compatible providers and ollama. This guide will show you how to configure different providers.\n\n## OpenAI\n\nIf you want to use OpenAI itself, you just need to pass in the OPENAI_API_KEY environment variable.\n\n```\nOPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\n# You can change the default models by uncommenting the following lines, and choosing your model.\n# INFERENCE_TEXT_MODEL=gpt-4.1-mini\n# INFERENCE_IMAGE_MODEL=gpt-4o-mini\n```\n\n## Ollama\n\nOllama is a local LLM provider that you can use to run your own LLM server. You'll need to pass ollama's address to karakeep and you need to ensure that it's accessible from within the karakeep container (e.g. no localhost addresses).\n\nOllama provides two API endpoints:\n\n1. **OpenAI-compatible API (Recommended)** - Uses the `/v1` chat endpoint which handles message formatting automatically\n2. **Native Ollama API** - Requires manual formatting for some models\n\n### Option 1: OpenAI-compatible API (Recommended)\n\nThis approach uses Ollama's OpenAI-compatible endpoint and is more reliable with various models:\n\n```\nOPENAI_API_KEY=ollama\nOPENAI_BASE_URL=http://ollama.mylab.com:11434/v1\n\n# Make sure to pull the models in ollama first. Example models:\nINFERENCE_TEXT_MODEL=gemma3\nINFERENCE_IMAGE_MODEL=llava\n```\n\n### Option 2: Native Ollama API\n\nAlternatively, you can use the native Ollama API:\n\n```\n# MAKE SURE YOU DON'T HAVE OPENAI_API_KEY set, otherwise it takes precedence.\n\nOLLAMA_BASE_URL=http://ollama.mylab.com:11434\n\n# Make sure to pull the models in ollama first. Example models:\nINFERENCE_TEXT_MODEL=gemma3\nINFERENCE_IMAGE_MODEL=llava\n\n# If the model you're using doesn't support structured output, you also need:\n# INFERENCE_OUTPUT_SCHEMA=plain\n```\n\n:::tip\nIf you experience issues with certain models (especially OpenAI's gpt-oss models or other models requiring specific chat formats), try using the OpenAI-compatible API endpoint instead.\n:::\n\n## Gemini\n\nGemini has an OpenAI-compatible API. You need to get an api key from Google AI Studio. You also need to set up a billing account, even if using the free tier.\n\n```\n\nOPENAI_BASE_URL=https://generativelanguage.googleapis.com/v1beta\nOPENAI_API_KEY=YOUR_API_KEY\n\n# Example models:\nINFERENCE_TEXT_MODEL=gemini-2.5-flash-lite\nINFERENCE_IMAGE_MODEL=gemini-2.5-flash-lite\n```\n\n## OpenRouter\n\n```\nOPENAI_BASE_URL=https://openrouter.ai/api/v1\nOPENAI_API_KEY=YOUR_API_KEY\n\n# Example models:\nINFERENCE_TEXT_MODEL=meta-llama/llama-4-scout\nINFERENCE_IMAGE_MODEL=meta-llama/llama-4-scout\n```\n\n## Perplexity\n\n```\nOPENAI_BASE_URL: https://api.perplexity.ai\nOPENAI_API_KEY: Your Perplexity API Key\nINFERENCE_TEXT_MODEL: sonar-pro\nINFERENCE_IMAGE_MODEL: sonar-pro\n```\n\n## Azure\n\nAzure has an OpenAI-compatible API.\n\nYou can get your API key from the Overview page of the Azure AI Foundry Portal or via \"Keys + Endpoints\" on the resource in the Azure Portal.\n\n:::warning\nThe [model name is the deployment name](https://learn.microsoft.com/en-us/azure/ai-foundry/openai/how-to/switching-endpoints#keyword-argument-for-model) you specified when deploying the model, which may differ from the base model name.\n:::\n\n```\n# Deployed via Azure AI Foundry:\nOPENAI_BASE_URL=https://{your-azure-ai-foundry-resource-name}.cognitiveservices.azure.com/openai/v1/\n\n# Deployed via Azure OpenAI Service:\nOPENAI_BASE_URL=https://{your-azure-openai-resource-name}.openai.azure.com/openai/v1/\n\nOPENAI_API_KEY=YOUR_API_KEY\nINFERENCE_TEXT_MODEL=YOUR_DEPLOYMENT_NAME\nINFERENCE_IMAGE_MODEL=YOUR_DEPLOYMENT_NAME\n```\n\n## Cloudflare\n\nCloudflare supports OpenAI compatible endpoints. You can generate an API Token from the Cloudflare dashboard (Workers AI).\n\n```\nOPENAI_BASE_URL=https://api.cloudflare.com/client/v4/accounts/{your-account-id}/ai/v1\nOPENAI_API_KEY=Your Cloudflare Workers AI Token\n\n# Example models:\nINFERENCE_TEXT_MODEL=@cf/meta/llama-3.1-8b-instruct-fast\nINFERENCE_IMAGE_MODEL=@cf/meta/llama-3.2-11b-vision-instruct\nINFERENCE_OUTPUT_SCHEMA=json\n```\n"
  },
  {
    "path": "docs/docs/03-configuration/_category_.json",
    "content": "{\n  \"label\": \"Configuration\",\n  \"position\": 4\n}\n"
  },
  {
    "path": "docs/docs/04-using-karakeep/_category_.json",
    "content": "{\n  \"label\": \"Using Karakeep\",\n  \"position\": 2\n}\n"
  },
  {
    "path": "docs/docs/04-using-karakeep/advanced-workflows.md",
    "content": "---\nsidebar_position: 10\nslug: advanced-workflows\n---\n\n# Advanced workflows\n\nPush Karakeep further with automation and integrations.\n\n## Rule engine\n\n- Create if-this-then-that style rules to auto-tag, favourite, or route bookmarks into lists based on metadata or content.\n- Useful for keeping inboxes tidy (e.g. auto-archive newsletters, auto-tag domains, or flag videos).\n\n## API\n\n- Use the API to script imports, syncs, or custom tools. Same surface area the apps use.\n- Great for integrating with personal scripts, cron jobs, or other services.\n\n## Webhooks\n\n- Subscribe to bookmark events and trigger your own systems when something is added, updated, or archived.\n- Pair with the API to build end-to-end automations (e.g. push new saves into a writing queue or a team chat).\n"
  },
  {
    "path": "docs/docs/04-using-karakeep/bookmarking.md",
    "content": "---\nsidebar_position: 1\nslug: bookmarking\n---\n\n# Bookmarking\n\nEverything in Karakeep starts as a bookmark. Here’s how the different types work and how to keep your home view tidy with favourites and archive.\n\n## Favourites\n\n- Star bookmarks you like so they sit in their own dedicated favourites view for quick return visits.\n- Handy for saved gems you want to re-open often like articles you enjoyed, references you come back to, or things worth sharing.\n\n## Archiving\n\n- Archive hides a bookmark from the homepage without deleting it.\n- Archived items stay searchable and keep all tags, highlights, and attachments.\n- Ideal for achieving inbox-zero style for your homepage.\n\n## Bookmark types\n\n- **Links**: URLs saved from the web or extension. Karakeep grabs metadata, previews, screenshots, and archives when configured.\n- **Text**: Quick notes or snippets you paste in. Great for ideas, quotes, or saving context alongside links.\n- **Media**: Images or PDFs you want to save for later. Karakeep automatically extracts content out of those files and makes them searchable.\n\n## Notes\n\n- Attach personal notes to any bookmark to capture context, reminders, or next steps.\n- Notes live with the bookmark and are searchable, so you can recall why something mattered.\n\n## Highlights\n\n- Save quotes, summaries, or TODOs while reading.\n- Highlights show up in the bookmark detail view/reader and are searchable, so you can jump straight to the key ideas.\n\n## Attachments\n\n- Store extra context alongside a bookmark: screenshots, page captures, videos, and files you upload.\n- **Screenshots & archives**: fallback when the original page changes or disappear.\n- **Uploaded files**: keep PDFs, notes, or supporting assets right with the link.\n- Manage attachments from the bookmark detail view: upload, download, or detach as needed.\n"
  },
  {
    "path": "docs/docs/04-using-karakeep/import.md",
    "content": "---\nsidebar_position: 8\nslug: import\n---\n\n# Import your library\n\n\nKarakeep supports importing bookmarks using the Netscape HTML Format, Pocket's new CSV format & Omnivore's JSONs. Titles, tags and addition date will be preserved during the import. An automatically created list will contain all the imported bookmarks.\n\n:::info\nAll the URLs in the bookmarks file will be added automatically, you will not be able to pick and choose which bookmarks to import!\n:::\n\n## Import from Chrome\n\n- Open Chrome and go to `chrome://bookmarks`\n- Click on the three dots on the top right corner and choose `Export bookmarks`\n- This will download an html file with all of your bookmarks.\n- To import the bookmark file, go to the settings and click \"Import Bookmarks from HTML file\".\n\n## Import from Firefox\n- Open Firefox and click on the menu button (☰) in the top right corner.\n- Navigate to Bookmarks > Manage bookmarks (or press Ctrl + Shift + O / Cmd + Shift + O to open the Bookmarks Library).\n- In the Bookmarks Library, click the Import and Backup button at the top. Select Export Bookmarks to HTML... to save your bookmarks as an HTML file.\n- To import a bookmark file, go back to the Import and Backup menu, then select Import Bookmarks from HTML... and choose your saved HTML file.\n\n## Import from Pocket\n\n- Go to the [Pocket export page](https://getpocket.com/export) and follow the instructions to export your bookmarks.\n- Pocket after a couple of minutes will mail you a zip file with all the bookmarks.\n- Unzip the file and you'll get a CSV file.\n- To import the bookmark file, go to the settings and click \"Import Bookmarks from Pocket export\".\n\n## Import from Omnivore\n\n- Follow Omnivore's [documentation](https://docs.omnivore.app/using/exporting.html) to export your bookmarks.\n- This will give you a zip file with all your data.\n- The zip file contains a lot of JSONs in the format `metadata_*.json`. You can either import every JSON file manually, or merge the JSONs into a single JSON file and import that.\n- To  merge the JSONs into a single JSON file, you can use the following command in the unzipped directory: `jq -r '.[]' metadata_*.json | jq -s > omnivore.json` and then import the `omnivore.json` file. You'll need to have the [jq](https://github.com/jqlang/jq) tool installed.\n\n## Import using the CLI\n\n:::warning\nImporting bookmarks using the CLI requires some technical knowledge and might not be very straightforward for non-technical users. Don't hesitate to ask questions in github discussions or discord though.\n:::\n\nIf you can get your bookmarks in a text file with one link per line, you can use the following command to import them using the [karakeep cli](../05-integrations/02-command-line.md):\n\n```\nwhile IFS= read -r url; do\n    karakeep --api-key \"<KEY>\" --server-addr \"<SERVER_ADDR>\" bookmarks add --link \"$url\"\ndone < all_links.txt\n```\n"
  },
  {
    "path": "docs/docs/04-using-karakeep/lists.md",
    "content": "---\nsidebar_position: 2\n---\n\n# Lists\n\nLists are the core organizational layer in Karakeep. Every saved item can sit in multiple lists so you can group links by project, topic, or audience without duplicating them.\n\n## Manual lists\n\n- Curated sets you add bookmarks to by hand. Great for projects, reading queues, or hand-picked collections.\n- Can be **private** (visible only to you) or **public** (share a read-only link).\n- Can be **collaborative**: invite people by email as viewers or editors. Editors can add their own bookmarks; viewers can browse. Your personal states (favourite/archive) stay yours even inside a shared list.\n\n## Smart lists\n\n- Auto-updating lists powered by a saved search query (e.g. `#ai -archived`).\n- Best for dynamic views like `Youtube links added last week` or `All reddit links from r/selfhosted`.\n"
  },
  {
    "path": "docs/docs/04-using-karakeep/quick-sharing.md",
    "content": "---\nsidebar_position: 7\nslug: quick-sharing\n---\n\n# Quick capture and sharing\n\nThe whole point of Karakeep is making it easy to hoard the content. That's why there are a couple of \n\n## Mobile Apps\n\n<img src=\"/img/quick-sharing/mobile.png\" alt=\"mobile screenshot\" width=\"300\"/>\n\n\n- **iOS app**: [App Store Link](https://apps.apple.com/us/app/karakeep-app/id6479258022).\n- **Android App**: [Play Store link](https://play.google.com/store/apps/details?id=app.hoarder.hoardermobile&pcampaignid=web_share).\n\n## Browser Extensions\n\n<img src=\"/img/quick-sharing/extension.png\" alt=\"mobile screenshot\" width=\"300\"/>\n\n- **Chrome extension**: [here](https://chromewebstore.google.com/detail/karakeep/kgcjekpmcjjogibpjebkhaanilehneje).\n- **Firefox addon**: [here](https://addons.mozilla.org/en-US/firefox/addon/karakeep/).\n\n- ## Community Extensions\n- **Safari extension**: [App Store Link](https://apps.apple.com/us/app/karakeeper-bookmarker/id6746722790).  For macOS and iOS to allow a simple way to add your bookmarks to your self hosted karakeep instance.\n"
  },
  {
    "path": "docs/docs/04-using-karakeep/search-query-language.md",
    "content": "---\nsidebar_position: 9\nslug: search-query-language\n---\n\n# Search Query Language\n\nKarakeep provides a search query language to filter and find bookmarks. Here are all the supported qualifiers and how to use them:\n\n## Basic Syntax\n\n- Use spaces to separate multiple conditions (implicit AND)\n- Use `and`/`or` keywords for explicit boolean logic\n- Prefix qualifiers with `-` or `!` to negate them (e.g., `-is:archived` or `!is:archived`)\n- Use parentheses `()` for grouping conditions (note that groups can't be negated)\n\n## Qualifiers\n\nHere's a comprehensive table of all supported qualifiers:\n\n| Qualifier                        | Description                                                                                                                                                                                               | Example Usage                                |\n| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |\n| `is:fav`                         | Favorited bookmarks                                                                                                                                                                                       | `is:fav`                                     |\n| `is:archived`                    | Archived bookmarks                                                                                                                                                                                        | `-is:archived`                               |\n| `is:tagged`                      | Bookmarks that has one or more tags                                                                                                                                                                       | `is:tagged`                                  |\n| `is:inlist`                      | Bookmarks that are in one or more lists                                                                                                                                                                   | `is:inlist`                                  |\n| `is:link`, `is:text`, `is:media` | Bookmarks that are of type link, text or media                                                                                                                                                            | `is:link`                                    |\n| `is:broken`                      | Bookmarks with broken/failed links (crawl failures or non-2xx status codes)                                                                                                                               | `is:broken`                                  |\n| `url:<value>`                    | Match bookmarks with URL substring                                                                                                                                                                        | `url:example.com`                            |\n| `title:<value>`                  | Match bookmarks with title substring                                                                                                               | `title:example`                              |\n|                                  | Supports quoted strings for titles with spaces                                                                                                   | `title:\"my title\"`                           |\n| `#<tag>` or `tag:<tag>`          | Match bookmarks with specific tag                                                                                                                                                                         | `#important` or `tag:important`              |\n|                                  | Supports quoted strings for tags with spaces                                                                                                                                                              | `#\"work in progress\"` or `tag:\"work in progress\"` |\n| `list:<name>`                    | Match bookmarks in specific list                                                                                                                                                                          | `list:reading`                               |\n|                                  | Supports quoted strings for list names with spaces                                                                                                                                                        | `list:\"to review\"`                           |\n| `after:<date>`                   | Bookmarks created on or after date (YYYY-MM-DD)                                                                                                                                                           | `after:2023-01-01`                           |\n| `before:<date>`                  | Bookmarks created on or before date (YYYY-MM-DD)                                                                                                                                                          | `before:2023-12-31`                          |\n| `feed:<name>`                    | Bookmarks imported from a particular rss feed                                                                                                                                                             | `feed:Hackernews`                            |\n| `source:<value>`                 | Match bookmarks from a specific source. Valid values: `api`, `web`, `cli`, `mobile`, `extension`, `singlefile`, `rss`, `import`                                                                          | `source:rss` `-source:web`                   |\n| `age:<time-range>`               | Match bookmarks based on how long ago they were created. Use `<` or `>` to indicate the maximum / minimum age of the bookmarks. Supported units: `d` (days), `w` (weeks), `m` (months), `y` (years). | `age:<1d` `age:>2w` `age:<6m` `age:>3y` |\n\n### Examples\n\n```plaintext\n# Find favorited bookmarks from 2023 that are tagged \"important\"\nis:fav after:2023-01-01 before:2023-12-31 #important\n\n# Find archived bookmarks that are either in \"reading\" list or tagged \"work\"\nis:archived and (list:reading or #work)\n\n# Find bookmarks that are not tagged or not in any list\n-is:tagged or -is:inlist\n# Find bookmarks with \"React\" in the title\ntitle:React\n```\n\n## Combining Conditions\n\nYou can combine multiple conditions using boolean logic:\n\n```plaintext\n# Find favorited bookmarks from 2023 that are tagged \"important\"\nis:fav after:2023-01-01 before:2023-12-31 #important\n\n# Find archived bookmarks that are either in \"reading\" list or tagged \"work\"\nis:archived and (list:reading or #work)\n\n# Find bookmarks that are not favorited and not archived\n-is:fav -is:archived\n\n# Using ! as an alias for negation\n!is:fav !is:archived\n\n# Using tag: as an alias for #\ntag:important tag:\"work in progress\"\n```\n\n## Text Search\n\nAny text not part of a qualifier will be treated as a full-text search:\n\n```plaintext\n# Search for \"machine learning\" in bookmark content\nmachine learning\n\n# Combine text search with qualifiers\nmachine learning is:fav\n```\n"
  },
  {
    "path": "docs/docs/04-using-karakeep/tags.md",
    "content": "---\nsidebar_position: 3\n---\n\n# Tags\n\nTags are lightweight labels you can attach to any bookmark to add meaning without rigid folders.\n\n- Use tags to capture topics, sources, people, or workflow states (e.g. `ai`, `design`, `to-read`).\n- Combine multiple tags to filter or build smart lists; tags travel with a bookmark wherever it appears.\n- AI tags might look a little messy, but the extra labels make finding things easier. Use tags for broad discovery and lists when you want a clean, hand-picked setup.\n"
  },
  {
    "path": "docs/docs/05-integrations/02-command-line.md",
    "content": "# Command Line Tool (CLI)\n\nKarakeep comes with a simple CLI for those users who want to do more advanced manipulation.\n\n## Features\n\n- Manipulate bookmarks, lists and tags\n- Mass import/export of bookmarks\n\n## Installation (NPM)\n\n```\nnpm install -g @karakeep/cli\n```\n\n\n## Installation (Docker)\n\n```\ndocker run --rm ghcr.io/karakeep-app/karakeep-cli:release --help\n```\n\n## Usage\n\n```\nkarakeep\n```\n\n```\nUsage: karakeep [options] [command]\n\nA CLI interface to interact with the karakeep api\n\nOptions:\n  --api-key <key>       the API key to interact with the API (env: KARAKEEP_API_KEY)\n  --server-addr <addr>  the address of the server to connect to (env: KARAKEEP_SERVER_ADDR)\n  -V, --version         output the version number\n  -h, --help            display help for command\n\nCommands:\n  bookmarks             manipulating bookmarks\n  lists                 manipulating lists\n  tags                  manipulating tags\n  whoami                returns info about the owner of this API key\n  help [command]        display help for command\n```\n\nAnd some of the subcommands:\n\n```\nkarakeep bookmarks\n```\n\n```\nUsage: karakeep bookmarks [options] [command]\n\nManipulating bookmarks\n\nOptions:\n  -h, --help             display help for command\n\nCommands:\n  add [options]          creates a new bookmark\n  get <id>               fetch information about a bookmark\n  update [options] <id>  updates bookmark\n  list [options]         list all bookmarks\n  delete <id>            delete a bookmark\n  help [command]         display help for command\n\n```\n\n```\nkarakeep lists\n```\n\n```\nUsage: karakeep lists [options] [command]\n\nManipulating lists\n\nOptions:\n  -h, --help                 display help for command\n\nCommands:\n  list                       lists all lists\n  delete <id>                deletes a list\n  add-bookmark [options]     add a bookmark to list\n  remove-bookmark [options]  remove a bookmark from list\n  help [command]             display help for command\n```\n\n## Obtaining an API Key\n\nTo use the CLI, you'll need to get an API key from your karakeep settings. You can validate that it's working by running:\n\n```\nkarakeep --api-key <key> --server-addr <addr> whoami\n```\n\nFor example:\n\n```\nkarakeep --api-key mysupersecretkey --server-addr https://try.karakeep.app whoami\n{\n  id: 'j29gnbzxxd01q74j2lu88tnb',\n  name: 'Test User',\n  email: 'test@gmail.com'\n}\n```\n\n\n## Other clients\n\nThere also exists a **non-official**, community-maintained, python package called [karakeep-python-api](https://github.com/thiswillbeyourgithub/karakeep_python_api) that can be accessed from the CLI, but is **not** official.\n"
  },
  {
    "path": "docs/docs/05-integrations/03-mcp.md",
    "content": "# Model Context Protocol Server (MCP)\n\nKarakeep comes with a Model Context Protocol server that can be used to interact with it through LLMs.\n\n## Supported Tools\n\n- Searching bookmarks\n- Adding and removing bookmarks from lists\n- Attaching and detaching tags to bookmarks\n- Creating new lists\n- Creating text and URL bookmarks\n\n\n## Usage with Claude Desktop\n\nFrom NPM:\n\n```json\n{\n  \"mcpServers\": {\n    \"karakeep\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"@karakeep/mcp\"\n      ],\n      \"env\": {\n        \"KARAKEEP_API_ADDR\": \"https://<YOUR_SERVER_ADDR>\",\n        \"KARAKEEP_API_KEY\": \"<YOUR_TOKEN>\"\n      }\n    }\n  }\n}\n```\n\nFrom Docker:\n\n```json\n{\n  \"mcpServers\": {\n    \"karakeep\": {\n      \"command\": \"docker\",\n      \"args\": [\n        \"run\",\n        \"-e\",\n        \"KARAKEEP_API_ADDR=https://<YOUR_SERVER_ADDR>\",\n        \"-e\",\n        \"KARAKEEP_API_KEY=<YOUR_TOKEN>\",\n        \"ghcr.io/karakeep-app/karakeep-mcp:latest\"\n      ]\n    }\n  }\n}\n```\n\n\n### Demo\n\n#### Search\n![mcp-1](/img/mcp-1.gif)\n\n#### Adding Text Bookmarks\n![mcp-2](/img/mcp-2.gif)\n\n#### Adding URL Bookmarks\n![mcp-2](/img/mcp-3.gif)\n"
  },
  {
    "path": "docs/docs/05-integrations/05-singlefile.md",
    "content": "# Using Karakeep with SingleFile Extension\n\nKarakeep supports being a destination for the [SingleFile extension](https://github.com/gildas-lormeau/SingleFile). This has the benefit of allowing you to use the singlefile extension to hoard links as you're seeing them in the browser. This is perfect for websites that don't like to get crawled, has annoying cookie banner or require authentication.\n\n## Setup\n\n1. Install the [SingleFile extension](https://github.com/gildas-lormeau/SingleFile).\n2. In the extension settings, select `Destinations`.\n3. Select `upload to a REST Form API`.\n4. In the URL, insert the address: `https://YOUR_SERVER_ADDRESS/api/v1/bookmarks/singlefile`.\n5. In the `authorization token` field, paste an API key that you can get from your karakeep settings.\n6. Set `data field name` to `file`.\n7. Set `URL field name` to `url`.\n8. (Optional) Add `?ifexists=MODE` to the URL where MODE is one of `skip`, `overwrite`, `overwrite-recrawl`, `append`, or `append-recrawl`. See \"Handling Existing Bookmarks\" section below for details.\n\nNow, go to any page and click the singlefile extension icon. Once it's done with the upload, the bookmark should show up in your karakeep instance. Note that the singlefile extension doesn't show any progress on the upload. Given that archives are typically large, it might take 30+ seconds until the upload is done and starts showing up in Karakeep.\n\n## Handling Existing Bookmarks\n\nWhen uploading a page that already exists in your archive (same URL), you can control the behavior by setting the `ifexists` query parameter in the upload URL. The available modes are:\n\n- `skip` (default): If the bookmark already exists, skip creating a new one\n- `overwrite`: Replace existing precrawled archive (only the most recent archive is kept)\n- `overwrite-recrawl`: Replace existing archive and queue a recrawl to update content\n- `append`: Add new archive version alongside existing ones\n- `append-recrawl`: Add new archive and queue a recrawl\n\nTo use these modes, append `?ifexists=MODE` to your upload URL, replacing `MODE` with your desired behavior.\n\nFor example:  \n`https://YOUR_SERVER_ADDRESS/api/v1/bookmarks/singlefile?ifexists=overwrite`\n\n\n## Recommended settings\n\nIn the singlefile extension, you probably will want to change the following settings for better experience:\n* Stylesheets > compress CSS content: on\n* Stylesheets > group duplicate stylesheets together: on\n* HTML content > remove frames: on\n\nAlso, you most likely will want to change the default `MAX_ASSET_SIZE_MB` in karakeep to something higher, for example `100`.\n\n:::info\nCurrently, we don't support screenshots for singlefile uploads, but this will change in the future.\n:::\n\n"
  },
  {
    "path": "docs/docs/05-integrations/06-rss-feeds.md",
    "content": "# RSS Feeds\n\nKarakeep offers RSS feed integration, allowing you to both consume RSS feeds from external sources and publish your lists as RSS feeds for others to subscribe to.\n\n## Publishing RSS Feeds\n\nYou can publish any of your lists as an RSS feed, making it easy to share your bookmarks with others or integrate them into RSS readers.\n\n### Enabling RSS for a List\n\n1. Navigate to one of your lists\n2. Click on the list settings (three dots menu)\n3. Toggle the \"RSS Feed\" switch to enable it\n4. Copy the generated RSS feed URL\n\n### What Gets Published\n\nRSS feeds include:\n- **Links**: Bookmarks of type \"link\" with their URL, title, description, and author\n- **Assets**: Uploaded files (PDFs, images) are included with a link to view them\n- **Tags**: Bookmark tags are exported as RSS categories\n- **Dates**: The bookmark creation date is used as the publication date\n\nNote: Text notes are not included in RSS feeds as they don't have an associated URL.\n\n### Security Considerations\n\n- Each RSS feed requires a unique token for access\n- Tokens can be regenerated at any time, which will invalidate the old URL\n- Disabling RSS for a list immediately revokes access\n\n## Consuming RSS Feeds\n\nKarakeep can automatically monitor RSS feeds and create bookmarks from new entries, making it perfect for staying up to date with blogs, news sites, and other content sources.\n\n### Adding an RSS Feed\n\n1. Go to **Settings** → **RSS Feeds**\n2. Click **Add Feed**\n3. Enter the feed details:\n   - **Name**: A friendly name for the feed\n   - **URL**: The RSS/Atom feed URL\n   - **Enabled**: Toggle to enable/disable the feed\n   - **Import Tags**: Enable to import RSS categories as bookmark tags\n\n### How It Works\n\n- Karakeep checks enabled RSS feeds **every hour**\n- New entries are automatically created as bookmarks\n- Duplicate entries are automatically detected and skipped\n"
  },
  {
    "path": "docs/docs/05-integrations/_category_.json",
    "content": "{\n  \"label\": \"Integrations\",\n  \"position\": 5\n}\n"
  },
  {
    "path": "docs/docs/06-administration/01-security-considerations.md",
    "content": "# Security Considerations\n\nIf you're going to give app access to untrusted users, there's some security considerations that you'll need to be aware of given how the crawler works. The crawler is basically running a browser to fetch the content of the bookmarks. Any untrusted user can submit bookmarks to be crawled from your server and they'll be able to see the crawling result. This can be abused in multiple ways:\n\n1. Untrusted users can submit crawl requests to websites that you don't want to be coming out of your IPs.\n2. Crawling user controlled websites can expose your origin IP (and location) even if your service is hosted behind cloudflare for example.\n3. The crawling requests will be coming out from your own network, which untrusted users can leverage to crawl internal non-internet exposed endpoints.\n\nTo mitigate those risks, you can do one of the following:\n\n1. Limit access to trusted users\n2. Let the browser traffic go through some VPN with restricted network policies.\n3. Host the browser container outside of your network.\n4. Use a hosted browser as a service (e.g. [browserless](https://browserless.io)). Note: I've never used them before.\n"
  },
  {
    "path": "docs/docs/06-administration/02-FAQ.md",
    "content": "# Frequently Asked Questions (FAQ)\r\n\r\n## User Management\r\n\r\n### Lost password\r\n\r\n#### If you are not an administrator\r\n\r\nAdministrators can reset the password of any user. Contact an administrator to reset the password for you.\r\n\r\n- Navigate to the `Admin Settings` page\r\n- Find the user in the `Users List`\r\n- In the `Actions` column, there is a button to reset the password\r\n- Enter a new password and press `Reset`\r\n- The new password is now set\r\n- If required, you can change your password again (so the admin does not know your password) in the `User Settings`\r\n\r\n#### If you are an administrator\r\n\r\nIf you are an administrator and lost your password, you have to reset the password in the database.\r\n\r\nTo reset the password:\r\n\r\n- Acquire some kind of tools that helps you to connect to the database:\r\n  - `sqlite3` on Linux: run `apt-get install sqlite3` (depending on your package manager)\r\n  - e.g. `dbeaver` on Windows\r\n- Shut down Karakeep\r\n- Connect to the `db.db` database, which is located in the `data` directory you have mounted to your docker container:\r\n  - by e.g. running `sqlite3 db.db` (in your `data` directory)\r\n  - or going through e.g. the `dbeaver` UI to locate the file in the data directory and connecting to it\r\n- Update the password in the database by running:\r\n  - `update user set password='$2a$10$5u40XUq/cD/TmLdCOyZ82ePENE6hpkbodJhsp7.e/BgZssUO5DDTa', salt='' where email='<YOUR_EMAIL_HERE>';`\r\n  - (don't forget to put your email address into the command)\r\n- The new password for your user is now `adminadmin`.\r\n- Start Karakeep again\r\n- Log in with your email address and the password `adminadmin` and change the password to whatever you want in the `User Settings`\r\n\r\n### Adding another administrator\r\n\r\nBy default, the first user to sign up gets promoted to administrator automatically.\r\n\r\nIn case you want to grant those permissions to another user:\r\n\r\n- Navigate to the `Admin Settings` page\r\n- Find the user in the `Users List`\r\n- In the `Actions` column, there is a button to change the Role\r\n- Change the Role to `Admin`\r\n- Press `Change`\r\n- The new administrator has to log out and log in again to get the new role assigned\r\n\r\n### Adding new users, when signups are disabled\r\n\r\nAdministrators can create new accounts any time:\r\n\r\n- Navigate to the `Admin Settings` page\r\n- Go to the `Users List`\r\n- Press the `Create User` Button.\r\n- Enter the information for the user\r\n- Press `create`\r\n- The new user can now log in\r\n"
  },
  {
    "path": "docs/docs/06-administration/03-openai.md",
    "content": "# Tagging Costs\n\nThis service uses OpenAI for automatic tagging. This means that you'll incur some costs if automatic tagging is enabled. There are two type of inferences that we do:\n\n## Text Tagging\n\nFor text tagging, we use the `gpt-4.1-mini` model. This model is [extremely cheap](https://openai.com/api/pricing). Cost per inference varies depending on the content size per article. Though, roughly, You'll be able to generate tags for almost 3000+ bookmarks for less than $1.\n\n## Image Tagging\n\nFor image uploads, we use the `gpt-4o-mini` model for extracting tags from the image. You can learn more about the costs of using this model [here](https://platform.openai.com/docs/guides/images?api-mode=chat#calculating-costs). To lower the costs, we're using the low resolution mode (fixed number of tokens regardless of image size). You'll be able to run inference for 1000+ images for less than a $1.\n"
  },
  {
    "path": "docs/docs/06-administration/05-troubleshooting.md",
    "content": "# Troubleshooting\n\n## SqliteError: no such table: user\n\nThis usually means that there's something wrong with the database setup (more concretely, it means that the database is not initialized). This can be caused by multiple problems:\n1. **Wiped DATA_DIR:** Your `DATA_DIR` got wiped (or the backing storage dir changed). If you did this intentionally, restart the container so that it can re-initalize the database.\n2. **Missing DATA_DIR**: You're not using the default docker compose file, and you forgot to configure the `DATA_DIR` env var. This will result into the database getting set up in a different directory than the one used by the service.\n\n## Chrome Failed to Read DnsConfig\n\nIf you see this error in the logs of the chrome container, it's a benign error and you can safely ignore it. Whatever problems you're having, is unrelated to this error.\n\n## AI Tagging not working (when using OpenAI)\n\nCheck the logs of the container and this will usually tell you what's wrong. Common problems are:\n1. Typo in the env variable `OPENAI_API_KEY` name resulting into logs saying something like \"skipping inference as it's not configured\".\n2. You forgot to call `docker compose up` after configuring open ai.\n3. OpenAI requires pre-charging the account with credits before using it, otherwise you'll get an error like \"insufficient funds\".\n\n## AI Tagging not working (when using Ollama)\n\nCheck the logs of the container and this will usually tell you what's wrong. Common problems are:\n1. Typo in the env variable `OLLAMA_BASE_URL` name resulting into logs saying something like \"skipping inference as it's not configured\".\n2. You forgot to call `docker compose up` after configuring ollama.\n3. You didn't change the `INFERENCE_TEXT_MODEL` env variable, resulting into karakeep attempting to use gpt models with ollama which won't work.\n4. Ollama server is not reachable by the karakeep container. This can be caused by:\n    1. Ollama server being in a different docker network than the karakeep container.\n    2. You're using `localhost` as the `OLLAMA_BASE_URL` instead of the actual address of the ollama server. `localhost` points to the container itself, not the docker host. Check this [stackoverflow answer](https://stackoverflow.com/questions/24319662/from-inside-of-a-docker-container-how-do-i-connect-to-the-localhost-of-the-mach) to find how to correctly point to the docker host address instead.\n\n## Crawling not working\n\nCheck the logs of the container and this will usually tell you what's wrong. Common problems are:\n1. You changed the name of the chrome container but didn't change the `BROWSER_WEB_URL` env variable.\n\n## Upgrading Meilisearch - Migrating the Meilisearch db version\n\n[Meilisearch](https://www.meilisearch.com/) is the database used by karakeep for searching in your bookmarks. The version used by karakeep is `1.13.3` and it is advised not to upgrade it without good reasons. If you do, you might see errors like `Your database version (1.11.1) is incompatible with your current engine version (1.13.3). To migrate data between Meilisearch versions, please follow our guide on https://www.meilisearch.com/docs/learn/update_and_migration/updating.`.\n\nLuckily we can easily workaround this:\n1. Stop the Meilisearch container.\n2. Inside the Meilisearch volume bound to `/meili_data`, erase/rename the folder called `data.ms`.\n3. Launch Meilisearch again.\n4. Login to karakeep as administrator and go to (as of v0.24.1) `Admin Settings > Background Jobs` then click on `Reindex All Bookmarks`.\n5. When the reindexing has finished, Meilisearch should be working as usual.\n\nIf you run into issues, the official documentation can be found [there](https://www.meilisearch.com/docs/learn/update_and_migration/updating).\n"
  },
  {
    "path": "docs/docs/06-administration/06-server-migration.md",
    "content": "# Migrating Between Servers\n\nThis guide explains how to migrate all of your data from one Karakeep server to another using the official CLI.\n\n## What the command does\n\nThe migration copies user-owned data from a source server to a destination server in this order:\n\n- User settings\n- Lists (preserving hierarchy and settings)\n- RSS feeds\n- AI prompts (custom prompts and their enabled state)\n- Webhooks (URL and events)\n- Tags (ensures tags by name exist)\n- Rule engine rules (IDs remapped to destination equivalents)\n- Bookmarks (links, text, and assets)\n  - After creation, attaches the correct tags and adds to the correct lists\n\nNotes:\n- Webhook tokens cannot be read via the API, so tokens are not migrated. Re‑add them on the destination if needed.\n- Asset bookmarks are migrated by downloading the original asset and re‑uploading it to the destination. Only images and PDFs are supported for asset bookmarks.\n- Link bookmarks on the destination may be de‑duplicated if the same URL already exists.\n\n## Prerequisites\n\n- Install the CLI:\n  - NPM: `npm install -g @karakeep/cli`\n  - Docker: `docker run --rm ghcr.io/karakeep-app/karakeep-cli:release --help`\n- Collect API keys and base URLs for both servers:\n  - Source: `--server-addr`, `--api-key`\n  - Destination: `--dest-server`, `--dest-api-key`\n\n## Quick start\n\n```\nkarakeep --server-addr https://src.example.com --api-key <SOURCE_API_KEY> migrate \\\n  --dest-server https://dest.example.com \\\n  --dest-api-key <DEST_API_KEY>\n```\n\nThe command is long‑running and shows live progress for each phase. You will be prompted for confirmation; pass `--yes` to skip the prompt.\n\n### Options\n\n- `--server-addr <url>`: Source server base URL\n- `--api-key <key>`: API key for the source server\n- `--dest-server <url>`: Destination server base URL\n- `--dest-api-key <key>`: API key for the destination server\n- `--batch-size <n>`: Page size for bookmark migration (default 50, max 100)\n- `-y`, `--yes`: Skip the confirmation prompt\n\n## What to expect\n\n- Lists are recreated parent‑first and retain their hierarchy.\n- Feeds, prompts, webhooks, and tags are recreated by value.\n- Rules are recreated after IDs (tags, lists, feeds) are remapped to their corresponding destination IDs.\n- After each bookmark is created, the command attaches the correct tags and adds it to the correct lists.\n\n## Caveats and tips\n\n- Webhook auth tokens must be re‑entered on the destination after migration.\n- If your destination already contains data, duplicate links may be de‑duplicated; tags and list membership are still applied to the existing bookmark.\n\n## Troubleshooting\n\n- If the command exits early, you can re‑run it, but note:\n  - Tags and lists that already exist are reused.\n  - Link de‑duplication avoids duplicate link bookmarks. Notes and assets will get re-created.\n  - Rules, webhooks, rss feeds will get re-created and you'll have to manually clean them up afterwards.\n  - The progress log indicates how far it got.\n- Use a smaller `--batch-size` if your source or destination is under heavy load.\n"
  },
  {
    "path": "docs/docs/06-administration/07-legacy-container-upgrade.md",
    "content": "# Legacy Container Upgrade\n\nKarakeep's 0.16 release consolidated the web and worker containers into a single container and also dropped the need for the redis container. The legacy containers will stop being supported soon, to upgrade to the new container do the following:\n\n1. Remove the redis container and its volume if it had one.\n2. Move the environment variables that you've set exclusively to the `workers` container to the `web` container.\n3. Delete the `workers` container.\n4. Rename the web container image from `hoarder-app/hoarder-web` to `hoarder-app/hoarder`.\n\n```diff\ndiff --git a/docker/docker-compose.yml b/docker/docker-compose.yml\nindex cdfc908..6297563 100644\n--- a/docker/docker-compose.yml\n+++ b/docker/docker-compose.yml\n@@ -1,7 +1,7 @@\n version: \"3.8\"\n services:\n   web:\n-    image: ghcr.io/hoarder-app/hoarder-web:${KARAKEEP_VERSION:-release}\n+    image: ghcr.io/karakeep-app/karakeep:${KARAKEEP_VERSION:-release}\n     restart: unless-stopped\n     volumes:\n       - data:/data\n@@ -10,14 +10,10 @@ services:\n     env_file:\n       - .env\n     environment:\n-      REDIS_HOST: redis\n       MEILI_ADDR: http://meilisearch:7700\n+      BROWSER_WEB_URL: http://chrome:9222\n+      # OPENAI_API_KEY: ...\n       DATA_DIR: /data\n-  redis:\n-    image: redis:7.2-alpine\n-    restart: unless-stopped\n-    volumes:\n-      - redis:/data\n   chrome:\n     image: gcr.io/zenika-hub/alpine-chrome:123\n     restart: unless-stopped\n@@ -37,24 +33,7 @@ services:\n       MEILI_NO_ANALYTICS: \"true\"\n     volumes:\n       - meilisearch:/meili_data\n-  workers:\n-    image: ghcr.io/hoarder-app/hoarder-workers:${KARAKEEP_VERSION:-release}\n-    restart: unless-stopped\n-    volumes:\n-      - data:/data\n-    env_file:\n-      - .env\n-    environment:\n-      REDIS_HOST: redis\n-      MEILI_ADDR: http://meilisearch:7700\n-      BROWSER_WEB_URL: http://chrome:9222\n-      DATA_DIR: /data\n-      # OPENAI_API_KEY: ...\n-    depends_on:\n-      web:\n-        condition: service_started\n\n volumes:\n-  redis:\n   meilisearch:\n   data:\n```\n"
  },
  {
    "path": "docs/docs/06-administration/08-hoarder-to-karakeep-migration.md",
    "content": "# Hoarder to Karakeep Migration\n\nHoarder is rebranding to Karakeep. Due to github limitations, the old docker image might not be getting new updates after the rebranding. You might need to update your docker image to point to the new karakeep image instead by applying the following change in the docker compose file.\n\n```diff\ndiff --git a/docker/docker-compose.yml b/docker/docker-compose.yml\nindex cdfc908..6297563 100644\n--- a/docker/docker-compose.yml\n+++ b/docker/docker-compose.yml\n@@ -1,7 +1,7 @@\n version: \"3.8\"\n services:\n   web:\n-    image: ghcr.io/hoarder-app/hoarder:${HOARDER_VERSION:-release}\n+    image: ghcr.io/karakeep-app/karakeep:${HOARDER_VERSION:-release}\n```\n\nYou can also change the `HOARDER_VERSION` environment variable but if you do so remember to change it in the `.env` file as well.\n\n## Migrating a Baremetal Installation\n\nIf you previously used the [Debian/Ubuntu install script](../02-installation/06-debuntu.md) to install Hoarder, there is an option to migrate your installation to Karakeep.\n\n```bash\nbash karakeep-linux.sh migrate\n```\n\nThis will migrate your installation with no user input required. After the migration, the script will also check for an update.\n"
  },
  {
    "path": "docs/docs/06-administration/_category_.json",
    "content": "{\n  \"label\": \"Administration\",\n  \"position\": 6\n}\n"
  },
  {
    "path": "docs/docs/07-community/01-community-projects.md",
    "content": "# Community Projects\n\nThis page lists community projects that are built around Karakeep, but not officially supported by the development team.\n\n:::warning\nThis list comes with no guarantees about security, performance, reliability, or accuracy. Use at your own risk.\n:::\n\n### Raycast Extension\n\n_By [@luolei](https://github.com/foru17)._\n\nA user-friendly Raycast extension that seamlessly integrates with Karakeep, bringing powerful bookmark management to your fingertips. Quickly save, search, and organize your bookmarks, texts, and images—all through Raycast's intuitive interface.\n\nGet it [here](https://www.raycast.com/luolei/karakeep).\n\n### Alfred Workflow\n\n_By [@yinan-c](https://github.com/yinan-c)_\n\nAn Alfred workflow to quickly hoard stuff or access your hoarded bookmarks!\n\nGet it [here](https://www.alfredforum.com/topic/22528-hoarder-workflow-for-self-hosted-bookmark-management/).\n\n### Obsidian Plugin\n\n_By [@jhofker](https://github.com/jhofker)_\n\nAn Obsidian plugin that syncs your Karakeep bookmarks with Obsidian, creating markdown notes for each bookmark in a designated folder.\n\nGet it [here](https://github.com/jhofker/obsidian-hoarder/), or install it directly from Obsidian's community plugin store ([link](https://obsidian.md/plugins?id=hoarder-sync)).\n\n### Telegram Bot\n\n_By [@Madh93](https://github.com/Madh93)_\n\nA Telegram Bot for saving bookmarks to Karakeep directly through Telegram.\n\nGet it [here](https://github.com/Madh93/karakeepbot).\n\n### Hoarder's Pipette\n\n_By [@DanSnow](https://github.com/DanSnow)_\n\nA chrome extension that injects karakeep's bookmarks into your search results.\n\nGet it [here](https://dansnow.github.io/hoarder-pipette/guides/installation/).\n\n### Karakeep-Python-API\n\n_By [@thiswillbeyourgithub](https://github.com/thiswillbeyourgithub/)_\n\nA python package to simplify access to the karakeep API. Can be used as a library or from the CLI. Aims for feature completeness and high test coverage but do check its feature matrix before relying too much on it.\n\nIts repository also hosts the [Community Script](https://github.com/thiswillbeyourgithub/karakeep_python_api/tree/main/community_scripts), for example:\n\n| Community Script | Description | Documentation |\n|----------------|-------------|---------------|\n| **Karakeep-Time-Tagger** | Automatically adds time-to-read tags (`0-5m`, `5-10m`, etc.) to bookmarks based on content length analysis. Includes systemd service and timer files for automated periodic execution. | [`Link`](https://github.com/thiswillbeyourgithub/karakeep_python_api/tree/main/community_scripts/karakeep-time-tagger) |\n| **Karakeep-List-To-Tag** | Converts a Karakeep list into tags by adding a specified tag to all bookmarks within that list. | [`Link`](https://github.com/thiswillbeyourgithub/karakeep_python_api/tree/main/community_scripts/karakeep-list-to-tag) |\n| **Omnivore2Karakeep-Highlights** | Imports highlights from Omnivore export data to Karakeep, with intelligent position detection and bookmark matching. Supports dry-run mode for testing. | [`Link`](https://github.com/thiswillbeyourgithub/karakeep_python_api/tree/main/community_scripts/omnivore2karakeep-highlights) |\n\n\nGet it [here](https://github.com/thiswillbeyourgithub/karakeep_python_api).\n\n### FreshRSS_to_Karakeep\n\n_By [@thiswillbeyourgithub](https://github.com/thiswillbeyourgithub/)_\n\nA python script to automatically create Karakeep bookmarks from your [FreshRSS](https://github.com/FreshRSS/FreshRSS) *favourites/saved* RSS item. Made to be called periodically. Based on the community project `Karakeep-Python-API` above, by the same author.\n\nGet it [here](https://github.com/thiswillbeyourgithub/freshrss_to_karakeep).\n\n### karakeep-sync\n_By [@sidoshi](https://github.com/sidoshi/)_\n\nSync links from Hacker News upvotes, Reddit Saves to Karakeep for centralized bookmark management.\n\nGet it [here](https://github.com/sidoshi/karakeep-sync)\n\n### Home Assistant Integration\n\n_By [@sli-cka](https://github.com/sli-cka)_\n\nA custom integration that brings Karakeep data into Home Assistant. It exposes your Karakeep statistics data (like lists, bookmarks, tag, etc.) as Home Assistant entities, enabling dashboards, automations, and notifications based on your Karakeep data.\n\nGet it [here](https://github.com/sli-cka/karakeep-homeassistant)\n"
  },
  {
    "path": "docs/docs/07-community/02-community-channels.md",
    "content": "# Community Channels\n\nStay connected with the Karakeep team and community for updates, support, and feature discussions.\n\n## Discord\n\n- Join the official server: [discord.gg/NrgeYywsFh](https://discord.gg/NrgeYywsFh)\n- Great for getting help, sharing setups, and chatting with the team and other users.\n\n## Twitter / X\n\n- Follow [@karakeep_app](https://twitter.com/karakeep_app) for release announcements, tips, and product news.\n- DM or tag us with feedback or things you'd like to see next.\n"
  },
  {
    "path": "docs/docs/07-community/_category_.json",
    "content": "{\n  \"label\": \"Community\",\n  \"position\": 7\n}\n"
  },
  {
    "path": "docs/docs/08-development/01-setup.md",
    "content": "# Setup\n\n## Quick Start\n\nFor the fastest way to get started with development, use the one-command setup script:\n\n```bash\n./start-dev.sh\n```\n\nThis script will automatically:\n- Start Meilisearch in Docker (on port 7700)\n- Start headless Chrome in Docker (on port 9222) \n- Install dependencies with `pnpm install` if needed\n- Start both the web app and workers in parallel\n- Provide cleanup when you stop with Ctrl+C\n\n**Prerequisites:**\n- Docker installed and running\n- pnpm installed (see manual setup below for installation instructions)\n\nThe script will output the running services:\n- Web app: http://localhost:3000\n- Meilisearch: http://localhost:7700  \n- Chrome debugger: http://localhost:9222\n\nPress Ctrl+C to stop all services and clean up Docker containers.\n\n## Manual Setup\n\nKarakeep uses `node` version 24. To install it, you can use `nvm` [^1]\n\n```\n$ nvm install  24\n```\n\nVerify node version using this command:\n```\n$ node --version\nv24.14.0\n```\n\nKarakeep also makes use of `corepack`[^2]. If you have `node` installed, then `corepack` should already be\ninstalled on your machine, and you don't need to do anything. To verify the `corepack` is installed run:\n\n```\n$ command -v corepack\n/home/<user>/.nvm/versions/node/v24.14.0/bin/corepack\n```\n\nTo enable `corepack` run the following command:\n\n```\n$ corepack enable\n```\n\nThen, from the root of the repository, install the packages and dependencies using:\n\n```\n$ pnpm install\n```\n\nOutput of a successful `pnpm install` run should look something like:\n\n```\nScope: all 20 workspace projects\nLockfile is up to date, resolution step is skipped\nPackages: +3129\n+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\nProgress: resolved 0, reused 2699, downloaded 0, added 3129, done\n\ndevDependencies:\n+ @karakeep/prettier-config 0.1.0 <- tooling/prettier\n\n. prepare$ husky\n└─ Done in 45ms\nDone in 5.5s\n```\n\nYou can now continue with the rest of this documentation.\n\n### First Setup\n\n- You'll need to prepare the environment variables for the dev env.\n- Easiest would be to set it up once in the root of the repo and then symlink it in each app directory (e.g. `/apps/web`, `/apps/workers`) and also `/packages/db`.\n- Start by copying the template by `cp .env.sample .env`.\n- The most important env variables to set are:\n  - `DATA_DIR`: Where the database and assets will be stored. This is the only required env variable. You can use an absolute path so that all apps point to the same dir.\n  - `NEXTAUTH_SECRET`: Random string used to sign the JWT tokens. Generate one with `openssl rand -base64 36`. Logging in will not work if this is missing!\n  - `MEILI_ADDR`: If not set, search will be disabled. You can set it to `http://127.0.0.1:7700` if you run meilisearch using the command below.\n  - `OPENAI_API_KEY`: If you want to enable auto tag inference in the dev env.\n- run `pnpm run db:migrate` in the root of the repo to set up the database.\n\n### Dependencies\n\n#### Meilisearch\n\nMeilisearch is the provider for the full text search (and at some point embeddings search too). You can get it running with `docker run -p 7700:7700 getmeili/meilisearch:v1.13.3`.\n\nMount persistent volume if you want to keep index data across restarts. You can trigger a re-index for the entire items collection in the admin panel in the web app.\n\n#### Chrome\n\nThe worker app will automatically start headless chrome on startup for crawling pages. You don't need to do anything there.\n\n### Web App\n\n- Run `pnpm web` in the root of the repo.\n- Go to `http://localhost:3000`.\n\n> NOTE: The web app kinda works without any dependencies. However, search won't work unless meilisearch is running. Also, new items added won't get crawled/indexed unless workers are running.\n\n### Workers\n\n- Run `pnpm workers` in the root of the repo.\n\n### Mobile App (iOS & Android)\n\n#### Prerequisites\n\nTo build and run the mobile app locally, you'll need:\n\n- **For iOS development**: \n  - macOS\n  - Xcode installed from the App Store\n  - iOS Simulator (comes with Xcode)\n\n- **For Android development**:\n  - Android Studio installed\n  - Android SDK configured\n  - Android Emulator or physical device\n\nFor detailed setup instructions, refer to the [Expo documentation](https://docs.expo.dev/guides/local-app-development/).\n\n#### Updating from an Older Version\n\nIf you are returning to mobile development after a significant update to the source (e.g. Expo version bump or major dependency changes), the build may fail with stale artifacts in workspace `node_modules`. Run a clean wipe before reinstalling:\n\n```bash\npnpm run clean:workspaces\npnpm install\npnpm --filter @karakeep/mobile clean:prebuild\n```\n\nThen continue with the prebuild and run steps below.\n\n#### Quick Start\n\n```sh\n# ios\npnpm ios\n\n# android\npnpm android\n```\n\nMore details below if you want to understand what's going on.\n\n\n#### Details\n\nThe app has three variants: development, preview, and release.\n\n| Variant | Description | Expo Dev Tools | Command (iOS) | Command (Android) |\n|---------|-------------|----------------|---------------|--------------------|\n| Development (default) | Requires the expo devserver to be running. Uses a separate bundle ID so it can be installed alongside the production app. | Yes | `pnpm ios` | `pnpm android` |\n| Preview | Standalone app that doesn't require the expo devserver. Uses its own bundle ID. | No | `pnpm --filter @karakeep/mobile ios:preview` | `pnpm --filter @karakeep/mobile android:preview` |\n| Release | Standalone app using the production bundle ID. Closest to a production build. | No | `pnpm --filter @karakeep/mobile ios:release` | `pnpm --filter @karakeep/mobile android:release` |\n\nIn 90% of the cases, you'll want to use the development variant.\n\nNote: Changing the code will hot reload the app. However, installing new packages requires restarting the expo server.\n\n\n### Browser Extension\n\n- `cd apps/browser-extension`\n- `pnpm dev`\n- This will generate a `dist` package\n- Go to extension settings in chrome and enable developer mode.\n- Press `Load unpacked` and point it to the `dist` directory.\n- The plugin will pop up in the plugin list.\n\nIn dev mode, opening and closing the plugin menu should reload the code.\n\n\n## Docker Dev Env\n\nIf the manual setup is too much hassle for you. You can use a docker based dev environment by running `docker compose -f docker/docker-compose.dev.yml up` in the root of the repo. This setup wasn't super reliable for me though.\n\n\n[^1]: [nvm](https://github.com/nvm-sh/nvm) is a node version manager. You can install it following [these\ninstructions](https://github.com/nvm-sh/nvm?tab=readme-ov-file#installing-and-updating).\n\n[^2]: [corepack](https://nodejs.org/api/corepack.html) is an experimental tool to help with managing versions of your\npackage managers.\n"
  },
  {
    "path": "docs/docs/08-development/02-directories.md",
    "content": "# Directory Structure\n\n## Apps\n\n| Directory                | Description                                            |\n| ------------------------ | ------------------------------------------------------ |\n| `apps/web`               | The main web app                                       |\n| `apps/workers`           | The background workers logic                           |\n| `apps/mobile`            | The react native based mobile app                      |\n| `apps/browser-extension` | The browser extension                                  |\n| `apps/landing`           | The landing page of [karakeep.app](https://karakeep.app) |\n\n## Shared Packages\n\n| Directory         | Description                                                                  |\n| ----------------- | ---------------------------------------------------------------------------- |\n| `packages/db`     | The database schema and migrations                                           |\n| `packages/trpc`   | Where most of the business logic lies built as TRPC routes                   |\n| `packages/shared` | Some shared code between the different apps (e.g. loggers, configs, assetdb) |\n\n## Toolings\n\n| Directory            | Description             |\n| -------------------- | ----------------------- |\n| `tooling/typescript` | The shared tsconfigs    |\n| `tooling/eslint`     | ESlint configs          |\n| `tooling/prettier`   | Prettier configs        |\n| `tooling/tailwind`   | Shared tailwind configs |\n"
  },
  {
    "path": "docs/docs/08-development/03-database.md",
    "content": "# Database Migrations\n\n- The database schema lives in `packages/db/schema.ts`.\n- Changing the schema, requires a migration.\n- You can generate the migration by running `pnpm run db:generate --name description_of_schema_change` in the root dir.\n- You can then apply the migration by running `pnpm run db:migrate`.\n\n## Drizzle Studio\n\nYou can start the drizzle studio by running `pnpm run db:studio` in the root of the repo.\n"
  },
  {
    "path": "docs/docs/08-development/04-architecture.md",
    "content": "# Architecture\n\n![Architecture Diagram](/img/architecture/arch.png)\n\n- Webapp: NextJS based using sqlite for data storage.\n- Workers: Consume the jobs from sqlite based job queue and executes them, there are three job types:\n  1. Crawling: Fetches the content of links using a headless chrome browser running in the workers container.\n  2. OpenAI: Uses OpenAI APIs to infer the tags of the content.\n  3. Indexing: Indexes the content in meilisearch for faster retrieval during search.\n"
  },
  {
    "path": "docs/docs/08-development/_category_.json",
    "content": "{\n  \"label\": \"Development\",\n  \"position\": 8\n}\n"
  },
  {
    "path": "docs/docs/api/_category_.json",
    "content": "{ \"label\": \"API\" }\n"
  },
  {
    "path": "docs/docs/api/add-bookmark-to-list.api.mdx",
    "content": "---\nid: add-bookmark-to-list\ntitle: \"Add a bookmark to a list\"\ndescription: \"Add a bookmark to a manual list. This operation is idempotent — adding an already-present bookmark has no effect.\"\nsidebar_label: \"Add a bookmark to a list\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzFVm2LGzcQ/iuD8iUxe/alpDTsh4LTNnBNKEfio5SLwePVrFexVlIk7Z0ds9Af0V/YX1JG+2I7uaYtlPbT+bQzo2eemXlGB2EdeYzKmispcoFSvrB2W6PfLuxrFaLIhKRQeOXYRuRiLiUgrHsjiBYQajQNatAqxCksKhVgjAoqgJJUOxvJRPj9198ApVRmA2gAtSeU+wvnKfDXMWqFAYwFKksq4lRkIuImiPxWMKQglpkIVDRexb3Ibw9iTejJz5tYifx22S4z4dBjTZF8SAahqKhGkR9E3DsSuQjRK7P5LLlFRdAY9aEhBm2iKhV5sCXEirr0RCZoh7XTHEWRknq339T3759/Y3cfv652MdriOQNWMZkw4Csp2kx4+tAoT1Lk0TeUCYM1G+jOIBOKATiMlWizfwnxwOc/Rj30wBeRr49GZ+iX7BGcNYECJ/DV5TP+c477JwuFNWNPnKKFewzcJCS5uwbmITRFQSGUjdb7KcN6dvn087g3BptYWa8+khwjv0j9AdFuKTVkrUJQZpOBMneolczAeqCd4ySZqx5ZYp92ceY0cn5fqsqR3lMEom3bhPQBBrgz+N4xbWMjlLYxn0JA57Qq0jTN3gf7MBC7fk8FT6vzPHtRddwXVtJfN9EcaiwqZeiC5xHXmoC8tx7YPXFdUwi4+VuhqqZG82mg3n8q2rN+uu0AHuMvjz34AzsmAtP9sbIsUK5JSXKj5WLGjRFmh26G2tlAZZgdjs3ZClYLfzdoQeO1yMUBpfQUQjtDp2Z3T0Um7tArhpyI6z93ZSux0VHkoorRhXw2i34/3aLHLZGbonMPTmUfYZjFV709dFg4sRMZe8v17G4+FbORbb6Z80hmPHvJSGT9j5fW18gIf/x5kShWprTszll3kJ5OL6eXJyM+4plfX32Gf/yoAiAE0uVFZUNMBR2Y7TRcAlf6QsULjZF8yk4V1K+B+fUVoNb2PsDeNjzONRrcHIOELA13yIAVPoNKbSqtNhWfYAiU/hoJayy2jQvgvN14rGuMqkAWgnfmnXn0CJguVr9uTPhwrjWQkc4qEwP0LQd4rgWO75CgTCrRat4PbgqygopQkp/CL7aBAg1syPBWI15dnNmW9lB6W5/X957WcHMFjZHkYTJ5SzEqswnwbfJ5RfswmQywr3GjzAg5KcIRc2icsz5C0fhg/cUaGaobPeBOIay6j6tE0kqrWsUVfGjI7+G4ArkYBIMmgzKFbiRxZVeGdvG7PkSpSHeaybSAioCh42W4ZAzJlSwpFlX6zkEYGE1hDivTaL2CO9QNQWn9+R3KSK4RpcBcD0+86GvrE8BGxzBwM+wgWOwdBT4cTkKqxprAGurGyxMBT0rI35kLmEy0MtvJJOUyh5s3r092i4oV2NTnqKHweK9JQk0RJUacdu4s+qN7En/gI1Zo6k1Scw42BhqnLfLGKpUmeKxqbnLr4fr7l0+SgDobYo1Ju/v1+dBDSj/w4jocd8H/9fjqNOhkFbZZJ6OHXolv0zMmiEzk43tmHHE+PXkrLDPBWsJOhwM39Y3XbcvHqW/5BXfU4qTYUgX+LUVeog70BX4ev+lXyxP4M9z9IZp9knzd8H8iE1vaH19j/AL7D289YaddtpnohCfl3hnMi4JcPHE9jb48WY/XNwuRCTxfHp8sixT3QUCHQ2exYHFs2xFfEkuG1rZ/AKDKROU=\nsidebar_class_name: \"put api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Add a bookmark to a list\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"put\"}\n  path={\"/lists/{listId}/bookmarks/{bookmarkId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nAdd a bookmark to a manual list. This operation is idempotent — adding an already-present bookmark has no effect.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the list.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"ListId\"},\"required\":true,\"name\":\"listId\",\"in\":\"path\"},{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the bookmark.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"204\":{\"description\":\"No content — the bookmark was added to the list successfully.\"},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"List or bookmark not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/admin-update-user.api.mdx",
    "content": "---\nid: admin-update-user\ntitle: \"Update a user (admin)\"\ndescription: \"Update a user's role, bookmark quota, storage quota, or browser crawling setting. Requires admin role. You cannot update your own user account via this endpoint.\"\nsidebar_label: \"Update a user (admin)\"\nhide_title: true\nhide_table_of_contents: true\napi: eJztV82OGzcMfhVCPTQxZm1vkl58KOD8AdvksE12ERSbRU2PaI9ijTTRz3pdY4A+RJ+wT1JQ82N77QRFkUuB+LBrSxRFfqTIj1thK3IYlDUXUkwEylKZ60pioGtPTmRCks+dqlhCTESzBQjRk/vRg7OaMphbuyrRreBztAEz8ME6XFL30zqYO7v25CB3uNbKLMFTCMosh/COPkflyEO6Oikcwm82Qo7G2ACxuXFjowO7NuliwDy30QS4UwihUB7IyMoqE4YiEwGXXkxuxJQVittMeMqjU2EjJjdbMSd05KYxFGJyc1vfZqJChyUFcj4J+LygEsVkK8KmIjERPjhllkdQXBUEFy/BLiAU1JgVbGsum0H3WFaaFfDe7+dPnoo6E65xV4pJcJEyYbDsRC6kyIRi1RWGQrBpLE0+PLdywwYdG7BQpKXfuximWner6AhskkYNf//5F1ijN1A5e6ckyU5qrbSGOUFeoFmSZNNzawKZwFdiVWmVpwQZffJ87wmE7PwT5UFkonKcTkGR512O5SkcycSSAxSbDMMmTnUmujz6lfNm76QygZZJ1kStcc5qG/hKZVTJ2sZ1Jtq0+4+n2xR90WboK8OSck/P3FpNaI701PXRK+FkaBNXYsD9bOhgad0+cvp8PB4/dOWncfep+cN54StrfAPzk/H4ODn2TJDgY56T94uo9eYbxrdVexKiQ2s+FBQKfiD8Uhpc1uj37BqK+uBx3PTKbxuPn51y8jlKaJ9Iym9l7lArCcpUMSTkufZgCFRWjEP/TPpK8g3RyK08me2HJk+hxLxQhs4coeQkAnLOOuDjQ64QJXmPy3+lqoglmoeK2vPHiCYDd/pvMxFUSKn4ig+KDujzE9lkMIbCOvUHyYQ0B/J5qqQQ7IoMKA+l8l6ZZdaFIRV+uq/4/gc4B7oPo0qjOo3wrlL0NXTfgt7Sp8eWvrZurqQkk8xsegqmTIIOi+8xP475sy9UEO7ACxvNd9AegJbuD4Vl0lTF5CQ37YkYpZwbcXHxo23T12vBJMTddRQjOi0mYotSOvK+HmGlRnfnIhN36BRbmHBqt5vILDDqICaiCKHyk9EouM1whQ5XRNUQq+okPWk1dBzlTSsPjS3sxx47es/ha27e50g9uHwz+5HEuNYnIW5g6ctr60pkC3/5cJUQ5bR4t+Mvr75RA2SKtLCshuFsfD0fjodjsQtT7+j08uIImH5TeUDwpBdnhfUhJUZnCjNUNNxcUJ6pcKYxkEuwqZyGcMWUc3p5Aai1XXsmp9xbSjTMejslPgOtfPAZMCPNoFDLQqtlwSvoPaX/RsIc81WsPBOzpcOyxKBy5E790Xw0P/wAHAcyoX1uvMgcr2O8fV0DPKzIFd/BvTDFfjZty2dSMoOCUJLrmTYsyfAcQIAmebaiDSycLQ8TZ01zuL6AaCQ5GAzeNyTew8/pzBva+MGgM/sSl8r0Jr9VPuzZ7GNVWRcgj85bdzZHNrXqTyRiP2s2ZwmkmValCjP4HMltYEfZORgEHR8CZXIdJU8TMDN0H160KhLXTS2BYQEVAH2DS3dJr5IjuaCQF2mflbBhzKxhxsRvBneoI8HCusM7lJEcI0qKOR6OwFgorUsGRh18h83zbmC62lTkebFb8SkacwJrqHm3jgj4CfrJR3MGg4FWZjUYJF+mcP3u7W76WqtQ7Ah/GrVIQkkBmQoNm+PcevvjqQUDL3Glp1YkJWcnYyBW2mIzMGiCR6rkJLcOLl++fpwKcWV9KDH1gHacORgT4VF66Y8fvsTtrqH8P+bKpg7ucZc6a0r5ti3+N31NS+VfZGLSDna3meAiwyLbLWf7tdN1zcspoXkU3VX/1COk8u0AskDt6SvYPWpdlY/hS1a2i2g2qcnoyL9EJla02Q2fNY9gTV1IFjSbL5p7zjhT9w4fEYA6605M85yq8FXZ273ueXl9lZpAM+KWiRwIh2uRpb/Jziapm6GS17ZCo1nG1P1Fo5JbDh52rAcdKjl1EonttpG44sJZ1z0wqZAyLnX9DxwY/7Q=\nsidebar_class_name: \"put api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Update a user (admin)\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"put\"}\n  path={\"/admin/users/{userId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nUpdate a user's role, bookmark quota, storage quota, or browser crawling setting. Requires admin role. You cannot update your own user account via this endpoint.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The ID of the user to update.\",\"example\":\"user_123\"},\"required\":true,\"name\":\"userId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The fields to update. All fields are optional — only provided fields will be changed.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"role\":{\"type\":\"string\",\"enum\":[\"user\",\"admin\"]},\"bookmarkQuota\":{\"type\":\"integer\",\"nullable\":true,\"minimum\":0},\"storageQuota\":{\"type\":\"integer\",\"nullable\":true,\"minimum\":0},\"browserCrawlingEnabled\":{\"type\":\"boolean\",\"nullable\":true}},\"description\":\"User update data\",\"example\":{\"role\":\"admin\",\"bookmarkQuota\":1000,\"storageQuota\":5000000000}}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"User updated successfully.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"success\":{\"type\":\"boolean\",\"description\":\"Whether the update was successful.\"}},\"required\":[\"success\"]}}}},\"400\":{\"description\":\"Bad request — invalid input data or attempted to update own user.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"403\":{\"description\":\"Forbidden — admin access required.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}},\"404\":{\"description\":\"User not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/attach-asset-to-bookmark.api.mdx",
    "content": "---\nid: attach-asset-to-bookmark\ntitle: \"Attach asset to a bookmark\"\ndescription: \"Attach a previously uploaded asset to a bookmark. The asset must be uploaded first via the POST /assets endpoint.\"\nsidebar_label: \"Attach asset to a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzdV9uKG0cQ/ZWi/WKLWckODjF6CMg3srFJlr0QwnpBpZkaTVs93ePuHmllMZCPyBfmS0LVXKRdrY0hhEBeFm1PdXXVqepzqnfKVeQxamdPMzVVGCOmxSwEipfupXOrEv1KJSqjkHpdsZ2aqplYAULlaa1dHcwW6so4zCgD5M0QHSAsOgdjuCyo+1DWIcKC9va59iHCWiPEguDs14tLmIhpALJZ5bSNY5WoiMugpteqDyqom0QFSmuv41ZNr3dqQejJz+pYqOn1TXOTqAo9lhTJBzEIaUElqulOxW1FaqpC9Nouj9LjYGurP9UEOiMbda7Jg8slwCEnlSi6xbIy7EmTzsztdlluPr74wd1+/r64jdGlLzhwHcWkD/w0U02iPH2qtadMTaOvKVEWSzZa7I0SpTmYCmOhOBneQSG+dNmWUzgOucX39DWgzYAzlCJIqTjY1NlINvJerCqjU6n65GNgBw+A4xYfKY0qUZXnHomaAn/V2bcBePq6R+yLTTJmIOTXpbg7dku2LrnoRtvVT7E0r7ocEhVST2RD4STCLFedp4vD9QVaS/60xCWpROW1MWe4pJlPC73mlbXOyLFdh7r0vWRMqceNoWxvWwfyV134fNgaI3petyvrNpa78RgEqUJqMASdd4BD7jzEQocBgzvdcM0AH6Jy0zStRaicDW0Nvnv67GstsMHQ1Z0yCHWaUgic/PZfboP/UzWbROXa0C9yLY9TsbUxuOB7zdf320r4/KGqXVmsY+G8/kwZ/PXHn3JjXgqTQXQrsqADlDoEbZcJaLtGo7MEnAe6rfi8ezWNdBsnlUH9cDX3tRio6zAC1Uf6/DjSnr/Augi5q+39o/9JO6UuexDne7oDJaaFtnTiCTPGH8h754G3C5uUFAL3xze4KuoS7X1H3f7jWykB7v3f7Hn9DW8U4OT8WDgW0soFyZLZe6omfU+GyW5P8U2nc3wByK97maq9UVO1wyzzFEIzwUpP1s+4wdFrjlUQ6z63dcqxNlFNVRFjFaaTSfTb8Qo9roiqMVbVgwzdeehp+l1nD20snNGBwl5wIduTD3V2gJlPlovMZixkYsS3UX68db5EjvDn3y4FW26Q872ivenbsWWWfdUOCOWINxqWyNzxJgavzezZ+On46YHsDmnNzk6PYBg+Mh1DIJOfFC5EaYi+TtouRVG5U050PDEYyQtIOiWebHRg34DGuE2AratZd0u0uNw7CQkYHWJIgOeYBAq9LIxeFrzSNkEihywwXdVVgMq7pceyxKhTZOb+YD/YR4+AUeeJpL1mvDgzZpiTAnQtC3iXQyo+IwNtpdLzWXfhxckcCsKM/Bh+dzWkaGFJlmdCArSS2Yq2kHtX3m2TDS3g6hRqm5GH0eiCYtR2GeBH2fOOtmE06sM+w6W2Q8jvdYgHMYe6qpyPkNY+OH+yQA61GnbIaDhvP84FpLnRpY5z+FST38J+yGvHzF4pQdvU1BlxZeeWbuOrzkWuybRcy7CAjoChxaU/ZHDJlcwppoV8ZyccGI1hBnOWgDms0dQkmn7nDG0zrhGJY66HJ7AOSuclwNrE0GMz8Cp3eeDFYcSVaiwInKX2lnpqZ4ow/WBPYDTiGzEaSS4zuDp/P/QbbHQswEmfo4FO/6CkiBlGHLfbWSyG7SIawEvM8NSZSHP2NvZwajcEjzWLMYvR2eu3T4SAmfdKFO7vRtr+sXD8LLh/HXd7NflvnhgtzRxIaJO0bLzriPx6GC6YtKd3pvWOy28SxRTCtrsd9/KVN03Dy9Ku/DTZM7nwfaYD/87UNEcT6CugPD7vFOkJfCncbhHtVgTD1PyfStSKtnefFw2POO3Nlyhag45aT4Rx9w6OpL1J+h2zNKUqftX25kAXGXyZ0dpXTCm6rzxuVCJ/Jdi2b0VsZG2nDNplLcKuWp+sIXhXgu5JjmT1IBy7XWtxydzYNAM6wpUMTNP8Dd4ASw8=\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Attach asset to a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/bookmarks/{bookmarkId}/assets\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nAttach a previously uploaded asset to a bookmark. The asset must be uploaded first via the POST /assets endpoint.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the bookmark.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The asset ID and type to attach.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\",\"description\":\"The ID of the previously uploaded asset.\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"pdf\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"avatar\",\"unknown\"],\"description\":\"The type classification for this asset.\"}},\"required\":[\"id\",\"assetType\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"201\":{\"description\":\"The asset was attached successfully.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"pdf\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"avatar\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Bookmark not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/attach-tags-to-bookmark.api.mdx",
    "content": "---\nid: attach-tags-to-bookmark\ntitle: \"Attach tags to a bookmark\"\ndescription: \"Attach one or more tags to a bookmark. Tags can be identified by ID or name. If a tag name is provided and the tag doesn't exist, it will be created automatically.\"\nsidebar_label: \"Attach tags to a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzVV9uO2zYQ/ZUB89DE0NqbIkUDPRRwboCbIF0kXhTFZgGPxbHEmCIVkrKtGAb6Ef3Cfkk7lCV7126QFnmpH9YWNTOcy5kzs1thK3IYlDUTKVKBIWBWTDH3U/vM2mWJbikSIclnTlUsJlIxjkJgDYF1UFpHEDD3ECwgzPdaQ2ArkKGBOYGSZIJaKJIwb2DyghUNljSEyQKQ1eMjKA+VsyslSQIaCaGItkFa8ua7ALRRPiSgAqyV1mw5c4SBpetgSwwqQ62boUgEuyTSG9GF4cVtIjxltVOhEenNVswJHblxHQqR3tzubhNRocOSAjkfBXxWUIki3YrQVCRS4YNTJj9JyLQgqI36VB8F6sAuovt9QkQiaINlpdmSIiX1psnL9cenP9rN5x+KTQg2e8qOqxBFOscnUuwS4ehTrRxJkQZXUyI4XSIV84NQIhQ7U2EoBAfDGuTDMysbDuHU5b5osZ5DeMlV5WyXtQ9Q4IqAVCjIAcIsYD6RM65b+/AWS5pxTJk1gUzgK7CqtMoimkYfPd9zJod2/pGyIBJROcZeUOTj21iuXgqdw4ZDClT6r9Jm/N4v1C7C4G1M1Zl3beAknzXnakymLhlAqEQiirpEwwiStMBaB5Huj3b8OS7PTRvKbXfuK2t86+X3l5fnKzF54Tu4tEUpMMCaHEHn4jfMdGfyK7L9X/AeMP/XUJ/G8p0ksvd0n8wnl49P83dtsA6Fderz3+Ty5+9/RCeexdaGYJdkmFRK5b0yeQLKrFArmTCQaVPxTfdSG2gTRpVGdT6pB3j0AR57IDpPn5x62jU0GBtgYWvzLauaWXkG5CfUDSVmhTJ04QglzjUBOWcdsPqQe6Ik7zH/KlOxAe4b2usPxf1iRgcP9m8P1X/Jil0jlRQKy6Oosj5GyXSWilFHdH60PXDebhR7jXndrTrWrp0WqdiilI68342wUqPVY5GIFTrFnrZd0L5uq9S3dAiVT0ej4JrhEh0uiaohVtXZDthb6HD/ei8PrS8cz9HAec9lbG8+Hjt9kvlmjiOKMa9HIZHsf7yyrkT28OdfpzGzDI93B4J/2YGxI9KbnhMP9eup8HB0zIAdod3ueJIsLBvjpLYRPx5eDi+PWrYPd3w1OUlP/1J5QPCkFxeF9SHCpKueMnkc8YyfCxUuNAZyMXkqoyFMC+XZNqDWdu2hsTXPqhIN5gcjPgGtfPBJ5M0ECpUXWuUFn6D3FL+NhDlmy7qKy0XusDwsCh/MB/PgAXA1mMja5uPDsdZARlZWmeBhD2TAu8xS8R0SlIkImI33NBCNzKAglOSG8Jut4x6Uk+FdiwBNjGxJDSycLe/CZ01zuJ5AbSQ5GAzeUwjK5B5+ijqvqfGDQef2FebK9C6/UT4c+ezrqrIuQFY7b93FHNnVqteAlUKYtS9nMUkzrUoVZvCpJtfAYRfiYhB0kwyUyXQtiSs7M7QJz/cmFop0y8CcFt7R0Ld56S7pTXIlFxR43SgI2Ag7RkMYw8zUWs9ghbomWFh39w5lJNeIomGuhyMwtt1BHflaB9/lpmfbaVOR58N+E+y20rjBcvc6IuBG9OkHcwGDgVZmORjEWMZw/e5NjzdYq1CAjThHDZnDtSYJJQWUGHDYqvMI6dXjKAE+Yt6nvUgEZydjoK60RV56F0oTPFQlg9w6uHrx6lGkZWZD7s50221++y38dPO+343bw4j5v6zuLSceTeJd0tL6dj8PbvrFl9k/vbMFt+tXIphxWHK7ZehfO73b8XFENy/8h4EQ6VIqz7+lSBeoPX0hiQ/f7cfaI/gnZ7u9yjRx7uian0QiltTcXdoj27ZEEb1oBZ63d10wdI8MnOwHu6TTGGcZVeGLsrdHw/Xql/dTHi77/w3KuDwIh2uRxL/R2RbmcWbFs63QaPI6bgeitcmjCO9OsnuTqx1F59Kx3bYSU6bS3a7PTqRWTsxu9xcThBgt\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Attach tags to a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/bookmarks/{bookmarkId}/tags\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nAttach one or more tags to a bookmark. Tags can be identified by ID or name. If a tag name is provided and the tag doesn't exist, it will be created automatically.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the bookmark.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The tags to attach. Each tag must have either a `tagId` or a `tagName`.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"tagId\":{\"type\":\"string\"},\"tagName\":{\"type\":\"string\"},\"attachedBy\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\"],\"default\":\"human\"}}}}},\"required\":[\"tags\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The IDs of the tags that were attached.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"attached\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"description\":\"The unique identifier of the tag.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"TagId\"}}},\"required\":[\"attached\"]}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Bookmark not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/check-bookmark-url.api.mdx",
    "content": "---\nid: check-bookmark-url\ntitle: \"Check if a URL exists in bookmarks\"\ndescription: \"Check if a URL is already bookmarked. Uses substring matching to find candidates, then normalizes URLs (ignoring hash fragments and trailing slashes) for exact comparison.\"\nsidebar_label: \"Check if a URL exists in bookmarks\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzVVtuOGzcM/RVCeUmMWXtT9GkeAjhJU2wToEGyi6LYLGB6RM8o1kiKqNldxzDQj+gX9ksKai7rvfSh6FOfPNaFPDwkD7VXPlDEZLw706pUVUPV9rX32xbj9iJaVShNXEUT5Igq1Rs5AGYDCBefPoBhQBsJ9Q7Wwy3Sc7hgYuBuzSkaV0OLqWrkI3nYGKehQqeNxkRcQGrIgfOxRWu+E4tZhuemdj7fbZAb2ESsW3KJAZ2GFNFY2WOL3BC/gI2PQLdYJah8GzAa9m6uCpWwZlVeqjEiVleFYqq6aNJOlZd7tSaMFJddalR5eXW4KlTAiC0lipwPcNVQi6rcq7QLpErVx/SImPOGMiPJQyYRsEbjOAHdGk6CdiSI5+pQqEjfOhNJqzLFjv6TtUI5bAValxNmxMC3juJOSTyROHjHxBLDD6en8nPf2a/rr1QlME6bCrPxm4ZSQ1FyM6b5OL2r8c+ZXsneynXWrqQqnE+w8Z3TgqryLpFL4hBDsNm4d4uvLF6fYNZnHKpQIUpVJtNjvnP2VBbENa4t/TOPZ2/Bb3Ioj8grwEcQC4L9KFgJ4yhgdbiXsMtjSFeHg+z+ePryMbEXDrvU+Gi+k4a//vgzu3idKw6S35ITX61hNq4uwLhrtEZnTHQbxNcDFhPdpkWwaJ7mb+KEbrENQsk9BOrQQ20pNV6avaZMNkrtq8VUUItccSd9MTHF67EVZKVUe9Q6EvNhgcEsrl+qQl1jNJKDnK5hu2djg51NqlRNSoHLxSLF3XyLEbdEYY4hPNlGg4Uxa++H89BjkTCOuvizsDAUylEvT6SIZ4kjH1PlcEgVw8c7ER5B+Mtv5znNxm28XJeoe0gv56fzUxETkzKnE57lx7NH+KdNUUZgspuTxnMSdqaKkgoUHRPdPDHpxGKimKMzFc3hvDEstgGt9TcMO9+JDrTosL4zwgVYw0kEFGsuoDF1Y03dyAoyU/51GtZYbbvAEKKvI7YtJlOhtbv5F/fFPXsGQhe5NDSnLC6tBXI6eCOCO5Q94P3CDeJDg3E5RavlUGXZyAoaQk1xDr/7TrQeanIyZQjQ5ci2tINN9O39/N7QGi7OoHOaIsxmnylJszK8ynfe045nsxH2R6yNmyB/MCKNE2buQvAxQdVF9vFkjQI1TDfg2iCs+s1VJmllTWvSCrJqwt0EkGQQjAoKxlW20ySZXTm6TW8GExtDtm9woQVMAuSel9HJZDKPQEpVk/fFiACjOSxHFb1G21GeaPd8DOpM2bDkIxI4D62PGWBnE4/cjNMOzneBWBan+ZezsSbwjvr2ikQgncLlF3cCs5k1bjub5ViWWQ3HeoMbkxrwuc7RQhXxxpKGlhJqTDjvr4tCTdezUoEsiaDScCQX53jGQResR00aNsYSPDetFLmP8PHtuxd5UAbPqcWseMOYe/AGyarOUopTbzxsy/2div7fXjC9ih0p/6HohXg/KPfdNJKw77T7qlCiPHJgv5cWuIj2cJDl/m0giq4NizJpVW7Q8sPpecza80/D+HsB//Jt8mQEW9pN75Vc8KpU+bUyjpL8GitULyQZa39lWVUU0tGtRy8LsTKNuJ9/OleFwvsj4cEIyNaHLXS7I9v7fX/iXCTvcFAj7iyB6iCj/2/jS/aa\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Check if a URL exists in bookmarks\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/bookmarks/check-url\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nCheck if a URL is already bookmarked. Uses substring matching to find candidates, then normalizes URLs (ignoring hash fragments and trailing slashes) for exact comparison.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The URL to check against existing bookmarks.\"},\"required\":true,\"description\":\"The URL to check against existing bookmarks.\",\"name\":\"url\",\"in\":\"query\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object indicating whether the URL is bookmarked. `bookmarkId` is `null` if not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarkId\":{\"type\":\"string\",\"nullable\":true,\"description\":\"The ID of the existing bookmark, or null if the URL is not bookmarked.\"}},\"required\":[\"bookmarkId\"]}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/create-backup.api.mdx",
    "content": "---\nid: create-backup\ntitle: \"Trigger a new backup\"\ndescription: \"Trigger a new full account backup. The backup is created asynchronously — use GET /backups/{backupId} to check its status.\"\nsidebar_label: \"Trigger a new backup\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzFVsGO2zYQ/ZUBc0hraO2k6EmHAt60KbZJ0UXjRVFsFvBYHEuMKVLhkLvrGAL6Ef3CfkkxlOy1Nz701pMEznD4ZubNI3fKdxQwGu+utCpVFQgjXWK1SZ0qlCaugunErEq1CKauKQCCowdYJ2sBq8onF2GVd0xh0dD4D4ZhiKYBeeuqJnjnE9st/PPX35CY4OefFjAbvHm2G36udA/RQ9VQtQETGThiTDxVhYpYsypv1YCO1V2hmKoUTNyq8nanVoSBwjzFRpW3d/1doQJx5x0Tq3Knvnv1Wj6nKQ2xBqDGO3hAhjikSfokHb/6RFWUrALFFBxpeDCxAYSXHTltXP3yCGvlXSQX5UDsOmuqHH72ieXUneKqoRblL247UqUaoqtCdUEaEs2A2egjH47BuFr1hUpM4eq8CZkpnrMVyiVrcWVJlTEk6oux2Xoez0Zi84WODC61KwpiWHm/aTFs3kjnz3oMdTiHgVxqpYdjyVShOFUVMatCrdHYFEjd9YWiEHz4lZixpv+QSy+9/pxMIC3RjVaHIj2V5DjhMb3nyRyg3/W9BP3+HGduHKbY+GC+kM5cjg3BZSYfRL8hJyRpDbNxdQHG3aM1ugAfgB47gfiMIJEe46yzaM5T46l2j9h2kvIJAtUPUFuKjZcR7jxnIqHMgdrPl6RG4Z4C51lJwapS7VDrQMz9DDszu3+tCnWPwUhhc/dG81CCNSYbVamaGDsuZ7MYttMNBtwQdVPszshFQzBGAL/OVXo3+sOARbAfTfEHSX04+XiWD5WQkyWP7KbK0UmamH/e+tCiIPzlj4WSkhi39rJdsh4gvZ6+mr4SKTExF/KAZ3599RX+g9EwIDDZ9UXjOUp1YE8b42pApyEQ6gsTLyxGCjk7U5Hoh2GJDWitf2DY+iTq1qLD+ikIF2ANRy5AJK6AxtSNNXUjK5m88nV6VCKGLvg6YNtiNBVau51+dB/dixcg5SIXR7GRxbm1QE533rgowpVHBPCUrZ2cocG43KLlfKRWDrKEhlBTmMKfPkGFDmpycmEQoMuZbWgL6+Db0/4+0ApuriA5TQEmkw8Uo3E1ww95zzva8mSyh32NtXEHyO8NxyPMnLrOhwhVCuzDxQoFanfYAfcGYTkYl7lIS2taE5fwOVHYQocBW4oUeBDz/Y0AxlU2aZLOLh09xjdjiLUhO0y1lAVMBLkRpC77Qw4hpZNrilWT7RJEgNEU5rAUfVrCPdpEsPbh9AzjtPSIcmDpRyBwHlofMsBkI+9rczlSBBbbjlgW9yucu7Ei8I6G8QpEIJPC5Ud3AZOJNW4zmeRc5nDz+/sD34aLy2eeo4Uq4IMlDS1F1BhxOmwXWTpsz/IEsgTORxpdMjn3Pg5SZz1q0rA2luAb0wrJfYDrH99+O5V7QaSpxSxzDvMInz4pVmffHbsnsfzfnyCDEB0pttzGoqW7UXBv1erpeSJyIUu7nfD2Jti+l+VMTXmjPMltfrEUahi2rNAb2qpSzauKuph12SY5+qvXhAjoQfuvf/uwkBvvVDef6WQOP5rQbY+C73aDx0J0oe9VMaLIOqF6uRT/Bbudlic=\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Trigger a new backup\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/backups\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nTrigger a new full account backup. The backup is created asynchronously — use GET /backups/\\{backupId\\} to check its status.\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"201\":{\"description\":\"Backup creation was triggered. The backup object is returned with a 'pending' status.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"assetId\":{\"type\":\"string\",\"nullable\":true},\"createdAt\":{\"type\":\"string\"},\"size\":{\"type\":\"number\"},\"bookmarkCount\":{\"type\":\"number\"},\"status\":{\"type\":\"string\",\"enum\":[\"pending\",\"success\",\"failure\"]},\"errorMessage\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"userId\",\"assetId\",\"createdAt\",\"size\",\"bookmarkCount\",\"status\"]}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/create-bookmark.api.mdx",
    "content": "---\nid: create-bookmark\ntitle: \"Create a new bookmark\"\ndescription: \"Create a new bookmark. The bookmark type (link, text, or asset) is determined by the `type` field in the request body. For link bookmarks, if the URL already exists, the existing bookmark is returned with a 200 status.\"\nsidebar_label: \"Create a new bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJztWuFuI7kNfhVC9+cumDjeRX/5xwHO9hbd3rYXXBIURdaA6Rnao7NGmpM0SbyGgT5En/Ce5EBpPDNOxln7Nijawlhg4UikRJHUR3LEtTAlWfTS6A+ZGInUEnq6NGZZoF2KRGTkUitLJhAj8S5MA4KmB5jVVAO4yan5C/yqJPhWSb1MwNOjT8BYQOfIfwfSQUaebCE1ZTBbgc8JpswxhbkklYHUYczSrxU5DzOTrQbw3ljgBZtNXAJyHghvf/4IqCxhtgJ6lM67JIyH31IvWrmkA0u+srzzg/Q5ILwdDsF59JUbiER4XDgxuhPb0zsxSYSjtLLSr8Tobi1mhJbsuPK5GN1NNpNE1HJemmwlRusn2tpVi4GoXN4qNdqT9syCZalkGixw8YtjvrVwaU4FhlmlfpqHvVlJYiTM7BdKvUhEadlyXpJjOi+9ovAjkjlvpV6IROhKKZzxnLcVJaLAx4+kF3yCN8PhcJMItGku7ynrcM+MUYRabBIxx3vDCtg3r43v2XeTCFcVBdpV71xURDb2X5Y4UOODurLSRDs85yBdFWw4ZR6Y39gClZhsEiGL0lh/Tc7V/t0np6ls2qu67bJYSpGIB5qx4RT/LsxMKmKSR0+aFxeJcFIvFM3jhHVObPcXk81mk6yF0XSYLcPsC8eUehmOV1nVRzdnBXgxEpWVfMLSUlAhZeNo6l5NbKIzS8uWvouzcYsJC/91IjMOBJHDj/1muO070R7Rtmt+rWwBmoJw4dfNF8hlgQvevszmLdMe32Jn+DsWey7I0Sdu5Wu3nWz43yZyuNJoF0/9djh8DkjjFo4CAvpcuh4EjYD+IoIeBWIv20f26+4FjNjwDczkXB4MIQfC41ejocfFQurFdYgph8Dx1qtclaYUMGOOUlU2eBjpjLkmDZrKz0HFr798P4r3qGcvqvfR7kXWfWK+AtImonJk99zHGOGbCbQWV8zsqXB/2FH1vguO3mOaU3b5YtBCPmZeFag5Tuzee5mJev2d1QJd5/b9ZwLLUddoB3cOoA+o2ouF+4jH+3C3zxNTS6RdbvwxXGU2P4Z8Xil1xYJFBDlqp6cR+hjme5mROUpQvJfpgXbJfaHetZ725Vwt0h4jzvbkRySDrw+AWPnc2MOMVc2UdDkdRp2hp6ua4zB9MMff6vB2AMN/b952oKj/03nc3rB3KJY5+bm7rK6KGdkXbtYfVnN/8vjVWq/0UpuHntgVGDg3rbd7/cB7kJk5pv2lA2LdYFBbv17pujs+Q63Jfqh95Am0ixp0ma7OjQPgiR4kFzEhuS2VwYw4nuM9emQTt6o7ysP6coRWFZNNL0GbTe/kzp18dye5fZrJ9iegTa5VZ1atkzY2nzRJQ/NZRcRq5e3wzRe+mDygqz+ZZFDjNxtidao9TrXHqfY41R6n2uNUe5xqj1Ptcao9TrXHqfY41R6H1x5/6nscucSseXb+7V//BqnvUUl+kS4rDxl6fMXCIzVZr76fvtcUmOZS0zk/z7AdgKw1Fph9EGoRco795IClQp73dKGaf/DsySkI2K7f0eUPzNgosqeIu9UxosnPlAVN8nv8ZXg6B2+WpPkhqZCOU+lkq+bQJUCPJe//RM8M0helQtmv4fbaPWJRBhG7EgRJg6Z8brjFoTQu2AP5DVxcNC0F7Ghk78m6kM6GPFSsMcssObe5wFJe3L/h64dWsgaDHevpqIQ5VorfXXPvSze6uPB2NViixSVROcCyfGYULnfrFcDEfoYfa3qIsrD0nS6Eaz583Lnbi9DogncOMMNkXB4GIsaK8OP99mX4r/+4CRZnt/25bWH4YavBpqGg1W5bnMbspluOxpFYyLUcTb3WDnUK6+7gzvt+/Yz/7O2+s3Bd3NU1W332UEHU9UNL2/v43QFyqeeGz8t2j0Z5MxgOhqL198Yi46sPzyzYTEoHCI7U/Dw3zocbtvUsfkNFzeCC2bn05wo92WBfmRK/tUrHawMqZR4crEzFzSIFalxQt+NF1d0tuHAJ5HKRK7nIeSTiXRI2mWG6rEoHpTULi0WBXqbIH00+6U/6m2+AHYa0r3GLB8dKAemsNFJ7BzUGAO7e2JL3aLpzpuP6eoVFppATZmQH8E9TQYoaFqS5qYgAdTjZklYwt6bY9fAHmsHtB6h0RhbOzq7J83Ozg+8Dz4+0cmdnW7GvcCF1I/JH6XxHZleV7CuQVtYZez5DFrVsOOBeIkzj5DQoaapkIf0Ufq3IrqBEiwV3JdUP39t3dJA6VVVGbNmppkf/rl4idioxsrFaQHpAF/Wy3aRZki05J5/mYZ4XYcFoAGOYcmydwj2qimBu7O4eUmdsIwoLsz0sgTZQGBsErJR3W91sgxtwEHY82HQwBWvMCIymCDCWKDRoudEnfQ5nZ3xpzs7CWcahE2C3QcAEP0cF9R2CgjyGUBjZGZob9gDRoeULGApqkuCcWxoNVZ2MAGcd8G3IhBn6r/78/rsQ0Rie+YPEaF1/3ehvOnt6E9dtxPg/71KLaNcJiu1HkxjT7sSs28HGeMSD6zVfjFurNhseDr7PbWxtRAtNbYmItzkEwSUxINf563mdwQePDej7JP/hjD5yjNOUSv8i7aQTkq9+ur4JCW3soitCciQschzg/0dCJCL6Yoh9YWwtFOpFFbIfEdfc1NV09wPpbgTsfqlCvepIuF5HihvGu81GJPVRAv6J0GPzO29NTqA=\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Create a new bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/bookmarks\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nCreate a new bookmark. The bookmark type (link, text, or asset) is determined by the `type` field in the request body. For link bookmarks, if the URL already exists, the existing bookmark is returned with a 200 status.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The bookmark to create.\",\"content\":{\"application/json\":{\"schema\":{\"allOf\":[{\"type\":\"object\",\"properties\":{\"title\":{\"type\":\"string\",\"nullable\":true,\"maxLength\":1000},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"note\":{\"type\":\"string\"},\"summary\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\",\"nullable\":true},\"crawlPriority\":{\"type\":\"string\",\"enum\":[\"low\",\"normal\"]},\"importSessionId\":{\"type\":\"string\"},\"source\":{\"type\":\"string\",\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]}}},{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\",\"format\":\"uri\"},\"precrawledArchiveId\":{\"type\":\"string\"}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\"}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\"}},\"required\":[\"type\",\"assetType\",\"assetId\"]}]}]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"A bookmark with this URL already exists. The existing bookmark is returned.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"userId\":{\"type\":\"string\"},\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"attachedBy\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\"]}},\"required\":[\"id\",\"name\",\"attachedBy\"]}},\"content\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true},\"description\":{\"type\":\"string\",\"nullable\":true},\"imageUrl\":{\"type\":\"string\",\"nullable\":true},\"imageAssetId\":{\"type\":\"string\",\"nullable\":true},\"screenshotAssetId\":{\"type\":\"string\",\"nullable\":true},\"pdfAssetId\":{\"type\":\"string\",\"nullable\":true},\"fullPageArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"precrawledArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"videoAssetId\":{\"type\":\"string\",\"nullable\":true},\"favicon\":{\"type\":\"string\",\"nullable\":true},\"htmlContent\":{\"type\":\"string\",\"nullable\":true},\"contentAssetId\":{\"type\":\"string\",\"nullable\":true},\"crawledAt\":{\"type\":\"string\",\"nullable\":true},\"crawlStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\",\"nullable\":true},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true},\"size\":{\"type\":\"number\",\"nullable\":true},\"content\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"assetType\",\"assetId\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"unknown\"]}},\"required\":[\"type\"]}]},\"assets\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"pdf\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"avatar\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"userId\",\"tags\",\"content\",\"assets\"],\"title\":\"Bookmark\"}}}},\"201\":{\"description\":\"The bookmark was created successfully.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"userId\":{\"type\":\"string\"},\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"attachedBy\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\"]}},\"required\":[\"id\",\"name\",\"attachedBy\"]}},\"content\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true},\"description\":{\"type\":\"string\",\"nullable\":true},\"imageUrl\":{\"type\":\"string\",\"nullable\":true},\"imageAssetId\":{\"type\":\"string\",\"nullable\":true},\"screenshotAssetId\":{\"type\":\"string\",\"nullable\":true},\"pdfAssetId\":{\"type\":\"string\",\"nullable\":true},\"fullPageArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"precrawledArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"videoAssetId\":{\"type\":\"string\",\"nullable\":true},\"favicon\":{\"type\":\"string\",\"nullable\":true},\"htmlContent\":{\"type\":\"string\",\"nullable\":true},\"contentAssetId\":{\"type\":\"string\",\"nullable\":true},\"crawledAt\":{\"type\":\"string\",\"nullable\":true},\"crawlStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\",\"nullable\":true},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true},\"size\":{\"type\":\"number\",\"nullable\":true},\"content\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"assetType\",\"assetId\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"unknown\"]}},\"required\":[\"type\"]}]},\"assets\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"pdf\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"avatar\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"userId\",\"tags\",\"content\",\"assets\"],\"title\":\"Bookmark\"}}}},\"400\":{\"description\":\"Bad request — invalid input data.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/create-highlight.api.mdx",
    "content": "---\nid: create-highlight\ntitle: \"Create a new highlight\"\ndescription: \"Create a new text highlight on a bookmark. Highlights are defined by character offsets within the bookmark's content and support color coding.\"\nsidebar_label: \"Create a new highlight\"\nhide_title: true\nhide_table_of_contents: true\napi: eJztV9tuGzcQ/ZUB89BWWF1S5GkfCig31E2AGImNonAMaLQ70jLikgzJta0KC/Qj+oX9kna4V9lKkAc/FEXfFkty5syZ4ZnhQRhLDoM0+iwXqcgcYaCf5bZQclsEkYicfOak5R0iFS/iOiBouoVAdwGKbi8YDQhrY3Ylut0MeiMe0BHktJGacljvISvQYRbIgdlsPAUPtzIUUkMoqDfwnYfM6EA6AOocfGWtcQEyo4yDzORSb2ciEQG3XqRXYvAmrhPhKaucDHuRXh3EmtCRW1ahEOnVdX2dCEefK/Lhucn3Ij3cC/GioFFQwUDDSQJSZ6piv0c44exl0jDRBpNEvCaaQ9UAnmsTiOG2IbFXtFbJLFI//+TZ9UH4rKAS+SvsLYlUmPUnyjgN1nGigiTPq51zzlm/1wcn9VbUifABXXgX4YzWdVWuyfE66fwrqxHxQ7t8rCqZ6z0pZW4F85iLRGwdkRaJWKuKmPycNlipINJuY50IJuiUSV0phWtFIg2uojoRTNQ3bKybJEpGkF6N+TiOfhxri6L1cV3XjRVvjfYNrT8unp6uh6YE8qEu/s/l4+UyEfJ06JUn9wVW2oQsTwB5hNqIiHr/Y2/XiQgyMPRBcURTSc8Wi4fF8xxzaNUG/vrjT5D6BpXMe+EzDkrpPYtKhxk2klTuH7HCMpOfzMMx1CWUmBVS09QR5pwfIOcasaUZk16S97j9JlNFVaK+b6g9P3uQoghwsD8i+RUf7Ak+cTsvNVahME7+/k9vYYZZm59HxYdgdqRB+o7ipKM/Yd7pzrL/ezxzGcytQnma4eH+3GFpI8Qxgh7psxOl0DUMbQJsTKUHvN5SJjeSu2NfrJAb8nEv3Un/mILznyiH6D8UhocWa3yMErnDi3kxjAI8Cbgbcj4OApVTIhUHzHNH3tdztHJ+81Qk4gadZGCRnna5SWCnfkUI1qfzeXD72Q4d7ojsDK19ECt3i9YCmE3M7pt2PzRYGP5oRPnAWWs7wWhQ6TllzxxH3CbSdhNLdPx4bVyJjPCXXy8ikVwN74f55lVXp8eNZkjVUX9ZHPWTRd8/hjbRiP9Iy6PGj5RZ6o1hb8x6Q8nT2WK2EEMSez6W52cP+OsXpQcET2ozLYwPsWy6AFgtecbieprKMFXIsySzKzOawUUhPdsGZMwe9qbiMa5EjdvBiE9ASc/jGo+QydDaeYDzwyC3xmxXWQ/Wma3DssQgM1RqP/uoP+onT4DTRTq0l5F/LpUC0rk1UgffyTrgsSpZ9pFDO/Sulq2ERCMrKAhzcjP4zVSQoYYtaZ7SCVDHyHa0h40z5XF93dIaLs+g0jk5mEw+UAhSbz38FM+8ob2fTDrY57iVuof8VvowwtwP25Xzxk3XyFBtfwJuJMKqWVxFklZKljKs4HNFbg8WHZYUyHlOBkE3Y7UTNHFmV5ruwovWROx4UQ2ZFpAB0De8dE56k5zJDYWsiOtshIHRDJaw4rliBTeoKoKNccc+pM45RxQNcz4cgTZQGhcBVir4jpteqS/2ljz/7P74mI01gdHUXG9HBHxTffpRT2EyUVLvJpMYyxIu378d3gn8xhk9CxzeKsqhpIA5Bpw1x/l69cdjG2reFvH90GyJxdnt0VBZZTCPY4Mi+F6WXOTGwfnL1z9EmWZ1LDF2CI1RQo4eccWX3nqHod/8C599jTiOujXPi6zvh7YPXIni6EnIKsJ/Dwcu50un6pp/x4rld+HQBeIrMRHNHYyNY0d7JqHBNeWqiE2DZ+T0YSuuk+7EMsvIhq/uvR71sfN3Hy5Y2NtnaRn7tHAYx3O8FakQiWgqKPaL+O8gFOptFRuxaGxyG8DjLnKva8So2iXU+xHCw6HZccEqVdciaUOJqiVqfjX9DdD6rAk=\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Create a new highlight\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/highlights\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nCreate a new text highlight on a bookmark. Highlights are defined by character offsets within the bookmark's content and support color coding.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The highlight to create, including the bookmark ID, text offsets, and optional color/note.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarkId\":{\"type\":\"string\"},\"startOffset\":{\"type\":\"number\"},\"endOffset\":{\"type\":\"number\"},\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"],\"default\":\"yellow\"},\"text\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"bookmarkId\",\"startOffset\",\"endOffset\",\"text\",\"note\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"201\":{\"description\":\"The created highlight.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarkId\":{\"type\":\"string\"},\"startOffset\":{\"type\":\"number\"},\"endOffset\":{\"type\":\"number\"},\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"],\"default\":\"yellow\"},\"text\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\",\"nullable\":true},\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"}},\"required\":[\"bookmarkId\",\"startOffset\",\"endOffset\",\"text\",\"note\",\"id\",\"userId\",\"createdAt\"],\"title\":\"Highlight\"}}}},\"400\":{\"description\":\"Bad request — invalid offsets or missing required fields.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Bookmark not found — the specified bookmarkId does not exist.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/create-list.api.mdx",
    "content": "---\nid: create-list\ntitle: \"Create a new list\"\ndescription: \"Create a new bookmark list. Lists can be manual (bookmarks are added explicitly) or smart (bookmarks are matched automatically by a search query).\"\nsidebar_label: \"Create a new list\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzNV19vGzcM/yqE+pIal4szbC9+GOBkLdC1wII2wTCkBkzf0T7VOkmVdHE844B9iH3CfZKBuj8+O266AR2wl8QnkRT5I/UjtRPGksMgjX6Ti4nIHGGgd9IHkYicfOak5U0xEddxCxA0bWBhzLpEtwYlfUiBFTxkqGFBUKKuUMFZJ+MBHQHmOeVAj1bJTAa1fQnGgS/RhWPJEkNWUA5YBVNikBkqtYXFFhA8ocsK+FyR275MRSICrryY3IvogJglwlNWORm2YnK/EwtCR25ahUJM7mf1LBGOPlfkw5XJt2KyO4rwtqAYDwQDDRApvO695B2fAMI8Hj+HpSSVg/TARqWjvJFuw2/FO+Gy8gG0CQyQp8C+Z0YH0oH9QMuwxDRcfPLszE74rKAS+VfYWhITYRafKOO8WMdJC5I872osaSDlg5N6JRJRSv2O9Ipjv0xEiY/913hcHyX3We3xgfYPUVtmp9TqpF14ao90VXKiGnREIiKonLGcllipICbdXp2ICNrXgqoTYdGRDly5T0V1pRQuFIlJcBXVdZN8zhP7EVFr45jVdbPtrdG+QfW78eXpAmkKI28K/9tlUZ6IoU6+kNyvp+8o+GcS9i8g/M+z++Q8Wy2UzAaiC2MUoWYrBfprw/LGYTDOn5aqPLn3Rj3rtdlocvydy2D4x4OkTVxpz58dV4/MRXJQQgMce60TLg78mSUiyMCORfISTQ1+Px4/LbsrzKElLvjrjz9B6gdUMgepbRUgx4BwRukqTQZMBaX0XupVw5UJHBITbGQoAAdE+o3KODP5SagPA5pCiVkhNZ07wpwzDuScccDqKaetJO9x9Y9MFVWJ+thQq5+K48xFB/f2B1l4xYp9Gk7c/juNVSiMk79THvMQCoKr2GIgmDVp7gUt7EmXpIg8PdrYHw5xDvQYLqxCeRrhfZk+Ymmji0MPoqcRqVAYbt3WxKZtkclRXMT+wzeR3APx7bjficopMRE7zHNH3tcXaOXFwyUXPDrJ6MUcttsNAN3tLUKwfnJxEdw2XaPDNZFN0donCWGObC2AWUaM3rby0PjCng869QcOvDl52K97HPhkjiOK8eWOQiJpf7w2rkT28Odfb2O2uWTf79v8qw69rlN+qZL65YYq99+tHz23tfS1F9hT6IBXpV4aPpTBbw64TMfpWOwLrodlevPmiTf9pvRx7lHL88L4EEu8G5f4eqNmbsD8XIZzhYFcBFlmlMJtIT3bBlTKbDxsTcWTTYkaV3sjPulGFZ6lEijkqlByVcRZx3uK/3UOC8zWlfVgnVk5LPvBLP2oP+oXL4CzRjq0xMGLU6WAdG6N1KEfkgAPr4zlM5jKYqXMp219RyNzKAhzcin8Zqo4Xa5I87RKgDpGtqYtLJ0pD8tsQwu4ewOVzsnBaPSBQpB65eHHqPOWtn406ty+wZXUvcvMxAOffWWtcQGyynnjzhfIrtpeAx4kwrzZnEeQ5kqWMswbXgWLDksK5Dwng6AbMEDqTFU5cWbnmh7DdWuimSiZWhgWkAHQN7h0h/QmOZNLClkR99kIO0YpTGHObXQOD6gqgqVxh2dInXOOKBrmfDgCbaA0LjpYqeA7bK66If92a8nz4lU/qLezvtHU3HJHBHxR/OSjPofRSEm9Ho1iLFO4e/9u/2KIrcfEOkcFmcONohxKCsidLG3UmRt79ciRwEs8QVMrEouzk9FQWWWQnxhLqQjOZMlFbhzc/PT6ZWwpzI8l6gERHLxo1Ik3z25P1//T509DTYNOwiMPs/yubQT3QnVPI+YPXtjtuJDvnKprXm7p7H62bwPxtZSI5vbFzrEmZrzrBo1zrofYNVTFxz8ZGOqk05hmGdnwrOxs0MNufvlwy8zePs/KOE0IhxuRxL8TIRLR1E5sGHFtJxTqVRXHBdHY5D6Ah23kqG3EqNot1NuBh7tdI3HL/FTXImlDiXwlan4s/A0IJEx+\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Create a new list\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/lists\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nCreate a new bookmark list. Lists can be manual (bookmarks are added explicitly) or smart (bookmarks are matched automatically by a search query).\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The list to create. For smart lists, a `query` field is required. For manual lists, `query` must not be set.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\",\"minLength\":1,\"maxLength\":100},\"description\":{\"type\":\"string\",\"minLength\":0,\"maxLength\":500},\"icon\":{\"type\":\"string\"},\"type\":{\"type\":\"string\",\"enum\":[\"manual\",\"smart\"],\"default\":\"manual\"},\"query\":{\"type\":\"string\",\"minLength\":1},\"parentId\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"name\",\"icon\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"201\":{\"description\":\"The created list.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"description\":{\"type\":\"string\",\"nullable\":true},\"icon\":{\"type\":\"string\"},\"parentId\":{\"type\":\"string\",\"nullable\":true},\"type\":{\"type\":\"string\",\"enum\":[\"manual\",\"smart\"],\"default\":\"manual\"},\"query\":{\"type\":\"string\",\"nullable\":true},\"public\":{\"type\":\"boolean\"},\"hasCollaborators\":{\"type\":\"boolean\"},\"userRole\":{\"type\":\"string\",\"enum\":[\"owner\",\"editor\",\"viewer\",\"public\"]}},\"required\":[\"id\",\"name\",\"icon\",\"parentId\",\"public\",\"hasCollaborators\",\"userRole\"],\"title\":\"List\"}}}},\"400\":{\"description\":\"Bad request — invalid input data (e.g., smart list missing query, or manual list with a query).\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/create-tag.api.mdx",
    "content": "---\nid: create-tag\ntitle: \"Create a new tag\"\ndescription: \"Create a new tag. Tag names are normalized (trimmed and converted to the user's preferred tag style).\"\nsidebar_label: \"Create a new tag\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy9VlFvGzcM/iuE+rDWuNjtsKd7GOB2K9C1wILVwTCkAUyf6DvVOkmVeE5uxgH7EfuF+yUDdWfHSYNhD8NeYoeiyI/kx08+KB8oIhvv3mlVqioSMq2wVoXSlKpogpypUr3JJ4Dg6BYY6zmssAaHLSXASOB8bNGa30nDc46mbUkDOg2Vd3uKTBrYAzcEXaL4TYIQaUsxih1rSNxbejFXhWKskyqv1Uo+bwqVqOqi4V6V1we1IYwUlx03qry+GW4KFelLR4lfe92r8vAI86qhHF1QSvqxOslSecfkWK5gCNZUuQWLz0nuHVSqGmpRvnEfSJXKbz5TxapQIUrD2FCSUwl85pU4GlerYRhxmUhaSsleN8Mw2lPwLo3Xv3356mnQI9Dcmv8QrdFPYC3+ZRFGq+JBJd89Bf7KYceNj5kHf/3xZ5746zw1YL8jByZBa1Iyri7AuD1aowvwEeguSKpH5TLd8SJYNE8XOmEtFN1hGyw9QqCGEWpL3Hihd/AptwWFQGqRuSYUi3uKKTOsi1aV6oBaR0ppWGAwi/0rVag9RoMbO3ZyOh7r32JnWZWqYQ6pXCw49vMdRtwRhTmG8NUqyYSnCOC3uUXvJ38YsQjwM+5/lLrHzOcbcGqDZJY6spsqJydVTF/eymoKwp9+XeW5CnF+uV+cH4/NOxL6jBzGbb0cSING9K/mL+cvZVEN54afoC8v331V6unQJEBIZLcXjU8sjYSN97sW4864OitFJNQXhi8sMsXcCFPRHFaNSRIb0Fp/m6D3nexyiw7r+yCpAGsSp0J2JhXQmLqxpm7EgilR/nQaNljtuiDy4+uIbYtsKrS2n39yn9yzZyCdJcfTiolxaS2Q08EbxwmmlQB8yOogOTQYl6e5Xk4UzEHW0BBqinP4zXdQoYOanIguAbpc2Y562EbfPqTCLW3g6h10TlOE2ewjMRtXJ/g+33lPfZrNjrAvsTbuBPmDSXyGOXUh+MhQdTH5eLFBgRpON2BvENbj4To3aW1Na3gNXzqKPQSM2BJTTDIMgqOEgXGV7bToP6wd3fGbKcTWkB23X9oChgHT2JdjklNImeSWuGryuQQRYDSHJaxdZ+0a9mg7gq2PD3MYp2VGlAPLPPITBK2PGWBnOR1783qiCKz6QEmMR0vK09gQeEfjJkYikKVK5Sd3AbOZNW43m+ValnD1y4cT3+DWcAM+8xwtVBFvLWloiVEj43y8LvJ1up5lDMQEzjNNLpmcRx8HXbAeNWnYGkvw3LRCch/h8oe3L+aykCJhLbqzZX38Mj9ewsO9oP5/r/goTGfyPRSjth4m9b0eX/qbQokgyP+HgzDzKtphEHMmnzzz99qbH/1CjeuU5XpHvVQ11nchA85SbTvJ/tVbORTHG8uqosD/6Htz9m5c/vxxJXI6/cpovZY7EW9Vkf+WShVqJENW6Ww7KIuu7rAW3zGmiC8+1O5HWp2rmo7Q9WcID4fRYyWCMwyqmErJAqQGeZX/Bt1jblI=\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Create a new tag\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/tags\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nCreate a new tag. Tag names are normalized (trimmed and converted to the user's preferred tag style).\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The tag name to create.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"}},\"required\":[\"name\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"201\":{\"description\":\"The created tag.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"}},\"required\":[\"id\",\"name\"]}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/delete-backup.api.mdx",
    "content": "---\nid: delete-backup\ntitle: \"Delete a backup\"\ndescription: \"Permanently delete a backup and its associated archive file.\"\nsidebar_label: \"Delete a backup\"\nhide_title: true\nhide_table_of_contents: true\napi: eJylVttuGzcQ/ZUB85IIa8kuUiTYhwJK4wBugsJIbBSFI0Cj5Ug7FpfckFxZirBAP6Jf2C8phru62WlaoE9akTPDM5dzyK1yNXmM7OyVVrnSZCjSGyyWTa0ypSkUnmvZVrm6Jl+hJRvNBjpDQJglW0CrgWMADMEVjJE0oC9KXhHM2dBQZSriIqj8TnXRg5pkKlDReI4bld9t1YzQkx83sVT53aSdZKpGjxVF8iEZhKKkClW+VXFTk8pViJ7t4gnOm5KgsfylIWBNNvKcyYObQyypxyt4aI1VbSQOE2uz3iyqh/vXr9z664/lOkZXvBbQHJNJB/pKqzZTnr407EmrPPqGMmWxEpPZziRTLDBqjKWSNDyF2tlAQaD/cP5Sfk4R/+qgcDaSjfDXH38e4YQHDH2tNYSmKCiEeWPMZihAXp5fPI11a7GJpfP8lfQ+2ptUW4huSRY4QMUhsF1kwHaFhnUGzgOta0lLatOjSbWmdRzVBiWn7/XgUM5jBKpt24T0G1l3NQXrIsxdYx8fjHVtuEizOboP7tvHu9k9FVFlqvYyyZG7KhdO078PyhgqLEq2dOYJNc4MAXnvPIh7qnBFIeDiP4Uqmwrt40C9/1C1J3Nz1wE8xJ8cJu1SHFPZ0vmxdAdiSp4yVbkadfMRRtvd2LVK+ORXO7Y03qhcbVFrTyG0I6x5tLpQmVqhZ4GYCtVvd82ZY2OiylUZYx3y0Sj6zXCJHpdE9RDrp4ogTOsj7Pj1vreHDoskckT0T9K/7uRjuu+rKydLHslMOJWMVNZ/vHO+QkH4y283qaRs507cJesO0sXwfHh+RNw9nvH11RP8+00OgBDIzM9KF2Jq4My5ZYV+yXaR1E06e8bxzGAkn7LjgoZwU3KQ2IDGuIcAG9dAdFChxcUhSMjAcIghA1HBDEpelIYXpaxgCJR+re5ZH6D2buGxqjBygUL3z/azffYMpFyiaB0tZHFsDJDVtWMbA/QjBnjK+FrO0MA2tWg67umZgkyhJNTkh/C7a6BACwuycicQoE2ZLWkDc++q0/4+0Axur6CxmjwMBp8oRraLAD8ln/e0CYPBDvY1LtjuIX/gEI8wh6aunY9QND44fzZDgVrvPWDFCNNuc5qKNDVccZzCl4b8Bg6XhDSDYKe2wLYwjSbp7NTSOv7ch5gzmU4ZpSzAETB0ddkdsg8pnZxTLMq0L0EEGA1hDFPbGDOFFZqGYO786RlstfSIUmDphyewDirnE8DGxLCrzZt+ROBmU1OQxd1KSN2YEThLHb08EQhTQv7ZnsFgYNguB4OUyxhuP37Yzxs8cCzBpTlHA4XHB0MaKoqoMeKwcxdp37sniQdZEkWm3iQN587GQlMbh5p0utThOVcy5M7D9dt3L5Jg1i7ECpNW99fi29OXwmMObg+S/78fGJ2GHF1YbdbJ4LaXzbv+kg4qU/n+vp5kSlgv29utjN+tN20ry2nC5DVyUM2krZqDfGuVz9EE+k5Kzz/2ov8C/glhv4h2k8TZNPJPZWpJm+NXRTtpM9VRNWHotsdFQXU8cjyOPTm6QN5efri8uVSZwlPFfaSwKfQ3EW23ncWNKErb7gEmhRF0bfs3Y8WbRQ==\nsidebar_class_name: \"delete api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Delete a backup\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"delete\"}\n  path={\"/backups/{backupId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nPermanently delete a backup and its associated archive file.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the backup.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BackupId\"},\"required\":true,\"name\":\"backupId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"204\":{\"description\":\"No content — the backup was deleted successfully.\"},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Backup not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/delete-bookmark.api.mdx",
    "content": "---\nid: delete-bookmark\ntitle: \"Delete a bookmark\"\ndescription: \"Permanently delete a bookmark and all its associated data (tags, highlights, assets).\"\nsidebar_label: \"Delete a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy1Vt1u2zYUfpUD9qY1FDsdOqzQxQB3TYGsxRC0CYYhNeBj8chiTZEsSSVWDQF7iD3hnmQ4lCzbadrtZleWyfPznb/vcCesI49RWXMpRS4kaYr0ytpNjX4jMiEpFF45FhC5uCJfoyETdQu9KCCsBmlAIwG1BhUDYAi2UBhJgsSI8DTiOmRQqXWl1bqKIWMRiuHZVGSCL0V+K/aOg1hkIlDReBVbkd/uxIrQk583sRL57aJbZMKhx5oi+ZAEQlFRjSLfidg6ErkI0Suz/iqE64qgMepzQ6AkmahKRR5sCbGiMRTGRFusnWZLipTU23Zd3396+ZPdfvmx2sZoi5cMXMUksgd+KUWXCU+fG+VJijz6hjJhsGah1UEoE4rBOIyV4GA8BWdNoMAB/HD+gn9Ocf9mobAmkonw959/naCFewxDOSSEpigohLLRup0ymBfnz7+2dmOwiZX16gvJ0d6rlGOIdkMGVIBahaDMOgNl7lArmYH1QFvHoXGGBjwp57SNM6eRo/peLQ5JPUYguq5LSB+Je59ZMDZCaRvz0DU6p1WRWnj2KdjHAdjVJyqiyITz3PBR9ZkurKR/b5k51FhUytCZJ5S40gTkvfXA6inHNYWA6/9kqmpqNA8NDfpT0Z10z20P8GB/cei4C1ZMiUv+Y2UP88txcmflYrbvkTDbHdqvEzxd/m4/O43XIhc7lNJTCN0MnZrdPReZuEOvGGZK1nDdl6jERkeRiypGF/LZLPp2ukGPGyI3RecenbvBwn7a3g7y0GPhYI7G/gPXsPd8PPxjhtkzx5HEeLqSkMiGjzfW18gIf/39OqVVmdKyOkfdQ3o+PZ+eHw3xiGd+dfkV/vFSBUAIpMuzyoaYirjPrDLrRIJc3TMVzzRG8ik6VdAUrisV2DaTpL0P0NoGooUaDa4PRkIGWgUmyG9RZpacrLDYNC6A83btsa4xqgJ56D+aj+bJE+B0Mb/1o8GHc62BjHRWmRhgaDPA07l37EOCMqlEy/kwpMnIEipCSX4Kf9gGCjSwJsPrgwBNimxDLZTe1qf1vacV3FxCYyR5mEw+UIzKrAP8nHTeUhsmkz3sK1wrM0J+p0I8whwa56yPUDQ+WH+2QobqRg24UwjL/nKZkrTUqlZxCZ8b8i0cVgYXg2DPuqBMoRtJXNmloW38ZTBRKtI9P3JaQEXA0Odl72Q0yZUsKRZVumcjDIymMIelabRewh3qhqC0/tSHMpJrRMkw18MTGAu19Qlgo2PY52bkwuvWUeDDcWGmaqwIrKF+vDwR8KSE/KM5g8lEK7OZTFIsc7h5/+5oe6hYgU19jhoKj/eaJNQUkTf3tFdngh/VE9EDHzEr0yCSmnMvY6Bx2qIkCaXSBE9VzU1uPVy9fvMskaazIdaY+HpYkK8fPikeTuHuQPz/31ukp5ejjdZlPUPuBla9HTd5EJnIj9b6IhNMCiyy23F33njddXycGpCfLgdSTdQrVeBvKfISdaDvxPv0/bAXnsG3UA6HaNrE3brhfyITG2pPnx/dostEP8sJRS8wLwpy8Uj12PriaMu8vnh3cX0hMoGnlPyAgpPpRzHtdr3ENVNO140QEwUxuq77BxUht/M=\nsidebar_class_name: \"delete api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Delete a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"delete\"}\n  path={\"/bookmarks/{bookmarkId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nPermanently delete a bookmark and all its associated data (tags, highlights, assets).\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the bookmark.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"204\":{\"description\":\"No content — the bookmark was deleted successfully.\"},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Bookmark not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/delete-highlight.api.mdx",
    "content": "---\nid: delete-highlight\ntitle: \"Delete a highlight\"\ndescription: \"Delete a highlight by its ID.\"\nsidebar_label: \"Delete a highlight\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVsGO2zYQ/ZUBc0kMrb0pUjTQoYDT3aDbBGiQ7KIoNgt4LI4lxhSpkNSuFUNAP6Jf2C8phpJlee2kOfRkQxwO38x788itsBU5DMqaKylSIUlToF9VXmiVF0EkQpLPnKo4QqTiIq4DQrELgWUDKni4upiKRATMvUhvxZDBi7tEeMpqp0Ij0tutWBI6cvM6FCK9vWvvElGhw5ICOR8DfFZQiSLditBUJFLhg1MmP8JyXRDURn2uCZQkE9RKkQO7glDQHh+jog2WleZUipTUmyYvHz69/MluvvxYbEKw2UuGrkIMGaBfSdEmwtHnWjmSIg2upkQYLDmqGEUlQjGeCkMhuB5HvrLGk+cafjg/559j6F2r5aiTyoOjUDtDklFn1gQygXdjVWmVRZpmnzynONEmu/xEGVNWOSY1qA7A0tp1iW7N/D5uaZsIH9CF31crT2G0bupySY7XychvrGZWW3eKKjJ1yUJoSGv7ILgp3KjcERmRiKWuiZUhaYW1DiLdBbaJCLQJp1KaWmtcMkfMRJsIYwN9V6A6XXrtyX2lK5kjDCTnJ4C0B6K4Hff3sJvj3vVV9ZgjouH88Wl3J3Qo2pbPfHH+/FhINwbrUFinvpCEf/76O2r/VZwwCHZNhkVVKu+VyRNQ5h61kglYB7SpuIJHQmOUs0qjOi2xPb3DRI0RDEhfHCMdygFjA6xsbf5PkWdWnpTCIYY5lJgVytCZI5QsESDnrAPePmXeS/Ie8+9KVdQlmseJ+v3TI5VEgPv8I54veWPsXDw/FHZvxFwnu0oqZoNN+Nl2ZD4ti47c/c48a6dFKrYopSPv2xlWanb/XCTiHp1ioLFd/XLH0m4CixAqn85mwTXTNTpcE1VTrKqTxttn2Nntmz4eOixczsj3PzCLvRuN3H/oMZ/MdcQwkfZBbBPxz2vrSmSEv/1xHRurzMrydq66g/R8ej49H5n4gGf+7uoI/7CoPCB40quzwvoQadxNszI5oJHA/J6pcKYxkIvVqYymcF0oz7kB2bU8NLaGYKFEg/k+iU9AKx98AnwvJnur9wmg9xR/jYQlZuu68lA5mzssSwwqQ62b6Ufz0Tx5AtwuvuC64eCPc62BjKysMoFvjSg0wMPRr/gMCcpEihbzfk5jkgUUhJLcFP60NWRoICfDLwECNLGyNTWwcrY85PeBlnBzBbWR5GAy+UAhKJN7+DnueUONn0x2sN9hrswA+a3yYYTZ11VlXYCsdt66syUy1GrYAfcKYdEtLmKTFlqVKizgc02ugf2bgckg2N25oEyma0nM7MLQJvzSp1gp0p1FcltABUDf9WV3yJCSmVxRyIq4zkkYGE1hDgu+WxZwj7omWFl3eIYykjmimJj5cATGQmldBFjr4He9edVLBK6bijx/3H3xkY0lgTXUjZcjAp4Un340ZzCZaGXWk0msZQ43798OeoMHFQqwUeeoIXP4oElCSQElBpx229njh+3R64E/sS9THxLFuYsxUFfaoiQJK6UJnqqSRW4dvLt4/SzaZmV9KDE6dv8+On4oPh7D7d77//tZ2fnE6Hbi65utbtsb5O3+ReZFItLx++wuETzfHLTdstBunG5b/hy1xM/QvT9GF5XK838p0hVqT99A/vR9b/LP4Gs4+49ommjD/PJJhUjEmppHD8n2rk1EN5cRRhcxzzKqwmjv0UXJXjvcHBeXby+vL0Ui8NBkH5lqPOAktO22i7hmE2nbAWk0FcbYtv8CRuRA+A==\nsidebar_class_name: \"delete api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Delete a highlight\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"delete\"}\n  path={\"/highlights/{highlightId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nDelete a highlight by its ID.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the highlight.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"HighlightId\"},\"required\":true,\"name\":\"highlightId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The deleted highlight is returned.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarkId\":{\"type\":\"string\"},\"startOffset\":{\"type\":\"number\"},\"endOffset\":{\"type\":\"number\"},\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"],\"default\":\"yellow\"},\"text\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\",\"nullable\":true},\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"}},\"required\":[\"bookmarkId\",\"startOffset\",\"endOffset\",\"text\",\"note\",\"id\",\"userId\",\"createdAt\"],\"title\":\"Highlight\"}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Highlight not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/delete-list.api.mdx",
    "content": "---\nid: delete-list\ntitle: \"Delete a list\"\ndescription: \"Delete a list. This removes the list only — bookmarks within it are not deleted.\"\nsidebar_label: \"Delete a list\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy1VlFv2zYQ/isH9qU1FDsdOrTQwwBnSYGsxRC0CYYhNeCzeLZYUyRLUo5VQ8B+xH7hfslwlCzbadbtZU9xyOPdd9/dfaedsI48RmXNtRS5kKQp0nsVosiEpFB45fhS5OIyXQGCViGO4bZUATxVdkMBYknpGKzRDfz1x5+wsHZdoV8HeFCxVAZUBPQExkbogsixyETEVRD5veCIQcwyEaiovYqNyO93YkHoyU/rWIr8ftbOMuHQY0WRfEgGoSipQpHvRGwciVyE6JVZfYP9tiSojfpSEyhJJqqlIg92OQBnLLTFymn2okhJvW1W1cPnN6/t9uuP5TZGW7xhwComEwZ8LUWbCU9fauVJijz6mjJhsGID3RlkQjEAh7EUnICn4KwJFBj0D+ev+M8p1l8tFNZEMjEROVD7gGHPHIS6KCiEZa11M2YQr85ffuvpzmAdS+vVV5KDr4vEKUS7JgMqQKVCUGaVgTIb1EpmYD3Q1nFKzEqPJXFM2zhxGjmj73F/IPIYgWjbNiF9ImdmM7XG0tbmcVh0Tqsi9ejkc7BPB7eLz1Rw0zrPHR1Vx3BhJf17e0yhwqJUhs48ocSFJiDvrQd+nvitKARc/SdXZV2heeyofz8W7UnH3HcAD/5nhw674oeJtBQ/lvYwoJwnd1QuJtwbYbLr2q0VPEF+s5+P2muRix1K6SmEdoJOTTYvRSY26BXDSyT1111ZlljrKHJRxuhCPplE34zX6HFN5Mbo3JOz1XvYT9S73h46LJzE0Wh/5Np1kY8HfGCWI3MeyUzkvZHI+h9vra+QEf7y222iU5ml5eecdQfp5fh8fH40rAOe6c31N/iHSxUAIZBenpU2xFS8vYopswI0EriqZyqeaYzkU3aqoF4LpzfXgFrbhwCNrSFaqNDg6uAkZGmQQwasehmUalVqtSr5BEOg9NdIWGCxrl0A5+3KY1VhVAXyoH8yn8yzZ8B0sYZ1I8GHU62BjHRWmciinNoL8HTWHceQoEwq0XzaD2ZyMoeSUJIfw++2hgINrMjwXiBAkzJbUwNLb6vT+j7QAu6uoTaSPIxGHylGZVYBfkpv3lETRqM97BtcKTNAThN/wBxq56yPUNQ+WH+2QIbqhhewUQjz7nKeSJprVak4hy81+QYOa4GLQbBXWVCm0LUkruzc0Db+3LtYKtKdJjItaTl1W2wIMrjkSi4pFmW6ZycMjMYwhbmptZ7DBnVNsLT+NIYykmvUrUeuR1p/UFmfANY6hj03F32LwG3jKPDhxbA/uRoLAmuoGy9PBDwpIf9kzmA00sqsR6OUyxTuPrwf+i1tXrCpz1FD4fFBk4SKIkqMOO6es6gPz5O4Ax+xGlNvkppzb2OgdtqiJAlLpQmeq4qb3Hq4uXz7IomlsyFWmHS6X4Yn3w6PJ3B3EPv/6SOj05Wj9dVmnTTuehm9T+s6iEzk/d6eZYJVgK92O27HO6/blo9Tx/H3yEFFk9ZKFfi3FPkSdaDvJPn8Q78AXsA/oesP0TRJrHXN/4lMrKk5fFu0szYT3eAmBN3ltCjIxaNnx55nR6vk8ur91e2VyASe6u8jvU2un8Sz23UWt6wvbTvAS3rD6Nr2b2ZfomE=\nsidebar_class_name: \"delete api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Delete a list\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"delete\"}\n  path={\"/lists/{listId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nDelete a list. This removes the list only — bookmarks within it are not deleted.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the list.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"ListId\"},\"required\":true,\"name\":\"listId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"204\":{\"description\":\"No content — the list was deleted successfully.\"},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"List not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/delete-tag.api.mdx",
    "content": "---\nid: delete-tag\ntitle: \"Delete a tag\"\ndescription: \"Delete a tag. This removes the tag from all bookmarks it was attached to.\"\nsidebar_label: \"Delete a tag\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVttuGzcQ/ZUB85IIa8kpUiTYhwJy7QBugsJIZBSFI0Cj5WiXEZdkSK4sRVigH9Ev7JcUw12tZMdN+9CnlTjk8MzlnOFeWEceo7LmWopcSNIUaYalyISkUHjl2CZycZksgBCxHMOsUgE81XZDAWJFvAorb2tArWFp7bpGvw6gItxjAIwRi4okRDsWmYhYBpHfiRl/55kIVDRexZ3I7/ZiSejJT5tYifxu3s4z4dBjTZF8SBtCUVGNIt+LuHMkchGiV+ZbxLOKoDHqS0OgJJmoVoo82NUBLyOhLdZOsxNFSurtrqzvP795bbdff6y2MdriDcNVMW2ZYXktRZsJT18a5UmKPPqGMmGwZntM9kwovt1hrASj9xScNYECI/7h/BV/HgL91UJhTSQT4a8//hzSyYnr6iEhNEVBIawarXdjhvDq/OW3jm4NNrGyXn0lObi6SPmEaNdkQAWoVQjKlBkos0GtZAbWA20dB8Qp6aGk/NI2TpxGDuh7eT9m8RSBaNs2IX0i5BmWYGyElW3M41vROa2K1JOTz8E+fbddfqYiikw4zx0cVZffwkr6986YQo1FpQydeUKJS01A3lsPfDylt6YQsPxPrqqmRvPYUX9+LNoH7XLXATz6nx+764oPppyl+2Nlj4TkOLmfcjFh7kz2qdVawdTxmwMxGq9FLvYopacQ2gk6Ndm8FJnYoFcMLqWoN3c1WWGjo8hFFaML+WQS/W68Ro9rIjdG554kVe/hQKV3/X7osHAIJ5z+yJXrbj5l9pBXvpnjSNtE3m8SWf/jrfU1MsJffpulZCqzsnyco+4gvRyfj89PaDrgmd5cf4N/MKoACIH06qyyIabSHWRLmRLQSOCanql4pjGST9Gpgnrtm95cs9TZ+wA720C0UKPB8ugkZKBViCFjLocMKlVWWpUVr2AIlL5GwhKLdeMCOG9Lj3WNURXILP9kPplnz4DTxeLVEYIXp1oDGemsMpFFODUX4EOiO75DgjKpRItpz8rkZAEVoSQ/ht9tAwUaKMnwFCBAkyJb065T8wf1vacl3F5DYyR5GI0+UozKlAF+Smfe0S6MRgfYN1gqM0B+r0I8wRwa56yPUDQ+WH+2RIbqhhOwUQiLzrhISVpoVau4gC8N+R0c5wEXg+CgsKBMoRtJXNmFoW38uXexUqQ7QeS08FDCbmoNlwwuuZIrikWV7OyEgdEYprAwjdYL2KBuCFbWP7xDGck16sYh18MTGAu19Qlgo2M45OaibxGY7RwFXrwYBiZXY0lgDXX08kTATAn5J3MGo5FWZj0apVimcPvh/dBvcK9iBTb1OWooPN5rklBTRIkRx91xVvTheFJ24CXWYuq3pOY87DHQOG1RkoSV0gTPVc1Nbj3cXL59kaTS2RBrTCrdz8HTp8JjAu6PSv//Pik6NTmZWG3WCeK+l8677tmRibyb0/NMMPPZsN9zC9563ba8nLqMHx9H5Uz6KlXg31LkK9SBvhPZ8w+95L+Af8LWL6LZJYHWDf8TmVjTbnhKtPM2Ex1XE4DONi0KcvHk1Knj+cnsuLx6fzW7EpnAh5L7SGKT6yfh7PfdjhlLStse0fF/Rte2fwPHe5L9\nsidebar_class_name: \"delete api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Delete a tag\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"delete\"}\n  path={\"/tags/{tagId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nDelete a tag. This removes the tag from all bookmarks it was attached to.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the tag.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"TagId\"},\"required\":true,\"name\":\"tagId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"204\":{\"description\":\"No content — the tag was deleted successfully.\"},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Tag not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/detach-asset-from-bookmark.api.mdx",
    "content": "---\nid: detach-asset-from-bookmark\ntitle: \"Detach asset from a bookmark\"\ndescription: \"Detach an asset from a bookmark.\"\nsidebar_label: \"Detach asset from a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy9Vm1v2zYQ/isH9ktrKHY6dFihDwPcNQWyFkPRJhiG1IDP4kliTZEsSSV2DQH7EfuF+yXDUZJf0qzrgGGf4lB3x+eeu3uOO2EdeYzKmkspciEpYlHPQ6D4ytvmhbXrBv1aZEJSKLxybCly8TLZARpAtoXS2wYQVoP9VGQiYhVEfiPGGEEsMhGoaL2KW5Hf7MSK0JOft7EW+c2iW2TCoceGIvmQDEJRU4Mi34m4dSRyEaJXpvoCzVVN0Br1qSVQkkxUpSIPtoRY0wkm2mDjNEdSpKTebKvm7uPzH+zm8/f1JkZbPGfgKiaTEfilFF0mPH1qlScp8uhbyoTBho1WB6NMKAbjMNaiy/4j9Inefw09FfCruHGwOAG9YPPgrAkUGPd358/4zyncXywU1kQyEf78/Y+h/ncYoO8dkhDaoqAQylbr7ZQxPDt/+mWca4NtrK1Xn0mmSJzui9QSEO2aDKgAjQpBmSoDZW5RK5mB9UAbxxkxKwOSRDJt4sxp5Hy+Rv6ByGMEouu6hPSBjMdG4Lv7dI2NUNrW3MeAzmlVpHGafQz2YSR29ZGKKDLhPA9fVD3ZhZX0z80yhwaLWhk684QSV5qAvLce2D2R3VAIWH1TqLpt0NwPNPhPRXfSPTc9wEP8xaHdLtgxMZjuj7XttURTZI/UXLmYjbMSZrvD2HSzRGmY7YaW7ATLhL8dRaD1WuRih1J6CqGboVOz26ciE7foFeNO7A2f++KV2OooclHH6EI+m0W/na7R45rITdG5B0dwiDAO3uvBHnosnN2Rfr3novY3H6vYnnK+mfNIZiwTyUhkw49X1jfICH/+9SrxrExp2Z2z7iE9nZ5Pz49Geo9n/vbyC/z7jyoAQiBdntU2xFTVkWplKkAjgct9puKZxkg+ZacKmsJVrQLHBtTa3gXY2haihQYNVocgIQOtQgwZsLxnUKuq1qqq+aSvY5YuWWGxbl0A523lsWkwqgJZDj6YD+bRI2C6WOr6WeHDudZARjqrTAww9B3gqSI4vkOCMqlEy/kwvinIEmpCSX4Kv9kWCjRQkeHdRrymOLM1bftFdVLfO1rB9SW0RpKHyeQ9xahMFeDH5POatmEyGWG/xUqZPeQ3KsQjzKF1zvoIReuD9WcrZKhu7wG3CmHZf1wmkpZaNSou4VNLfguH3cfFIBiVGJQpdCuJK7s0tIk/DSFKRbpXTqYFVAQMPS/jJfuQXMmSYlGn7xyEgdEU5rA0rdZLuEXdEpTWn96hjOQaUQrM9fAExkJjfQLY6hhGbvYqebV1FPhwv/lTNVYE1lA/Xp4IeFJC/sGcwWSilVlPJimXOVy/e7PvN7hTsQab+hw1FB7vNEloKKLEiNPenaV/755WAPARyzQNJqk5RxsDrdMWJUkolSZ4rBpucuvh7ctXT5KKOhtig0nAh405Pnkeeu/cH8jdYSl801OpF42jDdZlve7tBvG82T80gshEfvLq6OeOj8elvsgEjz+77Xbch9dedx0fp1bj19ZBPpPIShX4txR5iTrQV9J5/G5YCU/g75APh2i2SaV1y/+JTKxpe/pi4lfS/3jzSE+36DLRi0VKvv86Lwpy8cjvOPTiaK+9vHhzcXXBxJ9q/j2NT6EfBLTb9RZXrGldt8eXNI7Rdd1fn98Mqw==\nsidebar_class_name: \"delete api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Detach asset from a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"delete\"}\n  path={\"/bookmarks/{bookmarkId}/assets/{assetId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nDetach an asset from a bookmark.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the bookmark.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"},{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the asset.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"AssetId\"},\"required\":true,\"name\":\"assetId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"204\":{\"description\":\"No content — asset was detached successfully.\"},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Bookmark or asset not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/detach-tags-from-bookmark.api.mdx",
    "content": "---\nid: detach-tags-from-bookmark\ntitle: \"Detach tags from a bookmark\"\ndescription: \"Detach one or more tags from a bookmark. Tags can be identified by ID or name.\"\nsidebar_label: \"Detach tags from a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy1VttuGzcQ/ZUB85IIa8kpUjTYhwJy7ABugiBwZBSFY0Cj5WiXEZfckFzbG2GBfkS/sF/SDvci2VaDtEj9IEvkzHAuZ87MVtiKHAZlzbkUqZAUMCsWmPvXzpYn1m5KdBuRCEk+c6piQZGK0ygG1hBYB6V1BAFzD2tnS0BY9XpTYEuQoYEVgZJkglorkrBq4PyUVQ2WNBWJYG2RXonhRS+uE+Epq50KjUivtmJF6MjN61CI9Oq6vU5EhQ5LCuR8FPBZQSWKdCtCU5FIhQ9OmfyR74uCoDbqc73nkQO7hlDQznORCLrDstJsSZGS+q7Jy9tPL3+yd19+LO5CsNlLdlyFKDI4fi5FmwhHn2vlSIo0uJoSwWGKVKx2QolQ7EyFoRAcDGuQDydWNhzCY5djfoOFrkJTOOMCBMyhrH2AAm8ISIWCHCAsA+bncskJ7n68w5KWHFNmTSAT+AmsKq2yWPrZJ8/vHMihXX2iLIhEVI6BEhT5eBvLNUqhc9hwSIFK/03aDLaHhWojDN7FVB24w8CBkzxpDtWYTF0ygFCJRBR1iYYRJGmNtQ4i7Y9a/tsvz1UXyvVw7itrfOflD8fHhytxfuoHuHRFKTDALTnqa0PyO2Z6MPkN2f4veA+Y/2uoL2L5HiVy9LRP5ovj54/zd2mwDoV16svfLPDn739EJ05ia0OwGzKgPJTKe2XyBJS5Qa1kwkCmu4pfepDaQHdhVmlUh5O6g8cY4L4HYvD0xWNPh4YGYwOsbW2+Z1UzKw+A/GHl5lBiVihDR45Q4koTkHPWAatPuSdK8h7zbzIVG+ChoV5/Kh4WMzq4s3+9q/4ZKw6NVFIobDc3NAXWiISWitlAdX623bFeO4vdxszubgberp0WqdiilI68b2dYqdnNc5GIG3SKfY0Z66+7Oo1NHULl09ksuGa6QYcbomqKVXWwB3oLA/Lf9PLQ+cIR7Y2cD1zI7uX9wTOmmV/mOKIYM3sUEkn/5bV1JbKHv/y6iLllgFzsKP5sgONApVcjK+4qOJLh7mifAwdKu255lqwtG+OkdhE/nx5Pj/eadgx3/v78UXrGS+UBwZNeHxXWhwiUoXrK5IBGAiPoSIUjjYFcTJ7KaAqLQnm2Dai1vfXQ2JqnVYkG850Rn4BWPvgkMmcChcoLrfKCT9B7iv+NhBVmm7ryUDmbOyxLDCpDrZvpR/PRPHkCXA2msq79+HCuNZCRlVUmeOihDHifWyp+Q4IyEQHLeU8E0cgSCkJJbgq/2TquLDkZXo0I0MTINtR0C849+NzSCi7PoTaSHEwmHygEZXIPP0edN9T4yWRw+z3myowuv1U+7Pns66qyLkBWO2/d0QrZ1WrUgBuFsOwulzFJS61KFZbwuSbXwG4b4mIQDLMMlMl0LYkruzR0F171JtaKdMfBnBZQAdB3eRkeGU1yJdcUeOEoCNgIO0ZTmMPS1Fov4QZ1TbC27v4bykiuEUXDXA9HYGy3MDrytQ5+yM3It4umIs+H4y44LJBx3eTudUTAjejTj+YIJhOtzGYyibHM4fLi7Yg3uFWhABtxjhoyh7eaJJQUUGLAaafOQ2RUj8ME+IiZn3qRCM5BxkBdaYuSJKyVJniqSga5dfD+9PWzSMyV9YG7M90Ou1+/Mh9akx/243Y3Zv6HTbsjsL3B2SYdB2978r4a91Sm6vTe0tptS4lgemDJ7ZZxeul02/JxhCLv5zv2jtwmlefvUqRr1J6+Eu/Ti34KPYN/cnZYg0wTh4Su+ZdIxIaa+zt2pMauq6MXncCr7q0jxtmegUfjvE0GjXmWURW+Knu9NwtPz96eLc54FvTLfBmnvXB4K5L4Gd3tUBlHTDzbCo0mr+M4F51Vnhx4f/A8GDTd5DiUkO22k1gw87XtmJ/IhJyatv0LslDeoA==\nsidebar_class_name: \"delete api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Detach tags from a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"delete\"}\n  path={\"/bookmarks/{bookmarkId}/tags\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nDetach one or more tags from a bookmark. Tags can be identified by ID or name.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the bookmark.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The tags to detach. Each tag must have either a `tagId` or a `tagName`.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"tagId\":{\"type\":\"string\"},\"tagName\":{\"type\":\"string\"},\"attachedBy\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\"],\"default\":\"human\"}}}}},\"required\":[\"tags\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The IDs of the tags that were detached.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"detached\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"description\":\"The unique identifier of the tag.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"TagId\"}}},\"required\":[\"detached\"]}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Bookmark not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/download-backup.api.mdx",
    "content": "---\nid: download-backup\ntitle: \"Download a backup\"\ndescription: \"Download a completed backup as a zip archive. The backup must have a 'success' status.\"\nsidebar_label: \"Download a backup\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy1VttuGzcQ/ZUB85BEWEtOkaLBPhRQmqRwk4cgsVEUjgGNdke7jLgkw4ssWVigH9Ev7Je0w71IttK0L33SipzLmdsZ7oWx5DBIoy9KkYvS3GplsHyJxTpakYmSfOGkZQGRi1f9NSAUprGKApWwTLKAHhDupAV0RS03NIXLmobLJvoANW4IEB77WBTk/WPwAUP0U5GJgJUX+bXo/HpxkwlPRXQy7ER+vRdLQkduHkMt8uub9iYTFh02FMj5JOCLmhoU+V6EnSWRCx+c1NVJBIwpavklEsiSdJArSQ7MCsIIlvHQFjk8kQtJslTbXdXcfn7xg9nefV9vQzDFCwYtQxLpQF+Uos2Eoy9ROipFHlykTGhsWGQ5iGRCMgyLoRYchiNvjfbkGfp35+f8c4q4z+JKKjrNs8hEYXQgHVgZrVWySBWd3UnLR2Nu2rZtM/H8/NmplyuNMdTGybu/K/rn73+kfLxMWYdg1qRBemik91JXGUi9QSXLDIwD2loO+AGMQNswswo52m9V55DoYwRiQPr8FGmXbdAmwMpE/dDxcfyfvfm6e7P8TEUQmbCO+z/ILv+FKenfW2gODRa11HTmCEtcKgJyzjhg9Sk3QUPeY/WfTNWxQf3QUK8/Fe29jrruAB7s3xx68DUrprQl/6E2PM4VpSC52XIx67rIz/ZDN7azYd4Fz5vbDNMUnRK52GNZOvK+naGVs80zkYkNOslAU7r6665EK4wqiFzUIVifz2bB7aZrdLgmslO0p1zCfd1bGObvbS8PHRYO54gIPnIVO8/HdDDmmD1zHEmMZy4Jiaz/eGNcg4zwl18vU2KlXhlW56g7SM+m59Pzo8Ee8czfX5zgHy8lD6QntTqrjQ+pjEtj1g26tdQVoC6B63smw5nCQC5FJ4tEkNKzbUClzK2HnYkQDDSosToY8Rko6YPPgFkyg1pWtZJVzSfoPaVfPfCwB+tM5bBpMMgCldpNP+lP+tEj4HQx43XDwYdzpYB0aY3UwUPfaID3596yjxKkTiVazPshTUYWUBOW5Kbwm4lQoIaKNO8TAtQpsjXtYOVMc7++t7SEqwuIuiQHk8lHCkHqysOPSect7fxkMsB+j5XUI+R30ocjzD5aa1yAIjpv3NkSGaodNWAjERbd5SIlaaFkI8MCvkRyOzgskW5bDWwMUhcqlsSVXWjahp96EytJquNHTgvIwHSc8jI4GU1yJVcUijrdsxEGRlOYw0JHpRawQRUJVsbd9yF1yTWiZJjr4Qi0gca4BDCq4IfcvOxbBC53ljwfDic+VWNJYDR14+WIgCfF55/0GUwmSur1ZJJimcPVh3djv8GtDDWY1OeooHB4q6iEhgKWGHDaqTPBj+qJ6IGPmJepF0nNOchoiJaZhspujT2RDTe5cfD+1ZuniTat8aHBxNj92jx6byy/+iLZH6j/f3ycdPxytNLarKPIfc+t1/2C9yIT+dGuH+n1JhNMDiy533OXXjnVtnycGpEfNQdyTRRcSs/fpchXqDx9I+4nH/oN8RT+CWx/iHqXOFxF/icysabd8eOkvWkz0U10wtBdz4uCbDhSfPjEYJIeV87Pry9FJvA+NT+g4mT8q5j2+07ikqmnbUeIiYoYX9v+BSrqvTM=\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Download a backup\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/backups/{backupId}/download\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nDownload a completed backup as a zip archive. The backup must have a 'success' status.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the backup.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BackupId\"},\"required\":true,\"name\":\"backupId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The backup file as a zip archive.\",\"content\":{\"application/zip\":{\"schema\":{}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Backup not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/get-asset.api.mdx",
    "content": "---\nid: get-asset\ntitle: \"Get a single asset\"\ndescription: \"Download an asset's binary content. The response Content-Type header is set based on the asset's MIME type.\"\nsidebar_label: \"Get a single asset\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy9VduO2zYQ/ZUB89DE0NqbokUDPRRwrtgmAYJkF0WxWcBjaSSxpkiGHO1aMQT0I/qF/ZJiKNnrzW7zWL/YJofDM2cOz+yU8xSQtbNnpcpVTbyMkVhlqqRYBO1lS+XqpbuxxmEJaAEl4ocIa20x9FA4y2R5DucNQaDonY0EL8bVk/PeEzSEJQXQESIxrDFSCc4CN3RI9v7s/Svg3tNcZYqxjiq/VAlLVFeZilR0QXOv8sudWhMGCsuOG5VfXg1XmfIYsCWmEFNALBpqUeU7JRlVriIHbet7VQnizuovHYEuybKuNAVw1S0yQUNbbL2RNJp0abZ93d78+ewXt/36c7NldsUzgaw5hSTIZ6UaMhXoS6cDlSrn0FGmLLYSgVNEprRg8MiNkhr21EXB/ePpqXzdh/s98h/iPFBlqOD4MNnwmOb1PAPdYk0Lb+sM0HujiySJhS+rJ3Mp5afTp/fhXFjsuHFBf6US/vnr73TF89QbYLchKw1vdYxa8mp7jUaXGbgAtPVCjJA7FZB6RVteeINCy/d6eNuPYwRqkE+mWuLGTVpW2Uhvrhap9LjYTewPSjQVrveK6YJRudphWQaKcVig14vrpypT1xg0rs3YlWl7pKLCzrDKVcPsY75YcOjnGwy4IfJz9P5BtU0Z9hp7O8XDiEVqOBL7J6FgvPlY8gdG5GapI4WpfApS2fTjtQstCsLffj9XQo22lZPjUvUI6en8dH56pN4DnuWHs3v4D5s6AkIkU500LrKwA2vnNi2GjbY1oC0hEJYnmk8MMoVUnS5IVKqj5AY0xt1E6F0H7KBFi/VtkpiB0ZFjBmIEGTS6boyuG1kZ+5ilS9ZYbDofwQdXB2xbZF2gMf38s/1sHz0CoUte9ahmWVwaA2RL77TlCNMDBbyrWi93lKBHg1otJ4mlJKvpXc3hD9dBgRZqsuKgJM4olW2ohyq49m5/b2gNF2fQWXmTs9knYta2jvBrOvOW+jib7WF/wFrbA+R3OvIR5th57wJD0YXowsnopf5wAq41wmrcXCWSVka3mlfwpaPQw61RfuPX2hamK0k6u7K05RdTikqTGV+30AKaAUcvOVxySCmdrIiLJu1LEgFGc1jCynbGrOAaTUdQuXD3Dm1L6RGlxNKPQGAdtC4kgJ3huOfm+SQREI+LsrhfiakbawJnaXxegSiZXMw/2xOYzYy2m9ks1bKEi4/vDnqDG80NuKRzNFAEvDFUQkuMJTLOx+NiT4fjyaZAlsA6pikkiXMfY6HzMjKphEobgsfJZMX+Prx8Pdqqd5FbTHY3DYc3xPK2tK3NZNffPsPdrWX+31N5dJ0jmx6y0Th3k81ejsMtqkzl+zF3lSlxCdnc7eSSi2CGQZaTImWC37ps8uJSR/ldqrxCE+k79T/+OI3YJ/Bf+KZFtH0yc9PJP5WpDfVHs3i4GjI18pEgjLvLoiDPR+eOU18djZo3r85VpvCuO3/jxinvg2h2uzHiXNxnGA7gkhsJtGH4F4TkX+M=\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get a single asset\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/assets/{assetId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nDownload an asset's binary content. The response Content-Type header is set based on the asset's MIME type.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the asset.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"AssetId\"},\"required\":true,\"name\":\"assetId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The asset's binary content. The Content-Type header reflects the asset's MIME type (e.g., image/png, application/pdf).\"},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/get-backup.api.mdx",
    "content": "---\nid: get-backup\ntitle: \"Get a single backup\"\ndescription: \"Retrieve metadata for a single backup, including its current status (pending, success, or failure).\"\nsidebar_label: \"Get a single backup\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy9Vu+KGzcQf5VB+ZKYPftSUhr2Q8FJk3BNCiG5o5TLgce7Y69irbSRRnfnmIU+RJ+wT9KOdr322U4pFPrJa41m9Jt/v5mNcg15ZO3sRalytSR+gcUqNipTJYXC60ZkKlcfiL2mW4KaGEtkhIXzgBC0XRqCedLKQNvCxFLbJWgOUETvyTIERo4BHjdkRZZBiEVBIWTgPCxQm+jpyVhlinEZVH6tOhBB3WQqUBG95rXKrzdqTujJTyNXKr++aW8y1aDHmph8SBdCUVGNKt8oXjekchXYa7s8cueyIohWf4kEuiTLeqHJg1sAV1tnBA/dY90YsaNJl+Z+vazvPj//wd1//b66Z3bFcwGtOV3pQF+Uqs2Upy9ReypVzj5SpizWcmW+vZIpLTAa5EqJG55C42ygINC/Oz+Xn2PEYpUCU7kHsXCWybIoYNMYXaRsTj4H0ToREDf/TAWrTDVecs+6e1OXx0FrMxUD+YvTIgyB+JQsUzYag3MJinjfZqrwhEzllE9aCvor7QlsrOfkRTB3blWjX7100fLJG11lncJANtZSSn3NqUz1Racy1ZecumkzRd47/wuFgEv6F748yO21hG0I0i4k+w737h06M0C/aVsx+uz86XHOryxGrpzXX/9O+Z+//5Gq80XqAWC3Igs6QK1DSE2l7S0aXaamovtGIB4UCNM9TxqD+nRp7GI3lP0+ArVF+uwYaVf7YB3DwkV7+PB/qczClScT8xDAFGosKm3pzBOWkjBImQVRH0up1N/O8aGpKtZoDw31+mN1WAMJ4M7+zY4RXoliClt6nyvXk6w4Ka2fq0nXyWGy2XJDK7VB/nZLadEblasNlqWnENoJNnpy+1Rl6ha9FnwpSr24y8wCo2GVq4q5Cflkwn49XqHHFVEzxuaY3YVcegtbEnzb34cOi3ixx8YfJXndy/ucPIRWXhY/0jUhvnRJuiB9vHa+RkH486+XKZ7aLpyoi9cdpKfj8/H5HrsOeKbvL47wD0IdZCaRWZxVLnDK3rbvZCihLUHSeqb5zCCTT97pgsZwWekgtgGNcXcB1i4CO6jR4nJnJGRgdOCQgYyqDCq9rIxeVnKSul9+7ZafAzTeLT3WNbIu0Jj1+JP9ZB89AgmXjJ2uJ+RwagyQLRunLQfo6wvwYbs38kYJ2qYUzaZ9byYjM6gIS/Jj+M1FKNDCkqwMdwK0ybMVrWHhXf0wv3c0h6sLiLYkD6PRR2LWdhngx6TzltZhNNrCfo9LbQfI73TgPcwhNo3zLFM/OH82R4HaDBpwqxFmnXCWgjQzutY8gy+R/Bp2k1ySIcOuG4n9TkGS2Zmle37Zm1hoMh0tSlhAM2Do4rJ9ZDApmVwQF1WSixEBRmOYwkwIfga3aCKlnebBG9qWkiNKhiUfnsA6qJ1PAKPhsI3Ni75E4HLdUJDD7UlI2ZgTOEtde3kikE4J+Sd7BqOR0XY1GiVfpnD14d1Qb3CnuQKX6hwNFB7vDJXDEjbu1IXXB/XE7yBHQsfUX0nFub1jITbGYUklLLQheKxrKXLn4f1Pr58ktmxc4BoTUfe7yxviw33vsA83O87/vxbGjm72BpssLcKYm55hr/ulS+Z+PuxfN5kSghDxZiOVeuVN28pxKkbZLncEm2i41EG+S5Uv0AT6B88ff+iHwxP4FsL+EO068biJ8k9lakXr/S2xlQWl6+qEoRNPi4Ia3lM8mq7C1MO4efPqUlaTh/x8wMfJ+klQm01341L4p20HjImPBGDb/gWcCEi2\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get a single backup\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/backups/{backupId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nRetrieve metadata for a single backup, including its current status (pending, success, or failure).\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the backup.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BackupId\"},\"required\":true,\"name\":\"backupId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The requested backup.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"assetId\":{\"type\":\"string\",\"nullable\":true},\"createdAt\":{\"type\":\"string\"},\"size\":{\"type\":\"number\"},\"bookmarkCount\":{\"type\":\"number\"},\"status\":{\"type\":\"string\",\"enum\":[\"pending\",\"success\",\"failure\"]},\"errorMessage\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"userId\",\"assetId\",\"createdAt\",\"size\",\"bookmarkCount\",\"status\"]}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Backup not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/get-bookmark-highlights.api.mdx",
    "content": "---\nid: get-bookmark-highlights\ntitle: \"Get highlights of a bookmark\"\ndescription: \"Retrieve all text highlights within the specified bookmark.\"\nsidebar_label: \"Get highlights of a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJylVm2P2zYM/iuE+qUNfMl16LDCHwakW9vdOmCH9g7DcD0gjE3bamTJleTcuYGB/Yj9wv2SjfJLnEvaFeinBBZFPSQfPuROmIosemn0RSpikZN/YcymRLv5ReaFknnhnYhESi6xsmI7EYu35K2kLQEqBZ7uPRSjMdxJX0gNviBwFSUyk5TCunc6F5HwmDsR34jhISduI+Eoqa30jYhvdmJNaMkua1+I+Oa2vY1EhRZL8mRdMHBJQSWKeCd8U5GIhfNW6vwI6FVBUGv5sSaQKWnPYCyYLKCbYqJ7LCvFniTJVN03eXn34fkP5v7T98W99yZ5zsClDyYD8ItUtJGw9LGWllIRe1tTJDSWbLTeG0VCMpgKfSE4GEuuMtqR4wC+Oz/nn2Pcp1Iq3QHqxGhP2vN9rColk1DIxQfHTk5kyaw/UOJFJCrLZfeyg7B/aWKL1mLD0D2V7v99TMI9qkobCefR+t+zzJGfnOu6XJPlc9LpF04To4w9VW3SdclUakgpcyc4tZzu3BJpEYm1qonJlVKGtfIiHgzbSDBtT7nUtVK45jpzOdtIaOPpqwzl6dBrR/YzWUksoad0eQJIe8Csm0M6TbM5zV0fVY85IBrfn752u+fy2OWiffjkhBa3bTh9dv70mKvXGmtfGCs//dfn//z1d+itF6GDwZsNaZAOSumc1HkEUm9RyTQCY4HuK37rAZM5hEWlUJ7m8L72Y8tOEYgB6bNjpEPfgjYeMlPrh09/SxMlJj1Jk0MISygxKaSmM0uYMn2ArDUW+PqcOVGSc5h/lauiLlE/dNTfnx8xKADc+59w4CVfDIkL7/vC9KOAg2TRisVi4J9b7PZUbBfFdEg4sttBoWurRCx2mKaWnGsXWMnF9qmIxBatZLghaf1xV6qhRwvvKxcvFt428w1a3BBVc6yqk+reexg0/U1vDx0WDmoyXN5xLXu9moyYMdP8MscRzFjDgxELSfjzytgSGeGvf1yF9EqdGb7OUXeQns7P5+eTUTHiWV5eHOEfD6UDBEcqOyuM86GYQ5KlzgF1ClzlM+nPFHqyITqZ0ByueCQsLy94Eps7B42pwRsoUWO+d+IiUNJ5FwHP3mgyWiJA5yj86hTWmGzqykFlTW6xLNHLBJVq5u/1e/3oEXC6eIp2LcIfl0oB6bQyUnsHPd0AD/u/4jdS6JeC1bJv1uBkBQVhSnYOf5oaEtSQk+aNhAB1iGxDDWTWlIf1vaM1XF9ArVOyMJu9I++lzh38GO68ocbNZgPsS8ylHiH/Jp2fYHZ1VRnrIamtM/ZsjQy1Gm/AViKsusNVSNJKyVL6FXysyTawX0y4GATDbAepE1WnxJVdabr3P/UuMkmq00lOC0gP6Lq8DI+MLrmSGfmkCOfshIHRHJaw4umzgi2qmiAz9vANqVOuEQXHXA9LoA2UxgaAtfJuyM2oiVdNRY4/jmtZqMaawGjq2ssSAXeKi9/rM5jNlNSb2SzEsoTrt7+NfAsLC5jAc1SQWLxTlEJJHlP0OO+us9CP14Pgd7skD6/eJJBzsNFQV8pgSilkUhE8liWT3Fi4/PnVkyCelXG+xKDb/Rr2mg6WU5MBjjAfNuRuPwu+dcXt9GQyyngRYEnc9Yq6n+isnPHBeJ8O3kiwJLD9bsfcvLaqbflzoB+vx3tJDcKbSsf/UxFnqBx9IcTHb/vp8AQ+B3nYBXUTlJvXqViISGyoOVxx29s2El0nBxSdwTJJqPKTq0cDltV5nDivX16JSOChJj/Q4OD9JKzdrrO4Ys1p2xFl0CAG2Lb/AidXhhI=\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get highlights of a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/bookmarks/{bookmarkId}/highlights\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nRetrieve all text highlights within the specified bookmark.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the bookmark.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The highlights within this bookmark.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"highlights\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"bookmarkId\":{\"type\":\"string\"},\"startOffset\":{\"type\":\"number\"},\"endOffset\":{\"type\":\"number\"},\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"],\"default\":\"yellow\"},\"text\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\",\"nullable\":true},\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"}},\"required\":[\"bookmarkId\",\"startOffset\",\"endOffset\",\"text\",\"note\",\"id\",\"userId\",\"createdAt\"],\"title\":\"Highlight\"}}},\"required\":[\"highlights\"]}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Bookmark not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/get-bookmark-lists.api.mdx",
    "content": "---\nid: get-bookmark-lists\ntitle: \"Get lists of a bookmark\"\ndescription: \"Retrieve all lists that contain the specified bookmark.\"\nsidebar_label: \"Get lists of a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJylVttuGzcQ/ZUB85IIa8kpUjTQQwElTQI3fTAcG0XhCNBoOdIy4pIMyZWtCAv0I/qF/ZJ2uBddmwbIk1bkXA7ncma2wjryGJU1V1KMxZLiK2tXJfrVbyrEIDIhKeReORYRY3FD0StaE6DWoFkEYoERcmsiKgOxIAiOcrVQJGHe2hqKTERcBjG+F539IKaZCJRXXsWNGN9vxZzQk59UsRDj+2k9zYRDjyVF8iEJhLygEsV4K+LGkRiLEL0yyxOQtwVBZdTnikBJMpHBeLCLhG4fEz1i6TRbUqSkftwsy4dPL3+yj19+LB5jtPlLBq5iEumAX0lRZ8LT50p5kmIcfUWZMFiy0HwnlAnFYBzGQvBjPAVnTaDAD/jh8pJ/TnGfjakKB7D5gkxkA+icVnlK4OhTYCtnwmTnnyiPIhPOc7qjajAkV3ti6D1uGHakMvy/upKnmai7QJy5OHjraQpNpTXOOdIc0DoTKj8nWKeqIBOvzrg/Y6UROBUkU5VcjiWaCrXIRCjRR65JSQusdBTj7q7OxOeK/Oab/LlqrlW+Jzq3VhMatlJgeG1Z3nqM1ofzUlUgf2P1V1HbB0Oe/0sVLX+sFT2kk9b/tD6o0XvOVpubNrJ7cey1zkDcwzPd9QKTg6iPfTQFNa3TxYvL56cVfmewioX16su/7PD3n3+ljnyV+h6iXZEBFaBUISizzECZNWolM7Ae6NGxm6Pyj/QYR06jOl/4u7j1jb6PQHRIX5wi7bodjI2wsJU5dv09nZdbeTa9hxAmUGJeKEMXnlBylQF5bz2w+pBLpaQQcPlNpoqqRHNsqNUfiuNMJoA7+3uZf8OKKXDJfyxsOzdSQTF1i1HHVWG03fFhPdLtRAnk1x2lV16LsdiilJ5CqEfo1Gj9nAsavWKkKV7tdZOlrjuLGF0Yj0bRb4Yr9LgickN07uw4aC10Q+B9Kw8NFn7P3jT6wGlsPO/PpD7I7JnfkcS4eZOQyNqPt9aXyAh//f02RVaZhWV1fnUD6fnwcni5N1t6PJPrqxP8/aUKgBBILy4KG2LKYxdfZZaARgIn+ELFC42RfHqdymkItzxCJtdXPLbtQ4CNrSBaKNHgcmckZM34yYCHdQaFWhZaLQs+wRAo/RoJc8xXlQvgvF16LEuMKketN8OP5qN58gQ4XDx2m+7gw4nWQEY6q0wM0FYa4GHrO/Yhod0iZpO2T5ORGRSEkvwQ/rAV5GhgSYY3FwI06WUr2sDC2/Iwvw80h7srqIwkD4PBB4pRmWWAn5POe9qEwaCDfY1LZXrITHF7mEPlnPUR8soH6y/myFBdrwFrhTBrLmcpSDOtShVnkEYH7DYZTgZBtwyAMrmuJHFmZ4Ye4+vWxEKRbiiSwwIqAoYmLp2T3iRnckExL9I9G2FgNIQJzHg+zWCNuiJYWH/oQxnJOaJkmPPhCYyF0voEsNIxdLHp6fB24yjwYb/HpWzMCayhpr08EXCnhPFHcwGDgVZmNRikt0zg7ua3vt7gQcUCbKpz1JB7fNAkoaSIEiMOG3Xm+F49cT3wERMztSKpODsZA5XTFiVJWChN8FSVXOTWw/Uvb58l3nQ2xBITZbd72zuK7eplF4A9wuNe3O4mwPeswg2N7A0vHvvMhNuWQ+/7RZIJc3ywVbZTNhNMAiy63XI13nld13zc7ir30x2JJqqVKvC3FOMF6kBfednTm3YUPIP/QtutjGaTuFpX/E9kYkWbwy24nvLik3o3oWgEJnlOLu6pnkxT5uN+vLx7cysygYcsfMS6yfpZWNttI3HLLFPXPcrEOgywrv8BsYyNGA==\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get lists of a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/bookmarks/{bookmarkId}/lists\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nRetrieve all lists that contain the specified bookmark.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the bookmark.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The lists that contain this bookmark.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"lists\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"description\":{\"type\":\"string\",\"nullable\":true},\"icon\":{\"type\":\"string\"},\"parentId\":{\"type\":\"string\",\"nullable\":true},\"type\":{\"type\":\"string\",\"enum\":[\"manual\",\"smart\"],\"default\":\"manual\"},\"query\":{\"type\":\"string\",\"nullable\":true},\"public\":{\"type\":\"boolean\"},\"hasCollaborators\":{\"type\":\"boolean\"},\"userRole\":{\"type\":\"string\",\"enum\":[\"owner\",\"editor\",\"viewer\",\"public\"]}},\"required\":[\"id\",\"name\",\"icon\",\"parentId\",\"public\",\"hasCollaborators\",\"userRole\"],\"title\":\"List\"}}},\"required\":[\"lists\"]}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Bookmark not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/get-bookmark.api.mdx",
    "content": "---\nid: get-bookmark\ntitle: \"Get a single bookmark\"\ndescription: \"Retrieve a single bookmark by its ID, including its tags, content, and assets.\"\nsidebar_label: \"Get a single bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzVWNtuGzkS/ZUC52EToy07i1nMQA8LKJmbdzI7RmxjMXAEqNRdUjNikx2SLVkRGtiP2C/cL9kt9lVSeyInmYd5ktAssk5dWFU8O2Fysuil0VeJGIsl+ZfGrDK0KxGJhFxsZc6rYizekLeS1gQITuqlIpjXojDfgvQOrr6LQOpYFYnUy/DF49JFEBvtSfsIUCeAzpF3IxEJXhTje9FodGIaCUdxYaXfivH9TswJLdlJ4VMxvp+W00jkaDEjT9YFARenlKEY74Tf5iTGwnkr9fII+21KUGj5viCQCWkvF5IsmAX4tDODMdEDZrnikyTJRD1sl9nm3bffmIcPf0sfvDfxtwxc+iDSAL9KRBkJS+8LaSkRY28LioTGjIXmnVAkJIPJ0aeijAbQz41RhDrAX2ChvBgvUDk6NOdqAY48eANB1Z4Vf3GwKJRqnA7Pfrr95XUEnh58BOTj0XPYSKVgTnWsKAGpwxGWXG60oxHcVKcH5bAwFpRcpp5sK+Jgk5IGo9UWMvKYoEeQDjRRQslo3x9/UhvaENY6XlVomjC+L8huBSdleyCH8q+Xl/xznH/sD3Kekr2Eq03kLZjnSsbhMl68c7xvIEXM/B3FjCG3fHW9rLTK5PgSlJGILaGnZOIHVzOT8EUYXI6ELpTCOSc6B6hs0/4ESbRxKteUDKR2GYkFrg3f8cfWPS6XUi9vPPrCfVxfJEgXGRcSV8QxOSdYhVSFJXYTaa5GYlpGwhVZhlZ+CC7+8sdr409zT4Vje5qsKWx8wrEdTMyliMSG5pxdiv9nZi4VhermSTtOyUhUNXxRLdhglsxyY32wpXBkr4Zzqirb7QJai1ve7Clzn5yo1U0bWEDvMU4peTnortZmNjMtMtRiWu7VnnvWWJ+/d1qQ690+o+nXRWgqv29Atfo4FCX1qnKhVcP+O/ka7dWQE+Rlhku6G1L7mPCEm/FQnIcyMbZE2qXGP2VXniyeIs5l/5qBVRXkSZosxRY3ipJP2LyWCZknAcW1jE+MS+oz9arLtI/K11n5FDiN5ScqYOkvXwCx8KmxpwWrmCvpUjpNOkFP1/WO0/zBO36p29sJGw5KRpCubvCUB7XPqwg8tgT/hD9DJaEq8qfd3GGojY7PxRqm8yqY/O/2I+KhiIhwy7tNj/QNbjX/HCzzj7a9U2uZkx/6x+oim5P9nZv1yW7unNLZ+gW8XuiVNpuB3hU2TMvWtV++8Z4UZu5pP/WKWL8Z1NGvT7rpf5+j1mSv6hw5KO2iLrosV8/DoeCJgUouqoHkLlcGE+J+jmv0yCHuXPekDBuaETpXTMtBgW6a3pude/Pu3nB7OMkOD6DtrFVPVl2StjGfHj85RRkgfn354vixcaerQiw//P+t8d9//ye8i16GpzR4syLNj5xMOp4A+cG+RiWTCIwFesjZ4IN3CdeWi1yhHH6RdNnSvp37CFqkXx8jbawBbTwsTKEPVX/Okyg2yWAm7EOYQIZxKjWdW8KEMwTIWmOBt4d3bEbOcQafcFSYQA8PqvePxGFOBYDd+b0of88bg+OCfp+ampxhI5k9GIuL5tK4i11HMZScZGTXDT8SZlCxwySx5Fx5gbm8WL/gq4dWMsbgqXq5ik/NO4jU+9yNLy683Y5WaHFFlI8wzwe5lfqEhlH5uZaHCgtb0qN2bjiAleY+wdO6lzWHEsNi/DQMQlwnwp8fjM2QEf7jX7fBp1IvDG9nqytIL0aXo8seUdPimVxfHeFvF6VjaovU4jw1zocINp5lPovJKw7tufTnCplIYOtkTCO4TaXjswGVMhsHW1Mw9ZChxmV3iItASeddVNNiqVymgZNwUc2KVQzZHONVkTvIrVlazDL0MkaltqO3+q3+6itgdzGHVd0L/jhRCkgnuZHaO6hzDHD/0ueso+VJZpP6hoZDZpASJmRH8JspIEYNS9JMDBKgDpataAsLa7L9+G5oDndXUOiELJyd3ZD3Ui8d/D3s+Zm27uysgX2NS6lbyK+l8z3Mrsj57QlxYZ2x53NkqHm7A9YSYVYtzoKTZkpm0s8gsDDQ0YIcjI4FaughjuxM04N/VR+xkKSq4shuAekBXeWXRkl7ZCCRyMdpWOdDGBiNYAIz7iozWKMqKoppT4fUCceIwsEcD0ugDWTGBoCF8q7xTVsIuf04/tiSoiEacwKjqbpelgj4prjxW30OZ2fcnM/Ogi0TuHvzuqNlN9KnYEKeo4K6pbZU16jaztW93R6qfODZuCRTLRKSs5HRUNRtGLjfwrMwA3L3uP7uh+ehYubGeX6Kj3cNg/Yj+WPa+PAm7rrK/weQzVVt6fWy7oleldT7dg7hJjzu8bbTSHBFYJHdjlPzzqqy5M8VB8iVNpGOK8YjbGfftD8D8TnorRVth8jQkP1iLAIV2vSVJ/rk2Zu6Lz6Hx7Q3U6/e9nU2qHrRCtNyVc4CikpgEseU9+EejRYMv+21P35/K6oHbZ+j3G9EfbJoH9ZuV0nccuEtyxZlKMQMsCz/B5wM34s=\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get a single bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/bookmarks/{bookmarkId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nRetrieve a single bookmark by its ID, including its tags, content, and assets.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the bookmark.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"},{\"schema\":{\"type\":\"boolean\",\"default\":false,\"description\":\"If set to true, the bookmark's full content (HTML, text, etc.) will be included in the response. Set to false for lighter responses when only metadata is needed.\"},\"required\":false,\"description\":\"If set to true, the bookmark's full content (HTML, text, etc.) will be included in the response. Set to false for lighter responses when only metadata is needed.\",\"name\":\"includeContent\",\"in\":\"query\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The requested bookmark.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"userId\":{\"type\":\"string\"},\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"attachedBy\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\"]}},\"required\":[\"id\",\"name\",\"attachedBy\"]}},\"content\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true},\"description\":{\"type\":\"string\",\"nullable\":true},\"imageUrl\":{\"type\":\"string\",\"nullable\":true},\"imageAssetId\":{\"type\":\"string\",\"nullable\":true},\"screenshotAssetId\":{\"type\":\"string\",\"nullable\":true},\"pdfAssetId\":{\"type\":\"string\",\"nullable\":true},\"fullPageArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"precrawledArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"videoAssetId\":{\"type\":\"string\",\"nullable\":true},\"favicon\":{\"type\":\"string\",\"nullable\":true},\"htmlContent\":{\"type\":\"string\",\"nullable\":true},\"contentAssetId\":{\"type\":\"string\",\"nullable\":true},\"crawledAt\":{\"type\":\"string\",\"nullable\":true},\"crawlStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\",\"nullable\":true},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true},\"size\":{\"type\":\"number\",\"nullable\":true},\"content\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"assetType\",\"assetId\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"unknown\"]}},\"required\":[\"type\"]}]},\"assets\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"pdf\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"avatar\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"userId\",\"tags\",\"content\",\"assets\"],\"title\":\"Bookmark\"}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Bookmark not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/get-current-user-stats.api.mdx",
    "content": "---\nid: get-current-user-stats\ntitle: \"Get current user stats\"\ndescription: \"Retrieve usage statistics for the currently authenticated user, including bookmark counts by type, top domains, tag usage, bookmarking activity patterns, and storage usage.\"\nsidebar_label: \"Get current user stats\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzVV81u20YQfpXB5tIKtGQXPekQQKmbxE2KBrGNIHAMaESOxI2Wu8zuUDYjCOhD9An7JMUsKYmSpTg/p55EaHZmv/nZb2aWypXkkbWzF5kaqhnxb5X3ZPk6kL9k5KASlVFIvS7llBqqt8Re04KgCjgjCIysA+s0wNR54JwgbUyYGrDinCzrFJkyqAL5BLRNTZVpO4OJc/MC/RxSV1kOMKmB65ISYFdC5grUNiTAOGuuSjYKoowp64XmGkpkJi8n0WYQ2HmBFTX6KlGMs6CGN0ocCuo2UYHSymuu1fBmqSaEnvyo4lwNb25Xt4nyFEpnAwU1XKpfTk/lZzcA58SoTfTnywGIDguG1Fkmy2IKy9JIOLSzg49B7C1VSHMqUL7EfTVUbvKRUlaJKr3kh3WDxlbFszYCoXPaVsWEvFol8vUcF85rpqMHRj7N9YKyY/KrGK/Dstc68FHhSz3LjZ7lR06scxee1VdR9pi3Rtv5QUtM93xQgCHQIclKsvqp0l68vmkMt2bWSrdi15XnTdF1bKD3WKtEaaYiPA66qdrOucBe25mgi0X+OLrWwvq8ACvw/qK5/uw04mQ0I4F9qT/T8Ug8jPQ3OsO72o+70mI7AmvP0Shd2+pq3q465aLtbNS+9K8AnOvwjuhI2eQ6/OmsvPQj0veE/nDt1i9d5b8/kPmu9tbwV9ZE1N9WRER0jvVf0z1nv7VYsf4RVFm8awNqP73rXHQj34nzJqq7zsSHiLNrodbv98xi8W2Vuwc+6n/hCXbI7NJVPv0BqGFfvwUrnGoMTgypIfuKEkW2KgQcllol6o4mgtDId+Em2ghgumeyQdpUooK2M0PTRuCDNHJdlM43Dn1lIFp0x9O805P2OtBuv9l2l04v2e8cD/vEDi8/IL89qjvMG52KOpQ6cWqVqF9Pzx72+msrI4zz+jNl8O/f/8T2/iwODcBuThZ0gEIHCbaMNgs0OkvAeaD7UmK01/6l6QxK0/aIB41/k3u6x6KU1O8gUKsGakGcu3Zek4JCITU1kHEjDAoahHZwC+QXMvXIpFN5o4ZqiVnmKYTVAEs9WJypRC3QaymzWI2tuAnEFCvDaqhy5jIMBwP2dX+OHudEZR/L8sFoeJUTtBbATWOsXrXnocEiHnRmsEsJQHNzdxLbxENuFj/iMTVsD0kW48dz5wsUhH+8u4qlq+3Uibp43UA665/2T6UCNMdwbvCM3lw8wL8R6gAIgcz0JHeBJTq7s6fNwBNmJ5pPDDL56J1OqQ9XuQ5iG9AYdxegdhWwgwKtzIqb4kvASP3H6TYkkG9eQAJNQTfT7ATTeVUGKL2beSwKlFHamLr/wX6wT57AaDtfa2flz5ExQDYrnZaJun2pgLs1W8odGWgbUzQetQUWjYwhJ8zI9+G9qyBFCzOysiMQoI2ezamGqXfFbn7vaALXF1DZjDz0epfErO0swNOo84rq0OutYb/BmbYbyMIEHcyhKoWlZIgOzp9MUKCWGw1YaIRxIxzHII2NLjSP4VNFXtYBjwUx+SDJIFjP8+3eQZLZsaV7WXSiiakm07xtCQtoBgxNXNaXbExKJqfEaR7lYkSAUR9GMBa2HsMCTUVxD9i5Q9ss7kDRsOTDE1gHhfMRYGU4rGOzZlMQQgvy54ZfYzYmBM5S87w8UdyYwvCDPYFeTybbXi/6MoLrt6+3G9ad5hxcrHM0kHq8kwWmIMYMGfuNupDTRj2SFMhfYB1TeyQW5/qMhao0DjPKQNoM/KQLKXLn4c3585/70m9LF7hAu+3J6gXxzn4E4dCWudyS5v9u4Wyoq8P0q6Rh32XL1DcqMrU0bulITQBuEyVcI9LlUor+2pvVSv6OdS3r6Zar47KaqOalRnqfU62GapSmVHIkdVPFYWR/3RT23bSPF79fSQfd5dw9jo3W16ONrTu2l8vmxJVwymqlkhZE5Bi1krb6H6wyvyg=\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get current user stats\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/users/me/stats\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nRetrieve usage statistics for the currently authenticated user, including bookmark counts by type, top domains, tag usage, bookmarking activity patterns, and storage usage.\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Detailed usage statistics for the current user.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"numBookmarks\":{\"type\":\"number\"},\"numFavorites\":{\"type\":\"number\"},\"numArchived\":{\"type\":\"number\"},\"numTags\":{\"type\":\"number\"},\"numLists\":{\"type\":\"number\"},\"numHighlights\":{\"type\":\"number\"},\"bookmarksByType\":{\"type\":\"object\",\"properties\":{\"link\":{\"type\":\"number\"},\"text\":{\"type\":\"number\"},\"asset\":{\"type\":\"number\"}},\"required\":[\"link\",\"text\",\"asset\"]},\"topDomains\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"domain\":{\"type\":\"string\"},\"count\":{\"type\":\"number\"}},\"required\":[\"domain\",\"count\"]},\"maxItems\":10},\"totalAssetSize\":{\"type\":\"number\"},\"assetsByType\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\"},\"count\":{\"type\":\"number\"},\"totalSize\":{\"type\":\"number\"}},\"required\":[\"type\",\"count\",\"totalSize\"]}},\"bookmarkingActivity\":{\"type\":\"object\",\"properties\":{\"thisWeek\":{\"type\":\"number\"},\"thisMonth\":{\"type\":\"number\"},\"thisYear\":{\"type\":\"number\"},\"byHour\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"hour\":{\"type\":\"number\"},\"count\":{\"type\":\"number\"}},\"required\":[\"hour\",\"count\"]}},\"byDayOfWeek\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"day\":{\"type\":\"number\"},\"count\":{\"type\":\"number\"}},\"required\":[\"day\",\"count\"]}}},\"required\":[\"thisWeek\",\"thisMonth\",\"thisYear\",\"byHour\",\"byDayOfWeek\"]},\"tagUsage\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"},\"count\":{\"type\":\"number\"}},\"required\":[\"name\",\"count\"]},\"maxItems\":10},\"bookmarksBySource\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"count\":{\"type\":\"number\"}},\"required\":[\"source\",\"count\"]}}},\"required\":[\"numBookmarks\",\"numFavorites\",\"numArchived\",\"numTags\",\"numLists\",\"numHighlights\",\"bookmarksByType\",\"topDomains\",\"totalAssetSize\",\"assetsByType\",\"bookmarkingActivity\",\"tagUsage\",\"bookmarksBySource\"]}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/get-current-user.api.mdx",
    "content": "---\nid: get-current-user\ntitle: \"Get current user info\"\ndescription: \"Retrieve profile information for the currently authenticated user, including their name, email, and avatar.\"\nsidebar_label: \"Get current user info\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy9VcGO20YM/RWCObQ1tPam6EmHAk7bBGlyCJJdFMVmAdMSLU08mpnMUN51DQH9iH5hv6TgSOu1sznk1JOEGQ75SD4+HtAHjiTGu9c1ltiw/NLHyE6uE0cssOZURRPUAEt8zxIN7xhC9BtjGYzb+Njl97DxEaRlqEYHdg/US8tOTEXCNfSJYwHGVbavjWvU1kRw1HEB3JGxBZCrgXYkFOdYoFCTsLxBhZLwtsDEVR+N7LG8OeCaKXJc9tJieXM73BYYOQXvEicsD/jj5aV+zvFfPcLLcL5LX8tEY1feCTtRFxSC1RSMd4tPSf0cMFUtd6R/sg+MJfr1J64ECwxRKypmRGHqE5sk0bgGhwI16acXBbreWlpbxlJiz0OBuS7fZGk6ar7Np/UV2dzeR+u195bJ4TBoGT/3JnKtpTc1nj64HQa1+Ony+dPaXjttt4/mL67h37//yVx4kZsE4rfswCToTErGNUqDHVlTF+Aj8H3QeF+UXfheFsGS+XrBj/nxPXVB0ztDgMMItWNp/cRs7Q4pXXChzU+LjlFJFXfKL+VUHy2WeKC6jpzSsKBgFrvnWOCOotEi5qZO12MJNtRbwRJbkZDKxULifr6lSFvmMKcQnsyQcnDyAH6Tq/RmsocRi2I/YfsHTX2MfMr5YyU0suaRzbSX2QiL6edlpjWW+PsfV7m/ynR9rlmPkJ7PL+eXOnBGciGPeJbvXj/Bf7w0CQgS281F65NodWDt/bajuNXx1lmOTPWFkQtLwjFnZyqew1VrkvoGstbfJdj7HsRDR46aRyepAGuSpAJUCApoTdNa07R6Qilx/roa1lRt+5AnuYnU6QxXZO1+/tF9dM+ewfJRhYx3eri0FtjVwRsnCSa+A52zNWiMGozLLVotJ2plJytomWqOc/jT91CRg4ad6igDuZzZlvewib477+8dr+H6NfSu5giz2QcWMa5J8HN+84b3aTZ7gP2OGuOOkN+aJCeYUx+Cj6Jilny8WJNCDccXsDMEq/FylYu0sqYzsoLPPcc9BIrUsXBM2gyGB+Wc1Jm1syvH97oMsouNYTtOtZYFjAClsS4PQY4utZMblqrN9+pEgfEclrBSLVrBjmzPeV2cxTCuzpsiO9Z+RAbnofMxA+ytpIfavJgoAlf7wEkPH05S7saawTsexysyg05KKj+6C5jNrHHb2SznsoTr92+PfIM7Iy34zHOyUEW6s1xDx0I1Cc3H5ypLx+dZnkCPwHnhySST88HGQR+sp5pryHvm+yzUKnvvfn35w1y3QfBJOsoyNy4GfMVytqfydvpyEg+Pavl/L+ZReE4UeihG7TxMCnuDWWFRBVg3tyqEnh4OStXraIdBjzMbdX0/Kmxe5gWO85VFect7LHFZVRwkS7HtNfqTtayaeZT7V79dYYF0rpRfKGP2Pl2R25/4PhxGiytVgmHAYgKRlQEHXYP/ATixQLA=\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get current user info\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/users/me\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nRetrieve profile information for the currently authenticated user, including their name, email, and avatar.\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The current user's profile information.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\",\"nullable\":true},\"email\":{\"type\":\"string\",\"nullable\":true},\"image\":{\"type\":\"string\",\"nullable\":true},\"localUser\":{\"type\":\"boolean\"}},\"required\":[\"id\",\"localUser\"]}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/get-highlight.api.mdx",
    "content": "---\nid: get-highlight\ntitle: \"Get a single highlight\"\ndescription: \"Retrieve a single highlight by its ID.\"\nsidebar_label: \"Get a single highlight\"\nhide_title: true\nhide_table_of_contents: true\napi: eJylVm1v2zYQ/isH9ktrKHY6dFihDwPc9WVZCyxoEwxDGsBn6SyxpkiVPCVxDQH7EfuF+yXDUbIsJ27RYZ8kkHfH5+65e8itcjV5ZO3sWa5SVRD/qovS6KJklaicQuZ1LdsqVe+JvaYbAoSgbWEIyp0tLDegOcDZy6lKFGMRVHqlhlBBXScqUNZ4zRuVXm3VktCTnzdcqvTqur1OVI0eK2LyIRqErKQKVbpVvKlJpSqw17Z4AOqiJGis/twQ6Jws65UmD24FXI7wCSq6w6o2EkqTzs3dpqhuPz3/yd19+bG8Y3bZc4GuOZoM0M9y1SbK0+dGe8pVyr6hRFmsxKocWSVKC54auVSSj6dQOxsoSA4/nJ7K5yF0CUyBKT/EmjnLZFl8sK6NziJFs09BHI8Uxy0/USaM1V4IZd0du3RuXaFfC7f3C9kmKjB6/n21CsSjfdtUS/KyTzb/xm7mjPPHCCLbVEL/hoxxt0pKIeUpPJFViVqahqQfclphY1ilO8M2UUx3fCykbYzBpTAj9W8TZR3Tdxnq46k3gfxXqpJ5QqZ8fgRIe9AKV+P6HlZzXLs+qx5zRDScPz7t+kj3qbaVM5+dPn3YPpcWGy6d118oh3/++jt2/Is4V8BuTRZ0gEoHmdUEtL1Bo/MEnAe6qyWDe40mKGe1QX28xfb0DnM0RjAgffYQ6ZAOWMewco29f/b/afLM5Udb4RDDHCrMSm3pxBPm0iJA3jsP4j4V3isKAYvvClU2Fdr7gXr/6YMuiQD38Uc8vxLHWLl4PpeuF2FJUoQkVbNBF8JsO9KbVjqO/M1OLxtvVKq2mOeeQmhnWOvZzVOVqBv0WlDGWvXbHUW78SuZ65DOZuw30zV6XBPVU6zro1rbR9gp7NveHjoskstI6j8Ihb0UjQR/KLCcLHlEM5X2RqIR8ee18xUKwt/+uIhV1XblxF2y7iA9nZ5OT0e6PeCZn589wD9s6iB3GJnVSekCRw53o6xtAWhzEHJPNJ8YZPIxO53RFC5KHSQ2oEhWgI1rgB1UaLHYBwkJGB04JCBXYbLX9pAAhkDxa3NYYrZu6gC1d4XHqkLWGRqzmX60H+2jRyDlkjutmwxZnBsDZPPaacsB+i4DPJz7Ws7IQdtI0WLeD2kMsoCSMCc/hT9dAxlaKMjKE4AAbcxsTRtYeVcd8ntLS7g8g8bm5GEy+UDM2hYBfo4+b2kTJpMd7HMstB0gv9OBR5hDU9fOM2SND86fLFGg1oMH3GiERbe5iEVaGF1pXsDnhvwG9s8EIUMu0O6aBW0z0+QkzC4s3fEvfYiVJtPpo5QFNAOGri67Q4aQwuSKOCvjvgQRYDSFOSzkYlnADZqGYOX84Rna5sIRxcDChyewDirnI8DGcNjV5kXfInCxqSnI4m4lRDaWBM5SN16eCGRSQvrRnsBkYrRdTyYxlzlcvn839Bvcai7BxT5HA5nHW0M5VMSYI+O0cxeBH9yj0IMsiShTbxKbc2djoamNw5xyWGlD8FhX0uTOw/nL10+iZtYucIVRrvsn0RviI+/D+6O43Yv/f3hWdqIxuqfkIhfd2/ZqebV/kQWVqHT8PrtOlAy7GG230nWX3rStLMfGkmfoXiyjpOY6yH+u0hWaQN9I4fH7Xu6fwNdw9otoN1GT5Q2UKpWoNW3uPSTb6zZR3ZBGGJ3FPMuo5pHvgytThHe4Q968ulCJwkO5vSevMfpRXNttZ3EhctK2A8woLwKwbf8FIl9Czg==\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get a single highlight\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/highlights/{highlightId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nRetrieve a single highlight by its ID.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the highlight.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"HighlightId\"},\"required\":true,\"name\":\"highlightId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The requested highlight.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarkId\":{\"type\":\"string\"},\"startOffset\":{\"type\":\"number\"},\"endOffset\":{\"type\":\"number\"},\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"],\"default\":\"yellow\"},\"text\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\",\"nullable\":true},\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"}},\"required\":[\"bookmarkId\",\"startOffset\",\"endOffset\",\"text\",\"note\",\"id\",\"userId\",\"createdAt\"],\"title\":\"Highlight\"}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Highlight not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/get-list-bookmarks.api.mdx",
    "content": "---\nid: get-list-bookmarks\ntitle: \"Get bookmarks in a list\"\ndescription: \"Retrieve a paginated list of bookmarks within the specified list. For smart lists, bookmarks are computed from the list's query.\"\nsidebar_label: \"Get bookmarks in a list\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzVWdtuGzkS/ZUC5yGJ0ZaTxQx2oIcFlMxkxjvJjhHbWCwcAaK6S2pGbLJDsmUrQgP7EfuF8yWDIvsmuWW3Lw+zT3HUxapTFxaLh1umczTcCa1OEzZmS3QfhHVvtV5l3Kwsi1iCNjYiJxE2Zp/QGYFrBA45XwrFHSYghXWgFzCvl8G1cKlQ4FIEm2MsFqISG8F7bcBm3Dj/fxt1VnGDEOssL0jpwujMKyCxFxa+Fmg2IxYxx5eWja8YAbVsGjGLcWGE27Dx1ZbNkRs0k8KlbHw1LacRy7nhGTo01gvYOMWMs/GWuU2ObMysM0Itb3l6kSIUSnwtEESCypEPhrysMREWvOFZLkmLQJHIm80yu/7y49/1zbcf0hvndPwjARbOixDg04SVETP4tRAGEzZ2psCIKZ6RgAwCERMEIOcuZWV0F2JURUah4Dau8FM8ElzwQjo2Dr/sO3aujQNtEjQw30Bs0KcfEu5wBD+FtRachhe07sVoF/CCS4tPVtm4bLVxv9PC2muf5n63VZHNveCu8Y/8RmRFBuEzZUg4zLw5g64wCnI0VK04xJWHaOskLhNugAcHSu1dYaw2oeI55AbXQhcWDNpcK4tke4EuTn3pKbxxjf26tIKGAe493lTlahws3e/rXGuJXLFOPfYiOl2ARUeG/V7whuuW8MLCopASYq0cKgcvf734+CEChzcuAnTx6BVcCylhjiBULIsEE6jaTu3RCM6Ddm8cFtqAFMvUoWlELFynqEAruYEMHU+44yAsKMQEkyFF89f3oclfZeNdQLOXxyl5WimkVP7t9Wv6Z9fZyZ2dv7/rs4hV/pM+nudSxL5HnHyxpLSnfvT8C8YEMDd0RjkRIDWWOqLcGL4hV2in3q9CJLe3ZBkx37Ywmbjer5lOvD99nyOmCin5nLYiFUDZbMwBktzEqVhj0rN1yogt+FrT4Xbou+PLpVDLc8ddYe+31x4ZtohjtHTAL7iQhUEKE6qEVk3LiNkiy7gR33yWnl+90m5YeAKOzTBZXZh4gNrOwZkLFrFrnFOBSvo703Mh0R/tDpWlko+YFWopcRE+GO+WyHJtnPelsGhO+2sqzCrPXKhhJ/d84M7xOMXkbW+4Gp/JzbTIuGLTcqe3XZHFSv+ONi/X2cBa4e8LP03d7UD4ehiKFGoVQmhkf/wGb6OdHjVAXmR8iZd9Zg8JT6xF15fnvkqMDaKyqXYPWZUni4eI07FyRsBCB3mQJYOx4dcSk0csXosE9YOA8rWIB+YldZl811bavfJVVT4ETu35QAMk/fwNkBcu1WZYsoq5FDbFYdI0dJ9VK4bFg1Z8rI63AQv2WoaXDjt4SoPg0zoCjUU+Pv6PvpYQmvywndsPtbbxVKycai4kk/66uEfcNxHmd3m76MC5QUfNv3rb/MFjb2gvs+JbV21zqTq0sx4d5jYora/PEPVCrZS+7jm7/IJp2YT2+Q/eQWmmM+3XThPrHgZV9itN593f51wpNKdVjey1dlY1XZKr5l/f8FhPJ2dhILnMpeYJ0nnO19xxSnEbugdVWN+M0IZiWvYKtNP0zuzcmXd3htv9SbZ/AG1mrWqyaou0yfm0vRTXLBYjgHSXrW7JA7p4/6VZm91rcQTaAC0FsQClIdPGX9qI7Rix/bDMO6RaB00H8Fl9r2r5t9JH9/vXb27fwy5VOEPEN0zgj//+z2N76+kvcHqFiu5/mbA0vEYg1JpLkXjIeJMTqL1bGbXFk1xy0X8fawu94by6CBqk399GSsQXKO1goQu1b/Ypl8FYJ70FvH9hzXicCoXHBnlCaQY0Rhug5f56n6G1tPEGqPKD876iav3tnHuArf5Orn+mhT5o3r5LdcW/kpPE/I3ZiSdIT7aBFixPugVk0axrRtMPz2zLk8SgteUJz8XJ+g31DG4EofSxqj6H7NQEYepcbscnJ85sRitu+AoxH/E872VDKw01B/pbJQ8BC/nSIWPPKYXVlb1DyTYBJsu+N5IY3Wm9EDU4/8d7bTJOCP/57wsfVaEWmpaT1wHSm9Hr0esOB9bgmZyd3sLffBQWOFiUi+NUW+dzWMdVqCVwlQAl91i4Y8mJYSHvRIwjuEiFJd3ApdTXFja6IE4m44ovWyU2qnlt6lARpGKZerLGRhBaVOSNzHm8KnILudFLw7OMOxFzKTejz+qz+u47oHAR6xx2Bv04kRJQJbkWyhFj56sM+O6Wz8lGQyDNJtX+9EpmkCJP0IzgP7qAmCtYoiL2H4Er79kKNy3v3oTsGudweQqFIn736OgcnRNqaeEffs1vuLFHRzXsqoVVkP3GbzHbIqdLMwQO8XjOCWrerIC14DALH2c+SDNPrM4C9w8tkU/JaOmxmjejzM7azjqDhUAZWiOFBYQDbkNcaiONykPEJ0xgRh1+Bmsui8C97dgQKqEcoVdM+TB46yioYlN3daBz09KPTZ/32ZgjaIVhexlEoJ1ix5/VMRwd0VRxdOR9mcDlpw9NvfmnFtC+zrmEahZoOMBRWE69vVnue7wnIKkpYyXii7OWUVBU8wPQoAAv/fBKZ8fZT+9f+Z6Za+uIQxhva2rxF3S7RCD3W2F/L27b7v+XeE4K/ahz+rV8RGjEV/5hhrruuHmhaXvxNGLUSUhsu6WSvjSyLOnnQKpSh06EpU5zgD7uBuQRLym9Dqxws/e64uuXjZln64cDeth7yB1Y6jeSR+J4whvJYUzNY8YjQf0/8P53uH/rLaANw7SdHh5YwS8/VfPPKzhkvb6UqU3XZlso4ZmUbinhwPIIwsdJHGPehXprfCTozTz1y88XLHAtXfp8d9To8pi7kLbbIHFBR2tZNgj9UUsAy/JP7xni+g==\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get bookmarks in a list\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/lists/{listId}/bookmarks\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nRetrieve a paginated list of bookmarks within the specified list. For smart lists, bookmarks are computed from the list's query.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the list.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"ListId\"},\"required\":true,\"name\":\"listId\",\"in\":\"path\"},{\"schema\":{\"type\":\"string\",\"enum\":[\"asc\",\"desc\"],\"default\":\"desc\",\"description\":\"Sort order by creation date. Defaults to 'desc'.\"},\"required\":false,\"description\":\"Sort order by creation date. Defaults to 'desc'.\",\"name\":\"sortOrder\",\"in\":\"query\"},{\"schema\":{\"type\":\"number\",\"description\":\"Maximum number of items to return per page.\"},\"required\":false,\"description\":\"Maximum number of items to return per page.\",\"name\":\"limit\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"description\":\"Cursor from a previous response to fetch the next page.\",\"title\":\"Cursor\"},\"required\":false,\"description\":\"Cursor from a previous response to fetch the next page.\",\"name\":\"cursor\",\"in\":\"query\"},{\"schema\":{\"type\":\"boolean\",\"default\":false,\"description\":\"If set to true, the bookmark's full content (HTML, text, etc.) will be included in the response. Set to false for lighter responses when only metadata is needed.\"},\"required\":false,\"description\":\"If set to true, the bookmark's full content (HTML, text, etc.) will be included in the response. Set to false for lighter responses when only metadata is needed.\",\"name\":\"includeContent\",\"in\":\"query\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"A paginated list of bookmarks in the specified list.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarks\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"userId\":{\"type\":\"string\"},\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"attachedBy\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\"]}},\"required\":[\"id\",\"name\",\"attachedBy\"]}},\"content\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true},\"description\":{\"type\":\"string\",\"nullable\":true},\"imageUrl\":{\"type\":\"string\",\"nullable\":true},\"imageAssetId\":{\"type\":\"string\",\"nullable\":true},\"screenshotAssetId\":{\"type\":\"string\",\"nullable\":true},\"pdfAssetId\":{\"type\":\"string\",\"nullable\":true},\"fullPageArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"precrawledArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"videoAssetId\":{\"type\":\"string\",\"nullable\":true},\"favicon\":{\"type\":\"string\",\"nullable\":true},\"htmlContent\":{\"type\":\"string\",\"nullable\":true},\"contentAssetId\":{\"type\":\"string\",\"nullable\":true},\"crawledAt\":{\"type\":\"string\",\"nullable\":true},\"crawlStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\",\"nullable\":true},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true},\"size\":{\"type\":\"number\",\"nullable\":true},\"content\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"assetType\",\"assetId\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"unknown\"]}},\"required\":[\"type\"]}]},\"assets\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"pdf\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"avatar\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"userId\",\"tags\",\"content\",\"assets\"],\"title\":\"Bookmark\"}},\"nextCursor\":{\"type\":\"string\",\"nullable\":true,\"description\":\"Cursor for the next page, or null if no more results.\"}},\"required\":[\"bookmarks\",\"nextCursor\"],\"title\":\"PaginatedBookmarks\"}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"List not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/get-list.api.mdx",
    "content": "---\nid: get-list\ntitle: \"Get a single list\"\ndescription: \"Retrieve a single list by its ID.\"\nsidebar_label: \"Get a single list\"\nhide_title: true\nhide_table_of_contents: true\napi: eJylVtuO2zYQ/ZUB85IYWntTpEjghwLOFdvkIdjsoig2BjwWx9bEFKmQ1NqOIaAf0S/slxRDybe1GyzQJ8vkXA7ncmY2ylXkMbKzV1oN1ZziJw5RZUpTyD1XcqOG6pqiZ7onQAhs54bAcIgwXQPHAFdv+ypTEedBDe+UGAhqnKlAee05rtXwbqOmhJ78qI6FGt6Nm3GmKvRYUiQfkkDICypRDTcqritSQxWiZzs/gXJTENSWv9cErMlGnjF5cDOIRYtKsNAKy8qIFSbWZrWel8tvr1661Y9fi1WMLn8lgDkmEQF8pVWTKU/fa/ak1TD6mjJlsRQB0wpkigVAhbFQ8gBPoXI2UBDQv1xeys8pVrFJIZLegcudjWSjiGNVGc5T/AffguicCYSbfqNcclJ5yVbk1iPr02A1W8xnLo6gnUbZ1sbgVAIib28yxfk5wSYljqwE5DFWWoFTQbJ1KdVSoq3RqEyFEn2UstE0w9pENdzeNZn6XpNfP8pfVU8N5weiU+cMoRUrBYY3TuSdx+h8OC9VB/LXzvwUtVta8vJfc3Tycc+0TCed/3FzVE53kq0uN11kD+K40zoD8QDP+LhkVdOIkxeXz08L79ZiHQvn+Qdp+Oevv1NzvE4tCNEtyAIHKDlIM2fA9h4N6wycB1pVAvlBnUZaxUFlkM9X6D4+u747RLBD+uIUqbwErIswc7V96Pb/tEfu9NkUHrsfQYl5wZYuPKGWSgLy3nkQ9b6UQ0kh4PxRpoq6RPvQUKffVw8rIgHc2z/I7jtRTEFL/mPhOm5ORSMMqgZCJmGwaampkf4hf7/l0tobNVQb1NpTCM0AKx7cP5cyRc+CLUWou25zsu25IsYqDAeD6Nf9BXpcEFV9rKqzPNxZ2LLvx04eWizygoMx8EUS13o+HAa7sIpneUcSk5ZMQirrPt47X6Ig/P2PmxRLtjMn6vLqFtLz/mX/8oDYd3hGn69O8O8uOchUIzO7KFyIKXNT5xYl+gXbOaDVICm94HhhMJJPr+Oc+nBTcBDbgMa4ZYC1qyE6KNHifG8kZIn5QwYyITMoeF4YnhdygiFQ+rUappgv6ipA5d3cY1li5ByNWfe/2q/2yROQcMm8a/tBDkfGAFldObYxQFdbgMeNXokPDWxTiiajriuTkQkUhJp8H/50NeRoYU5W9gECtOllC1rDzLvyOL9LmsLtFdRWk4de7wvFyHYe4Lek85HWodfbwv6Mc7Y7yKnd95hDXVXOR8hrH5y/mKJArXYacM8Ik/ZykoI0MVxynEAaCLBfISQZMmvbiQxsc1NrksxOLK3im87EjMm0hChhAY6AoY3L1snOpGRyRjEv0r0YEWDUhxFMZOpM4B5NTTBz/tgHWy05omRY8uEJrIPS+QSwNjFsY/O6KxG4WVcU5HB7ElI2pgTOUttengikU8Lwq72AXs+wXfR66S0juL3+tKs3WHIswKU6RwO5x6UhDSVF1Bix36oLo+/UE7ODHAkVUyeSinMrY6GujENNGmZsCJ5yKUXuPHx++/5ZYsrKhVhiIulucfpA8XhjfNiFmz3bP27HbKniYBzJwBa223TMeJe2NZmbw25tG2dKGluuNhupsFtvmkaOu63ibrwnxkSfmoN8azWcoQn0E8xPrztCfwb/ha47RLtO/Gtq+acytaD1frVsxrKepF5MCNrLUZ5TFQ/UTuah8OtuQHx4d6Myhces+oBFk/WzkDabVuJGWKNpdggTiwjApvkXiRA7OA==\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get a single list\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/lists/{listId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nRetrieve a single list by its ID.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the list.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"ListId\"},\"required\":true,\"name\":\"listId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The requested list.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"description\":{\"type\":\"string\",\"nullable\":true},\"icon\":{\"type\":\"string\"},\"parentId\":{\"type\":\"string\",\"nullable\":true},\"type\":{\"type\":\"string\",\"enum\":[\"manual\",\"smart\"],\"default\":\"manual\"},\"query\":{\"type\":\"string\",\"nullable\":true},\"public\":{\"type\":\"boolean\"},\"hasCollaborators\":{\"type\":\"boolean\"},\"userRole\":{\"type\":\"string\",\"enum\":[\"owner\",\"editor\",\"viewer\",\"public\"]}},\"required\":[\"id\",\"name\",\"icon\",\"parentId\",\"public\",\"hasCollaborators\",\"userRole\"],\"title\":\"List\"}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"List not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/get-tag-bookmarks.api.mdx",
    "content": "---\nid: get-tag-bookmarks\ntitle: \"Get bookmarks with a tag\"\ndescription: \"Retrieve a paginated list of all bookmarks that have the specified tag attached.\"\nsidebar_label: \"Get bookmarks with a tag\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzVWetuG7sRfpUBz48kxlpOinPQA/0o4OTc3JP0BLGMokgEeLQ72mW8S25IrmxFENCH6BP2SYoh9yZ5Fa8vBdpfccThzDcXDoffboQuyaCTWp0lYipScjNMX2t9VaC5siISCdnYyJIlxFR8IGckrQgQSkylQkcJ5NI60EvAPIdFsxVchg4yXBG4jMCWFMulpAQcpoDOYZxRMhGRcJhaMf0oZvzvPBKW4spItxbTjxuxIDRkTiuXienH+XYeiRINFuTIWC9g44wKFNONcOuSxFRYZ6RKbwGfZQSVkl8qApmQcozFMGgG5zBlJHSDRZmzEkkyyW/WaXH9+cc/65uvP2Q3zun4R4YrnReZYXqWiG0kDH2ppKFETJ2pKBIKC153fj0Skq2X6DKxjb4Fl1RVcBjQxjV4DkZCS6xyJ6bhl32vzrVxoE1CBhZriA35TEKCjibwU9hrwWl4xvueTXbxLjG39GiVrcdWG/cHb2y8/lKRWQ+7rapi4QV3jb/DG1lUBYRlTo90VHhzhlxlFJRkuPJojCv30dZ6kctCuhEeHKizN5Wx2sDS6ILPiKGV1JUFQ7bUyhLbXpKLM193im5ca78prKBhhHsPN1W7GgdLd/u60DonVKJXj4OIzpZgybFhfxS84aYhPLOwrPIcYq0cKQfPf5u9exuBoxsXAbl48gKuJTcQAqnivEooAam8isajCZwH7d44LLWBXKaZI9OKWLjOSIFW+RoKcpigQ5AWFFHC7ebuqP7v+9Dmr7bxJqDZy+OcPa0Vcir/9PIl/7Pr7OlAFx/VwRlFHQXWimWZy9h3ipPPllUPVJFefKaYYZaGLx0nA7DWXk8UjcE1O8Tn9W4VMrl9MLeR8M2LklM3uFroxLsztBwJVeU5LvhAchls2+M5QhJNnMkVJQMHaBuJJa4032+H1h2mqVTpuUNX2bvtdReHreKYLF/ZS5R5ZYjDRCrhXfNtJGxVFGjkV5+lp1evtBsXnoBjPU5WVyYeobZ3fZZSROKaFlygOf9d6IXMyV/vjpTlwo+ElSrNaRkWjHdLFqU2zvtSWTJnwzUVppUnLtRwngcWmjnp9WC4Wp/ZzawqUIn5dqfDfWSLtf4dbV6ud4C1oj+WfqD6tgNh9TCUXKqrEEKTD8dv9DHa6VQj5GWBKV0MmT0kfGotuaE8D1VibIiUzbS7z64yWd5HnC+X9wwsdJB7WTIUG7zOKXnA5pVMSN8LKK5kPDIvmSvyN12l3SlfV+V94DSejzTA0k/fALFymTbjklUtcmkzGifNo/f7ese4ePCOd/X1NmLDXsvw0uEEz3kcfFxH4OHIx8f/MdQSQpMfd3KHoTY2HosVueZCMvmv2R3ivokIf8q7TQfuDb5q/jbY5g9ee2N7mZVf+2rbp9Whk/XgMHdB6Xx9gqhX6krp64G7y2+Yb9vQPv3FOyrNfKf91mti/cugzn6t6bz/+wKVInNW18heaxd102W5ev71DU8MdHIRBpKLMteYEN/nuEKHnOIudPeqsKEZoQvFfDso0E3TO7Nzb97dGW73J9nhAbSdterJqivSNufz7mnc8FKCAfKLtn4rj+jiw09nbXYfxxFoA7wV5BKUhkIb/3RjzmMi9sOy6NFkPTQ9wO+b11XHqG19dL9/+er2a+xChTtEfqUE/v3Pf3lsrz0DBk5fkeJXYCEtD68RSLXCXCYeMt2UDGrvVcZt8aTMUQ6/x7pCb3mvPoIW6fe3kc4wBaUdLHWl9q0+5i0Y62SwfvdfrQXGmVR0bAgTzjKQMdoAb/dv/IKs5XM3QpWfm/cV1ftvp9wD7PT3Uv0zb/Qx8/Zdpms+lZ1k+m8qTrjETzaeGdye9KvHklk1jKafnMUGk8SQtdsTLOXJ6hU3DDSSMfpI1cshNQ1HmDlX2unJiTPryRUavCIqJ1iWg2xoraHhQH+v5SFgYU96ZOw5J7B+r/co2Ta8bNk3RhbjB60X4u7m//hFmwIZ4V//PvMxlWqpeTt7HSC9mrycvOzRYC2e0/dnt/C3i9ICgqV8eZxp63wGm7hKlQKqBDi1x9Id58gkC3snY5rALJOWdTNrra8trHXFtEyBCtNOiY08L2IjJj1sBJlMM8/X2AhCf4q8kQXGV1VpoTQ6NVgU6GSMeb6efFKf1HffAYeLWedwLvjH0zwHUkmppXJM2vkaA9w97yXbaDmky9P6cHoll5ARJmQm8A9dQYwKUlLM5ROg8p5d0Tqwgzv5vaYFXJxBpZjiPTo6J+ekSi38xe/5ndb26KiBXfevGvJbZog6zLYq+cUMgUY8XiBDLdsdsJIIl2Hx0gfp0nOrl+AZKuiIfE5Gx5A11Bln9rJrq5ewlJSHvshhAekAbYhLY6RVeYj7hFO45PZ+CSvMq0C/7diQKuEckVfM+TB06x6oY9O0dOBL0/KPbZP32VgQaEXheBki4JNip5/UMRwd8UhxdOR9OYWLD2/beoNr6TLQvs4xh3oQaGnASdjOjb3d7hu85yC5JVMt4ouzkVFQ1cMD8JQAz/3kyhfH+59+eeE7ZqmtYwJhumnYxV/J9bhAjwv5FOwfxk3X/P8rn4dCg+ndZR27EPrqx2Z0mDbfXLrWOo8ENwYW2my4Qi9Mvt3yz4Em5YabSMuN4wAh3HfvAd9GBuFf0Xrve4kvRzEVnn8fD+h+Xzi+gaX56vFAHI/46nEYU/t54oGg/h+Y/G+4f4vd78Iw74aBe1bw8w/1MPMCDllvHlhq3bfZoApHzL/JwvXjAYS10zimso/01ijIyNvZ6NefZyLQJn0mfHdw6FOSu4g2myAx44tyu+0A8v8Z4Hb7HzCJvDs=\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get bookmarks with a tag\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/tags/{tagId}/bookmarks\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nRetrieve a paginated list of all bookmarks that have the specified tag attached.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the tag.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"TagId\"},\"required\":true,\"name\":\"tagId\",\"in\":\"path\"},{\"schema\":{\"type\":\"string\",\"enum\":[\"asc\",\"desc\"],\"default\":\"desc\",\"description\":\"Sort order by creation date. Defaults to 'desc'.\"},\"required\":false,\"description\":\"Sort order by creation date. Defaults to 'desc'.\",\"name\":\"sortOrder\",\"in\":\"query\"},{\"schema\":{\"type\":\"number\",\"description\":\"Maximum number of items to return per page.\"},\"required\":false,\"description\":\"Maximum number of items to return per page.\",\"name\":\"limit\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"description\":\"Cursor from a previous response to fetch the next page.\",\"title\":\"Cursor\"},\"required\":false,\"description\":\"Cursor from a previous response to fetch the next page.\",\"name\":\"cursor\",\"in\":\"query\"},{\"schema\":{\"type\":\"boolean\",\"default\":false,\"description\":\"If set to true, the bookmark's full content (HTML, text, etc.) will be included in the response. Set to false for lighter responses when only metadata is needed.\"},\"required\":false,\"description\":\"If set to true, the bookmark's full content (HTML, text, etc.) will be included in the response. Set to false for lighter responses when only metadata is needed.\",\"name\":\"includeContent\",\"in\":\"query\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"A paginated list of bookmarks that have the specified tag.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarks\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"userId\":{\"type\":\"string\"},\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"attachedBy\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\"]}},\"required\":[\"id\",\"name\",\"attachedBy\"]}},\"content\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true},\"description\":{\"type\":\"string\",\"nullable\":true},\"imageUrl\":{\"type\":\"string\",\"nullable\":true},\"imageAssetId\":{\"type\":\"string\",\"nullable\":true},\"screenshotAssetId\":{\"type\":\"string\",\"nullable\":true},\"pdfAssetId\":{\"type\":\"string\",\"nullable\":true},\"fullPageArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"precrawledArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"videoAssetId\":{\"type\":\"string\",\"nullable\":true},\"favicon\":{\"type\":\"string\",\"nullable\":true},\"htmlContent\":{\"type\":\"string\",\"nullable\":true},\"contentAssetId\":{\"type\":\"string\",\"nullable\":true},\"crawledAt\":{\"type\":\"string\",\"nullable\":true},\"crawlStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\",\"nullable\":true},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true},\"size\":{\"type\":\"number\",\"nullable\":true},\"content\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"assetType\",\"assetId\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"unknown\"]}},\"required\":[\"type\"]}]},\"assets\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"pdf\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"avatar\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"userId\",\"tags\",\"content\",\"assets\"],\"title\":\"Bookmark\"}},\"nextCursor\":{\"type\":\"string\",\"nullable\":true,\"description\":\"Cursor for the next page, or null if no more results.\"}},\"required\":[\"bookmarks\",\"nextCursor\"],\"title\":\"PaginatedBookmarks\"}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Tag not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/get-tag.api.mdx",
    "content": "---\nid: get-tag\ntitle: \"Get a single tag\"\ndescription: \"Retrieve a single tag by its ID, including the number of bookmarks using it.\"\nsidebar_label: \"Get a single tag\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVtuO2zYQ/ZUB85IYWntTpEighwJOc8E2eQgSL4pis4DH4lhiTJEKOdq1YwjoR/QL+yXFULLX63XTAu2TbM5weDhn5gy3yjcUkI13F1rlqiSeYakypSkWwTRiULn6SBwM3RAgRONKS8BYwmIDhiNcvMrAuMK22rgSuCJwbb2gAH4JC+9XNYZVhFY2guGxyhRjGVV+pWbyvc5UpKINhjcqv9qqBWGgMG25UvnVdXedqQYD1sQUYnKIRUU1qnyreNOQylXkYNxD0LOKoHXma0tgNDk2S9ODEoiMpSChNdaNlSCGjLbrTVnffnnx3K+//VitmX3xQuAaTi4zLC+06jIV6GtrAmmVc2gpUw5rsXOyZ8rI6Q1ypQR9oNh4FykK4h/Oz+XzEKiEpMikU2ZvDVfQRiwJIiObyKaIgrfwjsmxxMCmsaZI1E2+RAl0IjV+8YUKVplqghDNpodh9MP0dbt7nDK09csdkwcOPc/HDi83U2YsKtKz5PZPYNCcDFm1NboTlq67x8CVXGaAfgT0u7Cu7/E6hH12/vQhPZcOW658MN9Iw5+//5Hq52WqUmC/IgcmQm2iFLh0wg1aozPwAWjdCMgj4pjWPGksmtOU7av5rjYPEeyRPjtRSFiC8wxL37rjU/9LuRRen6iL436bQo1FZRydBUKNC0tAIfgAsn0slNYUpab/TajE/nGgYf9YHZdAAngX/4Db17Ix5Sydz5UfZE4uKS2aq4nI0WSburdTokbhZqc1bbAqV1vUOlCM3QQbM7l5qjJ1g8EIsr6Ce3NPyBJbyypXFXMT88mEw2a8woAromaMTXNSp4YIO3V6N/hDj0XwH8jkJ6GtP/lQLPdJlZPlHslN5YOTyoYfb3yoURD+8ussZdK4pZftcuse0tPx+fj8QPn2eKYfLh7g3xtNlPlAdnlW+ciJt53+i/aj0yCEnhk+s8gU0u1MQWOYVSZKbEBr/W2EjW+BPdToRAL3QyQDayLHTDQyZlCZsrKmrGQFY6T0dRoWWKzaJkITfBmwrpFNgdZuxp/dZ/foEUi6ZB703SCLU2uBnG68cRxhqCzA+13eyBkajEsUzadDS6Ygc6gINYUx/OZbKNBBSU4GKwG6dLMVbWAZfH2f31tawOUFtE5TgNHoEzEbV0b4Ke15R5s4Gu1gf8DSuD3k9ybyAebYNo0PDEUbog9nCxSozX4H3BiEeW+cpyTNrakNz+FrS2EDdyNWyJBx1A+tYbCTMDt3tOafhxBLQ7ZXQ0kLGAaMfV52h+xDCpNL4qLq3wa0ZgFGY5jC3LXWzuEGbUuw9OH+GcZp4YhSYOEjEDgPtQ8JYGs57nKzU3kQbY+yuNf9xMaCwDvq2ysQgXRKzD+7MxiNrHGr0SjdZQqXH9/v660fwz7VOVooAt5a0lATo0bGcb9d5Hy/Pck6yJIIMQ0uqTh3Pg7axnrUpGFpLMFjU0uR+wAfXr15knSy8ZGH4Tc8Ld4S33t7HTfh9k7q//fHWi8qB1Ory3pd3A4KetU/6DKV9y+g60yJAIhhu5VKvAy262Q5FZs86+4ENMmsNlF+a5Uv0Ub6zuUefxxk/wn8HbZhEd0m6bRt5Z/K1Io2+0dady0vjNSyCUBvmxYFNXyw68HQFBneT5G3r2cqU3hffI/ENkU/iWi77T1mIi5ddwdQ/gvArvsLHy8NAA==\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get a single tag\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/tags/{tagId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nRetrieve a single tag by its ID, including the number of bookmarks using it.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the tag.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"TagId\"},\"required\":true,\"name\":\"tagId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The requested tag with usage statistics.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"numBookmarks\":{\"type\":\"number\"},\"numBookmarksByAttachedType\":{\"type\":\"object\",\"properties\":{\"ai\":{\"type\":\"number\"},\"human\":{\"type\":\"number\"}}}},\"required\":[\"id\",\"name\",\"numBookmarks\",\"numBookmarksByAttachedType\"],\"title\":\"Tag\"}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Tag not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/karakeep-api.info.mdx",
    "content": "---\nid: karakeep-api\ntitle: \"Karakeep API\"\ndescription: \"Karakeep is a self-hostable bookmarking and read-it-later service. This API allows you to manage bookmarks, lists, tags, highlights, assets, and backups programmatically.\"\nsidebar_label: Introduction\nsidebar_position: 0\nhide_title: true\ncustom_edit_url: null\n---\n\nimport ApiLogo from \"@theme/ApiLogo\";\nimport Heading from \"@theme/Heading\";\nimport SchemaTabs from \"@theme/SchemaTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Export from \"@theme/ApiExplorer/Export\";\n\n<span\n  className={\"theme-doc-version-badge badge badge--secondary\"}\n  children={\"Version: 1.0.0\"}\n>\n</span>\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Karakeep API\"}\n>\n</Heading>\n\n\n\nKarakeep is a self-hostable bookmarking and read-it-later service. This API allows you to manage bookmarks, lists, tags, highlights, assets, and backups programmatically.\n\n## Authentication\n\nAll endpoints require a Bearer token passed in the `Authorization` header. You can generate an API key from the Karakeep web UI under **Settings > API Keys**.\n\n## Pagination\n\nList endpoints support cursor-based pagination via `cursor` and `limit` query parameters. The response includes a `nextCursor` field — pass it as the `cursor` parameter to fetch the next page. A `null` value for `nextCursor` indicates there are no more results.\n\n## Bookmark Types\n\nBookmarks can be one of three types:\n- **link** — A URL bookmark with optional crawled metadata.\n- **text** — A plain text note.\n- **asset** — An uploaded file (image or PDF).\n\n<div\n  style={{\"marginBottom\":\"2rem\"}}\n>\n  <Heading\n    id={\"authentication\"}\n    as={\"h2\"}\n    className={\"openapi-tabs__heading\"}\n    children={\"Authentication\"}\n  >\n  </Heading><SchemaTabs\n    className={\"openapi-tabs__security-schemes\"}\n  >\n    <TabItem\n      label={\"HTTP: Bearer Auth\"}\n      value={\"bearerAuth\"}\n    >\n      \n      \n      \n      \n      <div>\n        <table>\n          <tbody>\n            <tr>\n              <th>\n                Security Scheme Type:\n              </th><td>\n                http\n              </td>\n            </tr><tr>\n              <th>\n                HTTP Authorization Scheme:\n              </th><td>\n                bearer\n              </td>\n            </tr><tr>\n              <th>\n                Bearer format:\n              </th><td>\n                JWT\n              </td>\n            </tr>\n          </tbody>\n        </table>\n      </div>\n    </TabItem>\n  </SchemaTabs>\n</div>\n      "
  },
  {
    "path": "docs/docs/api/list-backups.api.mdx",
    "content": "---\nid: list-backups\ntitle: \"Get all backups\"\ndescription: \"Retrieve a list of all backups for the authenticated user, including their status and metadata.\"\nsidebar_label: \"Get all backups\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy9VsGO2zYQ/ZUBc2kNrb0pevKhgDdtgm1SIEh2URTJAh5LY4kxRTLk0LuOIaAf0S/slxRDybJ310B76kkCZzh8nHnzhnvlPAVk7ex1pebK6MhXWG6Sj6pQFcUyaC9WNVcfiIOmLQGCuIFbAxoDq94d1i4ANwSYuCHLukSmClKkUIC2pUmVtrV46ACRkVMEtBW0xFgh41QVirGOav5JHRDcFSpSmYLmnZp/2qsVYaCwSNyo+ae77q5QgaJ3NlJU87364fJSPo9RL86BlcNKZ5ksyw703ghe7ezsS5RtexXLhlqUP955UnPlVl+oZFUoHyRnrPtDh4gnjhgC7lShNFMb/z2Ark58Igdta9UVShJ3fd6EMRKfsxXKJmNwZUjNOSTqClUGkjos+GykqL/RicGmdkVBDCvnNi2GzSuXLJ/16Et4DgPZ1EoVPdmqX4mpLCkKo9aoTQqk7rpCUQgu/EYxYk3/4S6dVPtr0oEqia4rNSbpmJLTCw/Xe3qZEfpd9zTmoZZi6Qr14+XL53y6tUJwF/Q3quDvP//KnL/KxAR2G7KgI7Q6Rm1rIf4Wja4KcAHowctBT7jH9MAzb1CfZ90xqw/YeknGIwSq66G2xI2TBq4pMwylRdRsNbZypLClEHMbpWDUXO2xqgLF2M3Q69n2pSrUFoOWjOeyDuY+A2tMhtVcNcw+zmczDrvpBgNuiPwUvX8mFjeiBH0E6T1J0tvBH3osAv2kwT/KzYeWOmnzMRFystwju6n54CTVzT+vXWhREP76+42SjGi7drJdbt1Dejm9nF6KymjOeRzxLN5fP8M/GnUEhEhmfdG4yJIdOPBJ9EwkLBBWF5ovDDKFfDtd0hRuGh0ltgiPu4+wcwnYQYsW62OQWGSBigWI+hXQ6Loxum5kJbNavrYaZdYHVwdsWxSFNWY3/Ww/2xcvYHGUXe2sLC6MAbKVd9pyhIHngI/J6uWMCrTNJVouBmblIEtoCCsKU/jDJSjRQk1WpgUB2nyzDe1gHVz7uL73tILba0i2ogCTyUdi1raO8FPe85Z2cTI5wH6PtbYj5Hci1UfMMXnvAkOZQnThYoUC1Y87YKsRlr1xmZO0NLrVvISvicIOPAZsiSlEKQbBYVgM44iksktLD/xqCLHWZPqmlrSAZsDY5+VwyBhSKrkmLptslyACjKawgKUI1xK2aBLlqfjoDG2rPBpzYKlHILAOWhcywGQ4HnJzNVAEbnaeoiweVmKuxorAWerbKxCBdEqcf7YXMJkYbTeTSb7LAm4/vBv5BveaG3CZ52igDHhv6GQM99tFlcbtWZ1AlsA6psElk/PgYyF547CiCtbaEHynWyG5C/D+59ffT2VgeBe5xaxyFnMLvyE+HcpPe3B/lMn/4+3Ry8yJHMsQFqXcD3J6Mh8KJWIgS/u9sPI2mK6T5Uw8eZwcxTQ/VQrVt1LW3w3t5GFSluQ5q65J+d3w9BUi8jgK+5tfbmTOPRbFJyKYox9eIXZ3Enu/7z1upOm7ThUDiCwCqpOB9w8jvIXe\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get all backups\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/backups\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nRetrieve a list of all backups for the authenticated user, including their status and metadata.\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"A list of all backups.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"backups\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"assetId\":{\"type\":\"string\",\"nullable\":true},\"createdAt\":{\"type\":\"string\"},\"size\":{\"type\":\"number\"},\"bookmarkCount\":{\"type\":\"number\"},\"status\":{\"type\":\"string\",\"enum\":[\"pending\",\"success\",\"failure\"]},\"errorMessage\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"userId\",\"assetId\",\"createdAt\",\"size\",\"bookmarkCount\",\"status\"]}}},\"required\":[\"backups\"]}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/list-bookmarks.api.mdx",
    "content": "---\nid: list-bookmarks\ntitle: \"Get all bookmarks\"\ndescription: \"Retrieve a paginated list of all bookmarks for the authenticated user. Supports filtering by archived/favourited status and sorting by date.\"\nsidebar_label: \"Get all bookmarks\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzVWc1uIzcSfpUCc5jEaMuexZ50CKCZ7CTezGyM2MZiMSNAVHe1mhGb7JBs2RqhgX2IfcI8SVBk/8lqe1oaH7InG+oq1lfF+iE/7pgu0HAntLpK2JRJYd0brdc5N2vLIpagjY0o6Dubsl/RGYEbBA4FXwnFHSZAKqBT4FLCslGFVBtwGQIvXYbKidjLlhbNBG7KotDGWUiFdGiEWsFyC9zEmdhgcpHyjS6NIHnruCstcJWA1cbVkgl3OGERc3xl2fQj6wDPI2YxJuUtm37csSVyg2ZWuoxNP86recQKbniODo31AjbOMOdsumNuWyCbsqXWErk6cP2dh9rHWYObsCpiBn8vhcGETVMuLR6hHDHFczLcfGERE6Tze4lmy6roJIwHITwO5YB6i7P7NgKpdbS7LGKoypy2itu4hk17lWDKS+nYNPzyGNCNNg60SQKo2KDP07D98EPQteA0vCK9V2N8PHrJ1m/Kv19IcYTbqsyXXnDf+Af+IPIyh/CZakY4zL05g640Cgo0VFk4xpVjVmu9kCIX7piN27f5tjRWG0iNzqkHGNwIXVowaAutLJLtFF2c+dJX+OBa+044ie0KI9w73VTtahwsHVdOdT4OIrpKwaIjw86UGHnDTcN7ZSEtpYRYK4fKwbc/3X54H4HDBxcBunjyHdwLapAIQsWyTDABofwSjUcTuAmre+O+gUqxyqgkGxEL9xkq0EpuIUfHE+44CAsKMcFkTNL89X1o96+28TagebSPc/K0XpC28m+Xl/Rn39nZwJRqJxRZqj0lTV4UkoaU0OriN0vqA5mil79hTFAKQ2PTiWC8XbMnyo3hWwJNNfnlJURyWHxVxHyDwmTmBr/mOhGpGP4cMVVKyZdUdLTVVVuCIyTbWXRYJFXUnwCD3x1frYRa3fjJ8WV73XCwZRyjpWNHyoUsDVKYUCWkNa8iZss850Z89rv08ssr7caFJ+DYjpPVpYlHLNsbkYVgEbvHJSWopP9zvRSS0OKDQ2UpuSNmhVpJTMMH490SOR2rvC900LoazqlwanrhRA01O/CBO8fjDJM3g+FqfSY3szLnis2rvS72kSzW6++t5uV6BawV/pL6M93zDoSvT0ORQq1DCI0cjt/oMtrrRiPkRc5XeDdk9inhmbXohvZ5KBNjg6hspt0xWkWSHiNOA+SagIUOcpQlg7Hh9xKTE5Q3IkF9FFC+EfHIfclcLt92mfZF+Torj4HTeD7SAEm/fAOku5o24zarXEphMxwnTcfr61pjXDxI40M93kYoPGoZXjpU8JyOfF/XEegA5OPj/xlqCaHJj6vcYaiNja/FyinnwmbSf7dfEPdNhPkq75SemBs0av412OafHHtje5kVn/vLttenpyrr5DB3Qel8fYGol2qt9P3A7PIK86oN7csP3lHbTDPtp14T6w+DevfrlW76vy+5Umiu6hx51NpZ3XRJrj7/+obHBjo5CweSu0JqnnjigG+447TFXeiOyrChM0IXink1KNCdpvfOzlGfe9mjN/ZPssMH0PasVZ+suiRt93zeXX8bqooRQLq11vfhEV18+Hpc02ztBTgCbYBUQaSgNOTa+OsZ8RoT9jgsyx7V10PTA3zd3KA6kq3y0f375evDG9edCjNEfMYE/vjv/zy2N56EA6fXqOimlwtLh9cIhNpwKRIPGR8KAvXoVkZt8aKQXAzfx7pEf+B54fH2EXikdE9Cl2liN1chPTnRgeyi77xFs2k4QX/wYzueJAatrS54IS42rynfuRG0I74e688hBg2NlTlX2OnFhTPbyZobvkYsJrwoDoiUW6JGwwp0K6Uw/VzLQ8BC4Ht05g35Xl83e6RmGwqy7OuaxOg+5oWoOP0/77TJOSH8579vfRYIlWpSJ68DpNeTy8llj6lp8cyurw7wtx+FBQ4WZXqeaesoOu0Vm9haYm4N8uRcuHPJiQcg70SME7jNhKW1iTjW9xa2uiTmIOeKr7pFbOSv7jYCqq4IMrHKPKVgIwjlFXkjSx6vy8JCYfTK8DznRDlLuZ18Up/UN9/ArOOhhVb040xKQJUUWihHvJKvCuD76VqQjZbmWMzq3PKLLCBDnhCh/R9dQswVrFARmY7AlfdsjdtAYO3t7z0u4e4KSkUs5NnZDToiti1873V+xq09O2tg1+VXQ35PJEaH2QYeHQLTdb7kBLVoNWAjOCzCx4UP0sLTfwvwJAp0VDhtRkfiNOwO7eyi6woLSAXKUNYUFhAOuA1xaYy0Sz5Fz8EMFtSdFrDhsgwM0Z4NoRL/VuAXpv0weNDG6tg0HQmo51v6se1RfjeWCFphKC+DCFQpdvpJncPZGU3EszPvywzufn3f5hvcC5eB9nnOJdRzrGWqJkGd+lKr7vuTp8mAKIRaxCdnI6OgrGcfvXkgfOsPXtT3rn94950n7gptHd1/p7uGAPsR3f6jyuMq3HWt8i/3NBO6Uq9/dzfq0H/7w2ceMeoe9ONuR2l8Z2RV0c+B7qOunAhL3eUJYrMfimdfWwaBrXG7/wLjk5NNmSeMT7E8+ILyjO29Y8eJ1k9423gGUf+940RAx71QPIOlebU4EcdXvFo8jal9XjgR1P8DE/+M+wfsfBeGeXdS8q+uEQtz0pdx0J7FMRZ9rQP6nVZpz20//uOWBXqizzjvn3D61B9X297au12QuKWJXlWsccFPeFbRZeFPhCDUDw==\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get all bookmarks\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/bookmarks\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nRetrieve a paginated list of all bookmarks for the authenticated user. Supports filtering by archived/favourited status and sorting by date.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"boolean\",\"description\":\"Filter by archived status.\"},\"required\":false,\"description\":\"Filter by archived status.\",\"name\":\"archived\",\"in\":\"query\"},{\"schema\":{\"type\":\"boolean\",\"description\":\"Filter by favourited status.\"},\"required\":false,\"description\":\"Filter by favourited status.\",\"name\":\"favourited\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"enum\":[\"asc\",\"desc\"],\"default\":\"desc\",\"description\":\"Sort order by creation date. Defaults to 'desc'.\"},\"required\":false,\"description\":\"Sort order by creation date. Defaults to 'desc'.\",\"name\":\"sortOrder\",\"in\":\"query\"},{\"schema\":{\"type\":\"number\",\"description\":\"Maximum number of items to return per page.\"},\"required\":false,\"description\":\"Maximum number of items to return per page.\",\"name\":\"limit\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"description\":\"Cursor from a previous response to fetch the next page.\",\"title\":\"Cursor\"},\"required\":false,\"description\":\"Cursor from a previous response to fetch the next page.\",\"name\":\"cursor\",\"in\":\"query\"},{\"schema\":{\"type\":\"boolean\",\"default\":false,\"description\":\"If set to true, the bookmark's full content (HTML, text, etc.) will be included in the response. Set to false for lighter responses when only metadata is needed.\"},\"required\":false,\"description\":\"If set to true, the bookmark's full content (HTML, text, etc.) will be included in the response. Set to false for lighter responses when only metadata is needed.\",\"name\":\"includeContent\",\"in\":\"query\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"A paginated list of bookmarks.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarks\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"userId\":{\"type\":\"string\"},\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"attachedBy\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\"]}},\"required\":[\"id\",\"name\",\"attachedBy\"]}},\"content\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true},\"description\":{\"type\":\"string\",\"nullable\":true},\"imageUrl\":{\"type\":\"string\",\"nullable\":true},\"imageAssetId\":{\"type\":\"string\",\"nullable\":true},\"screenshotAssetId\":{\"type\":\"string\",\"nullable\":true},\"pdfAssetId\":{\"type\":\"string\",\"nullable\":true},\"fullPageArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"precrawledArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"videoAssetId\":{\"type\":\"string\",\"nullable\":true},\"favicon\":{\"type\":\"string\",\"nullable\":true},\"htmlContent\":{\"type\":\"string\",\"nullable\":true},\"contentAssetId\":{\"type\":\"string\",\"nullable\":true},\"crawledAt\":{\"type\":\"string\",\"nullable\":true},\"crawlStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\",\"nullable\":true},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true},\"size\":{\"type\":\"number\",\"nullable\":true},\"content\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"assetType\",\"assetId\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"unknown\"]}},\"required\":[\"type\"]}]},\"assets\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"pdf\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"avatar\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"userId\",\"tags\",\"content\",\"assets\"],\"title\":\"Bookmark\"}},\"nextCursor\":{\"type\":\"string\",\"nullable\":true,\"description\":\"Cursor for the next page, or null if no more results.\"}},\"required\":[\"bookmarks\",\"nextCursor\"],\"title\":\"PaginatedBookmarks\"}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/list-highlights.api.mdx",
    "content": "---\nid: list-highlights\ntitle: \"Get all highlights\"\ndescription: \"Retrieve a paginated list of all highlights across all bookmarks for the authenticated user.\"\nsidebar_label: \"Get all highlights\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy1V1GP2zYM/iuE9rIFvtx12FMeBqTb2nXtsKK9wzBcA4SxmViNLKmUnLssMLAfsV+4XzJQdhznklu7FXvKQZbIj+THj7ydcp4Yo3b2RaEmyugQf9Sr0uhVGYPKVEEhZ+3lgpqoNxRZ04YAweNKW4xUgLwBtwQ0Bsr+LWDOLoR0unBuXSGvAywdQywJsI4l2ajzZKEOxGOVqYiroCa3aoBglqlAec06btXkdqcWhEw8rWOpJrezZpYpj4wVReKQLoS8pArVZKfi1pOaKFtXC+KTUH7Ge13VFbSfBb+OVAWIDphizRY8sURJY9VkiulDrZkKNVmiCfQ51jJlsaKU7EpHlSktFj7UxFvVZGciCJG1XZ1E8F3NwTEs2VVSD6aNdnUApuCdDSS+lxTzMiXc0n3s/UcdDfUWPiG8/+6qCzVvPR3HOhO/rYUg4X59dSU/x66nZ5h2YJm4yJ2NZKM8Re+NcEo7e/k+yPsz6XSL95RL3j0L+aNuvR+MDu4iM24FtlTz4zb2RJdeeli+JlMhIsdflstA8ZSgTabIFv/wNXfG8TlakK0raZstGePulGS1UJlaMZFVmVqYmqSPClpibaKa7C82mYp0H8+ZtLUxuBCSRK6pyZR1kT7poj4furT4I1nJmaS60zNAmiNq3g7ze5zNYe66qDrMCVHvf+htdmiEXnCSRyFw1xofjfixTul0ru+FDByDPAW9BOugckzSP7UREj+MsxxK8ADOAPLrfVcMxLJpxNA3V09O2+jGiuQ61r9TAX/98WdC9zSJKUS3Jgs6QKVD0HaVgbYbNLpIoOneC6wHnSYpvvQG9fkeO3DzHiufAA8RJKRNpiqKpZO5s0p18yiyri6Pwg/Em72412zURO2wKJhCaC7R68vNE5WpDbKWoqQu7D63SdhzvozRh8nlZeTteI2MayI/Ru9PZPVaxlNrQbRG8vSyuw8tFkE/mEtvJfiu/wfTqc+FeJY40jU16S5JY6Y/njmuUBD+9Ot1IoK2SyfPJeoW0pPx1fhqoNs9nunrFyf4+486AEIgs7woXYiSnX4Qa7sCtAUwYXGh44XBSJyi0zmN4brUQWzL8HZ3AbauFomv0OLqYCRkSZBDBjK3s4EqZ4AhUPq1BSwwX9c+gGe3YqwqlLFvzHb8zr6zX3wB08MuoJ2Vw6kxQLbwTtsoUyY1BuAxX734KEDbVKL5tCNXMjKHkrAgHsNvroYcLazIyp5DgDZFtqZtO86O6ntHC7h5AbUtiGE0eksxarsK8G1685K2YTTaw+4asIP8SkbTAXOovXccoZ17FwsUqL5/ARuNMG8/zlOS5mkZmEMajXDYaaQYdBi02uamLkgqOz/owhyWmkzb15IW0BEwtHnZO+lNPjasYQpzEag5bNDUlCTsyIe2RdrXkmGpB9OJknW5edpRBK63noIcPu1XQKnGgsBZatuLiUA6JUze2QsYjYy269EoxTKFmzever7BnY4luMRzNJAz3hkqoKKIBUYct89FmPrnSaBAjkCGQXclkXN/x0LtjcOCClhqQ/ClroTkjuH198++SrufdyFWmISuW2eeU3yw7j5sw91BLP/3rbmVmYEiy7wVpdx1ino0UGaZEj2Q091OiHnDpmnkuF3LRGcLHUQvHlkHh7H9u8X3LNQ1bQfLcCKfmqi0Cn86js9Yhh/H1G+tB1Czw6xJ/4BkqlWalLb21TTPyQ9DOdlKxUo/+p7/cK0yhccz48GMSNb3K6ndDmzvdu2Na9HEplF76EkjVTNrmuZvOTne2g==\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get all highlights\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/highlights\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nRetrieve a paginated list of all highlights across all bookmarks for the authenticated user.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"number\",\"description\":\"Maximum number of items to return per page.\"},\"required\":false,\"description\":\"Maximum number of items to return per page.\",\"name\":\"limit\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"description\":\"Cursor from a previous response to fetch the next page.\",\"title\":\"Cursor\"},\"required\":false,\"description\":\"Cursor from a previous response to fetch the next page.\",\"name\":\"cursor\",\"in\":\"query\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"A paginated list of highlights.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"highlights\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"bookmarkId\":{\"type\":\"string\"},\"startOffset\":{\"type\":\"number\"},\"endOffset\":{\"type\":\"number\"},\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"],\"default\":\"yellow\"},\"text\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\",\"nullable\":true},\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"}},\"required\":[\"bookmarkId\",\"startOffset\",\"endOffset\",\"text\",\"note\",\"id\",\"userId\",\"createdAt\"],\"title\":\"Highlight\"}},\"nextCursor\":{\"type\":\"string\",\"nullable\":true,\"description\":\"Cursor for the next page, or null if no more results.\"}},\"required\":[\"highlights\",\"nextCursor\"],\"title\":\"PaginatedHighlights\"}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/list-lists.api.mdx",
    "content": "---\nid: list-lists\ntitle: \"Get all lists\"\ndescription: \"Retrieve all bookmark lists for the authenticated user, including both manual and smart lists.\"\nsidebar_label: \"Get all lists\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy9Vs1uGzcQfpUBc2mFtWQXPelQQEmbwE0OQWKjKGwBGi1HWkZckiFnZW+FBfoQfcI+STvc1Y9toe2pJ63I4cw3f9/MTvlAEdl4d63VVFmT+INJnFShNKUymiB3aqo+EUdDWwK0Fpbeb2qMGxD5BCsfgSsCbLgix6ZEJg1NoliAcaVttHFrWHquoEbXoAV0GlKNkXsNY1UoxnVS0zvVm58XKlHZRMOtmt7t1JIwUpw1XKnp3bybFypSCt4lSmq6U99dXsrPU8gzaweA/sGRhmULPkKqMJKGB8NVBl02MZLjDFdwlN4xORZ1GIIVZ4x3ky9JdO5UKiuqUb64DaSmyi+/UMmqUCFKLNn0iLLhEzGMEVtVKMNUp39/bvSJTOJo3Fp1hXJY09mLJ54/vy+Ua6zFpSU15dhQVyhTnhPsChVQonF9xvwZLb3AS0FyTS257LOtCpVzLUnVtMLGspru77pCfW0otv/JXmiW1pQnokvvLaETLRWmN17kfUT2MZ2Xkix/8vYfUUu1RPmvDXv52Bp6yCeD/XnXSf19bUwkLS+MVkNuhsiexPHw6gzEEzzzQrFhAZY7QHXPbfQFNe/yxfeXVy/r/dZJA/pofvu72P/8/Y9c3q9z4wD7DTkwCWqTknFracwtWqML6Ql6DGLmWfkzPfIkWDTnC/8Yt0esQ0Z+ikA8EKg1ceWFW9aUyxylhdXEDiyTKG5JsnW3U020aqp2qHWklLoJBjPZXkkCMBophJzU4br3f19NFXNI08mEYzveYMQNURhjCC947EZ4qtcAfpVD9H6Qhx6LAD+hn8/id2/5lIQOYRDL4kcWk2LLQqoYPt76WKMg/PmXGyXxMG7l5bl43UO6Gl+OL9Ux/wc8s4/XL/AfLk0ChER2dVH5xBKdAy0L2wrDRkJ9YfjCIlPM3pmSxnBTmSS6hcr9Q4LWN8BeqBnXRyWp6MmzAGHmAiqzrqxZV3KCKVH+dRqWWG6akCBEv45Y1yj8b207vnf37tUrmB2HgvFODoWXyengjeMEQ40DPi3VIDY0GJdTtJgNdZWVLKAi1BTH8KtvoEQHa3IyxgjQZc821MIq+vppfh9oCbfX0DhNEUajz8Rs3DrBD/nNe2rTaLSH/RHXxh0gS0ueYE5NCD6yzI7k48USBWo4vICtQVj0l4scpIU1teEFZKqDgBFrYopJkkGwH2XDsCTJ7MLRI78ZVKwM2b6lJSxgGDD1cdkbOaiUTK6Iy364iRIBRmOYwUL4dAFbtA3lmf3EhnE6D+6sWPIRCZyH2scMsLGc9rF5vR//N22gJIf7k5SzsSTwjvr2ikQgnZKm9+4CRiNr3GY0yr7M4PbTh+MukSeyz3WOFsqID5Y01MSokXHcPxdOOjzP3ARyBM4zDSK5OPcyDppgPWrSsDKW4BtTS5H7CB9/fPvtOE88n7jGzHH9eFXviPOaY89tQrsjRf4PS1HPMSdMLDNMaHI3MOlhMBRKeEAOdjspyNtou06Oh/F6Nz/yaN6hCtV3UabeDbWyMZUlBc6Ea5u8tTzfgIQZD4z+7qcbVSh8yofP+C9r3+9Arj3Rvdv1EjfS712nigFE7n/VyaT7C6UyuQc=\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get all lists\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/lists\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nRetrieve all bookmark lists for the authenticated user, including both manual and smart lists.\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"All lists owned by or shared with the current user.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"lists\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"description\":{\"type\":\"string\",\"nullable\":true},\"icon\":{\"type\":\"string\"},\"parentId\":{\"type\":\"string\",\"nullable\":true},\"type\":{\"type\":\"string\",\"enum\":[\"manual\",\"smart\"],\"default\":\"manual\"},\"query\":{\"type\":\"string\",\"nullable\":true},\"public\":{\"type\":\"boolean\"},\"hasCollaborators\":{\"type\":\"boolean\"},\"userRole\":{\"type\":\"string\",\"enum\":[\"owner\",\"editor\",\"viewer\",\"public\"]}},\"required\":[\"id\",\"name\",\"icon\",\"parentId\",\"public\",\"hasCollaborators\",\"userRole\"],\"title\":\"List\"}}},\"required\":[\"lists\"]}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/list-tags.api.mdx",
    "content": "---\nid: list-tags\ntitle: \"Get all tags\"\ndescription: \"Retrieve a paginated list of all tags. Supports filtering by name, attached-by source, and sorting by name, usage count, or relevance.\"\nsidebar_label: \"Get all tags\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzNVs2OGzcMfhWCubTGrL0peppDAW/aBGlyCBIvimLXgOkZ2qNYI00kjXcdw0Afok/YJykojf+dzaKnnrw7osiP5KePXKNt2FFQ1rwtMUetfBjR3GOGJfvCqUaOMMePHJziJQNBQ3NlKHAJYg12BqQ1BJr7Pnxqm8a64GGmdGCnzBymKzBUcwYUAhUVl1fTFXjbukK+mRK8deHIsPU0Zyhsa0IG1oFjzUsyBfcxQ4mD+R1GlOMMPRetU2GF+d0ap0yO3bANFeZ34804w4Yc1RzY+Wjgi4prwnyNYdUw5uiDgMRNho6/tMpxifmMtOcMBQzm8eeVNYGUkbIoqcaXlt0KN9m3HWbIpq0FaHSTYUwKJUyXjIAveUatDph3x0/AkCr9l/CkMMOqrclghsYaxvETUbY9ulk9P9YT7orWeeue4cq09ZTF0LRa01Qz5sG1/IRrrWp1WpCxmPvGGs9enP90fS0/x0weXiCwkAoeVKgOueeFboU1gU0QN9Q0WhXxrQw+e/F1IRE7/cyF4GqcvKygEpLE2p0VOUexwoFr//3bqrxY9VSJSwdtfWPtoia38OdFPjG4WQ27po+i2ffAkLroMlHs/GSzOWrinSSTbR/FEdAnYY0zDCoIL+Tlozg1/BheJYJd4P8xkU7lLN2DmXUQKgZxJbTgqDdyFdQMjIXaOgbHvtVCh9NUQlLKAyDjlO/P1y/PiXdrqA2Vdeorl/DPX3/HyDdRsSDYBRtQHmrlvTLzDJRZklZlBMSPjYQ84WPgxzBoNKnLTNwrwSPVTazcIYLYmU2GNYfKivTPOfaaRDtx0KXm2S232tk6jTmuqSwde78ZUKMGy5eY4ZKcklIneqTjlP5W3aoQGp8PBsGt+gtytGBu+tQ0Z2NmVDF0HuK7rBjedfaQsAjuA9n/JGmnyIfiv6uCRJY8ohnmnRFm3R+vratJEP7+xyi2V5mZleuSdYL0sn/dv8Y9/XZ4hh/enuHfHSoPBJ717KqyPkh1YNpRW2adzD3HVF6pcKUpsIvZqYL7MKqUF98yVe2Dh5VtIVioyYgybZ34LIqXz6J2ZVCpeaXVvJIv5D3HX1PClIpF23honJ07qmsKqiCtV/17c29evAApF5vQ6Zp8HGoNbMrGKhM8dHQHOmZqIzFKUCa2aDLsaBWdTKBiKtn14U/bQkEG5mxkyWAgEzNb8ApmztbH/X3gKdy+hdaU7KDX+8RB1gIPv8Q773jle70t7A9JwjvI70XG95h9WkIgTZ+rKQnUZncDlopgkg4nsUiTOEwmEMcI7FcGaUZ8/nGkgDKFbkuWzk72b34CM8U6vWgpC6gA5FNdtkF2LqWTMw5FdSw7fRjCRGRnAkvSLUdhOoqhTCk94uhY+uH4TJ+62mwlFEQ4vXzciWrsxpTBGk7PyzGDvBSf35sr6PW0MoteL+YyhNuP73d8S9PRRp6ThsLRg+YSag5UUqB+ui6StLsepQnkExgbuDOJ5NzaGGgbbankUhZGhh9ULSS3Dj78+vrHvsyVxvrQTZZu8r/hsNs4Tx/gei+Q/6OVNUnRgV5vsqSm605vu1EyzlDUQv5fr4W2t05vNvI5LTiiwqXyoib7fWjBq/MtNbIIc4zb1jeudBvlc0yP1sLnXNgtfs8x3q5ye9vxfqjERT7DJCmxAunSsCi4Obx1tp6Jl910e/PbCDOk4+FwMgyi9+2GZg5TXa+TxUjEb7PBLfIohriRqf8v9k2eOw==\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get all tags\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/tags\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nRetrieve a paginated list of all tags. Supports filtering by name, attached-by source, and sorting by name, usage count, or relevance.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\"},\"required\":false,\"name\":\"nameContains\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"enum\":[\"name\",\"usage\",\"relevance\"],\"default\":\"usage\"},\"required\":false,\"name\":\"sort\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\",\"none\"]},\"required\":false,\"name\":\"attachedBy\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\"},\"required\":false,\"name\":\"cursor\",\"in\":\"query\"},{\"schema\":{\"type\":\"number\",\"nullable\":true},\"required\":false,\"name\":\"limit\",\"in\":\"query\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"A paginated list of tags with usage counts.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"numBookmarks\":{\"type\":\"number\"},\"numBookmarksByAttachedType\":{\"type\":\"object\",\"properties\":{\"ai\":{\"type\":\"number\"},\"human\":{\"type\":\"number\"}}}},\"required\":[\"id\",\"name\",\"numBookmarks\",\"numBookmarksByAttachedType\"],\"title\":\"Tag\"}},\"nextCursor\":{\"type\":\"string\",\"nullable\":true,\"description\":\"Cursor for the next page, or null if no more results.\"}},\"required\":[\"tags\",\"nextCursor\"]}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/remove-bookmark-from-list.api.mdx",
    "content": "---\nid: remove-bookmark-from-list\ntitle: \"Remove a bookmark from a list\"\ndescription: \"Remove a bookmark from a manual list.\"\nsidebar_label: \"Remove a bookmark from a list\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzlVtuKG0cQ/ZWi/WKLWWkdHGLmISDHa9jYBGPvEsJaoNJMSdNWT3e7L1rJYiAfkS/Ml4TquUjaXTsOhLzkabU9dTl1O1V7YSw5DNLoy1LkwlFtNvTCmHWNbv3KmfqN9EFkoiRfOGlZUOTiXRIDhEUnCUtnakCoUUdUoKQPY5GJgCsv8hvBRryYZcJTEZ0MO5Hf7MWC0JGbxlCJ/GbWzDJh0WFNgZxPAr6oqEaR70XYWRK58MFJvboH56oiiFp+igSyJB3kUpIDs4RQ0YCFtlhbxVYkyVJtd6v69uPzH8z28/fVNgRTPGfAMiQRBnxZiiYTjj5F6agUeXCRMqGxZgHVCmRCMgCLoRJN9i8h7pP6j1H3dfsq8sVB6AT9jDW8NdqT5wC+O3/Gf05x/2KgMDqQDvDn73+coIVb9ND2T9m2Q5998LEoyPtlVGo3ZmjPzs/v2+7Rg/SgTQCpT+rX+WU9tFbJInXt5KNn5QcybxYfqeDWtY57PMg2rMKU9Pf1mUKNRSU1nTnCEheKgJwzDlg9hVCT97j6JlNVrFHfNdTpj0VzUqqbFuDB/uxQ3gtWFE3TpAw+vZ/Ba40xVMbJz1QO9XmRpgyCWZPm1NbSe6lXGUi9QSXLDIwD2lr2fyfPgbZhYhXKhzM8BHxo0mMEA9IH+ojni/0OzcMFX5qo70L435c6+Q+VYXYuSVFgjTSxuZjwbPjJviWjZtJn00/2hylvBNOu2/SkGp0SudhjWTryvpmglZPNU5GJDTrJqFPuus9t5ZYYVRC5qEKwPp9MgtuN1+hwTWTHaO2D9NZZ6EntdScPLRaO7WgfvOeStp6Pt8KQcPbMcSQxJrEkJLLuxyvjamSEP/96lbIs9dKwOkfdQno6Ph+fH3HlgGf69vIe/uGj9IDgSS3PKuNDqmmfWalXgLoELvaZDGcKA7kUnSxoDFeV9GwbUClz62FnIgTDCxJXByM+S/zmM+BVmUElV5WSq4pf0HtKf3UJCyzW0Xqwzqwc1jUGWSCz6Qf9QT96BJwuXiPtpPDjVCkgXVojdWBeTl0HeEoHln2UPdPOp93sJiNzqAhLcmP4zUQoUMOKNN8KBKhTZGvaHYh+SNktLeD6EqIuycFo9J5CkHrl4cek85p2fjTqYb/FldQD5EQKB8w+WmtcgCI6b9zZAhmqHTRgIxHm7cd5StJcyVqGOXyK5HZwuCW4GAT9cgOpCxVL4srONW3DT52JpSTV0ianBWQA9G1eeieDSa7kkkJRpe9shIHRGKYw11GpOWxQRYKlcac+pC65RpQMcz0cgTZQG5cARhV8n5thHV7tLHl+7F98qsaCwGhqx8sRAU+Kzz/oMxiNlNTr0SjFMoXrd2+OlrQMFZjU56igcHirqISaApYYcNyqM+8P6on/gZ+YpKkTSc3Zy2iIVhksefFLRfBY1tzkxsHbl6+eJA61xocaE313d8gXb0j1wMW5P+yEbz8+W+o4WmJN1rLfviPQm3TGeZGJfLjnhsnk16NbaZYJpgBW2u+5F6+dahp+Tu3GF+yBQhPRltLz71LkS1SevhLR43fdUngCX8LdPaLeJaZWkf8TmVjT7nCN8gX6H3o9yk4zazLR8kWKvRWYFgXZcKR6b5vzDhjW28uLNxdXFyITeEr+d8g+OXgQ2X7fSlwxuTXNADSRHWNsmr8ApAaEbg==\nsidebar_class_name: \"delete api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Remove a bookmark from a list\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"delete\"}\n  path={\"/lists/{listId}/bookmarks/{bookmarkId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nRemove a bookmark from a manual list.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the list.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"ListId\"},\"required\":true,\"name\":\"listId\",\"in\":\"path\"},{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the bookmark.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"204\":{\"description\":\"No content — the bookmark was removed from the list successfully.\"},\"400\":{\"description\":\"Bookmark is not in the list.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"List or bookmark not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/replace-asset-on-bookmark.api.mdx",
    "content": "---\nid: replace-asset-on-bookmark\ntitle: \"Replace asset on a bookmark\"\ndescription: \"Replace an existing asset on a bookmark with a different previously uploaded asset.\"\nsidebar_label: \"Replace asset on a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy9VttuGzcQ/ZUB+5IIa8kpUjTYhwLKDXATtIZjoygcARrtzmoZcUmG5FpShAX6Ef3Cfkkx3Isky0lT9PJiyNy5nLmdmZ0wlhwGafRFLlLhyCrMaOo9hZ/1c2NWFbqVSEROPnPSsqBIxVUrBqiBNtIHqZeArANGA8Ki04O1DCUg5LIoyJEOYB3dSVN7tYXaKoM55a3iWCQi4NKL9Fb0br2YJcJTVjsZtiK93YkFoSM3rUMp0ttZM0uERYcVBXI+CvispApFuhNha0mkwgcn9fIkgOuSoNbyY00gc9JBFpIcmAJCSQN8xkQbrKxiS5JkrjbbZbX+8Ox7s/n0XbkJwWTPGLgMUaQHfpGLJhGOPtbSUS7S4GpKhMaKhRZ7oURIBmMxlKJJ/iX0Qzr/FvRY8S/ixk7iCPSsFScfnpt8y8hPkV687KFpWndtEgx0rRY/DE1kNDH0zOhAOrA9tFbJLHbo5INnow/kySw+UBZEIqzjfg6SfNTtIH9VQj8Ds/acUsAeb0U6jEVzlKfbwdOsadpP3hrtWxTfnj89TcxPBrog4Y/ffu/crdH3bnLwdZaR90Wt1HbMdXl6/uTUzo3GOpTGyU+UR0scwPM4JhDMijRID5X0XuplAlLfoZJ5AsYBbSyjv5fuQJswsQrlw4ke8rdvrkMEog3/6UMR98PBvttwtQlQmFrfx/BPSp6ZnP663lOoMCulpjNHmONCEZBzxgGrx2RX5D0uv8pUWVeo7xvq9E87JQLc25/tR/AVK8YMRv+hNMzIto5B8rSlYtKTh5/s9jzSTGI+/WTXtWEjmDfdXc+KtVMiFTvMc0feNxO0cnL3RCTiDp1k0O20tJ/byhVYqyBSUYZgfTqZBLcdr9DhisiO0doHR6iz0M/Rm04eWiwc2gGhv+OKtp4PaX3IN3vmOKIY82YUEkn347VxFTLCH3+5jknmTrnac9GrvkEPaKCvYcMcVhj+xjlqA3gyPh+fHzDigH56eXES7fBRMjV4UsVZaXyIDdAXJi5FnQN3xpkMZwoDuZgLmdEYrkvp2TagUmbtYWtq5psKNS73RnwCSvrgE+DtmEApl6WSy5Jf2qon0ckCs1VtPVhnlg6rCoPMkJnjvX6vv/kGOLm8Kdqx4sepUkA6t0bqwLQTWxTwmDws+8hB6ljQ+bSb9GhkDiVhTm4Mv5oaMtSwJM3HRLwLOLIVbaFwpjruhjUt4OYCap2Tg9HoHQWmfg8/RJ03tPWjUQ/7EpdSD5DfSh8OMPvaWuMCZLXzxp0tkKHaQQPuJMK8/TiPSZorWckwh481uS3sTwcuBkFP2iB1puqcuLJzTZvwojNRSFItyXJaQAZeDDEvvZPBJFeyoJCV3ULZBAZGY5jCXNdKzeEOVU1QGHfsQ+qca0TRMNfDEWgDlXERYK2C73MzEOr11pLnx+FwitVYEK/TdhgdEfBc+fS9PoPRSEm9Go1iLFO4uXp772Qzsc9RQeZwrSiHigLmGHDcqvOWGNTjtgB+YkanTiQ2Zy+j99deIRXBI1lxkxsHly9fP46Ea40PFUau7w6O4cg8PSzvz+Nuvz7+q9u0JaWD9dgkLa/uOnK+HS47LxKRHp157aTy83AoJIIJg9V2O+7cG6eahp9jc/J5u6fnSOK59Pw7F2mBytMXMvDoqts3j+FzyLtH1Nu4BVTN/4lErGh7fKLyWfo/eu7T08yaRLT0EoNvv75oHZ1xwx9on9wLDLrVmGYZ2XAgewhmdrBmL2+uebV0R2wVrwjhcC2S+DdCbKcibqz4thMK9bKOZ4JoHfMiwuM9dm9vxXAeTMJu10pcM/M2zZCTyMSckab5E6WWyl4=\nsidebar_class_name: \"put api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Replace asset on a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"put\"}\n  path={\"/bookmarks/{bookmarkId}/assets/{assetId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nReplace an existing asset on a bookmark with a different previously uploaded asset.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the bookmark.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"},{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the asset.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"AssetId\"},\"required\":true,\"name\":\"assetId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The ID of the new asset to replace the existing one.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"assetId\":{\"type\":\"string\",\"description\":\"The ID of the new asset to use as a replacement.\"}},\"required\":[\"assetId\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"204\":{\"description\":\"No content — asset was replaced successfully.\"},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Bookmark or asset not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/search-bookmarks.api.mdx",
    "content": "---\nid: search-bookmarks\ntitle: \"Search bookmarks\"\ndescription: \"Full-text search across all bookmarks. Searches bookmark titles, content, descriptions, and notes. Results default to relevance sorting.\"\nsidebar_label: \"Search bookmarks\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzVWd1u2zgWfpUDzkXaQHHSxV75YgG3s93JTrsTNAkWi9ZAjqVji2OKVEnKiWsY2IeYJ5wnWRxSluRESZQ0A8xe2RAPzx/PHz9uhCnJopdGn2ZiLByhTfO3xiwLtEsnEpGRS60smUKMxftKqSNPNx4iJWBqjXOASsFst2sE52GRXPMNvPSKXAKp0Z60T6DD1yWAOgNtPLkRfCJXKe8gozlWyoM3YEnRCnVK4Iz1Ui9GIhEeF06MP4tW2WkiHKWVlX4txp83YkZoyU4qn4vx5+l2mogSLRbkybpA4NKcChTjjfDrkth8b6Ve3LH6IqedvV8rsmuIdCOxTYSlr5W0lImxtxUN3pkIjQWL/CoSIZk0rItt8pBapKuCbUaX1koK1qD2Djug9poYdz7fVurcWA/GZmRhbizY6PAR/Bj3Onb5QbP9YASXjuAAXXoAxsIBMzsIOzP0dDRDR1l7MHsumaNyd3zyh4pv/MpffmEhA/yrq2IWCPcV/Yg3sqgKiMtg5iA9FS5GpK+shpIslLigIWY/hVtjhZKF9E+JkH2Z7yrrjIW5NQUglJZW0lSOXV4a7Yhlz8mnOficQHNa7+SHfG04DDDv+aJqU9Mo6XFbZ8YoQi06wd6r0ekcHIX6ETIzCN7VowMH80qpXTmCVz9dfPyQAFe2BMino9dwLbmmEUidqiqjDKQOLHYWcZUL3IPwEJBKLnJPtiFxcJ2TBqPVGgrymKFHkA40UUbZkKD589vQnF8t413U5tY5TtnSmiEf5V9OTvhn39gJh4TU6CkDJZ3nHGmaChTo01zqRTCgW1NZh9oHzBPLUsk0tLTjXx0z7okhM/uVUlaytNwAvYxqNdI6pGgtrtkcztbHWcjsblpuE5FaYrsmvne1MJmcy/7lROhKKZxxOnIQbJvkHEDJXpIrynrSZ5uIOa4Mt8v71j0uFlIvzj36yj0ur+1PrkpTcjw+zFGqynIPKklnvGu6TYSrigKt/BZO6eXZ8yQxyD1Rj/UwWlPZdADbTpcupUjENc04QBX/L8xMKtaWbjxpx2GfCCf1QtE8LthglixKY32wpXJkT/tjKk5ALxyoMZt7FtB7THPK3va6q7GZzcyrArWYbvfq22eWWPPf4xboOglsNP0yD/PZwwbE1ftVUVIvowut6vff4DTaq1MD6GWBC7rsE3sf8cQ58n3n3BeJqSXSLjf+KbvKbP4Ucm4tZ6xYrCBPkmQptXitKHvG5pXMyDxJUVzJdOC55L5Q79pIe5S+jsqnqLOzfKAApn75AoiVz40ddljVTEmX0zBqHrnP6h3D/ME7PtbtbcCGWyUjUMcMnvIw+H0VgUej4J/wp68kxCI/LHP7Vd3J+F5dkWMuHib/u3iEPBQREbK83XRP3+BW86/eMn9v2xtay5z81mXbXKzuy6xnu7l1SmvrC3i90kttrnt6V9gw3TauffnGO+iYuaf91Cli3WZQn37N6bz7fYZakz2tY+RWaRd10WW6ev4NBU/0VHIRB5LLUhnMiPs5rtAjH3HruidFWN+M0Lpiuu0laKfpvdm5M+/uDbe3J9n+AbSZterJqg3S5syn7cV4BzsJVpDvs/VNeUAV7784G7t/NU4Y7eCtIOegDRTGUgOWiNtumXUgu442HYXPdnerFjDbBu/+9eTN3bvYpY49RH6jDH7/729Bt7cBUANvlqT5DlhIx8NrAlKvUMksqEw3JSt161bGZfG4VCj772NtoN9gUQZ9uxoETfmeRD43jFMuYngiQ3viuDH+ON4L+YDJrnYwX5j/xAazzJJz22Ms5fHqDYc9WskHE9KyXo6u2IFoufelGx8fe7seLdHikqgcYVn2QoQ1B762srd+rukh6sI2dBDKc3ZBfevs4JSNR1hySG8m42tZIOIcDX/eG1sga/jPf1+EYJB6bng7Wx1VejM6GZ10oJxGn8nZ6R39m0XpAMGRmh/lxnn2TnMH58s3A7WWMDuS/kghAwVsnUxpBBe5dMybwWBz7WBtKoYWCtS4aJm4JNztXQKcZAnkcpEHzIFR4JBlEQ2eYbqsSgelNQuLRYFepqjUevRFf9E//ADsLtK+vunzx4lSQDorjdSegaeQHID7UVuyjAYHuZrUIRaYXEFOmJEdwX9MBSlqWJBmdJwAdbBsSeuIcO2d7zXN4PIUKs2Q5uHhOXnGIh38Lez5mdbu8HCndp2FtcofGOVodXZVyfc+iFBYDW2WzQ5YSYSruHgVnHQV8MGrGltu0W0+jBbl2cE/fLJXbXG4grkkFbOb3QLSA7rol52QhuV9+B1M4IqL1BWsUFURQtqTIXXGZ0SBMZ+HpTvVrPbNrjABl37HH5tSFU5jRmA0xfSyRMCZ4sZf9BEcHnJjPDwMtkzg8tOH9uXhWvocTIhzVFC3swbKGsXtXJ6a7aFMBRwtvEnUJCE4dzQaqroFAvc6eBXmLy5/Zz++fx2QvdI4z9fg8WaHkMVXEZjd97qyaQvmn+qhJRakTgVv79SxAu+3n7oGTxPBFYRXNxsO5Uurtlv+HDFBrsyZdFxh7kE/u/549aludq/hgTeVXlWXtK7fWUKIirEIuPJw2X/wg8UDSncfMZ6p/NOeHR7QZfcU8Uw9vuMp4n6dmjeDZyr1/wCvP2D+Hci9dcO0nW7C42ciYm8LaRd3T9KUyu6uO8g5c2lGrn/8/UJEZKELFu9PJV3UDvW6w3uziRQX3IW3W7EzIXRlseU5/38FJqeO\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Search bookmarks\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/bookmarks/search\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nFull-text search across all bookmarks. Searches bookmark titles, content, descriptions, and notes. Results default to relevance sorting.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The search query string.\"},\"required\":true,\"description\":\"The search query string.\",\"name\":\"q\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"enum\":[\"asc\",\"desc\",\"relevance\"],\"default\":\"relevance\",\"description\":\"Sort order for results. Defaults to 'relevance'. Use 'asc' or 'desc' for date-based sorting.\"},\"required\":false,\"description\":\"Sort order for results. Defaults to 'relevance'. Use 'asc' or 'desc' for date-based sorting.\",\"name\":\"sortOrder\",\"in\":\"query\"},{\"schema\":{\"type\":\"number\",\"description\":\"Maximum number of items to return per page.\"},\"required\":false,\"description\":\"Maximum number of items to return per page.\",\"name\":\"limit\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"description\":\"Cursor from a previous response to fetch the next page.\",\"title\":\"Cursor\"},\"required\":false,\"description\":\"Cursor from a previous response to fetch the next page.\",\"name\":\"cursor\",\"in\":\"query\"},{\"schema\":{\"type\":\"boolean\",\"default\":false,\"description\":\"If set to true, the bookmark's full content (HTML, text, etc.) will be included in the response. Set to false for lighter responses when only metadata is needed.\"},\"required\":false,\"description\":\"If set to true, the bookmark's full content (HTML, text, etc.) will be included in the response. Set to false for lighter responses when only metadata is needed.\",\"name\":\"includeContent\",\"in\":\"query\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"A paginated list of bookmarks matching the search query.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarks\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"userId\":{\"type\":\"string\"},\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"attachedBy\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\"]}},\"required\":[\"id\",\"name\",\"attachedBy\"]}},\"content\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true},\"description\":{\"type\":\"string\",\"nullable\":true},\"imageUrl\":{\"type\":\"string\",\"nullable\":true},\"imageAssetId\":{\"type\":\"string\",\"nullable\":true},\"screenshotAssetId\":{\"type\":\"string\",\"nullable\":true},\"pdfAssetId\":{\"type\":\"string\",\"nullable\":true},\"fullPageArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"precrawledArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"videoAssetId\":{\"type\":\"string\",\"nullable\":true},\"favicon\":{\"type\":\"string\",\"nullable\":true},\"htmlContent\":{\"type\":\"string\",\"nullable\":true},\"contentAssetId\":{\"type\":\"string\",\"nullable\":true},\"crawledAt\":{\"type\":\"string\",\"nullable\":true},\"crawlStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\",\"nullable\":true},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true},\"size\":{\"type\":\"number\",\"nullable\":true},\"content\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"assetType\",\"assetId\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"unknown\"]}},\"required\":[\"type\"]}]},\"assets\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"pdf\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"avatar\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"userId\",\"tags\",\"content\",\"assets\"],\"title\":\"Bookmark\"}},\"nextCursor\":{\"type\":\"string\",\"nullable\":true,\"description\":\"Cursor for the next page, or null if no more results.\"}},\"required\":[\"bookmarks\",\"nextCursor\"],\"title\":\"PaginatedBookmarks\"}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/sidebar.ts",
    "content": "import type { SidebarsConfig } from \"@docusaurus/plugin-content-docs\";\n\nconst sidebar: SidebarsConfig = {\n  apisidebar: [\n    {\n      type: \"doc\",\n      id: \"api/karakeep-api\",\n    },\n    {\n      type: \"category\",\n      label: \"Bookmarks\",\n      items: [\n        {\n          type: \"doc\",\n          id: \"api/list-bookmarks\",\n          label: \"Get all bookmarks\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/create-bookmark\",\n          label: \"Create a new bookmark\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/search-bookmarks\",\n          label: \"Search bookmarks\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/check-bookmark-url\",\n          label: \"Check if a URL exists in bookmarks\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-bookmark\",\n          label: \"Get a single bookmark\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/delete-bookmark\",\n          label: \"Delete a bookmark\",\n          className: \"api-method delete\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/update-bookmark\",\n          label: \"Update a bookmark\",\n          className: \"api-method patch\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/summarize-bookmark\",\n          label: \"Summarize a bookmark\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/attach-tags-to-bookmark\",\n          label: \"Attach tags to a bookmark\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/detach-tags-from-bookmark\",\n          label: \"Detach tags from a bookmark\",\n          className: \"api-method delete\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-bookmark-lists\",\n          label: \"Get lists of a bookmark\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-bookmark-highlights\",\n          label: \"Get highlights of a bookmark\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/attach-asset-to-bookmark\",\n          label: \"Attach asset to a bookmark\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/replace-asset-on-bookmark\",\n          label: \"Replace asset on a bookmark\",\n          className: \"api-method put\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/detach-asset-from-bookmark\",\n          label: \"Detach asset from a bookmark\",\n          className: \"api-method delete\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"Lists\",\n      items: [\n        {\n          type: \"doc\",\n          id: \"api/list-lists\",\n          label: \"Get all lists\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/create-list\",\n          label: \"Create a new list\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-list\",\n          label: \"Get a single list\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/delete-list\",\n          label: \"Delete a list\",\n          className: \"api-method delete\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/update-list\",\n          label: \"Update a list\",\n          className: \"api-method patch\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-list-bookmarks\",\n          label: \"Get bookmarks in a list\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/add-bookmark-to-list\",\n          label: \"Add a bookmark to a list\",\n          className: \"api-method put\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/remove-bookmark-from-list\",\n          label: \"Remove a bookmark from a list\",\n          className: \"api-method delete\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"Tags\",\n      items: [\n        {\n          type: \"doc\",\n          id: \"api/list-tags\",\n          label: \"Get all tags\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/create-tag\",\n          label: \"Create a new tag\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-tag\",\n          label: \"Get a single tag\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/delete-tag\",\n          label: \"Delete a tag\",\n          className: \"api-method delete\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/update-tag\",\n          label: \"Update a tag\",\n          className: \"api-method patch\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-tag-bookmarks\",\n          label: \"Get bookmarks with a tag\",\n          className: \"api-method get\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"Highlights\",\n      items: [\n        {\n          type: \"doc\",\n          id: \"api/list-highlights\",\n          label: \"Get all highlights\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/create-highlight\",\n          label: \"Create a new highlight\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-highlight\",\n          label: \"Get a single highlight\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/delete-highlight\",\n          label: \"Delete a highlight\",\n          className: \"api-method delete\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/update-highlight\",\n          label: \"Update a highlight\",\n          className: \"api-method patch\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"Assets\",\n      items: [\n        {\n          type: \"doc\",\n          id: \"api/upload-asset\",\n          label: \"Upload a new asset\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-asset\",\n          label: \"Get a single asset\",\n          className: \"api-method get\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"Users\",\n      items: [\n        {\n          type: \"doc\",\n          id: \"api/get-current-user\",\n          label: \"Get current user info\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-current-user-stats\",\n          label: \"Get current user stats\",\n          className: \"api-method get\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"Admin\",\n      items: [\n        {\n          type: \"doc\",\n          id: \"api/admin-update-user\",\n          label: \"Update a user (admin)\",\n          className: \"api-method put\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"Backups\",\n      items: [\n        {\n          type: \"doc\",\n          id: \"api/list-backups\",\n          label: \"Get all backups\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/create-backup\",\n          label: \"Trigger a new backup\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-backup\",\n          label: \"Get a single backup\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/delete-backup\",\n          label: \"Delete a backup\",\n          className: \"api-method delete\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/download-backup\",\n          label: \"Download a backup\",\n          className: \"api-method get\",\n        },\n      ],\n    },\n  ],\n};\n\nexport default sidebar.apisidebar;\n"
  },
  {
    "path": "docs/docs/api/summarize-bookmark.api.mdx",
    "content": "---\nid: summarize-bookmark\ntitle: \"Summarize a bookmark\"\ndescription: \"Trigger AI summarization for a bookmark. The summary is generated asynchronously and attached to the bookmark. Returns the updated bookmark record.\"\nsidebar_label: \"Summarize a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzNV9tuGzcQ/ZUB85IIa8kpUjTQQwGlbQA3AWrENorCMaDRcrTLiEtueJGtCAv0I/qF/ZJiuBdJltIaaB/6pDU5nDlzOzPeCluTw6CsuZBiKnysKnTqC72xdlWhW4lMSPK5UzXLiKm4dqooyMHsAnrh9ByW1gHCons3huuSOokNKA8FGTZEEtBvTF46a2z0egNoJGAImJckIVgIJe1p+UAhOuPTaaxlUtDfgqPcOjkWmQhYeDG9FT1sL+4y4SmPToWNmN5uxYLQkZvFUIrp7V1zl4kaHVYUyPkk4POSKhTTrQibmjgWwSlTHAeAkRj1ORIoSSaopSIHdnkIXGSCHrCqNWtSpKR+2BTV/afX39mHL9+WDyHY/DUDVyGJ9MAvpGgy4ehzVI6kmAYXKRMGKxZa7IQyoRhMjaEU7IwjX1vjybMD35yf888x7iFy9yqUBzHtMsW4c2sCmcAasK61ylN+J588qzkRJ7v4RHkQmagdF1NQLQglj2PZZCJ3xAZn4eRtZSXH8+R1JkzUGhccLw5LM0TvCZLo8lKtaR/TwlpNaNjsEteWS+Vr9wGLQpniKmCI/p/tZYJMrLgefcxz8l6wCaWjIw4TGcmv7ppMHLTQf6/e2PC08HTpf5qsjS5/gtodTKyVyMQ9Lbi6NH9XdqE0pSYJZDxXaCa8MoWmZXvhkluqqq0LyZfoyV2cqqmDfrnlutuvsoOa2quDg6Q/zvDpxAwY7pqGrb46f3ncZjcGYygtc6iEP3//I3XZm0Q+EOyKDJNhpTw7m4Eya9RKZmAd0EPNPjxqwUAPYVJrVKebb4j9jm32EYge6atjpD3lgLEBljaax6b/TffnVp4skUMIM6gwL5WhM0couXSAnLMO+Pk4EQJ5j8WTVJWxQvNYUfd+fFQmCeBO/92Oin/ihylwyX4oLU/G2vrkJRPuVEx6JvWT7Y6Vm8kwP7mEyK374RKdFlOxRSkded9MsFaT9UuRiTU6xXBT0LrrNlVLjDqIqShDqP10MgluM16hwxVRPca6PjmYOg39OHrXyUOLhZ3am4tXnMvW8v50HCLNltmPJMaEmIRE1n28ta5CRvjzr9cpvMosLT9nr1tIL8fn4/O9KTfgmV1eHOEfLpUHBE96eVZaH1Iy+xgrU6SFgbN8psKZxkAueady4p1DedYNqLW997CxkVeKCg0WOyU+A6188Bnw2pBBqYpSq6LkE/Se0q+RsMB8FWsPtbOFw6rCoHLUejP+aD6aZ8+Aw8ULQNsifDjTGsjI2ioTPHTlBnjY/zXbkKBMStF81jVrUjKHklCSG8NvNkKOZtibAE3ybEUbWDpbHeb3nhZwcwHRSHIwGl1RCMoUHr5Pb97Rxo9GPexLLJQZIL9XPuxh9rFmxoU8Om/d2QIZaj28gLVCmLeX8xSkuVaVCnP4HMltYLdTtQtgv5aAMrmOkjizc0MP4YdOxVKRbnmSwwIqALbL3mBkUMmZXFLI28WFlTAwGsMM5jx55rBGHSktogc2lJGcI0qKOR+OwFiorEsAow6+j83AidebmjwfDhtlysaCwBpq28sRAXeKn340ZzAaaWVWo1HyZQY3H94/2rZsqnPUkDu81yShooASA47b50z0w/NE+MBHzM7UiaTi7GUMxFpblCSBRyY8VxUXuXVw+ePbF4k8mbMqTLzdbZBXPT/treqPG3G7mwH/122/5ae90cj7AVPstiPo22FX5rE9PVicdxx9lwlmGBbfbrnUb5xuGj5O1cz/KOwYOvG4VJ6/pZguUXv6m8g9/9ANmxfwNcTdIZpNGgQ68l8iEyvaHC77De8/LTEkFK3ALM+pDntPj+Y1k/0wwC5/ubrmBeiQ4x9xelJ/Etd220pcM4c1zQAzcRojbJq/ACkPBf8=\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Summarize a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/bookmarks/{bookmarkId}/summarize\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nTrigger AI summarization for a bookmark. The summary is generated asynchronously and attached to the bookmark. Returns the updated bookmark record.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the bookmark.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The bookmark with the updated summary.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"userId\":{\"type\":\"string\"}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"userId\"]}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Bookmark not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/update-bookmark.api.mdx",
    "content": "---\nid: update-bookmark\ntitle: \"Update a bookmark\"\ndescription: \"Partially update a bookmark. Only the fields provided in the request body will be updated. Supports updating common fields (title, note, archived, favourited) as well as type-specific fields.\"\nsidebar_label: \"Update a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzdWNuOG8cR/ZVC+0VazHJXhgMbfAhAKTaytgIvpF0EwWoBFqeLMy32dI/6Qi5FEPBH5AvzJUn1XLmkZQbRU/hCsqequi6nbrMTtiaHQVlzI8VUxFpioNfWrip0K5EJST53qmYCMRW36IJCrbfQEALCoqWdwK9GbyGUBEtFWnqonV0rSRKUSceOPkXyARZWbmGjtIYFtXLkBN7HurYu+OZEmQJyW1XWdNJeBBU0ZWBsoAzQ5aVak8xgiWsbnQokXwJ62JDW/B22NV36mnK1VHkrYyIyEbDwYvogOhO9eMyEp5xFbMX0YScWhI7cLIZSTB8e94+ZqNFhRYGcTwQ+L6lCMd0JvkRMhQ9OmeLIWXclQTTqUyRQkkxQS0UO7DI5o3ebyAQ9YVVrlqRISf20LarNxx++t0+f/1Q+hWDzH1hxNl9Me8VvpNhngn2qHEkxDS5SJgxWTLQYiDKhWJkaQynYmDYKr63csgnHKrf+DraNzXFgtzbCBk1gmrxEUxAYIsl/F9SHnS3LrQlkAl+Eda1VnpB29dHzbSc8aRcfKQ8iE7VjXAZFPvG20R5RLqzVhIZ9MCDg9HMfqwrd9lTATNQaF+xXdt8+E4yuY8J97/4/EpGJCp/ekikYPa+ur6/3mcgdMcRn4SwNotOn6JbWVRg4Q51ifQ7idoZYjKG07izSOi608iWdR80QuW055Nkcf7OS0+E8hkBP5/kOvafwZsDcHzCkD2eEr63xDdS+vb4+nRZtoTpI3K8Eb3XCDV/GzT4TVevBM2F1Jn7Zif9TsgUsCmWK9wFD9OfkC5lYcUH2Mc/Je4Y6Kh0dsZvISOZ67LNYfU4u/vriTyf+Cff8N9XE2+jys6pGpybWSmRiQwtGl+bflV0oTalLBDKeAZkJr0yhadk8cMksVXH7TLZET+7mFKYOGsYD426MsgNMjXBwEPTnET4dmF6HxybFvrt+dZxV96YpSurzf9LqX7/9M7WY16n7QrArMqA8VMqzsRkos0atZAbWAT3VbMOzFOQycVVrVKeTr/f90G7HGohO0++ONe16Lg8fsLTRfM3mllt5EiKHKsygwrxUhi4doWToADlnHTD7JBUE8h6Ls0SVsULzXFDLPzmCSVJwkP84zCI/MqNoy2hFobSyGTXyks3kkWMqrrqK6a92w1yyZ+SQW3dDVWp7YodSOvJ+f4W1ulq/EplYo1OsZTMINI+bCC0xau6IZQi1n15dBbedrNDhiqieYF2fHMhaCd0Y9ktLD40ubMtoHnzPIWxuHk+FvYP5ZrYjkXEdTEQia3/81PXsn/9+l7zK0Hg3zF8/dkAcTzhNPRiX2eakrzujIpJq1vC/C0t/MOogw2Hj6d/BRn/czQvDyWgsGHEfdv/DB0OTH+mYevnonoOWPepvyiwte4YR0uj2anI9uR6Z2cdudntzZEj/UHlA8KSXl6X1IeG9QyEvGWgkcCJcqnCpMZBLSFA5TeCuVJ5lA2ptN83YGyxUaLAYhPgMtPLBZ8CrRQalKkqtipJPknH8bSQsMF/FOm1FhcOqwqBy3qQmH8wH8803wNDiJaGpInw40xrIyNoqEzy0GQl4WCJrvqNfseaztp4lIXMoCSW5CfzDRsjRQEGGtz0CNMmyFW1h6Wx1mAsbWsD9DUQjycHFxXsKvI95+HPi+YW2/uKiU/sWC2V6ld8qH0Y6+2angzw6b93lAlnVuueAtUKYNw/nyUlzrSoV5vApktvCsHdxMHiBbAY1UCbXURJHdm7oKbxpRaT9JLUSdguokDZB9kt3SS+SI7mkkJfpOQthxWgCM5hzc57DGnUkWFp3eIcykmNESTDHwxEYC5V1ScGog+9807eNu21Nng/7rTNFY0FgDTWlyBGlndVPP5hLuLjQyqwuLpItM7h/97bHG2xUKMEmnKOG3OFGk4SKAkoMOGnYOc169tQTgY/S9tySJHB2NAZirS3yts5TBbxQFYPcOrj9y08vU3+prQ8VptbWbpn3z98BPM/C3dAj/x9eHTRFfzRmDOta0+se+sWbR6DpaAt/zASXHybZ7TgP7p3e7/k4QZ3fNAytLjVEqTz/lmK6RO3pC5598a5t1i/h97RsD9FsU0fVkf+JTKxoe/i2YM/zY1M1khYNQVufLxnHIwFHU88+6zhmeU51+CLt42hkuJ3dvfkr9832rUSVZiLhcMPzLW4abRvQp3acznZCoyliGnpEI3Tfrrrj5eSwKSezTvpjt2so7riw7ve9e1KhZc/s9/8GMwq28A==\nsidebar_class_name: \"patch api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Update a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"patch\"}\n  path={\"/bookmarks/{bookmarkId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nPartially update a bookmark. Only the fields provided in the request body will be updated. Supports updating common fields (title, note, archived, favourited) as well as type-specific fields.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the bookmark.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The fields to update. Only the fields you want to change need to be provided.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"summary\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true,\"maxLength\":1000},\"createdAt\":{\"type\":\"string\",\"nullable\":true},\"url\":{\"type\":\"string\",\"format\":\"uri\"},\"description\":{\"type\":\"string\",\"nullable\":true},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true},\"text\":{\"type\":\"string\",\"nullable\":true},\"assetContent\":{\"type\":\"string\",\"nullable\":true}}}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The updated bookmark.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"userId\":{\"type\":\"string\"}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"userId\"]}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Bookmark not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/update-highlight.api.mdx",
    "content": "---\nid: update-highlight\ntitle: \"Update a highlight\"\ndescription: \"Partially update a highlight. Supports changing the color or note.\"\nsidebar_label: \"Update a highlight\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytV+Fu2zYQfpUD+6c1FDsdOqzQjwFu16JZCyxoEwxDGsBn8SyxpkiVpOKohoA9xJ5wTzIcJcty7BYFVv+xQx6P3313/O6yFbYih0FZcyFFKupKYqA3Ki+0yosgEiHJZ05VbCFScYkuKNS6gc4SEIqd8RQ+1FVlXfCQFWhyZXIIBUFmtXVgHRgbaCoSETD3Ir0RwzVe3CbCU1Y7FRqR3mzFktCRm9ehEOnNbXubiAodlhTI+Wjgs4JKFOlWhKYikQofnDL5EeCrgqA26nNNoCSZoFaKHNhVRLaHLhJB91hWml0pUlLfN3m5+fT8F3v/5efiPgSbPWfoKkSTAfqFFG0iHH2ulSMp0uBqSoTBkq2KkVUiFOOpMBSC4+Ej5MMLKxuO4hj1SpGWHoLtqZ7CH0Y3EXe/1dgaNmgC20TGCQyR5D+XBJWzd0qS5NgyawKZwBdhVWmVxYzPPnm+7QSZdvmJMs5+5bg+giLPuzGTpzgnU5ec0Ya0thvB0XHEuSMyIhFLXZO4bRPBFXDqvKm1xiUzy/y18cNOfGWN7+7+6fz8NE8dO/IwmT8o4KW16xLdmt/GQ9RtInxAF/5YrTyF0b6pyyU53icjv7H7/9jkQl9hrYNId4ZtIgLdh+8g+LszkQh1OvTak/sKK5kjTsj8BJD24K3cjPk9ZHPMXR9VjzkiGu4f33Z74nmKrpKenT89Lp5rg3UorFNfSMK/f/8Tn9aLKDwQ7JoMKA+l8l6ZPAFl7lArmbCQ0X3FETwoNEY5qzSq0yW2T+8gNGMEA9Jnx0iHcFhCYWVr82NftTxZCocY5lBiVihDZ45QcokAOWcd8PEp570k7zH/LldFXaJ56Kg/Pz2qkghw73+U51d8UPRyUVIorOw0Nis4TNbaVMwGZfCz7UiSW645cne7llI7LVKxRSkded/OsFKzu6ciEXfoFOOMbPXbXZJ2D7AIofLpbBZcM12jwzVRNcWqOtmOeg+7JvS2t4cOC0cz6oYfOIm9GI164kAx38xxRDOR9kasEvHHa+tKZIS//3kVeeXieL9vPa92tTiI+151OokYPWxlVpYtmbEunKfT8+n5qC0OscwvL45iHzaVBwRPenVWWB9iBeyEgGcGNBK4NM5UONMYyEVmVEZTuCqUZ9+AjLHrgMFCiQbzvROfgFY++AR40kj2ncEngN5T/DYSlpit68pzp8wdliUGlfFsM/1oPppHj4Cp5pGhe1e8ONcayMjKKhM89DUKeKgaFd8hQZmY3sW8f+LRyQIKQkluCn/ZGjI0kJPhAYwATYxsTQ2snC0Pa2NDS7i+gNpIcjCZfKAQlMk9/BrPvKXGTyY72JeYKzNAfqd8GGH23YgGWe28dWdLZKjVcALuFMKi21xEkhZalSos4HNNroH9FMbJINi1aFAm07UkzuzC0H142buIo0pUV6YFVAD0HS+7SwaXnMkVhayI++yEgdEU5rDgtrSAO9Q1wcq6wzuUkZwjio45H47AWCitiwBrHfyOmxd9icBVU5Hnxd2Kj9lYElhD3dN0RMCvzKcfzRlMJlqZ9WQSY5nD9ft3Q73BRoUCbKxz1JA53GiSUFJAiQGn3XFuD8Px2CaAl7qpuDOJxbmzMVBX2qIkCSulCR6rkovcOrj87fWTqLiV9aHEKPb9xHl9NJU/fIbbfdv4QdN8J0Sj7sfjAWvptlfgm/0g7EUi0vFYfJsIFgE22m65Gq+dbltejgXH0/9egKNMS+X5txTpCrWnb4T3+H3fRJ7A13D2i2iaqPM8WaVCJGJNzYP5veX5tXu8EUZn8bK77IzLaeThqB23ye7EPMuoCt+0vR31ssv51cs3LOf9/wllbNbCYZwLcdPB7Wovdom4thUaTV7Hbiw6pyz+eNg7HvSKGNZJQrbbzuKK9a1tB36i3jEzbfsf6cH7pg==\nsidebar_class_name: \"patch api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Update a highlight\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"patch\"}\n  path={\"/highlights/{highlightId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nPartially update a highlight. Supports changing the color or note.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the highlight.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"HighlightId\"},\"required\":true,\"name\":\"highlightId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The fields to update. Only the fields you want to change need to be provided.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"]},\"note\":{\"type\":\"string\",\"nullable\":true}}}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The updated highlight.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarkId\":{\"type\":\"string\"},\"startOffset\":{\"type\":\"number\"},\"endOffset\":{\"type\":\"number\"},\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"],\"default\":\"yellow\"},\"text\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\",\"nullable\":true},\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"}},\"required\":[\"bookmarkId\",\"startOffset\",\"endOffset\",\"text\",\"note\",\"id\",\"userId\",\"createdAt\"],\"title\":\"Highlight\"}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Highlight not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/update-list.api.mdx",
    "content": "---\nid: update-list\ntitle: \"Update a list\"\ndescription: \"Partially update a list. Only the fields provided in the request body will be updated.\"\nsidebar_label: \"Update a list\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy1V+9u2zYQf5UD+6U1FNsZWqzwhwFu1mJZCyxIEwxDasBn8SyxpkiVpGK7hoA9xJ5wTzIcJct27KQb0PVDapHH+/vj744bYUtyGJQ1l1KMRFVKDPRB+SASIcmnTpW8KUbiCl1QqPUaGiFA0MqHPvxm9BpCTjBXpKWH0tl7JUmCMnHZ0ZeKfICZlWtYKq1hRq0O2ReJCJh5MboTbNWLSSI8pZVTYS1GdxsxI3TkxlXIxehuUk8SUaLDggI5HwV8mlOBYrQRYV2SGAkfnDLZkf83OUFl1JeKQEkyQc0VObDz6GOMRCSCVliUmrUoUlKv1lmx/Pz6R7v6+ipfhWDT1+ywClGEHb6Uok4Eh6gcSTEKrqJEGCxYQDcCiVDsQIkhFxxAm5A3Vq7Z7WM320QG26bpOMdrW8ESTWCZNEeTERgiyZ8z6irAEaXWBDKBDWFZapXGYg8+e7Z2Int29plSrn7pGBpBkefdJqLjHBfKfCCTcXXOE1HgqvsaDusHJTg+bSqtccbJbPK2p214oO1V1KbSU2rqCAkynOpv2qgT8aUit/5WLKy0mmmV7gnOrNWERtTxH9fRl9b4JkE/DIeni9kivYPYdyqIOhFsnTxSp/9aie+W60bgWJBMVfCdL9BUqEUifIEu8OWXNMdKBzHa7j1VsSN7j9csETn6C8vy1mGwzp+Wqjy5a6uf9NouDTn+lipY/nGvaBlXWvuT+oAU7rhabW3azO7lsTt1wsU9fyaHxCMaDL4cnh/D7tZgFXLr1FeS8Peff0XmeBOJFIJdkAHloVDeK5MloMw9aiUTsA5oVbLLD3AaaBUGpUZ1GqG7/HTsue9B5+nLY085EjA2wNxW5nvyVWrlyRIemh9DgWmuDJ05QslIAnLOOuDjfYZDQd5j9q9U5VWB5qGi9nxfPEREdHCnf6+6b/mgaDmmoJBb2XSPNI+wYX4SA6YTP9g0LabmG0TuftsTK6fFSGxQSkfe1wMs1eD+nIGKTrF3MUftdlOV7a3LQyj9aDAIbt1foMMFUdnHsjzZT1sN2y76vpWHxheOYa+df+TSNZb3m3qXWLbMcUQxvpRRSCTtj3fWFcge/vr7TcwmQ+J610rfbsG3bVaPVapbbihu971jtt1aSz17Qi3DbGnSzC2b5NQ36s/7w/5wb0rokjK+ujzypdtUHhA86flZbn2IAJpZuyjQLZTJAI0ERtaZCmcaA7mYYpVSH25y5Vk3oNZ22YwGwUKBBrOdEp/EBuQT4HErgVxluVZZzivoPcX/jYQZpouqjENc5rAoMKiUh77+J/PJPHsGXDMenppryYtjrYGMLK0ywUMLccBDvinZRjcRTsctOUQlU8gJJbk+/GErSNFARoaHUgI0MbIFrWHubHEIsiXN4PYSKiPJQa/3kUJQJvPwUzzznta+19u6fYWZMp3LkXV2PvuqLK0LkFbOW3c2Q3a17E7AvUKYNpvTmKSpVoUKU4jggN08ysXgebcZC0CZVFeSuLJTQ6tw0aqIM1zkZU4LqADom7xsjXQquZJzCmke91kJO0Z9GMOUm98U7lFXBHPrDm0oI7lGFBVzPRyBsVBYFx2sdPDb3LxpIQI365I8L25XfKzGjMAaau64IwK+rn70yZxBr6eVWfR6MZYx3F5/6PAGSxVysBHnqCF1uNQkoaCAEgP2m+PcWLrjscEAL3FHoFYkgnMrY6AqtUV+XMyVJniuCga5dXD187sXkbBL60OBZo8GbvefKw9v4GbXcP7HF05DcHttlAcN5uhNy+d38a3A/X7UPhomiWAm4K3NhiF563Rd83JLSXeTHZ1H0pfK828pRnPUnp4I9Pl124hewGPetYto1rFr6Iq/RCIWtN49bOoJj1Xx8kYPms2Lxs4Zw2nv8FE3r5PtiXGaUhmelJ3stcKr8c3FL9wX2gdUEXu9cLgUSfwbPW2wF9tNXNsIjSarYjMXjVLuInjYhB40nRjWyVxsNo3EDfNbXXepiXzHmanrfwD1Vlbw\nsidebar_class_name: \"patch api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Update a list\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"patch\"}\n  path={\"/lists/{listId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nPartially update a list. Only the fields provided in the request body will be updated.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the list.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"ListId\"},\"required\":true,\"name\":\"listId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The fields to update. Only the fields you want to change need to be provided.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\",\"minLength\":1,\"maxLength\":100},\"description\":{\"type\":\"string\",\"nullable\":true,\"minLength\":0,\"maxLength\":500},\"icon\":{\"type\":\"string\"},\"parentId\":{\"type\":\"string\",\"nullable\":true},\"query\":{\"type\":\"string\",\"minLength\":1},\"public\":{\"type\":\"boolean\"}}}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The updated list.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"description\":{\"type\":\"string\",\"nullable\":true},\"icon\":{\"type\":\"string\"},\"parentId\":{\"type\":\"string\",\"nullable\":true},\"type\":{\"type\":\"string\",\"enum\":[\"manual\",\"smart\"],\"default\":\"manual\"},\"query\":{\"type\":\"string\",\"nullable\":true},\"public\":{\"type\":\"boolean\"},\"hasCollaborators\":{\"type\":\"boolean\"},\"userRole\":{\"type\":\"string\",\"enum\":[\"owner\",\"editor\",\"viewer\",\"public\"]}},\"required\":[\"id\",\"name\",\"icon\",\"parentId\",\"public\",\"hasCollaborators\",\"userRole\"],\"title\":\"List\"}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"List not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/update-tag.api.mdx",
    "content": "---\nid: update-tag\ntitle: \"Update a tag\"\ndescription: \"Rename a tag. The new name will be normalized and trimmed.\"\nsidebar_label: \"Update a tag\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVttuGzcQ/ZUB85IIa8kpUiTYhwJKmqBu8mA4MorCEaDRcrTLiEsyJNeSIizQj+gX9kuK4a4ulpWgQOoHSyKHwzNnZs5wK6wjj1FZcyVFLhonMdIES5EJSaHwyvGeyMUNGawJECKWQ5hUBIZWkNZWSmuYExjra9TqK0lAIyF6VdckhyITEcsg8jsx4c9pJgIVjVdxI/K7rZgTevLjJlYiv5u200w49FhTJB+SQSgqqlHkWxE3jkQuQvTKPIbIoBqjvjQESpKJaqHIg11ArCjBFpmgNdZOsxNFSur1pqxXn1+9tOuvP1frGG3xiuGqmEwmWF5J0WbC05dGeZIij76hTHDYIuewrqTIhOLbHcZKMHo2phBfW7lhzI8xMnERy0QeYyqsiWQiG6NzWhUpH6PPgU+cCd/OP1MRRSac5+xFRYF3O1CnJLXpj1EFZ03oTH+6vDwPrcu/3LH1PyFT8gyu7JuAj/m+48O96bSL5MXl88fgbw02sbI+Vd8/f/2dcv46VRZEuyQDKkCtQlCmzECZe9RKZmA90NrxVSfhRlrHkdOozge6r8BDPR0jEDukL87QzJm3ERa2Mae3/gjJhZVn2DztkTHUWFTK0IUnlDjXBOS99cDHh5yVmkLA8j+5qpoazamj/vzwUSITwIP/6aHP3vJB0RdqTbGysmuoouIwubFyMWIRGW1Tz7WCNcTf7xSi8VrkYotSegqhHaFTo/vnIhP36BVjSwz1211KFtjoKHJRxehCPhpFvxku0eOSyA3RubPq0nvYacr73h46LBzBkbh95MR1Nx9L3J5WvpnjSGYi741E1n95x2rKCH//Y5K45IK4OWjL213l7Tr/qLOUWVjeYII69M+Hl8PLI2nbQx9fXz0Kdb+pAiAE0ouLyoaYkjy3dlmjXypTJpXn7F+oeKExkk9EqIJ4QKjAvgG1tqsAG9tAtFCjwfLgJGSgVYghY8EJGVSqrLQqK17BECh9GglzLJaNC+C8LT3WNUZVoNab4SfzyTx5AswsC37XOrw41hrISGeViQH6MgR8KAmO75CgTMrmbNz3b3Iyg4pQkh/Cn7aBAg2UZHhUEqBJkS1pAwtv64elsKI53F5BYyR5GAw+UozKlAF+SWfe0yYMBjvY11gqs4f8QYV4hDk0zlkfoWh8sP5ijgzV7U/AvUKYdZuzRNJMq1rFGXxpyG/gMEO7ab3Tf1Cm0I0kzuzM0Dq+6V0sFOlOOpkWUBEwdLzsLtm75EwuKBZV2mcnDIyGMIaZabSewT3qhmBh/cM7lJGcI0qOOR+eXw1QW58ANjqGHTev+xKBycZR4MXdSkjZmBNYQ10neiLgpgr5J3MBg4FWZjkYpFjGcHvzYV9vsFKxApvqHDUUHleaJNQUUWLEYXectX9/PM0A4CVWbepNUnHubAw0TluUJGGhNMFTVXORWw/Xv757lkTV2RBrNEfNepsmbfeeOm3A7WEm/OC7q1Oao2HWZp1YbntZveveZpnIu8fMNBPc6ryx3XLN3XrdtrycyopfaAdVTdorVeDvUuQL1IG+E8rTm34aPINvYesX0WySeOuGf4lMLGmzf2+10zYTXXMmAN3em+6aCy6Xo7OPJmqb7U6Mi4Jc/K7t9GgcXY8nb35jde7fdXWat8LjSmTpfwLa1VYS/bS2FRpN2aSBKjqnrOX4cBScSH8K6ywV221nMWH9atsDM/ybmWnbfwG89Bx+\nsidebar_class_name: \"patch api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Update a tag\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"patch\"}\n  path={\"/tags/{tagId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nRename a tag. The new name will be normalized and trimmed.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the tag.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"TagId\"},\"required\":true,\"name\":\"tagId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The new tag name.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"}}}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The updated tag.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"}},\"required\":[\"id\",\"name\"]}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Tag not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docs/api/upload-asset.api.mdx",
    "content": "---\nid: upload-asset\ntitle: \"Upload a new asset\"\ndescription: \"Upload a binary file as a new asset. The uploaded asset can then be attached to a bookmark via the POST /bookmarks/{bookmarkId}/assets endpoint.\"\nsidebar_label: \"Upload a new asset\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzNVttuGzcQ/ZUB89IKK8kp+rQPBZS0AdwkrRHbKIrYgGZ3R1pGXJIhubY3wgL9iH5hv6QYcrW6WAX82DeCHB6eufDMbIWx5DBIoy8rkYvWKoPVwnsKIhMV+dJJy6ciF7fxDBAKqdF1sJKKAD0gaHoE5DszuKkJEghVaQ9K1BBq0lAQYAhY1lRBMAxkzKZBt4EHiWwCV79f38B8t+3n293ysurnEc0D6coaqcNMZCLg2ov8s4iEvbjPhKeydTJ0Iv+8FQWhI7doQy3yz/f9fSYcfW3Jhzem6kS+PXGQuUenghl8YPeaVgVp0YX5yrhmWmFAfro0OpAOjHLGgrd9WVMTV6GzJHJhii9Uclyt46gHSZ5P+cloJQMvxLuBQrGPpOj7xF06qtjheOe+79O+t0b7BPbDxcV5x1IuHtHv0+PbsiTvV61S3Qw+UWid9tBQQHYBsDBtiGk5TuiJ92itkmUsofkXz++93PWIx4U3GvrgpF4/q71YVlp+bQlkRTrIlSTHdORap2o6x7Mfid5E9Je88vHy4y/AZmBWx6Ac84jp5bdDMN02BbmzYGx5FgekhqIL5CMg7/yGzQsZGifXUqNKQBqb/6R6UjS7aB9HZfDngMV9Ntbi7YCYFCHV248Xr5+X2K3GNtTGyW9UwT9//R35vIn/D4LZkAbpoZHeS73OQOoHVLLKwDigJ8sET6oq0FOYW4XyfD2N4aEnbGyiesAgMu0z0VCoDeuaNT5WH7IUiEFK2HVyD+R8VIvWKZGLLVaVI+/7OVo5f3gtMvGATmKhhpJNxykCK2xVELmoQ7A+n8+D62YbdLghsjO09mz+BoRd0t4P9pC4MPUDHbtmz9PLh2o2BoJfZj+imcgHI5ENi3fGNcgMf/3jJtaD1CvD19nrROn17GJ2IfYpH/ksri6f8R8PJeu+J7Wa1sYHjs6o5lKvAXUFjrCayjBVGMhF72RJ3CGkZ2xApcyjh860/H8b1Ljeg/gMlPTBZ8Aan0Et17WS65p3Uvay+EiB5aa1Hqwza4dNg0GWyHp2p+/0q1fA4WK9SALFmwulxh7iYfgfgMfFavmNin8pp2i5GCorgiyhJqzIzeBP08butibNTZQAdfRsQx2snGmO8/tIBdxeQqsrcjCZXFMIUq89/BTvvKfOTyY72lfIX3xH+YP04YCzb601LkDZOm/ctECmascbsZsu0+EyBmmpZCPDEr625Dqw6LChQM6ndr3rHyB1qdqKOLNLTU/h7QCxkqTSp+awgAzcFmNcdo+MkJzJFYWyjucMwsRoBgtY6lapJTygaglWxh2/IXXFOaIIzPlwBNpAY1wk2Krgd7F5s5saWL48b+52fMxGQWD0oImOKIq5z+/0FCYTJfVmMom+LOD204f9CPIoQw0m1jkqKB0+KqrGZjhL11mVxutRnYC3QJtAg0kszp2NPlH972TDRW4cXP387vuo/axMDUaV07EF7Oescaw6/YbbvVL+L6eyJE0HEt5nSV23gwIPvShObKwfvLPdciHfOtX3vB1rlce2vf7GIS4T6fdFyd5QJ3LxNgVjOrSzWGEiPzuW9dnu0qIsyYYD82dzDMvw2EA4Bqyqw+DYmIrvMHDEzfbLRBKPZfpEliP54Qh1d8Biu00WNyxDfS+ygW6UJdHzyPcva0od9w==\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Upload a new asset\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/assets\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nUpload a binary file as a new asset. The uploaded asset can then be attached to a bookmark via the POST /bookmarks/\\{bookmarkId\\}/assets endpoint.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The file to upload as multipart/form-data.\",\"content\":{\"multipart/form-data\":{\"schema\":{\"type\":\"object\",\"properties\":{\"file\":{\"title\":\"File to be uploaded\"}},\"required\":[\"file\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The asset was uploaded successfully. Returns metadata about the uploaded asset.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"assetId\":{\"type\":\"string\",\"description\":\"The unique identifier assigned to the uploaded asset.\"},\"contentType\":{\"type\":\"string\",\"description\":\"The MIME type of the uploaded file.\"},\"size\":{\"type\":\"number\",\"description\":\"The size of the uploaded file in bytes.\"},\"fileName\":{\"type\":\"string\",\"description\":\"The original file name of the uploaded file.\"}},\"required\":[\"assetId\",\"contentType\",\"size\",\"fileName\"],\"title\":\"UploadedAsset\"}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/docusaurus.config.ts",
    "content": "import type * as Preset from \"@docusaurus/preset-classic\";\nimport type { Config } from \"@docusaurus/types\";\nimport type * as OpenApiPlugin from \"docusaurus-plugin-openapi-docs\";\nimport { themes as prismThemes } from \"prism-react-renderer\";\n\nconst config: Config = {\n  title: \"Karakeep Docs\",\n  tagline: \"Dinosaurs are cool\",\n  favicon: \"img/favicon.ico\",\n\n  // Set the production url of your site here\n  url: \"https://docs.karakeep.app\",\n  // Set the /<baseUrl>/ pathname under which your site is served\n  // For GitHub pages deployment, it is often '/<projectName>/'\n  baseUrl: \"/\",\n\n  // GitHub pages deployment config.\n  // If you aren't using GitHub pages, you don't need these.\n  organizationName: \"karakeep-app\", // Usually your GitHub org/user name.\n  projectName: \"karakeep\", // Usually your repo name.\n\n  onBrokenLinks: \"warn\",\n  onBrokenMarkdownLinks: \"warn\",\n\n  // Even if you don't use internationalization, you can use this field to set\n  // useful metadata like html lang. For example, if your site is Chinese, you\n  // may want to replace \"en\" with \"zh-Hans\".\n  i18n: {\n    defaultLocale: \"en\",\n    locales: [\"en\"],\n  },\n\n  presets: [\n    [\n      \"classic\",\n      {\n        docs: {\n          sidebarPath: \"./sidebars.ts\",\n          sidebarItemsGenerator: async ({\n            defaultSidebarItemsGenerator,\n            ...args\n          }) => {\n            const sidebarItems = await defaultSidebarItemsGenerator(args);\n            return sidebarItems.filter(\n              (item) => !(item.type == \"category\" && item.label === \"API\"),\n            );\n          },\n          editUrl: \"https://github.com/karakeep-app/karakeep/tree/main/docs/\",\n          routeBasePath: \"/\",\n          docItemComponent: \"@theme/ApiItem\",\n        },\n        blog: false,\n        theme: {\n          customCss: \"./src/css/custom.css\",\n        },\n      } satisfies Preset.Options,\n    ],\n  ],\n  plugins: [\n    [\n      \"@docusaurus/plugin-client-redirects\",\n      {},\n    ],\n    [\n      \"docusaurus-plugin-openapi-docs\",\n      {\n        id: \"api\",\n        docsPluginId: \"classic\",\n        config: {\n          karakeep: {\n            specPath: \"../packages/open-api/karakeep-openapi-spec.json\",\n            outputDir: \"docs/api\",\n            sidebarOptions: {\n              groupPathsBy: \"tag\",\n            },\n          } satisfies OpenApiPlugin.Options,\n        },\n      },\n    ],\n  ],\n  themes: [\"docusaurus-theme-openapi-docs\"],\n\n  themeConfig: {\n    image: \"img/opengraph-image.png\",\n    navbar: {\n      title: \"\",\n      logo: {\n        alt: \"Karakeep Logo\",\n        src: \"img/logo-full.svg\",\n        srcDark: \"img/logo-full-white.svg\",\n        width: \"120px\",\n      },\n      items: [\n        {\n          type: \"docsVersionDropdown\",\n          position: \"right\",\n        },\n        {\n          href: \"https://karakeep.app\",\n          label: \"Homepage\",\n          position: \"right\",\n        },\n        {\n          href: \"https://github.com/karakeep-app/karakeep\",\n          label: \"GitHub\",\n          position: \"right\",\n        },\n        {\n          href: \"https://discord.gg/NrgeYywsFh\",\n          label: \"Discord\",\n          position: \"right\",\n        },\n      ],\n    },\n\n    algolia: {\n      appId: \"V93C1M14G6\",\n      // Public API key: it is safe to commit it\n      apiKey: \"0eb8853d9740822fb9d21620d5515f35\",\n      indexName: \"karakeep\",\n      contextualSearch: true,\n      insights: true,\n    },\n    prism: {\n      theme: prismThemes.github,\n      darkTheme: prismThemes.dracula,\n    },\n  } satisfies Preset.ThemeConfig,\n};\n\nexport default config;\n"
  },
  {
    "path": "docs/package.json",
    "content": "{\n  \"name\": \"@karakeep/docs\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"docusaurus\": \"docusaurus\",\n    \"start\": \"docusaurus start\",\n    \"build\": \"docusaurus build\",\n    \"swizzle\": \"docusaurus swizzle\",\n    \"deploy\": \"docusaurus deploy\",\n    \"clear\": \"docusaurus clear\",\n    \"serve\": \"docusaurus serve\",\n    \"write-translations\": \"docusaurus write-translations\",\n    \"write-heading-ids\": \"docusaurus write-heading-ids\",\n    \"typecheck\": \"tsc --noEmit\",\n    \"gen-api\": \"rm -r docs/api && pnpm docusaurus gen-api-docs all && echo '{ \\\"label\\\": \\\"API\\\" }' > docs/api/_category_.json\"\n  },\n  \"dependencies\": {\n    \"@docusaurus/core\": \"3.8.1\",\n    \"@docusaurus/plugin-client-redirects\": \"^3.8.1\",\n    \"@docusaurus/preset-classic\": \"3.8.1\",\n    \"@mdx-js/react\": \"^3.0.0\",\n    \"clsx\": \"^2.1.0\",\n    \"docusaurus-plugin-openapi-docs\": \"^4.3.7\",\n    \"docusaurus-theme-openapi-docs\": \"^4.4.0\",\n    \"lucide-react\": \"^0.501.0\",\n    \"prism-react-renderer\": \"^2.4.1\",\n    \"react\": \"^19.2.1\",\n    \"react-dom\": \"^19.2.1\",\n    \"url\": \"^0.11.4\"\n  },\n  \"devDependencies\": {\n    \"@docusaurus/module-type-aliases\": \"3.8.1\",\n    \"@docusaurus/tsconfig\": \"3.8.1\",\n    \"@docusaurus/types\": \"3.8.1\",\n    \"typescript\": \"^5.9\"\n  },\n  \"browserslist\": {\n    \"production\": [\n      \">0.5%\",\n      \"not dead\",\n      \"not op_mini all\"\n    ],\n    \"development\": [\n      \"last 3 chrome version\",\n      \"last 3 firefox version\",\n      \"last 5 safari version\"\n    ]\n  },\n  \"engines\": {\n    \"node\": \">=18.0\"\n  }\n}\n"
  },
  {
    "path": "docs/sidebars.ts",
    "content": "import type { SidebarsConfig } from \"@docusaurus/plugin-content-docs\";\n\nimport openapiSidebar from \"./docs/api/sidebar\";\n\nconst sidebars: SidebarsConfig = {\n  sidebar: [\n    { type: \"autogenerated\", dirName: \".\" },\n    {\n      type: \"category\",\n      label: \"API\",\n      items: openapiSidebar,\n    },\n  ],\n};\n\nexport default sidebars;\n"
  },
  {
    "path": "docs/src/css/custom.css",
    "content": "/**\n * Modern Docusaurus theme\n * Clean, minimal aesthetics with excellent dark mode support\n */\n\n/* Import Inter font for modern typography */\n@import url(\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap\");\n\n/* ============================================\n   CSS Custom Properties (Light Mode)\n   ============================================ */\n:root {\n  /* Primary brand colors - modern blue/indigo */\n  --ifm-color-primary: #6366f1;\n  --ifm-color-primary-dark: #4f46e5;\n  --ifm-color-primary-darker: #4338ca;\n  --ifm-color-primary-darkest: #3730a3;\n  --ifm-color-primary-light: #818cf8;\n  --ifm-color-primary-lighter: #a5b4fc;\n  --ifm-color-primary-lightest: #c7d2fe;\n\n  /* Background colors */\n  --ifm-background-color: #ffffff;\n  --ifm-background-surface-color: #fafafa;\n  --docs-color-border: #e5e7eb;\n\n  /* Text colors */\n  --ifm-font-color-base: #111827;\n  --ifm-font-color-secondary: #6b7280;\n  --ifm-heading-color: #111827;\n\n  /* Typography */\n  --ifm-font-family-base: \"Inter\", -apple-system, BlinkMacSystemFont,\n    \"Segoe UI\", Roboto, Oxygen, Ubuntu, sans-serif;\n  --ifm-font-family-monospace: \"JetBrains Mono\", \"Fira Code\", \"SF Mono\", Monaco,\n    Consolas, monospace;\n  --ifm-font-size-base: 16px;\n  --ifm-line-height-base: 1.7;\n  --ifm-heading-font-weight: 600;\n\n  /* Spacing */\n  --ifm-spacing-horizontal: 1.5rem;\n  --ifm-global-radius: 0.5rem;\n\n  /* Code */\n  --ifm-code-font-size: 0.875rem;\n  --ifm-code-padding-vertical: 0.1rem;\n  --ifm-code-padding-horizontal: 0.375rem;\n  --ifm-code-background: #f3f4f6;\n  --docusaurus-highlighted-code-line-bg: rgba(99, 102, 241, 0.1);\n\n  /* Navbar */\n  --ifm-navbar-background-color: rgba(255, 255, 255, 1);\n  --ifm-navbar-shadow: none;\n  --ifm-navbar-height: 4rem;\n  --ifm-navbar-padding-horizontal: 1.5rem;\n  --ifm-navbar-padding-vertical: 0;\n\n  /* Sidebar */\n  --doc-sidebar-width: 280px;\n  --ifm-menu-color: #4b5563;\n  --ifm-menu-color-active: var(--ifm-color-primary);\n  --ifm-menu-color-background-active: rgba(99, 102, 241, 0.08);\n  --ifm-menu-color-background-hover: rgba(0, 0, 0, 0.03);\n\n  /* Table of contents */\n  --ifm-toc-border-color: var(--docs-color-border);\n\n  /* Cards and containers */\n  --ifm-card-background-color: #ffffff;\n  --ifm-alert-border-left-width: 0;\n  --ifm-alert-border-radius: 0.75rem;\n  --ifm-alert-padding-horizontal: 1.25rem;\n  --ifm-alert-padding-vertical: 1rem;\n\n  /* Transitions */\n  --transition-fast: 150ms ease;\n  --transition-normal: 200ms ease;\n}\n\n/* ============================================\n   Dark Mode\n   ============================================ */\n[data-theme=\"dark\"] {\n  /* Primary brand colors - brighter for dark mode */\n  --ifm-color-primary: #818cf8;\n  --ifm-color-primary-dark: #6366f1;\n  --ifm-color-primary-darker: #4f46e5;\n  --ifm-color-primary-darkest: #4338ca;\n  --ifm-color-primary-light: #a5b4fc;\n  --ifm-color-primary-lighter: #c7d2fe;\n  --ifm-color-primary-lightest: #e0e7ff;\n\n  /* Background colors - true dark */\n  --ifm-background-color: #0a0a0a;\n  --ifm-background-surface-color: #111111;\n  --docs-color-border: #262626;\n\n  /* Text colors */\n  --ifm-font-color-base: #e5e7eb;\n  --ifm-font-color-secondary: #9ca3af;\n  --ifm-heading-color: #f9fafb;\n\n  /* Code */\n  --ifm-code-background: #1f1f1f;\n  --docusaurus-highlighted-code-line-bg: rgba(129, 140, 248, 0.15);\n\n  /* Navbar */\n  --ifm-navbar-background-color: rgba(10, 10, 10, 1);\n\n  /* Sidebar */\n  --ifm-menu-color: #a1a1aa;\n  --ifm-menu-color-background-active: rgba(129, 140, 248, 0.12);\n  --ifm-menu-color-background-hover: rgba(255, 255, 255, 0.05);\n\n  /* Cards */\n  --ifm-card-background-color: #141414;\n}\n\n/* ============================================\n   Global Styles\n   ============================================ */\nhtml {\n  font-feature-settings: \"cv02\", \"cv03\", \"cv04\", \"cv11\";\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\nbody {\n  letter-spacing: -0.011em;\n}\n\n/* ============================================\n   Typography\n   ============================================ */\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n  font-weight: var(--ifm-heading-font-weight);\n  letter-spacing: -0.025em;\n}\n\nh1 {\n  font-size: 2.25rem;\n  line-height: 1.2;\n  margin-bottom: 1.5rem;\n}\n\nh2 {\n  font-size: 1.5rem;\n  line-height: 1.3;\n  margin-top: 2.5rem;\n  padding-bottom: 0.5rem;\n  border-bottom: 1px solid var(--docs-color-border);\n}\n\nh3 {\n  font-size: 1.25rem;\n  line-height: 1.4;\n  margin-top: 2rem;\n}\n\n\n/* ============================================\n   Navbar\n   ============================================ */\n.navbar {\n  border-bottom: 1px solid var(--docs-color-border);\n  transition: background-color var(--transition-normal);\n}\n\n.navbar__brand {\n  margin-right: 1.5rem;\n}\n\n.navbar__title {\n  font-weight: 600;\n}\n\n.navbar__items {\n  gap: 0.25rem;\n}\n\n.navbar__item {\n  font-size: 0.875rem;\n  font-weight: 500;\n  padding: 0.5rem 0.875rem;\n  border-radius: 0.375rem;\n  transition:\n    background-color var(--transition-fast),\n    color var(--transition-fast);\n}\n\n.navbar__item:hover {\n  background-color: var(--ifm-menu-color-background-hover);\n  text-decoration: none;\n}\n\n.navbar__link--active {\n  color: var(--ifm-color-primary);\n}\n\n/* Search button styling */\n.navbar__search-input {\n  background-color: var(--ifm-background-surface-color);\n  border: 1px solid var(--docs-color-border);\n  border-radius: 0.5rem;\n  font-size: 0.875rem;\n  padding: 0.5rem 1rem 0.5rem 2.25rem;\n  transition:\n    border-color var(--transition-fast),\n    box-shadow var(--transition-fast);\n}\n\n.navbar__search-input:focus {\n  border-color: var(--ifm-color-primary);\n  box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);\n  outline: none;\n}\n\n[data-theme=\"dark\"] .navbar__search-input:focus {\n  box-shadow: 0 0 0 3px rgba(129, 140, 248, 0.15);\n}\n\n/* ============================================\n   Sidebar\n   ============================================ */\n.theme-doc-sidebar-container {\n  border-right: 1px solid var(--docs-color-border) !important;\n}\n\n.menu {\n  padding: 1rem 0.75rem;\n  font-size: 0.875rem;\n}\n\n.menu__list {\n  transition: height var(--transition-normal);\n}\n\n.menu__list-item {\n  margin-bottom: 0.125rem;\n}\n\n.menu__link {\n  padding: 0.5rem 0.75rem;\n  border-radius: 0.375rem;\n  font-weight: 450;\n  transition: all var(--transition-fast);\n}\n\n.menu__link:hover:not(.menu__link--active) {\n  background-color: var(--ifm-menu-color-background-hover);\n}\n\n.menu__link--active {\n  background-color: var(--ifm-menu-color-background-active);\n  color: var(--ifm-color-primary);\n  font-weight: 500;\n}\n\n.menu__link--sublist-caret::after {\n  background-size: 1.25rem 1.25rem;\n  opacity: 0.5;\n}\n\n.theme-doc-sidebar-item-category-level-1 > .menu__list-item-collapsible {\n  font-weight: 600;\n  font-size: 0.75rem;\n  text-transform: uppercase;\n  letter-spacing: 0.05em;\n  color: var(--ifm-font-color-secondary);\n}\n\n.theme-doc-sidebar-item-category-level-1\n  > .menu__list-item-collapsible\n  .menu__link {\n  font-weight: 600;\n}\n\n/* ============================================\n   Main Content Area\n   ============================================ */\n.main-wrapper {\n  background-color: var(--ifm-background-color);\n}\n\n[data-theme=\"dark\"] .main-wrapper,\n[data-theme=\"dark\"] .navbar,\n[data-theme=\"dark\"] .theme-doc-sidebar-container {\n  background-color: #0a0a0a;\n}\n\n[data-theme=\"dark\"] .navbar {\n  background-color: rgba(10, 10, 10, 0.8);\n}\n\narticle {\n  max-width: 100%;\n  min-width: 0;\n}\n\n.theme-doc-markdown {\n  margin-top: 1rem;\n  max-width: 100%;\n}\n\n/* Breadcrumbs */\n.breadcrumbs {\n  margin-bottom: 1.5rem;\n}\n\n.breadcrumbs__item {\n  font-size: 0.875rem;\n}\n\n.breadcrumbs__link {\n  color: var(--ifm-font-color-secondary);\n  padding: 0.25rem 0.5rem;\n  border-radius: 0.25rem;\n  transition: all var(--transition-fast);\n}\n\n.breadcrumbs__link:hover {\n  background-color: var(--ifm-menu-color-background-hover);\n  text-decoration: none;\n}\n\n/* ============================================\n   Code Blocks\n   ============================================ */\ncode {\n  border: 1px solid var(--docs-color-border);\n  border-radius: 0.375rem;\n  font-size: var(--ifm-code-font-size);\n  padding: var(--ifm-code-padding-vertical) var(--ifm-code-padding-horizontal);\n  background-color: var(--ifm-code-background);\n}\n\npre {\n  border: 1px solid var(--docs-color-border);\n  border-radius: 0.75rem;\n  background-color: var(--ifm-code-background) !important;\n}\n\npre code {\n  border: none;\n  padding: 0;\n  background: transparent;\n}\n\n.prism-code {\n  font-size: 0.875rem;\n  line-height: 1.6;\n}\n\n/* Code block title */\n.theme-code-block-title {\n  background-color: var(--ifm-code-background);\n  border-bottom: 1px solid var(--docs-color-border);\n  font-size: 0.8125rem;\n  font-weight: 500;\n  padding: 0.625rem 1rem;\n}\n\n/* Copy button */\n.theme-code-block button {\n  border-radius: 0.375rem;\n  transition: all var(--transition-fast);\n  opacity: 0;\n}\n\n.theme-code-block:hover button {\n  opacity: 1;\n}\n\n/* ============================================\n   Admonitions / Alerts\n   ============================================ */\n.alert {\n  border: 1px solid var(--docs-color-border);\n  box-shadow: none;\n}\n\n.alert--info {\n  --ifm-alert-background-color: rgba(99, 102, 241, 0.08);\n  --ifm-alert-border-color: rgba(99, 102, 241, 0.3);\n  border-color: var(--ifm-alert-border-color);\n}\n\n.alert--warning {\n  --ifm-alert-background-color: rgba(245, 158, 11, 0.08);\n  --ifm-alert-border-color: rgba(245, 158, 11, 0.3);\n  border-color: var(--ifm-alert-border-color);\n}\n\n.alert--danger {\n  --ifm-alert-background-color: rgba(239, 68, 68, 0.08);\n  --ifm-alert-border-color: rgba(239, 68, 68, 0.3);\n  border-color: var(--ifm-alert-border-color);\n}\n\n.alert--success {\n  --ifm-alert-background-color: rgba(34, 197, 94, 0.08);\n  --ifm-alert-border-color: rgba(34, 197, 94, 0.3);\n  border-color: var(--ifm-alert-border-color);\n}\n\n[data-theme=\"dark\"] .alert--info {\n  --ifm-alert-background-color: rgba(129, 140, 248, 0.1);\n  --ifm-alert-border-color: rgba(129, 140, 248, 0.25);\n}\n\n[data-theme=\"dark\"] .alert--warning {\n  --ifm-alert-background-color: rgba(251, 191, 36, 0.1);\n  --ifm-alert-border-color: rgba(251, 191, 36, 0.25);\n}\n\n[data-theme=\"dark\"] .alert--danger {\n  --ifm-alert-background-color: rgba(248, 113, 113, 0.1);\n  --ifm-alert-border-color: rgba(248, 113, 113, 0.25);\n}\n\n[data-theme=\"dark\"] .alert--success {\n  --ifm-alert-background-color: rgba(74, 222, 128, 0.1);\n  --ifm-alert-border-color: rgba(74, 222, 128, 0.25);\n}\n\n.admonition-heading h5 {\n  font-size: 0.875rem;\n  text-transform: uppercase;\n  letter-spacing: 0.025em;\n}\n\n/* ============================================\n   Tables\n   ============================================ */\ntable {\n  display: block;\n  width: 100%;\n  border-collapse: collapse;\n  margin: 1.5rem 0;\n  font-size: 0.875rem;\n  overflow-x: auto;\n}\n\nth {\n  background-color: var(--ifm-background-surface-color);\n  font-weight: 600;\n  text-align: left;\n  padding: 0.75rem 1rem;\n  border-bottom: 2px solid var(--docs-color-border);\n}\n\ntd {\n  padding: 0.75rem 1rem;\n  border-bottom: 1px solid var(--docs-color-border);\n}\n\ntr:last-child td {\n  border-bottom: none;\n}\n\n/* ============================================\n   Blockquotes\n   ============================================ */\nblockquote {\n  border-left: 3px solid var(--ifm-color-primary);\n  background-color: var(--ifm-background-surface-color);\n  margin: 1.5rem 0;\n  padding: 1rem 1.5rem;\n  border-radius: 0 0.5rem 0.5rem 0;\n  color: var(--ifm-font-color-secondary);\n}\n\nblockquote p:last-child {\n  margin-bottom: 0;\n}\n\n/* ============================================\n   Lists\n   ============================================ */\nul,\nol {\n  padding-left: 1.5rem;\n  margin-bottom: 1.25rem;\n}\n\nli {\n  margin-bottom: 0.5rem;\n}\n\nli::marker {\n  color: var(--ifm-font-color-secondary);\n}\n\n/* ============================================\n   Pagination\n   ============================================ */\n.pagination-nav {\n  margin-top: 3rem;\n  gap: 1rem;\n}\n\n.pagination-nav__link {\n  border: 1px solid var(--docs-color-border);\n  border-radius: 0.75rem;\n  padding: 1rem 1.25rem;\n  transition: all var(--transition-fast);\n}\n\n.pagination-nav__link:hover {\n  border-color: var(--ifm-color-primary);\n  background-color: transparent;\n  text-decoration: none;\n}\n\n.pagination-nav__sublabel {\n  font-size: 0.75rem;\n  text-transform: uppercase;\n  letter-spacing: 0.05em;\n  color: var(--ifm-font-color-secondary);\n  margin-bottom: 0.25rem;\n}\n\n.pagination-nav__label {\n  font-weight: 500;\n  font-size: 0.9375rem;\n}\n\n/* ============================================\n   Scrollbar Styling\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: var(--docs-color-border);\n  border-radius: 4px;\n}\n\n::-webkit-scrollbar-thumb:hover {\n  background: var(--ifm-font-color-secondary);\n}\n\n/* ============================================\n   Algolia Search\n   ============================================ */\n.DocSearch-Button {\n  border-radius: 0.5rem !important;\n  padding: 0 1rem !important;\n  height: 2.25rem !important;\n  background-color: var(--ifm-background-surface-color) !important;\n  border: 1px solid var(--docs-color-border) !important;\n  transition:\n    border-color var(--transition-fast),\n    box-shadow var(--transition-fast) !important;\n}\n\n.DocSearch-Button:hover {\n  border-color: var(--ifm-color-primary) !important;\n  box-shadow: none !important;\n}\n\n.DocSearch-Button-Placeholder {\n  font-size: 0.875rem !important;\n  font-weight: 400 !important;\n}\n\n.DocSearch-Button-Keys {\n  min-width: auto !important;\n  gap: 0.125rem !important;\n}\n\n.DocSearch-Button-Key {\n  background: var(--docs-color-border) !important;\n  border-radius: 0.25rem !important;\n  padding: 0.125rem 0.375rem !important;\n  font-size: 0.75rem !important;\n  box-shadow: none !important;\n  border: none !important;\n}\n\n/* ============================================\n   API Docs Specific Styles\n   ============================================ */\n\n.api-method > .menu__link {\n  align-items: center;\n  justify-content: start;\n}\n\n.openapi-tabs__code-container {\n  border-radius: 0.75rem;\n  border: 1px solid var(--docs-color-border);\n}\n\n.openapi-markdown__details {\n  border: 1px solid var(--docs-color-border);\n  border-radius: 0.5rem;\n  background-color: var(--ifm-background-surface-color);\n}\n\n/* HTTP Method badges */\n.api-method .menu__link::before,\n.openapi-method-badge {\n  border-radius: 0.25rem;\n  font-size: 0.625rem;\n  font-weight: 600;\n  text-transform: uppercase;\n  letter-spacing: 0.025em;\n  padding: 0.125rem 0.375rem;\n}\n\n/* ============================================\n   Responsive Adjustments\n   ============================================ */\n@media (max-width: 996px) {\n  .navbar {\n    padding: 0 1rem;\n  }\n\n  h1 {\n    font-size: 1.875rem;\n  }\n\n  h2 {\n    font-size: 1.375rem;\n  }\n\n  .footer {\n    padding: 2rem 0;\n  }\n}\n\n@media (max-width: 576px) {\n  :root {\n    --ifm-spacing-horizontal: 1rem;\n  }\n\n  h1 {\n    font-size: 1.625rem;\n  }\n\n  .pagination-nav {\n    flex-direction: column;\n  }\n\n  .pagination-nav__link {\n    width: 100%;\n  }\n}\n\n/* ============================================\n   Smooth Animations\n   ============================================ */\n.menu__list-item-collapsible--active .menu__link--sublist-caret::after,\n.menu__list-item-collapsible .menu__link--sublist-caret::after {\n  transition: transform var(--transition-fast);\n}\n\n/* Fade in for content */\n.theme-doc-markdown > * {\n  animation: fadeIn 0.3s ease-out;\n}\n\n@keyframes fadeIn {\n  from {\n    opacity: 0;\n    transform: translateY(8px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n/* Reduce motion for users who prefer it */\n@media (prefers-reduced-motion: reduce) {\n  *,\n  *::before,\n  *::after {\n    animation-duration: 0.01ms !important;\n    animation-iteration-count: 1 !important;\n    transition-duration: 0.01ms !important;\n  }\n}\n"
  },
  {
    "path": "docs/src/theme/DocSidebarItem/Category/index.tsx",
    "content": "import React, {\n  type ComponentProps,\n  type ReactNode,\n  useEffect,\n  useMemo,\n} from 'react';\nimport clsx from 'clsx';\nimport {\n  ThemeClassNames,\n  useThemeConfig,\n  usePrevious,\n  Collapsible,\n  useCollapsible,\n} from '@docusaurus/theme-common';\nimport {isSamePath} from '@docusaurus/theme-common/internal';\nimport {\n  isActiveSidebarItem,\n  findFirstSidebarItemLink,\n  useDocSidebarItemsExpandedState,\n} from '@docusaurus/plugin-content-docs/client';\nimport Link from '@docusaurus/Link';\nimport {translate} from '@docusaurus/Translate';\nimport useIsBrowser from '@docusaurus/useIsBrowser';\nimport DocSidebarItems from '@theme/DocSidebarItems';\nimport type {Props} from '@theme/DocSidebarItem/Category';\nimport {\n  Rocket,\n  Package,\n  Settings,\n  BookOpen,\n  Plug,\n  Wrench,\n  Users,\n  Code,\n  type LucideIcon,\n} from 'lucide-react';\n\n// Map category labels to Lucide icons\nconst categoryIcons: Record<string, LucideIcon> = {\n  'Getting Started': Rocket,\n  'Installation': Package,\n  'Configuration': Settings,\n  'Using Karakeep': BookOpen,\n  'Integrations': Plug,\n  'Administration': Wrench,\n  'Community': Users,\n  'Development': Code,\n  'API': Code,\n};\n\n// If we navigate to a category and it becomes active, it should automatically\n// expand itself\nfunction useAutoExpandActiveCategory({\n  isActive,\n  collapsed,\n  updateCollapsed,\n}: {\n  isActive: boolean;\n  collapsed: boolean;\n  updateCollapsed: (b: boolean) => void;\n}) {\n  const wasActive = usePrevious(isActive);\n  useEffect(() => {\n    const justBecameActive = isActive && !wasActive;\n    if (justBecameActive && collapsed) {\n      updateCollapsed(false);\n    }\n  }, [isActive, wasActive, collapsed, updateCollapsed]);\n}\n\n/**\n * When a collapsible category has no link, we still link it to its first child\n * during SSR as a temporary fallback. This allows to be able to navigate inside\n * the category even when JS fails to load, is delayed or simply disabled\n * React hydration becomes an optional progressive enhancement\n * see https://github.com/facebookincubator/infima/issues/36#issuecomment-772543188\n * see https://github.com/facebook/docusaurus/issues/3030\n */\nfunction useCategoryHrefWithSSRFallback(\n  item: Props['item'],\n): string | undefined {\n  const isBrowser = useIsBrowser();\n  return useMemo(() => {\n    if (item.href && !item.linkUnlisted) {\n      return item.href;\n    }\n    // In these cases, it's not necessary to render a fallback\n    // We skip the \"findFirstCategoryLink\" computation\n    if (isBrowser || !item.collapsible) {\n      return undefined;\n    }\n    return findFirstSidebarItemLink(item);\n  }, [item, isBrowser]);\n}\n\nfunction CollapseButton({\n  collapsed,\n  categoryLabel,\n  onClick,\n}: {\n  collapsed: boolean;\n  categoryLabel: string;\n  onClick: ComponentProps<'button'>['onClick'];\n}) {\n  return (\n    <button\n      aria-label={\n        collapsed\n          ? translate(\n              {\n                id: 'theme.DocSidebarItem.expandCategoryAriaLabel',\n                message: \"Expand sidebar category '{label}'\",\n                description: 'The ARIA label to expand the sidebar category',\n              },\n              {label: categoryLabel},\n            )\n          : translate(\n              {\n                id: 'theme.DocSidebarItem.collapseCategoryAriaLabel',\n                message: \"Collapse sidebar category '{label}'\",\n                description: 'The ARIA label to collapse the sidebar category',\n              },\n              {label: categoryLabel},\n            )\n      }\n      aria-expanded={!collapsed}\n      type=\"button\"\n      className=\"clean-btn menu__caret\"\n      onClick={onClick}\n    />\n  );\n}\n\nexport default function DocSidebarItemCategory({\n  item,\n  onItemClick,\n  activePath,\n  level,\n  index,\n  ...props\n}: Props): ReactNode {\n  const {items, label, collapsible, className, href} = item;\n  const {\n    docs: {\n      sidebar: {autoCollapseCategories},\n    },\n  } = useThemeConfig();\n  const hrefWithSSRFallback = useCategoryHrefWithSSRFallback(item);\n\n  const isActive = isActiveSidebarItem(item, activePath);\n  const isCurrentPage = isSamePath(href, activePath);\n\n  const {collapsed, setCollapsed} = useCollapsible({\n    // Active categories are always initialized as expanded. The default\n    // (`item.collapsed`) is only used for non-active categories.\n    initialState: () => {\n      if (!collapsible) {\n        return false;\n      }\n      return isActive ? false : item.collapsed;\n    },\n  });\n\n  const {expandedItem, setExpandedItem} = useDocSidebarItemsExpandedState();\n  // Use this instead of `setCollapsed`, because it is also reactive\n  const updateCollapsed = (toCollapsed: boolean = !collapsed) => {\n    setExpandedItem(toCollapsed ? null : index);\n    setCollapsed(toCollapsed);\n  };\n  useAutoExpandActiveCategory({isActive, collapsed, updateCollapsed});\n  useEffect(() => {\n    if (\n      collapsible &&\n      expandedItem != null &&\n      expandedItem !== index &&\n      autoCollapseCategories\n    ) {\n      setCollapsed(true);\n    }\n  }, [collapsible, expandedItem, index, setCollapsed, autoCollapseCategories]);\n\n  return (\n    <li\n      className={clsx(\n        ThemeClassNames.docs.docSidebarItemCategory,\n        ThemeClassNames.docs.docSidebarItemCategoryLevel(level),\n        'menu__list-item',\n        {\n          'menu__list-item--collapsed': collapsed,\n        },\n        className,\n      )}>\n      <div\n        className={clsx('menu__list-item-collapsible', {\n          'menu__list-item-collapsible--active': isCurrentPage,\n        })}>\n        <Link\n          className={clsx('menu__link', {\n            'menu__link--sublist': collapsible,\n            'menu__link--sublist-caret': !href && collapsible,\n            'menu__link--active': isActive,\n          })}\n          onClick={\n            collapsible\n              ? (e) => {\n                  onItemClick?.(item);\n                  if (href) {\n                    // When already on the category's page, we collapse it\n                    // We don't use \"isActive\" because it would collapse the\n                    // category even when we browse a children element\n                    // See https://github.com/facebook/docusaurus/issues/11213\n                    if (isCurrentPage) {\n                      e.preventDefault();\n                      updateCollapsed();\n                    } else {\n                      // When navigating to a new category, we always expand\n                      // see https://github.com/facebook/docusaurus/issues/10854#issuecomment-2609616182\n                      updateCollapsed(false);\n                    }\n                  } else {\n                    e.preventDefault();\n                    updateCollapsed();\n                  }\n                }\n              : () => {\n                  onItemClick?.(item);\n                }\n          }\n          aria-current={isCurrentPage ? 'page' : undefined}\n          role={collapsible && !href ? 'button' : undefined}\n          aria-expanded={collapsible && !href ? !collapsed : undefined}\n          href={collapsible ? hrefWithSSRFallback ?? '#' : hrefWithSSRFallback}\n          {...props}>\n          {(() => {\n            const Icon = categoryIcons[label];\n            if (Icon) {\n              return (\n                <span style={{display: 'flex', alignItems: 'center', gap: '0.625rem'}}>\n                  <Icon size={16} strokeWidth={1.5} style={{opacity: 0.6}} />\n                  {label}\n                </span>\n              );\n            }\n            return label;\n          })()}\n        </Link>\n        {href && collapsible && (\n          <CollapseButton\n            collapsed={collapsed}\n            categoryLabel={label}\n            onClick={(e) => {\n              e.preventDefault();\n              updateCollapsed();\n            }}\n          />\n        )}\n      </div>\n\n      <Collapsible lazy as=\"ul\" className=\"menu__list\" collapsed={collapsed}>\n        <DocSidebarItems\n          items={items}\n          tabIndex={collapsed ? -1 : 0}\n          onItemClick={onItemClick}\n          activePath={activePath}\n          level={level + 1}\n        />\n      </Collapsible>\n    </li>\n  );\n}\n"
  },
  {
    "path": "docs/static/.nojekyll",
    "content": ""
  },
  {
    "path": "docs/static/_redirects",
    "content": "/Installation/*        /installation/:splat   301\n/Guides/*              /guides/:splat   301\n/Development/*         /development/:splat   301\n/API/*                 /api/:splat   301\n/intro                                          /getting-started/intro   301\n/screenshots                                    /getting-started/screenshots   301\n/configuration                                  /configuration/environment-variables   301\n/quick-sharing                                  /using-karakeep/quick-sharing   301\n/import                                         /using-karakeep/import   301\n/guides/search-query-language                   /using-karakeep/search-query-language   301\n/openai                                         /administration/openai   301\n/command-line                                   /integrations/command-line   301\n/mcp                                            /integrations/mcp   301\n/guides/different-ai-providers                  /configuration/different-ai-providers   301\n/guides/singlefile                              /integrations/singlefile   301\n/security-considerations                        /administration/security-considerations   301\n/FAQ                                            /administration/FAQ   301\n/troubleshooting                                /administration/troubleshooting   301\n/guides/server-migration                        /administration/server-migration   301\n/guides/legacy-container-upgrade                /administration/legacy-container-upgrade   301\n/guides/hoarder-to-karakeep-migration           /administration/hoarder-to-karakeep-migration   301\n/community-projects                             /community/community-projects   301\n"
  },
  {
    "path": "docs/static/robots.txt",
    "content": "User-agent: *\nDisallow: /v*/\nDisallow: /next/\n"
  },
  {
    "path": "docs/tsconfig.json",
    "content": "{\n  \"extends\": \"@docusaurus/tsconfig\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\"\n  },\n  \"include\": [\n    \"../node_modules/docusaurus-plugin-openapi-docs/src/plugin-content-docs-types.d.ts\"\n  ]\n}\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/01-intro.md",
    "content": "---\nslug: /\n---\n\n# Introduction\n\nKarakeep (previously Hoarder) is an open source \"Bookmark Everything\" app that uses AI for automatically tagging the content you throw at it. The app is built with self-hosting as a first class citizen.\n\n![Screenshot](https://raw.githubusercontent.com/karakeep-app/karakeep/main/screenshots/homepage.png)\n\n\n## Features\n\n- 🔗 Bookmark links, take simple notes and store images and pdfs.\n- ⬇️ Automatic fetching for link titles, descriptions and images.\n- 📋 Sort your bookmarks into lists.\n- 🔎 Full text search of all the content stored.\n- ✨ AI-based (aka chatgpt) automatic tagging and summarization. With supports for local models using ollama!\n- 🤖 Rule-based engine for customized management.\n- 🎆 OCR for extracting text from images.\n- 🔖 [Chrome plugin](https://chromewebstore.google.com/detail/karakeep/kgcjekpmcjjogibpjebkhaanilehneje) and [Firefox addon](https://addons.mozilla.org/en-US/firefox/addon/karakeep/) for quick bookmarking.\n- 📱 An [iOS app](https://apps.apple.com/us/app/karakeep-app/id6479258022), and an [Android app](https://play.google.com/store/apps/details?id=app.hoarder.hoardermobile&pcampaignid=web_share).\n- 📰 Auto hoarding from RSS feeds.\n- 🔌 REST API and multiple clients.\n- 🌐 Multi-language support.\n- 🖍️ Mark and store highlights from your hoarded content.\n- 🗄️ Full page archival (using [monolith](https://github.com/Y2Z/monolith)) to protect against link rot.\n- ▶️ Auto video archiving using [yt-dlp](https://github.com/yt-dlp/yt-dlp).\n- ☑️ Bulk actions support.\n- 🔐 SSO support.\n- 🌙 Dark mode support.\n- 💾 Self-hosting first.\n- ⬇️ Bookmark importers from Chrome, Pocket, Linkwarden, Omnivore, Tab Session Manager.\n- 🔄 Automatic sync with browser bookmarks via [floccus](https://floccus.org/).\n- [Planned] Offline reading on mobile, semantic search across bookmarks, ...\n\n**⚠️ This app is under heavy development.**\n\n\n## Demo\n\nYou can access the demo at [https://try.karakeep.app](https://try.karakeep.app). Login with the following creds:\n\n```\nemail: demo@karakeep.app\npassword: demodemo\n```\n\nThe demo is seeded with some content, but it's in read-only mode to prevent abuse.\n\n## About the name\n\nThe name Karakeep is inspired by the Arabic word \"كراكيب\" (karakeeb), a colloquial term commonly used to refer to miscellaneous clutter, odds and ends, or items that may seem disorganized but often hold personal value or hidden usefulness. It evokes the image of a messy drawer or forgotten box, full of stuff you can't quite throw away—because somehow, it matters (or more likely, because you're a hoarder!).\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/02-installation/01-docker.md",
    "content": "# Docker Compose [Recommended]\n\n### Requirements\n\n- Docker\n- Docker Compose\n\n### 1. Create a new directory\n\nCreate a new directory to host the compose file and env variables.\n\nThis is where you’ll place the `docker-compose.yml` file from the next step and the environment variables.\n\nFor example you could make a new directory called \"karakeep-app\" with the following command:\n```\nmkdir karakeep-app\n```\n\n\n### 2. Download the compose file\n\nDownload the docker compose file provided [here](https://github.com/karakeep-app/karakeep/blob/main/docker/docker-compose.yml) directly into your new directory.\n\n```\nwget https://raw.githubusercontent.com/karakeep-app/karakeep/main/docker/docker-compose.yml\n```\n\n### 3. Populate the environment variables\n\nTo configure the app, create a `.env` file in the directory and add this minimal env file:\n\n```\nKARAKEEP_VERSION=release\nNEXTAUTH_SECRET=super_random_string\nMEILI_MASTER_KEY=another_random_string\nNEXTAUTH_URL=http://localhost:3000\n```\n\nYou **should** change the random strings. You can use `openssl rand -base64 36` in a seperate terminal window to generate the random strings. You should also change the `NEXTAUTH_URL` variable to point to your server address.\n\nUsing `KARAKEEP_VERSION=release` will pull the latest stable version. You might want to pin the version instead to control the upgrades (e.g. `KARAKEEP_VERSION=0.10.0`). Check the latest versions [here](https://github.com/karakeep-app/karakeep/pkgs/container/karakeep).\n\nPersistent storage and the wiring between the different services is already taken care of in the docker compose file.\n\nKeep in mind that every time you change the `.env` file, you'll need to re-run `docker compose up`.\n\nIf you want more config params, check the config documentation [here](/configuration).\n\n### 4. Setup OpenAI\n\nTo enable automatic tagging, you'll need to configure OpenAI. This is optional though but highly recommended.\n\n- Follow [OpenAI's help](https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key) to get an API key.\n- Add the OpenAI API key to the env file:\n\n```\nOPENAI_API_KEY=<key>\n```\n\nLearn more about the costs of using openai [here](/openai).\n\n<details>\n    <summary>If you want to use Ollama (https://ollama.com/) instead for local inference.</summary>\n\n    **Note:** The quality of the tags you'll get will depend on the quality of the model you choose.\n\n    - Make sure ollama is running.\n    - Set the `OLLAMA_BASE_URL` env variable to the address of the ollama API.\n    - Set `INFERENCE_TEXT_MODEL` to the model you want to use for text inference in ollama (for example: `llama3.1`)\n    - Set `INFERENCE_IMAGE_MODEL` to the model you want to use for image inference in ollama (for example: `llava`)\n    - Make sure that you `ollama pull`-ed the models that you want to use.\n    - You might want to tune the `INFERENCE_CONTEXT_LENGTH` as the default is quite small. The larger the value, the better the quality of the tags, but the more expensive the inference will be.\n\n</details>\n\n### 5. Start the service\n\nStart the service by running:\n\n```\ndocker compose up -d\n```\n\nThen visit `http://localhost:3000` and you should be greeted with the Sign In page.\n\n### [Optional] 6. Enable optional features\n\nCheck the [configuration docs](/configuration) for extra features to enable such as full page archival, full page screenshots, inference languages, etc.\n\n### [Optional] 7. Setup quick sharing extensions\n\nGo to the [quick sharing page](/quick-sharing) to install the mobile apps and the browser extensions. Those will help you hoard things faster!\n\n## Updating\n\nUpdating Karakeep will depend on what you used for the `KARAKEEP_VERSION` env variable.\n\n- If you pinned the app to a specific version, bump the version and re-run `docker compose up -d`. This should pull the new version for you.\n- If you used `KARAKEEP_VERSION=release`, you'll need to force docker to pull the latest version by running `docker compose up --pull always -d`.\n\nNote that if you want to upgrade/migrate `Meilisearch` versions, refer to the [troubleshooting](/troubleshooting) page.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/02-installation/02-unraid.md",
    "content": "# Unraid\n\n## Docker Compose Manager Plugin (Recommended)\n\nYou can use [Docker Compose Manager](https://forums.unraid.net/topic/114415-plugin-docker-compose-manager/) plugin to deploy Karakeep using the official docker compose file provided [here](https://github.com/karakeep-app/karakeep/blob/main/docker/docker-compose.yml). After creating the stack, you'll need to setup some env variables similar to that from the docker compose installation docs [here](/installation/docker#3-populate-the-environment-variables).\n\n## Community Apps\n\n:::info\nThe community application template is maintained by the community.\n:::\n\nKarakeep can be installed on Unraid using the community application plugins. Karakeep is a multi-container service, and because unraid doesn't natively support that, you'll have to install the different pieces as separate applications and wire them manually together.\n\nHere's a high level overview of the services you'll need:\n\n- **Karakeep** ([Support post](https://forums.unraid.net/topic/165108-support-collectathon-karakeep/)): Karakeep's main web app.\n- **Browserless** ([Support post](https://forums.unraid.net/topic/130163-support-template-masterwishxbrowserless/)): The chrome headless service used for fetching the content. Karakeep's official docker compose doesn't use browserless, but it's currently the only headless chrome service available on unraid, so you'll have to use it.\n- **MeiliSearch** ([Support post](https://forums.unraid.net/topic/164847-support-collectathon-meilisearch/)): The search engine used by Karakeep. It's optional but highly recommended. If you don't have it set up, search will be disabled.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/02-installation/03-archlinux.md",
    "content": "# Arch Linux\n\n## Installation\n\n> [Karakeep on AUR](https://aur.archlinux.org/packages/karakeep) is not maintained by the karakeep official.\n\n1. Install karakeep\n\n    ```shell\n    paru -S karakeep\n    ```\n\n2. (**Optional**) Install optional dependencies\n\n    ```shell\n    # karakeep-cli: karakeep cli tool\n    paru -S karakeep-cli\n\n    # ollama: for automatic tagging\n    sudo pacman -S ollama\n\n    # yt-dlp: for download video\n    sudo pacman -S yt-dlp\n    ```\n\n    You can use Open-AI instead of `ollama`. If you use `ollama`, you need to download the ollama model. Please refer to: [https://ollama.com/library](https://ollama.com/library).\n\n3. Set up\n\n    Environment variables can be set in `/etc/karakeep/karakeep.env` according to [configuration page](/configuration). **The environment variables that are not specified in `/etc/karakeep/karakeep.env` need to be added by yourself.**\n\n4. Enable service\n\n    ```shell\n    sudo systemctl enable --now karakeep.target\n    ```\n\n    Then visit `http://localhost:3000` and you should be greated with the sign in page.\n\n## Services and Ports\n\n`karakeep.target` include 3 services: `karakeep-web.service`, `karakeep-works.service`, `karakeep-browser.service`.\n\n- `karakeep-web.service`: Provide karakeep webui service, uses `3000` port by default.\n\n- `karakeep-workers.service`: Provide karakeep workers service, no port.\n\n- `karakeep-browser.service`: Provide browser headless service, uses `9222` port by default.\n\nNow `karakeep` depends on `meilisearch`, and `karakeep-workers.service` wants `meilisearch.service`, starting `karakeep.target` will start `meilisearch.service` at the same time.\n\n## How to Migrate from Hoarder to Karakeep\n\nThe PKGBUILD has been fully updated to replace all references to `hoarder` with `karakeep`. If you want to preserve your existing `hoarder` data during the upgrade, please follow the steps below:\n\n**1. Stop the old services**\n\n```shell\nsudo systemctl stop hoarder-web.service hoarder-worker.service hoarder-browser.service\nsudo systemctl disable --now hoarder.target\n```\n\n**2. Uninstall Hoarder**  \nAfter uninstalling, you can manually remove the old `hoarder` user and group if needed.\n```shell\nparu -R hoarder\n```\n\n**3. Rename the old data directory**\n```shell\nsudo mv /var/lib/hoarder /var/lib/karakeep\n```\n\n**4. Install Karakeep**\n```shell\nparu -S karakeep\n```\n\n**5. Fix ownership of the data directory**\n```shell\nsudo chown -R karakeep:karakeep /var/lib/karakeep\n```\n\n**6. Set Karakeep**  \nEdit `/etc/karakeep/karakeep.env` according to [configuration page](/configuration). **The environment variables that are not specified in `/etc/karakeep/karakeep.env` need to be added by yourself.**\n\nOr you can copy old hoarder env file to karakeep:\n```shell\nsudo cp -f /etc/hoarder/hoarder.env /etc/karakeep/karakeep.env\n```\n\n**7. Start Karakeep**\n```shell\nsudo systemctl enable --now karakeep.target\n```\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/02-installation/04-kubernetes.md",
    "content": "# Kubernetes\n\n### Requirements\n\n- A kubernetes cluster\n- kubectl\n- kustomize\n\n### 1. Get the deployment manifests\n\nYou can clone the repository and copy the `/kubernetes` directory into another directory of your choice.\n\n### 2. Populate the environment variables and secrets\n\nTo configure the app, copy the `.env_sample` to `.env` and change to your specific needs.\n\nYou should also change the `NEXTAUTH_URL` variable to point to your server address.\n\nUsing `KARAKEEP_VERSION=release` will pull the latest stable version. You might want to pin the version instead to control the upgrades (e.g. `KARAKEEP_VERSION=0.10.0`). Check the latest versions [here](https://github.com/karakeep-app/karakeep/pkgs/container/karakeep).\n\nTo see all available configuration options check the [documentation](https://docs.karakeep.app/configuration).\n\nTo configure the neccessary secrets for the application copy the `.secrets_sample` file to `.secrets` and change the sample secrets to your generated secrets.\n\n> Note: You **should** change the random strings. You can use `openssl rand -base64 36` to generate the random strings. \n\n### 3. Setup OpenAI\n\nTo enable automatic tagging, you'll need to configure OpenAI. This is optional though but highly recommended.\n\n- Follow [OpenAI's help](https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key) to get an API key.\n- Add the OpenAI API key to the `.env` file:\n\n```\nOPENAI_API_KEY=<key>\n```\n\nLearn more about the costs of using openai [here](/openai).\n\n<details>\n    <summary>[EXPERIMENTAL] If you want to use Ollama (https://ollama.com/) instead for local inference.</summary>\n\n    **Note:** The quality of the tags you'll get will depend on the quality of the model you choose. Running local models is a recent addition and not as battle tested as using openai, so proceed with care (and potentially expect a bunch of inference failures).\n\n    - Make sure ollama is running.\n    - Set the `OLLAMA_BASE_URL` env variable to the address of the ollama API.\n    - Set `INFERENCE_TEXT_MODEL` to the model you want to use for text inference in ollama (for example: `mistral`)\n    - Set `INFERENCE_IMAGE_MODEL` to the model you want to use for image inference in ollama (for example: `llava`)\n    - Make sure that you `ollama pull`-ed the models that you want to use.\n\n\n</details>\n\n### 4. Deploy the service\n\nDeploy the service by running:\n\n```\nmake deploy\n```\n\n### 5. Access the service\n\n#### via LoadBalancer IP\n\nBy default, these manifests expose the application as a LoadBalancer Service. You can run `kubectl get services` to identify the IP of the loadbalancer for your service.\n\nThen visit `http://<loadbalancer-ip>:3000` and you should be greated with the Sign In page.\n\n> Note: Depending on your setup you might want to expose the service via an Ingress, or have a different means to access it.\n\n#### Via Ingress\n\nIf you want to use an ingress, you can customize the sample ingress in the kubernetes folder and change the host to the DNS name of your choice.\n\nAfter that you have to configure the web service to the type ClusterIP so it is only reachable via the ingress.\n\nIf you have already deployed the service you can patch the web service to the type ClusterIP with the following command:\n\n` kubectl -n karakeep patch service web -p '{\"spec\":{\"type\":\"ClusterIP\"}}' `\n\nAfterwards you can apply the ingress and access the service via your chosen URL.\n\n#### Setting up HTTPS access to the Service\n\nTo access karakeep securely you can configure the ingress to use a preconfigured TLS certificate. This requires that you already have the needed files, namely your .crt and .key file, on hand.\n\nAfter you have deployed the karakeep manifests you can deploy your certificate for karakeep in the `karakeep` namespace with this example command. You can name the secret however you want. But be aware that the secret name in the ingress definition has to match the secret name.\n\n` $ kubectl --namespace karakeep create secret tls karakeep-web-tls --cert=/path/to/crt --key=/path/to/key `\n\nIf the secret is successfully created you can now configure the Ingress to use TLS via this changes to the spec:\n\n```` yaml\n spec:\n  tls:\n  - hosts:\n      - karakeep.example.com\n    secretName: karakeep-web-tls\n````\n\n> Note: Be aware that the hosts have to match between the tls spec and the HTTP spec.\n\n### [Optional] 6. Setup quick sharing extensions\n\nGo to the [quick sharing page](/quick-sharing) to install the mobile apps and the browser extensions. Those will help you hoard things faster!\n\n## Updating\n\nEdit the `KARAKEEP_VERSION` variable in the `kustomization.yaml` file and run `make clean deploy`.\n\nIf you have chosen `release` as the image tag you can also destroy the web pod, since the deployment has an ImagePullPolicy set to always the pod always pulls the image from the registry, this way we can ensure that the newest release image is pulled.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/02-installation/05-pikapods.md",
    "content": "# PikaPods [Paid Hosting]\n\n:::info\nNote: PikaPods shares some of its revenue from hosting Karakeep with the maintainer of this project.\n:::\n\n[PikaPods](https://www.pikapods.com/) offers managed paid hosting for many open source apps, including Karakeep.\nServer administration, updates, migrations and backups are all taken care of, which makes it well suited\nfor less technical users. As of Nov 2024, running Karakeep there will cost you ~$3 per month.\n\n### Requirements\n\n- A _PikaPods_ account. Can be created for free [here](https://www.pikapods.com/register). You get an initial welcome credit of $5.\n\n### 1. Choose app\n\nChoose _Karakeep_ from their [list of apps](https://www.pikapods.com/apps) or use this [direct link](https://www.pikapods.com/pods?run=hoarder). This will either\nopen a new dialog to add a new _Karakeep_ pod or ask you to log in.\n\n### 2. Add settings\n\nThere are a few settings to configure in the dialog:\n\n- **Basics**: Give the pod a name and choose a region that's near you.\n- **Env Vars**: Here you can disable signups or set an OpenAI API key. All settings are optional.\n- **Resources**: The resources your _Karakeep_ pod can use. The defaults are fine, unless you have a very large collection.\n\n### 3. Start pod and add user\n\nAfter hitting _Add pod_ it will take about a minute for the app to fully start. After this you can visit\nthe pod's URL and add an initial user under _Sign Up_. After this you may want to disable further sign-ups\nby setting the pod's `DISABLE_SIGNUPS` _Env Var_ to `true`.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/02-installation/06-debuntu.md",
    "content": "# Debian 12/Ubuntu 24.04\n\n:::warning\nThis script is a stripped-down version of those found in the [Proxmox Community Scripts](https://github.com/community-scripts/ProxmoxVE) repo. It has been adapted to work on baremetal Debian 12 or Ubuntu 24.04 installs **only**. Any other use is not supported and you use this script at your own risk.\n:::\n\n### Requirements\n\n- **Debian 12** (Buster) or\n- **Ubuntu 24.04** (Noble Numbat)\n\nThe script will download and install all dependencies (except for Ollama), install Karakeep, do a basic configuration of Karakeep and Meilisearch (the search app used by Karakeep), and create and enable the systemd service files needed to run Karakeep on startup. Karakeep and Meilisearch are run in the context of their low-privilege user environments for more security.\n\nThe script functions as an update script in addition to an installer. See **[Updating](#updating)**.\n\n### 1. Download the script from the [Karakeep repository](https://github.com/karakeep-app/karakeep/blob/main/karakeep-linux.sh)\n\n```\nwget https://raw.githubusercontent.com/karakeep-app/karakeep/main/karakeep-linux.sh\n```\n\n### 2. Run the script\n\n> This script must be run as `root`, or as a user with `sudo` privileges.\n\n    If this is a fresh install, then run the installer by using the following command:\n\n    ```shell\n    bash karakeep-linux.sh install\n    ```\n\n### 3. Create an account/sign in\n\n    Then visit `http://localhost:3000` and you should be greated with the Sign In page.\n\n## Updating\n\n> This script must be run as `root`, or as a user with `sudo` privileges.\n\n    If Karakeep has previously been installed using this script, then run the updater like so:\n\n    ```shell\n     bash karakeep-linux.sh update\n    ```\n\n## Services and Ports\n\n`karakeep.target` includes 4 services: `meilisearch.service`, `karakeep-web.service`, `karakeep-workers.service`, `karakeep-browser.service`.\n\n- `meilisearch.service`: Provides full-text search, Karakeep Workers service connects to it, uses port `7700` by default.\n\n- `karakeep-web.service`: Provides the karakeep web service, uses `3000` port by default.\n\n- `karakeep-workers.service`: Provides the karakeep workers service, no port.\n\n- `karakeep-browser.service`: Provides the headless browser service, uses `9222` port by default.\n\n## Configuration, ENV file, database locations\n\nDuring installation, the script created a configuration file for `meilisearch`, an `ENV` file for Karakeep, and located config paths and database paths separate from the installation path of Karakeep, so as to allow for easier updating. Their names/locations are as follows:\n\n- `/etc/meilisearch.toml` - a basic configuration for meilisearch, that contains configs for the database location, disabling analytics, and using a master key, which prevents unauthorized connections.\n- `/var/lib/meilisearch` - Meilisearch DB location.\n- `/etc/karakeep/karakeep.env` - The Karakeep `ENV` file. Edit this file to configure Karakeep beyond the default. The web service and the workers service need to be restarted after editing this file:\n\n    ```shell\n    sudo systemctl restart karakeep-workers karakeep-web\n    ```\n\n- `/var/lib/karakeep` - The Karakeep database location. If you delete the contents of this folder you will lose all your data.\n\n## Still Running Hoarder?\n\nThere is a way to upgrade. Please see [Guides > Hoarder to Karakeep Migration](https://docs.karakeep.app/Guides/hoarder-to-karakeep-migration)\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/02-installation/07-minimal-install.md",
    "content": "# Minimal Installation\n\n:::warning\nUnless necessary, prefer the [full installation](/installation/docker) to leverage all the features of Karakeep. You'll be sacrificing a lot of functionality if you go with the minimal installation route.\n:::\n\nKarakeep's default installation has a dependency on Meilisearch for the full text search, Chrome for crawling and OpenAI/Ollama for AI tagging. You can however run Karakeep without those dependencies if you're willing to sacrifice those features.\n\n- If you run without meilisearch, the search functionality will be completely disabled.\n- If you run without chrome, crawling will still work, but you'll lose ability to take screenshots of websites and websites with javascript content won't get crawled correctly.\n- If you don't setup OpenAI/Ollama, AI tagging will be disabled.\n\nThose features are important for leveraging Karakeep's full potential, but if you're running in constrained environments, you can use the following minimal docker compose to skip all those dependencies:\n\n```yaml\nservices:\n  web:\n    image: ghcr.io/karakeep-app/karakeep:release\n    restart: unless-stopped\n    volumes:\n      - data:/data\n    ports:\n      - 3000:3000\n    environment:\n      DATA_DIR: /data\n      NEXTAUTH_SECRET: super_random_string\nvolumes:\n  data:\n```\n\nOr just with the following docker command:\n\n```base\ndocker run -d \\\n  --restart unless-stopped \\\n  -v data:/data \\\n  -p 3000:3000 \\\n  -e DATA_DIR=/data \\\n  -e NEXTAUTH_SECRET=super_random_string \\\n  ghcr.io/karakeep-app/karakeep:release\n```\n\n:::warning\nYou **MUST** change the `super_random_string` to a true random string which you can generate with `openssl rand -hex 32`.\n:::\n\nCheck the [configuration docs](/configuration) for extra features to enable such as full page archival, full page screenshots, inference languages, etc.\n\n\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/02-installation/08-truenas.md",
    "content": "# TrueNAS\n\nKarakeep is available directly from TrueNAS's app catalog ([link](https://apps.truenas.com/catalog/karakeep/)).\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/02-installation/_category_.json",
    "content": "{\n    \"label\": \"Installation\",\n}\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/03-configuration.md",
    "content": "# Configuration\n\nThe app is mainly configured by environment variables. All the used environment variables are listed in [packages/shared/config.ts](https://github.com/karakeep-app/karakeep/blob/main/packages/shared/config.ts). The most important ones are:\n\n| Name                            | Required                              | Default         | Description                                                                                                                                                                                                                                                                                                             |\n| ------------------------------- | ------------------------------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| PORT                            | No                                    | 3000            | The port on which the web server will listen. DON'T CHANGE THIS IF YOU'RE USING DOCKER, instead changed the docker bound external port.                                                                                                                                                                                 |\n| WORKERS_PORT                    | No                                    | 0 (Random Port) | The port on which the worker will export its prometheus metrics on `/metrics`. By default it's a random unused port. If you want to utilize those metrics, fix the port to a value (and export it in docker if you're using docker).                                                                                    |\n| WORKERS_HOST                    | No                                    | 127.0.0.1       | Host to listen to for requests to WORKERS_PORT. You will need to set this if running in a container, since localhost will not be reachable from outside                                                                                                                                                                 |\n| WORKERS_ENABLED_WORKERS         | No                                    | Not set         | Comma separated list of worker names to enable. If set, only these workers will run. Valid values: crawler,inference,search,adminMaintenance,video,feed,assetPreprocessing,webhook,ruleEngine.                                                                                                                          |\n| WORKERS_DISABLED_WORKERS        | No                                    | Not set         | Comma separated list of worker names to disable. Takes precedence over `WORKERS_ENABLED_WORKERS`.                                                                                                                                                                                                                       |\n| DATA_DIR                        | Yes                                   | Not set         | The path for the persistent data directory. This is where the db lives. Assets are stored here by default unless `ASSETS_DIR` is set.                                                                                                                                                                                   |\n| ASSETS_DIR                      | No                                    | Not set         | The path where crawled assets will be stored. If not set, defaults to `${DATA_DIR}/assets`.                                                                                                                                                                                                                             |\n| NEXTAUTH_URL                    | Yes                                   | Not set         | Should point to the address of your server. The app will function without it, but will redirect you to wrong addresses on signout for example.                                                                                                                                                                          |\n| NEXTAUTH_SECRET                 | Yes                                   | Not set         | Random string used to sign the JWT tokens. Generate one with `openssl rand -base64 36`.                                                                                                                                                                                                                                 |\n| MEILI_ADDR                      | No                                    | Not set         | The address of meilisearch. If not set, Search will be disabled. E.g. (`http://meilisearch:7700`)                                                                                                                                                                                                                       |\n| MEILI_MASTER_KEY                | Only in Prod and if search is enabled | Not set         | The master key configured for meilisearch. Not needed in development environment. Generate one with `openssl rand -base64 36 \\| tr -dc 'A-Za-z0-9'`                                                                                                                                                                     |\n| MAX_ASSET_SIZE_MB               | No                                    | 50              | Sets the maximum allowed asset size (in MB) to be uploaded                                                                                                                                                                                                                                                              |\n| DISABLE_NEW_RELEASE_CHECK       | No                                    | false           | If set to true, latest release check will be disabled in the admin panel.                                                                                                                                                                                                                                               |\n| PROMETHEUS_AUTH_TOKEN           | No                                    | Random          | Enable a prometheus metrics endpoint at `/api/metrics`. This endpoint will require this token being passed in the Authorization header as a Bearer token. If not set, a new random token is generated everytime at startup. This cannot contain any special characters or you may encounter a 400 Bad Request response. |\n| RATE_LIMITING_ENABLED           | No                                    | false           | If set to true, API rate limiting will be enabled.                                                                                                                                                                                                                                                                      |\n| DB_WAL_MODE                     | No                                    | false           | Enables WAL mode for the sqlite database. This should improve the performance of the database. There's no reason why you shouldn't set this to true unless you're running the db on a network attached drive. This will become the default at some time in the future.                                                  |\n| SEARCH_NUM_WORKERS              | No                                    | 1               | Number of concurrent workers for search indexing tasks. Increase this if you have a high volume of content being indexed for search.                                                                                                    |\n| SEARCH_JOB_TIMEOUT_SEC          | No                                    | 30              | How long to wait for a search indexing job to finish before timing out. Increase this if you have large bookmarks with extensive content that takes longer to index.                                                                    |\n| WEBHOOK_NUM_WORKERS             | No                                    | 1               | Number of concurrent workers for webhook delivery. Increase this if you have multiple webhook endpoints or high webhook traffic.                                                                                                        |\n| ASSET_PREPROCESSING_NUM_WORKERS | No                                    | 1               | Number of concurrent workers for asset preprocessing tasks (image processing, OCR, etc.). Increase this if you have many images or documents that need processing.                                                                                                                                                      |\n| RULE_ENGINE_NUM_WORKERS         | No                                    | 1               | Number of concurrent workers for rule engine processing. Increase this if you have complex automation rules that need to be processed quickly.                                                                                                                                                                          |\n\n## Asset Storage\n\nKarakeep supports two storage backends for assets: local filesystem (default) and S3-compatible object storage. S3 storage is automatically detected when an S3 endpoint is passed.\n\n| Name                             | Required          | Default | Description                                                                                               |\n| -------------------------------- | ----------------- | ------- | --------------------------------------------------------------------------------------------------------- |\n| ASSET_STORE_S3_ENDPOINT          | No                | Not set | The S3 endpoint URL. Required for S3-compatible services like MinIO. **Setting this enables S3 storage**. |\n| ASSET_STORE_S3_REGION            | No                | Not set | The S3 region to use.                                                                                     |\n| ASSET_STORE_S3_BUCKET            | Yes when using S3 | Not set | The S3 bucket name where assets will be stored.                                                           |\n| ASSET_STORE_S3_ACCESS_KEY_ID     | Yes when using S3 | Not set | The S3 access key ID for authentication.                                                                  |\n| ASSET_STORE_S3_SECRET_ACCESS_KEY | Yes when using S3 | Not set | The S3 secret access key for authentication.                                                              |\n| ASSET_STORE_S3_FORCE_PATH_STYLE  | No                | false   | Whether to force path-style URLs for S3 requests. Set to true for MinIO and other S3-compatible services. |\n\n:::info\nWhen using S3 storage, make sure the bucket exists and the provided credentials have the necessary permissions to read, write, and delete objects in the bucket.\n:::\n\n:::warning\nSwitching between storage backends after data has been stored will require manual migration of existing assets. Plan your storage backend choice carefully before deploying.\n:::\n\n## Authentication / Signup\n\nBy default, Karakeep uses the database to store users, but it is possible to also use OAuth.\nThe flags need to be provided to the `web` container.\n\n:::info\nOnly OIDC compliant OAuth providers are supported! For information on how to set it up, consult the documentation of your provider.\n:::\n\n:::info\nWhen setting up OAuth, the allowed redirect URLs configured at the provider should be set to `<KARAKEEP_ADDRESS>/api/auth/callback/custom` where `<KARAKEEP_ADDRESS>` is the address you configured in `NEXTAUTH_URL` (for example: `https://try.karakeep.app/api/auth/callback/custom`).\n:::\n\n| Name                                        | Required | Default                | Description                                                                                                                                                                                           |\n| ------------------------------------------- | -------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| DISABLE_SIGNUPS                             | No       | false                  | If enabled, no new signups will be allowed and the signup button will be disabled in the UI                                                                                                           |\n| DISABLE_PASSWORD_AUTH                       | No       | false                  | If enabled, only signups and logins using OAuth are allowed and the signup button and login form for local accounts will be disabled in the UI                                                        |\n| EMAIL_VERIFICATION_REQUIRED                 | No       | false                  | Whether email verification is required during user signup. If enabled, users must verify their email address before they can use their account. If you enable this, you must configure SMTP settings. |\n| OAUTH_WELLKNOWN_URL                         | No       | Not set                | The \"wellknown Url\" for openid-configuration as provided by the OAuth provider                                                                                                                        |\n| OAUTH_CLIENT_SECRET                         | No       | Not set                | The \"Client Secret\" as provided by the OAuth provider                                                                                                                                                 |\n| OAUTH_CLIENT_ID                             | No       | Not set                | The \"Client ID\" as provided by the OAuth provider                                                                                                                                                     |\n| OAUTH_SCOPE                                 | No       | \"openid email profile\" | \"Full list of scopes to request (space delimited)\"                                                                                                                                                    |\n| OAUTH_PROVIDER_NAME                         | No       | \"Custom Provider\"      | The name of your provider. Will be shown on the signup page as \"Sign in with `<name>`\"                                                                                                                |\n| OAUTH_ALLOW_DANGEROUS_EMAIL_ACCOUNT_LINKING | No       | false                  | Whether existing accounts in karakeep stored in the database should automatically be linked with your OAuth account. Only enable it if you trust the OAuth provider!                                  |\n| OAUTH_TIMEOUT                               | No       | 3500                   | The wait time in milliseconds for the OAuth provider response. Increase this if you are having `outgoing request timed out` errors                                                                    |\n\nFor more information on `OAUTH_ALLOW_DANGEROUS_EMAIL_ACCOUNT_LINKING`, check the [next-auth.js documentation](https://next-auth.js.org/configuration/providers/oauth#allowdangerousemailaccountlinking-option).\n\n## Inference Configs (For automatic tagging)\n\nEither `OPENAI_API_KEY` or `OLLAMA_BASE_URL` need to be set for automatic tagging to be enabled. Otherwise, automatic tagging will be skipped.\n\n:::warning\n\n- The quality of the tags you'll get will depend on the quality of the model you choose.\n- You might want to tune the `INFERENCE_CONTEXT_LENGTH` as the default is quite small. The larger the value, the better the quality of the tags, but the more expensive the inference will be (money-wise on OpenAI and resource-wise on ollama).\n  :::\n\n| Name                                 | Required | Default                | Description                                                                                                                                                                                                                                                                                                                                                                           |\n| ------------------------------------ | -------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| OPENAI_API_KEY                       | No       | Not set                | The OpenAI key used for automatic tagging. More on that in [here](/openai).                                                                                                                                                                                                                                                                                                           |\n| OPENAI_BASE_URL                      | No       | Not set                | If you just want to use OpenAI you don't need to pass this variable. If, however, you want to use some other openai compatible API (e.g. azure openai service), set this to the url of the API.                                                                                                                                                                                       |\n| OLLAMA_BASE_URL                      | No       | Not set                | If you want to use ollama for local inference, set the address of ollama API here.                                                                                                                                                                                                                                                                                                    |\n| OLLAMA_KEEP_ALIVE                    | No       | Not set                | Controls how long the model will stay loaded into memory following the request (example value: \"5m\").                                                                                                                                                                                                                                                                                 |\n| INFERENCE_TEXT_MODEL                 | No       | gpt-4.1-mini           | The model to use for text inference. You'll need to change this to some other model if you're using ollama.                                                                                                                                                                                                                                                                           |\n| INFERENCE_IMAGE_MODEL                | No       | gpt-4o-mini            | The model to use for image inference. You'll need to change this to some other model if you're using ollama and that model needs to support vision APIs (e.g. llava).                                                                                                                                                                                                                 |\n| EMBEDDING_TEXT_MODEL                 | No       | text-embedding-3-small | The model to be used for generating embeddings for the text.                                                                                                                                                                                                                                                                                                                          |\n| INFERENCE_CONTEXT_LENGTH             | No       | 2048                   | The max number of tokens that we'll pass to the inference model. If your content is larger than this size, it'll be truncated to fit. The larger this value, the more of the content will be used in tag inference, but the more expensive the inference will be (money-wise on openAI and resource-wise on ollama). Check the model you're using for its max supported content size. |\n| INFERENCE_MAX_OUTPUT_TOKENS          | No       | 2048                   | The maximum number of tokens that the inference model is allowed to generate in its response. This controls the length of AI-generated content like tags and summaries. Increase this if you need longer responses, but be aware that higher values will increase costs (for OpenAI) and processing time.                                                                             |\n| INFERENCE_USE_MAX_COMPLETION_TOKENS  | No       | false                  | \\[OpenAI Only\\] Whether to use the newer `max_completion_tokens` parameter instead of the deprecated `max_tokens` parameter. Set to `true` if using GPT-5 or o-series models which require this. Will become the default in a future release.                                                                                                                                         |\n| INFERENCE_LANG                       | No       | english                | The language in which the tags will be generated.                                                                                                                                                                                                                                                                                                                                     |\n| INFERENCE_NUM_WORKERS                | No       | 1                      | Number of concurrent workers for AI inference tasks (tagging and summarization). Increase this if you have multiple AI inference requests and want to process them in parallel.                                                                                                                                                                                                       |\n| INFERENCE_ENABLE_AUTO_TAGGING        | No       | true                   | Whether automatic AI tagging is enabled or disabled.                                                                                                                                                                                                                                                                                                                                  |\n| INFERENCE_ENABLE_AUTO_SUMMARIZATION  | No       | false                  | Whether automatic AI summarization is enabled or disabled.                                                                                                                                                                                                                                                                                                                            |\n| INFERENCE_JOB_TIMEOUT_SEC            | No       | 30                     | How long to wait for the inference job to finish before timing out. If you're running ollama without powerful GPUs, you might want to increase the timeout a bit.                                                                                                                                                                                                                     |\n| INFERENCE_FETCH_TIMEOUT_SEC          | No       | 300                    | \\[Ollama Only\\] The timeout of the fetch request to the ollama server. If your inference requests take longer than the default 5mins, you might want to increase this timeout.                                                                                                                                                                                                        |\n| INFERENCE_SUPPORTS_STRUCTURED_OUTPUT | No       | Not set                | \\[DEPRECATED\\] Whether the inference model supports structured output or not. Use INFERENCE_OUTPUT_SCHEMA instead. Setting this to true translates to INFERENCE_OUTPUT_SCHEMA=structured, and to false translates to INFERENCE_OUTPUT_SCHEMA=plain.                                                                                                                                   |\n| INFERENCE_OUTPUT_SCHEMA              | No       | structured             | Possible values are \"structured\", \"json\", \"plain\". Structured is the preferred option, but if your model doesn't support it, you can use \"json\" if your model supports JSON mode, otherwise \"plain\" which should be supported by all the models but the model might not output the data in the correct format.                                                                        |\n\n:::info\n\n- You can append additional instructions to the prompt used for automatic tagging, in the `AI Settings` (in the `User Settings` screen)\n- You can use the placeholders `$tags`, `$aiTags`, `$userTags` in the prompt. These placeholders will be replaced with all tags, ai generated tags or human created tags when automatic tagging is performed (e.g. `[karakeep, computer, ai]`)\n  :::\n\n## Crawler Configs\n\n| Name                                     | Required | Default   | Description                                                                                                                                                                                                                                                                                                                                                                   |\n| ---------------------------------------- | -------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| CRAWLER_NUM_WORKERS                      | No       | 1         | Number of allowed concurrent crawling jobs. By default, we're only doing one crawling request at a time to avoid consuming a lot of resources.                                                                                                                                                                                                                                |\n| BROWSER_WEB_URL                          | No       | Not set   | The browser's http debugging address. The worker will talk to this endpoint to resolve the debugging console's websocket address. If you already have the websocket address, use `BROWSER_WEBSOCKET_URL` instead. If neither `BROWSER_WEB_URL` nor `BROWSER_WEBSOCKET_URL` are set, the worker will use plain http requests skipping screenshotting and javascript execution. |\n| BROWSER_WEBSOCKET_URL                    | No       | Not set   | The websocket address of browser's debugging console. If you want to use [browserless](https://browserless.io), use their websocket address here. If neither `BROWSER_WEB_URL` nor `BROWSER_WEBSOCKET_URL` are set, the worker will use plain http requests skipping screenshotting and javascript execution.                                                                 |\n| BROWSER_CONNECT_ONDEMAND                 | No       | false     | If set to false, the crawler will proactively connect to the browser instance and always maintain an active connection. If set to true, the browser will be launched on demand only whenever a crawling is requested. Set to true if you're using a service that provides you with browser instances on demand.                                                               |\n| CRAWLER_DOWNLOAD_BANNER_IMAGE            | No       | true      | Whether to cache the banner image used in the cards locally or fetch it each time directly from the website. Caching it consumes more storage space, but is more resilient against link rot and rate limits from websites.                                                                                                                                                    |\n| CRAWLER_STORE_SCREENSHOT                 | No       | true      | Whether to store a screenshot from the crawled website or not. Screenshots act as a fallback for when we fail to extract an image from a website. You can also view the stored screenshots for any link.                                                                                                                                                                      |\n| CRAWLER_FULL_PAGE_SCREENSHOT             | No       | false     | Whether to store a screenshot of the full page or not. Disabled by default, as it can lead to much higher disk usage. If disabled, the screenshot will only include the visible part of the page                                                                                                                                                                              |\n| CRAWLER_SCREENSHOT_TIMEOUT_SEC           | No       | 5         | How long to wait for the screenshot finish before timing out. If you are capturing full-page screenshots of long webpages, consider increasing this value.                                                                                                                                                                                                                    |\n| CRAWLER_FULL_PAGE_ARCHIVE                | No       | false     | Whether to store a full local copy of the page or not. Disabled by default, as it can lead to much higher disk usage. If disabled, only the readable text of the page is archived.                                                                                                                                                                                            |\n| CRAWLER_JOB_TIMEOUT_SEC                  | No       | 60        | How long to wait for the crawler job to finish before timing out. If you have a slow internet connection or a low powered device, you might want to bump this up a bit                                                                                                                                                                                                        |\n| CRAWLER_NAVIGATE_TIMEOUT_SEC             | No       | 30        | How long to spend navigating to the page (along with its redirects). Increase this if you have a slow internet connection                                                                                                                                                                                                                                                     |\n| CRAWLER_VIDEO_DOWNLOAD                   | No       | false     | Whether to download videos from the page or not (using yt-dlp)                                                                                                                                                                                                                                                                                                                |\n| CRAWLER_VIDEO_DOWNLOAD_MAX_SIZE          | No       | 50        | The maximum file size for the downloaded video. The quality will be chosen accordingly. Use -1 to disable the limit.                                                                                                                                                                                                                                                          |\n| CRAWLER_VIDEO_DOWNLOAD_TIMEOUT_SEC       | No       | 600       | How long to wait for the video download to finish                                                                                                                                                                                                                                                                                                                             |\n| CRAWLER_ENABLE_ADBLOCKER                 | No       | true      | Whether to enable an adblocker in the crawler or not. If you're facing troubles downloading the adblocking lists on worker startup, you can disable this.                                                                                                                                                                                                                     |\n| CRAWLER_YTDLP_ARGS                       | No       | []        | Include additional yt-dlp arguments to be passed at crawl time separated by %%: https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#general-options                                                                                                                                                                                                                           |\n| BROWSER_COOKIE_PATH                      | No       | Not set   | Path to a JSON file containing cookies to be loaded into the browser context. The file should be an array of cookie objects, each with name and value (required), and optional fields like domain, path, expires, httpOnly, secure, and sameSite (e.g., `[{\"name\": \"session\", \"value\": \"xxx\", \"domain\": \".example.com\"}`]).                                                   |\n| HTML_CONTENT_SIZE_INLINE_THRESHOLD_BYTES | No       | 5 \\* 1024 | The thresholds in bytes after which larger assets will be stored in the assetdb (folder/s3) instead of inline in the database.                                                                                                                                                                                                                                                |\n\n<details>\n\n  <summary>More info on BROWSER_COOKIE_PATH</summary>\n\nBROWSER_COOKIE_PATH specifies the path to a JSON file containing cookies to be loaded into the browser context for crawling.\n\nThe JSON file must be an array of cookie objects, each with:\n\n- name: The cookie name (required).\n- value: The cookie value (required).\n- Optional fields: domain, path, expires, httpOnly, secure, sameSite (values: \"Strict\", \"Lax\", or \"None\").\n\nExample JSON file:\n\n```json\n[\n  {\n    \"name\": \"session\",\n    \"value\": \"xxx\",\n    \"domain\": \".example.com\",\n    \"path\": \"/\",\n    \"expires\": 1735689600,\n    \"httpOnly\": true,\n    \"secure\": true,\n    \"sameSite\": \"Lax\"\n  }\n]\n```\n\n</details>\n\n## OCR Configs\n\nKarakeep uses [tesseract.js](https://github.com/naptha/tesseract.js) to extract text from images.\n\n| Name                     | Required | Default   | Description                                                                                                                                                                                                                               |\n| ------------------------ | -------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| OCR_CACHE_DIR            | No       | $TEMP_DIR | The dir where tesseract will download its models. By default, those models are not persisted and stored in the OS' temp dir.                                                                                                              |\n| OCR_LANGS                | No       | eng       | Comma separated list of the language codes that you want tesseract to support. You can find the language codes [here](https://tesseract-ocr.github.io/tessdoc/Data-Files-in-different-versions.html). Set to empty string to disable OCR. |\n| OCR_CONFIDENCE_THRESHOLD | No       | 50        | A number between 0 and 100 indicating the minimum acceptable confidence from tessaract. If tessaract's confidence is lower than this value, extracted text won't be stored.                                                               |\n\n## Webhook Configs\n\nYou can use webhooks to trigger actions when bookmarks are created, changed or crawled.\n\n| Name                | Required | Default | Description                                       |\n| ------------------- | -------- | ------- | ------------------------------------------------- |\n| WEBHOOK_TIMEOUT_SEC | No       | 5       | The timeout for the webhook request in seconds.   |\n| WEBHOOK_RETRY_TIMES | No       | 3       | The number of times to retry the webhook request. |\n\n:::info\n\n- The WEBHOOK_TOKEN is used for authentication. It will appear in the Authorization header as Bearer token.\n  ```\n  Authorization: Bearer <WEBHOOK_TOKEN>\n  ```\n- The webhook will be triggered with the job id (used for idempotence), bookmark id, bookmark type, the user id, the url and the operation in JSON format in the body.\n\n  ```json\n  {\n    \"jobId\": \"123\",\n    \"type\": \"link\",\n    \"bookmarkId\": \"exampleBookmarkId\",\n    \"userId\": \"exampleUserId\",\n    \"url\": \"https://example.com\",\n    \"operation\": \"crawled\"\n  }\n  ```\n\n  :::\n\n## SMTP Configuration\n\nKarakeep can send emails for various purposes such as email verification during signup. Configure these settings to enable email functionality.\n\n| Name          | Required | Default | Description                                                                                     |\n| ------------- | -------- | ------- | ----------------------------------------------------------------------------------------------- |\n| SMTP_HOST     | No       | Not set | The SMTP server hostname or IP address. Required if you want to enable email functionality.     |\n| SMTP_PORT     | No       | 587     | The SMTP server port. Common values are 587 (STARTTLS), 465 (SSL/TLS), or 25 (unencrypted).     |\n| SMTP_SECURE   | No       | false   | Whether to use SSL/TLS encryption. Set to true for port 465, false for port 587 with STARTTLS.  |\n| SMTP_USER     | No       | Not set | The username for SMTP authentication. Usually your email address.                               |\n| SMTP_PASSWORD | No       | Not set | The password for SMTP authentication. For services like Gmail, use an app-specific password.    |\n| SMTP_FROM     | No       | Not set | The \"from\" email address that will appear in sent emails. This should be a valid email address. |\n\n## Proxy Configuration\n\nIf your Karakeep instance needs to connect through a proxy server, you can configure the following settings:\n\n| Name                               | Required | Default | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |\n| ---------------------------------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| CRAWLER_HTTP_PROXY                 | No       | Not set | HTTP proxy server URL for outgoing HTTP requests (e.g., `http://proxy.example.com:8080`). You can pass multiple comma separated proxies and the used one will be chosen at random. The proxy is used for crawling, RSS feed fetches and webhooks.                                                                                                                                                                                                                                       |\n| CRAWLER_HTTPS_PROXY                | No       | Not set | HTTPS proxy server URL for outgoing HTTPS requests (e.g., `http://proxy.example.com:8080`). You can pass multiple comma separated proxies and the used one will be chosen at random. The proxy is used for crawling, RSS feed fetches and webhooks.                                                                                                                                                                                                                                     |\n| CRAWLER_NO_PROXY                   | No       | Not set | Comma-separated list of hostnames/IPs that should bypass the proxy (e.g., `localhost,127.0.0.1,.local`)                                                                                                                                                                                                                                                                                                                                                                                 |\n| CRAWLER_ALLOWED_INTERNAL_HOSTNAMES | No       | Not set | By default, Karakeep blocks worker-initiated requests whose DNS resolves to private, loopback, or link-local IP addresses. Use this to allowlist specific hostnames for internal access (e.g., `internal.company.com,.local`). Supports domain wildcards by prefixing with a dot (e.g., `.internal.company.com`). Note: Internal IP validation is bypassed when a proxy is configured for the URL as the local DNS resolver won't necessarily be the same as the one used by the proxy. |\n\n:::info\nThese proxy settings will be used by the crawler and other components that make outgoing HTTP requests.\n:::\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/04-screenshots.md",
    "content": "# Screenshots\n\n## Homepage\n\n![Homepage](/img/screenshots/homepage.png)\n\n## Homepage (Dark Mode)\n\n![Homepage](/img/screenshots/homepage-dark.png)\n\n## Tags\n\n![All Tags](/img/screenshots/all-tags.png)\n\n## Lists\n\n![All Lists](/img/screenshots/all-lists.png)\n\n## Bookmark Preview\n\n![Bookmark Preview](/img/screenshots/bookmark-preview.png)\n\n## Settings\n\n![Settings](/img/screenshots/settings.png)\n\n## Admin Panel\n\n![Ammin](/img/screenshots/admin.png)\n\n\n## iOS Sharing\n\n<img src=\"/img/screenshots/share-sheet.png\" width=\"400px\"  />\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/05-quick-sharing.md",
    "content": "# Quick Sharing Extensions\n\nThe whole point of Karakeep is making it easy to hoard the content. That's why there are a couple of \n\n## Mobile Apps\n\n<img src=\"/img/quick-sharing/mobile.png\" alt=\"mobile screenshot\" width=\"300\"/>\n\n\n- **iOS app**: [App Store Link](https://apps.apple.com/us/app/karakeep-app/id6479258022).\n- **Android App**: [Play Store link](https://play.google.com/store/apps/details?id=app.hoarder.hoardermobile&pcampaignid=web_share).\n\n## Browser Extensions\n\n<img src=\"/img/quick-sharing/extension.png\" alt=\"mobile screenshot\" width=\"300\"/>\n\n- **Chrome extension**: [here](https://chromewebstore.google.com/detail/karakeep/kgcjekpmcjjogibpjebkhaanilehneje).\n- **Firefox addon**: [here](https://addons.mozilla.org/en-US/firefox/addon/karakeep/).\n\n- ## Community Extensions\n- **Safari extension**: [App Store Link](https://apps.apple.com/us/app/karakeeper-bookmarker/id6746722790).  For macOS and iOS to allow a simple way to add your bookmarks to your self hosted karakeep instance.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/06-openai.md",
    "content": "# OpenAI Costs\n\nThis service uses OpenAI for automatic tagging. This means that you'll incur some costs if automatic tagging is enabled. There are two type of inferences that we do:\n\n## Text Tagging\n\nFor text tagging, we use the `gpt-4.1-mini` model. This model is [extremely cheap](https://openai.com/api/pricing). Cost per inference varies depending on the content size per article. Though, roughly, You'll be able to generate tags for almost 3000+ bookmarks for less than $1.\n\n## Image Tagging\n\nFor image uploads, we use the `gpt-4o-mini` model for extracting tags from the image. You can learn more about the costs of using this model [here](https://platform.openai.com/docs/guides/images?api-mode=chat#calculating-costs). To lower the costs, we're using the low resolution mode (fixed number of tokens regardless of image size). You'll be able to run inference for 1000+ images for less than a $1.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/07-development/01-setup.md",
    "content": "# Setup\n\n## Quick Start\n\nFor the fastest way to get started with development, use the one-command setup script:\n\n```bash\n./start-dev.sh\n```\n\nThis script will automatically:\n- Start Meilisearch in Docker (on port 7700)\n- Start headless Chrome in Docker (on port 9222) \n- Install dependencies with `pnpm install` if needed\n- Start both the web app and workers in parallel\n- Provide cleanup when you stop with Ctrl+C\n\n**Prerequisites:**\n- Docker installed and running\n- pnpm installed (see manual setup below for installation instructions)\n\nThe script will output the running services:\n- Web app: http://localhost:3000\n- Meilisearch: http://localhost:7700  \n- Chrome debugger: http://localhost:9222\n\nPress Ctrl+C to stop all services and clean up Docker containers.\n\n## Manual Setup\n\nKarakeep uses `node` version 22. To install it, you can use `nvm` [^1]\n\n```\n$ nvm install  22\n```\n\nVerify node version using this command:\n```\n$ node --version\nv22.14.0\n```\n\nKarakeep also makes use of `corepack`[^2]. If you have `node` installed, then `corepack` should already be\ninstalled on your machine, and you don't need to do anything. To verify the `corepack` is installed run:\n\n```\n$ command -v corepack\n/home/<user>/.nvm/versions/node/v22.14.0/bin/corepack\n```\n\nTo enable `corepack` run the following command:\n\n```\n$ corepack enable\n```\n\nThen, from the root of the repository, install the packages and dependencies using:\n\n```\n$ pnpm install\n```\n\nOutput of a successful `pnpm install` run should look something like:\n\n```\nScope: all 20 workspace projects\nLockfile is up to date, resolution step is skipped\nPackages: +3129\n+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\nProgress: resolved 0, reused 2699, downloaded 0, added 3129, done\n\ndevDependencies:\n+ @karakeep/prettier-config 0.1.0 <- tooling/prettier\n\n. prepare$ husky\n└─ Done in 45ms\nDone in 5.5s\n```\n\nYou can now continue with the rest of this documentation.\n\n### First Setup\n\n- You'll need to prepare the environment variables for the dev env.\n- Easiest would be to set it up once in the root of the repo and then symlink it in each app directory (e.g. `/apps/web`, `/apps/workers`) and also `/packages/db`.\n- Start by copying the template by `cp .env.sample .env`.\n- The most important env variables to set are:\n  - `DATA_DIR`: Where the database and assets will be stored. This is the only required env variable. You can use an absolute path so that all apps point to the same dir.\n  - `NEXTAUTH_SECRET`: Random string used to sign the JWT tokens. Generate one with `openssl rand -base64 36`. Logging in will not work if this is missing!\n  - `MEILI_ADDR`: If not set, search will be disabled. You can set it to `http://127.0.0.1:7700` if you run meilisearch using the command below.\n  - `OPENAI_API_KEY`: If you want to enable auto tag inference in the dev env.\n- run `pnpm run db:migrate` in the root of the repo to set up the database.\n\n### Dependencies\n\n#### Meilisearch\n\nMeilisearch is the provider for the full text search (and at some point embeddings search too). You can get it running with `docker run -p 7700:7700 getmeili/meilisearch:v1.13.3`.\n\nMount persistent volume if you want to keep index data across restarts. You can trigger a re-index for the entire items collection in the admin panel in the web app.\n\n#### Chrome\n\nThe worker app will automatically start headless chrome on startup for crawling pages. You don't need to do anything there.\n\n### Web App\n\n- Run `pnpm web` in the root of the repo.\n- Go to `http://localhost:3000`.\n\n> NOTE: The web app kinda works without any dependencies. However, search won't work unless meilisearch is running. Also, new items added won't get crawled/indexed unless workers are running.\n\n### Workers\n\n- Run `pnpm workers` in the root of the repo.\n\n### Mobile App (iOS & Android)\n\n#### Prerequisites\n\nTo build and run the mobile app locally, you'll need:\n\n- **For iOS development**: \n  - macOS computer\n  - Xcode installed from the App Store\n  - iOS Simulator (comes with Xcode)\n\n- **For Android development**:\n  - Android Studio installed\n  - Android SDK configured\n  - Android Emulator or physical device\n\nFor detailed setup instructions, refer to the [Expo documentation](https://docs.expo.dev/guides/local-app-development/).\n\n#### Running the app\n\n- `cd apps/mobile`\n- `pnpm exec expo prebuild --no-install` to build the app.\n\n**For iOS:**\n- `pnpm exec expo run:ios`\n- The app will be installed and started in the simulator.\n\n**Troubleshooting iOS Setup:**\nIf you encounter an error like `xcrun: error: SDK \"iphoneos\" cannot be located`, you may need to set the correct Xcode developer directory:\n```bash\nsudo xcode-select -s /Applications/Xcode.app/Contents/Developer\n```\n\n**For Android:**\n- Start the Android emulator or connect a physical device.\n- `pnpm exec expo run:android`\n- The app will be installed and started on the emulator/device.\n\nChanging the code will hot reload the app. However, installing new packages requires restarting the expo server.\n\n### Browser Extension\n\n- `cd apps/browser-extension`\n- `pnpm dev`\n- This will generate a `dist` package\n- Go to extension settings in chrome and enable developer mode.\n- Press `Load unpacked` and point it to the `dist` directory.\n- The plugin will pop up in the plugin list.\n\nIn dev mode, opening and closing the plugin menu should reload the code.\n\n\n## Docker Dev Env\n\nIf the manual setup is too much hassle for you. You can use a docker based dev environment by running `docker compose -f docker/docker-compose.dev.yml up` in the root of the repo. This setup wasn't super reliable for me though.\n\n\n[^1]: [nvm](https://github.com/nvm-sh/nvm) is a node version manager. You can install it following [these\ninstructions](https://github.com/nvm-sh/nvm?tab=readme-ov-file#installing-and-updating).\n\n[^2]: [corepack](https://nodejs.org/api/corepack.html) is an experimental tool to help with managing versions of your\npackage managers.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/07-development/02-directories.md",
    "content": "# Directory Structure\n\n## Apps\n\n| Directory                | Description                                            |\n| ------------------------ | ------------------------------------------------------ |\n| `apps/web`               | The main web app                                       |\n| `apps/workers`           | The background workers logic                           |\n| `apps/mobile`            | The react native based mobile app                      |\n| `apps/browser-extension` | The browser extension                                  |\n| `apps/landing`           | The landing page of [karakeep.app](https://karakeep.app) |\n\n## Shared Packages\n\n| Directory         | Description                                                                  |\n| ----------------- | ---------------------------------------------------------------------------- |\n| `packages/db`     | The database schema and migrations                                           |\n| `packages/trpc`   | Where most of the business logic lies built as TRPC routes                   |\n| `packages/shared` | Some shared code between the different apps (e.g. loggers, configs, assetdb) |\n\n## Toolings\n\n| Directory            | Description             |\n| -------------------- | ----------------------- |\n| `tooling/typescript` | The shared tsconfigs    |\n| `tooling/eslint`     | ESlint configs          |\n| `tooling/prettier`   | Prettier configs        |\n| `tooling/tailwind`   | Shared tailwind configs |\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/07-development/03-database.md",
    "content": "# Database Migrations\n\n- The database schema lives in `packages/db/schema.ts`.\n- Changing the schema, requires a migration.\n- You can generate the migration by running `pnpm run db:generate --name description_of_schema_change` in the root dir.\n- You can then apply the migration by running `pnpm run db:migrate`.\n\n## Drizzle Studio\n\nYou can start the drizzle studio by running `pnpm run db:studio` in the root of the repo.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/07-development/04-architecture.md",
    "content": "# Architecture\n\n![Architecture Diagram](/img/architecture/arch.png)\n\n- Webapp: NextJS based using sqlite for data storage.\n- Workers: Consume the jobs from sqlite based job queue and executes them, there are three job types:\n  1. Crawling: Fetches the content of links using a headless chrome browser running in the workers container.\n  2. OpenAI: Uses OpenAI APIs to infer the tags of the content.\n  3. Indexing: Indexes the content in meilisearch for faster retrieval during search.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/07-development/_category_.json",
    "content": "{\n    \"label\": \"Development\",\n}\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/08-security-considerations.md",
    "content": "# Security Considerations\n\nIf you're going to give app access to untrusted users, there's some security considerations that you'll need to be aware of given how the crawler works. The crawler is basically running a browser to fetch the content of the bookmarks. Any untrusted user can submit bookmarks to be crawled from your server and they'll be able to see the crawling result. This can be abused in multiple ways:\n\n1. Untrusted users can submit crawl requests to websites that you don't want to be coming out of your IPs.\n2. Crawling user controlled websites can expose your origin IP (and location) even if your service is hosted behind cloudflare for example.\n3. The crawling requests will be coming out from your own network, which untrusted users can leverage to crawl internal non-internet exposed endpoints.\n\nTo mitigate those risks, you can do one of the following:\n\n1. Limit access to trusted users\n2. Let the browser traffic go through some VPN with restricted network policies.\n3. Host the browser container outside of your network.\n4. Use a hosted browser as a service (e.g. [browserless](https://browserless.io)). Note: I've never used them before.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/09-command-line.md",
    "content": "# Command Line Tool (CLI)\n\nKarakeep comes with a simple CLI for those users who want to do more advanced manipulation.\n\n## Features\n\n- Manipulate bookmarks, lists and tags\n- Mass import/export of bookmarks\n\n## Installation (NPM)\n\n```\nnpm install -g @karakeep/cli\n```\n\n\n## Installation (Docker)\n\n```\ndocker run --rm ghcr.io/karakeep-app/karakeep-cli:release --help\n```\n\n## Usage\n\n```\nkarakeep\n```\n\n```\nUsage: karakeep [options] [command]\n\nA CLI interface to interact with the karakeep api\n\nOptions:\n  --api-key <key>       the API key to interact with the API (env: KARAKEEP_API_KEY)\n  --server-addr <addr>  the address of the server to connect to (env: KARAKEEP_SERVER_ADDR)\n  -V, --version         output the version number\n  -h, --help            display help for command\n\nCommands:\n  bookmarks             manipulating bookmarks\n  lists                 manipulating lists\n  tags                  manipulating tags\n  whoami                returns info about the owner of this API key\n  help [command]        display help for command\n```\n\nAnd some of the subcommands:\n\n```\nkarakeep bookmarks\n```\n\n```\nUsage: karakeep bookmarks [options] [command]\n\nManipulating bookmarks\n\nOptions:\n  -h, --help             display help for command\n\nCommands:\n  add [options]          creates a new bookmark\n  get <id>               fetch information about a bookmark\n  update [options] <id>  updates bookmark\n  list [options]         list all bookmarks\n  delete <id>            delete a bookmark\n  help [command]         display help for command\n\n```\n\n```\nkarakeep lists\n```\n\n```\nUsage: karakeep lists [options] [command]\n\nManipulating lists\n\nOptions:\n  -h, --help                 display help for command\n\nCommands:\n  list                       lists all lists\n  delete <id>                deletes a list\n  add-bookmark [options]     add a bookmark to list\n  remove-bookmark [options]  remove a bookmark from list\n  help [command]             display help for command\n```\n\n## Obtaining an API Key\n\nTo use the CLI, you'll need to get an API key from your karakeep settings. You can validate that it's working by running:\n\n```\nkarakeep --api-key <key> --server-addr <addr> whoami\n```\n\nFor example:\n\n```\nkarakeep --api-key mysupersecretkey --server-addr https://try.karakeep.app whoami\n{\n  id: 'j29gnbzxxd01q74j2lu88tnb',\n  name: 'Test User',\n  email: 'test@gmail.com'\n}\n```\n\n\n## Other clients\n\nThere also exists a **non-official**, community-maintained, python package called [karakeep-python-api](https://github.com/thiswillbeyourgithub/karakeep_python_api) that can be accessed from the CLI, but is **not** official.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/09-mcp.md",
    "content": "# Model Context Protocol Server (MCP)\n\nKarakeep comes with a Model Context Protocol server that can be used to interact with it through LLMs.\n\n## Supported Tools\n\n- Searching bookmarks\n- Adding and removing bookmarks from lists\n- Attaching and detaching tags to bookmarks\n- Creating new lists\n- Creating text and URL bookmarks\n\n\n## Usage with Claude Desktop\n\nFrom NPM:\n\n```json\n{\n  \"mcpServers\": {\n    \"karakeep\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"@karakeep/mcp\"\n      ],\n      \"env\": {\n        \"KARAKEEP_API_ADDR\": \"https://<YOUR_SERVER_ADDR>\",\n        \"KARAKEEP_API_KEY\": \"<YOUR_TOKEN>\"\n      }\n    }\n  }\n}\n```\n\nFrom Docker:\n\n```json\n{\n  \"mcpServers\": {\n    \"karakeep\": {\n      \"command\": \"docker\",\n      \"args\": [\n        \"run\",\n        \"-e\",\n        \"KARAKEEP_API_ADDR=https://<YOUR_SERVER_ADDR>\",\n        \"-e\",\n        \"KARAKEEP_API_KEY=<YOUR_TOKEN>\",\n        \"ghcr.io/karakeep-app/karakeep-mcp:latest\"\n      ]\n    }\n  }\n}\n```\n\n\n### Demo\n\n#### Search\n![mcp-1](/img/mcp-1.gif)\n\n#### Adding Text Bookmarks\n![mcp-2](/img/mcp-2.gif)\n\n#### Adding URL Bookmarks\n![mcp-2](/img/mcp-3.gif)\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/10-import.md",
    "content": "# Importing Bookmarks\n\n\nKarakeep supports importing bookmarks using the Netscape HTML Format, Pocket's new CSV format & Omnivore's JSONs. Titles, tags and addition date will be preserved during the import. An automatically created list will contain all the imported bookmarks.\n\n:::info\nAll the URLs in the bookmarks file will be added automatically, you will not be able to pick and choose which bookmarks to import!\n:::\n\n## Import from Chrome\n\n- Open Chrome and go to `chrome://bookmarks`\n- Click on the three dots on the top right corner and choose `Export bookmarks`\n- This will download an html file with all of your bookmarks.\n- To import the bookmark file, go to the settings and click \"Import Bookmarks from HTML file\".\n\n## Import from Firefox\n- Open Firefox and click on the menu button (☰) in the top right corner.\n- Navigate to Bookmarks > Manage bookmarks (or press Ctrl + Shift + O / Cmd + Shift + O to open the Bookmarks Library).\n- In the Bookmarks Library, click the Import and Backup button at the top. Select Export Bookmarks to HTML... to save your bookmarks as an HTML file.\n- To import a bookmark file, go back to the Import and Backup menu, then select Import Bookmarks from HTML... and choose your saved HTML file.\n\n## Import from Pocket\n\n- Go to the [Pocket export page](https://getpocket.com/export) and follow the instructions to export your bookmarks.\n- Pocket after a couple of minutes will mail you a zip file with all the bookmarks.\n- Unzip the file and you'll get a CSV file.\n- To import the bookmark file, go to the settings and click \"Import Bookmarks from Pocket export\".\n\n## Import from Omnivore\n\n- Follow Omnivore's [documentation](https://docs.omnivore.app/using/exporting.html) to export your bookmarks.\n- This will give you a zip file with all your data.\n- The zip file contains a lot of JSONs in the format `metadata_*.json`. You can either import every JSON file manually, or merge the JSONs into a single JSON file and import that.\n- To  merge the JSONs into a single JSON file, you can use the following command in the unzipped directory: `jq -r '.[]' metadata_*.json | jq -s > omnivore.json` and then import the `omnivore.json` file. You'll need to have the [jq](https://github.com/jqlang/jq) tool installed.\n\n## Import using the CLI\n\n:::warning\nImporting bookmarks using the CLI requires some technical knowledge and might not be very straightforward for non-technical users. Don't hesitate to ask questions in github discussions or discord though.\n:::\n\nIf you can get your bookmarks in a text file with one link per line, you can use the following command to import them using the [karakeep cli](https://docs.karakeep.app/command-line):\n\n```\nwhile IFS= read -r url; do\n    karakeep --api-key \"<KEY>\" --server-addr \"<SERVER_ADDR>\" bookmarks add --link \"$url\"\ndone < all_links.txt\n```\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/11-FAQ.md",
    "content": "# Frequently Asked Questions (FAQ)\r\n\r\n## User Management\r\n\r\n### Lost password\r\n\r\n#### If you are not an administrator\r\n\r\nAdministrators can reset the password of any user. Contact an administrator to reset the password for you.\r\n\r\n- Navigate to the `Admin Settings` page\r\n- Find the user in the `Users List`\r\n- In the `Actions` column, there is a button to reset the password\r\n- Enter a new password and press `Reset`\r\n- The new password is now set\r\n- If required, you can change your password again (so the admin does not know your password) in the `User Settings`\r\n\r\n#### If you are an administrator\r\n\r\nIf you are an administrator and lost your password, you have to reset the password in the database.\r\n\r\nTo reset the password:\r\n\r\n- Acquire some kind of tools that helps you to connect to the database:\r\n  - `sqlite3` on Linux: run `apt-get install sqlite3` (depending on your package manager)\r\n  - e.g. `dbeaver` on Windows\r\n- Shut down Karakeep\r\n- Connect to the `db.db` database, which is located in the `data` directory you have mounted to your docker container:\r\n  - by e.g. running `sqlite3 db.db` (in your `data` directory)\r\n  - or going through e.g. the `dbeaver` UI to locate the file in the data directory and connecting to it\r\n- Update the password in the database by running:\r\n  - `update user set password='$2a$10$5u40XUq/cD/TmLdCOyZ82ePENE6hpkbodJhsp7.e/BgZssUO5DDTa', salt='' where email='<YOUR_EMAIL_HERE>';`\r\n  - (don't forget to put your email address into the command)\r\n- The new password for your user is now `adminadmin`.\r\n- Start Karakeep again\r\n- Log in with your email address and the password `adminadmin` and change the password to whatever you want in the `User Settings`\r\n\r\n### Adding another administrator\r\n\r\nBy default, the first user to sign up gets promoted to administrator automatically.\r\n\r\nIn case you want to grant those permissions to another user:\r\n\r\n- Navigate to the `Admin Settings` page\r\n- Find the user in the `Users List`\r\n- In the `Actions` column, there is a button to change the Role\r\n- Change the Role to `Admin`\r\n- Press `Change`\r\n- The new administrator has to log out and log in again to get the new role assigned\r\n\r\n### Adding new users, when signups are disabled\r\n\r\nAdministrators can create new accounts any time:\r\n\r\n- Navigate to the `Admin Settings` page\r\n- Go to the `Users List`\r\n- Press the `Create User` Button.\r\n- Enter the information for the user\r\n- Press `create`\r\n- The new user can now log in\r\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/12-troubleshooting.md",
    "content": "# Troubleshooting\n\n## SqliteError: no such table: user\n\nThis usually means that there's something wrong with the database setup (more concretely, it means that the database is not initialized). This can be caused by multiple problems:\n1. **Wiped DATA_DIR:** Your `DATA_DIR` got wiped (or the backing storage dir changed). If you did this intentionally, restart the container so that it can re-initalize the database.\n2. **Missing DATA_DIR**: You're not using the default docker compose file, and you forgot to configure the `DATA_DIR` env var. This will result into the database getting set up in a different directory than the one used by the service.\n\n## Chrome Failed to Read DnsConfig\n\nIf you see this error in the logs of the chrome container, it's a benign error and you can safely ignore it. Whatever problems you're having, is unrelated to this error.\n\n## AI Tagging not working (when using OpenAI)\n\nCheck the logs of the container and this will usually tell you what's wrong. Common problems are:\n1. Typo in the env variable `OPENAI_API_KEY` name resulting into logs saying something like \"skipping inference as it's not configured\".\n2. You forgot to call `docker compose up` after configuring open ai.\n3. OpenAI requires pre-charging the account with credits before using it, otherwise you'll get an error like \"insufficient funds\".\n\n## AI Tagging not working (when using Ollama)\n\nCheck the logs of the container and this will usually tell you what's wrong. Common problems are:\n1. Typo in the env variable `OLLAMA_BASE_URL` name resulting into logs saying something like \"skipping inference as it's not configured\".\n2. You forgot to call `docker compose up` after configuring ollama.\n3. You didn't change the `INFERENCE_TEXT_MODEL` env variable, resulting into karakeep attempting to use gpt models with ollama which won't work.\n4. Ollama server is not reachable by the karakeep container. This can be caused by:\n    1. Ollama server being in a different docker network than the karakeep container.\n    2. You're using `localhost` as the `OLLAMA_BASE_URL` instead of the actual address of the ollama server. `localhost` points to the container itself, not the docker host. Check this [stackoverflow answer](https://stackoverflow.com/questions/24319662/from-inside-of-a-docker-container-how-do-i-connect-to-the-localhost-of-the-mach) to find how to correctly point to the docker host address instead.\n\n## Crawling not working\n\nCheck the logs of the container and this will usually tell you what's wrong. Common problems are:\n1. You changed the name of the chrome container but didn't change the `BROWSER_WEB_URL` env variable.\n\n## Upgrading Meilisearch - Migrating the Meilisearch db version\n\n[Meilisearch](https://www.meilisearch.com/) is the database used by karakeep for searching in your bookmarks. The version used by karakeep is `1.13.3` and it is advised not to upgrade it without good reasons. If you do, you might see errors like `Your database version (1.11.1) is incompatible with your current engine version (1.13.3). To migrate data between Meilisearch versions, please follow our guide on https://www.meilisearch.com/docs/learn/update_and_migration/updating.`.\n\nLuckily we can easily workaround this:\n1. Stop the Meilisearch container.\n2. Inside the Meilisearch volume bound to `/meili_data`, erase/rename the folder called `data.ms`.\n3. Launch Meilisearch again.\n4. Login to karakeep as administrator and go to (as of v0.24.1) `Admin Settings > Background Jobs` then click on `Reindex All Bookmarks`.\n5. When the reindexing has finished, Meilisearch should be working as usual.\n\nIf you run into issues, the official documentation can be found [there](https://www.meilisearch.com/docs/learn/update_and_migration/updating).\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/13-community-projects.md",
    "content": "# Community Projects\n\nThis page lists community projects that are built around Karakeep, but not officially supported by the development team.\n\n:::warning\nThis list comes with no guarantees about security, performance, reliability, or accuracy. Use at your own risk.\n:::\n\n### Raycast Extension\n\n_By [@luolei](https://github.com/foru17)._\n\nA user-friendly Raycast extension that seamlessly integrates with Karakeep, bringing powerful bookmark management to your fingertips. Quickly save, search, and organize your bookmarks, texts, and images—all through Raycast's intuitive interface.\n\nGet it [here](https://www.raycast.com/luolei/karakeep).\n\n### Alfred Workflow\n\n_By [@yinan-c](https://github.com/yinan-c)_\n\nAn Alfred workflow to quickly hoard stuff or access your hoarded bookmarks!\n\nGet it [here](https://www.alfredforum.com/topic/22528-hoarder-workflow-for-self-hosted-bookmark-management/).\n\n### Obsidian Plugin\n\n_By [@jhofker](https://github.com/jhofker)_\n\nAn Obsidian plugin that syncs your Karakeep bookmarks with Obsidian, creating markdown notes for each bookmark in a designated folder.\n\nGet it [here](https://github.com/jhofker/obsidian-hoarder/), or install it directly from Obsidian's community plugin store ([link](https://obsidian.md/plugins?id=hoarder-sync)).\n\n### Telegram Bot\n\n_By [@Madh93](https://github.com/Madh93)_\n\nA Telegram Bot for saving bookmarks to Karakeep directly through Telegram.\n\nGet it [here](https://github.com/Madh93/karakeepbot).\n\n### Hoarder's Pipette\n\n_By [@DanSnow](https://github.com/DanSnow)_\n\nA chrome extension that injects karakeep's bookmarks into your search results.\n\nGet it [here](https://dansnow.github.io/hoarder-pipette/guides/installation/).\n\n### Karakeep-Python-API\n\n_By [@thiswillbeyourgithub](https://github.com/thiswillbeyourgithub/)_\n\nA python package to simplify access to the karakeep API. Can be used as a library or from the CLI. Aims for feature completeness and high test coverage but do check its feature matrix before relying too much on it.\n\nIts repository also hosts the [Community Script](https://github.com/thiswillbeyourgithub/karakeep_python_api/tree/main/community_scripts), for example:\n\n| Community Script | Description | Documentation |\n|----------------|-------------|---------------|\n| **Karakeep-Time-Tagger** | Automatically adds time-to-read tags (`0-5m`, `5-10m`, etc.) to bookmarks based on content length analysis. Includes systemd service and timer files for automated periodic execution. | [`Link`](https://github.com/thiswillbeyourgithub/karakeep_python_api/tree/main/community_scripts/karakeep-time-tagger) |\n| **Karakeep-List-To-Tag** | Converts a Karakeep list into tags by adding a specified tag to all bookmarks within that list. | [`Link`](https://github.com/thiswillbeyourgithub/karakeep_python_api/tree/main/community_scripts/karakeep-list-to-tag) |\n| **Omnivore2Karakeep-Highlights** | Imports highlights from Omnivore export data to Karakeep, with intelligent position detection and bookmark matching. Supports dry-run mode for testing. | [`Link`](https://github.com/thiswillbeyourgithub/karakeep_python_api/tree/main/community_scripts/omnivore2karakeep-highlights) |\n\n\nGet it [here](https://github.com/thiswillbeyourgithub/karakeep_python_api).\n\n### FreshRSS_to_Karakeep\n\n_By [@thiswillbeyourgithub](https://github.com/thiswillbeyourgithub/)_\n\nA python script to automatically create Karakeep bookmarks from your [FreshRSS](https://github.com/FreshRSS/FreshRSS) *favourites/saved* RSS item. Made to be called periodically. Based on the community project `Karakeep-Python-API` above, by the same author.\n\nGet it [here](https://github.com/thiswillbeyourgithub/freshrss_to_karakeep).\n\n### karakeep-sync\n_By [@sidoshi](https://github.com/sidoshi/)_\n\nSync links from Hacker News upvotes, Reddit Saves to Karakeep for centralized bookmark management.\n\nGet it [here](https://github.com/sidoshi/karakeep-sync)\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/14-guides/01-legacy-container-upgrade.md",
    "content": "# Legacy Container Upgrade\n\nKarakeep's 0.16 release consolidated the web and worker containers into a single container and also dropped the need for the redis container. The legacy containers will stop being supported soon, to upgrade to the new container do the following:\n\n1. Remove the redis container and its volume if it had one.\n2. Move the environment variables that you've set exclusively to the `workers` container to the `web` container.\n3. Delete the `workers` container.\n4. Rename the web container image from `hoarder-app/hoarder-web` to `hoarder-app/hoarder`.\n\n```diff\ndiff --git a/docker/docker-compose.yml b/docker/docker-compose.yml\nindex cdfc908..6297563 100644\n--- a/docker/docker-compose.yml\n+++ b/docker/docker-compose.yml\n@@ -1,7 +1,7 @@\n version: \"3.8\"\n services:\n   web:\n-    image: ghcr.io/hoarder-app/hoarder-web:${KARAKEEP_VERSION:-release}\n+    image: ghcr.io/karakeep-app/karakeep:${KARAKEEP_VERSION:-release}\n     restart: unless-stopped\n     volumes:\n       - data:/data\n@@ -10,14 +10,10 @@ services:\n     env_file:\n       - .env\n     environment:\n-      REDIS_HOST: redis\n       MEILI_ADDR: http://meilisearch:7700\n+      BROWSER_WEB_URL: http://chrome:9222\n+      # OPENAI_API_KEY: ...\n       DATA_DIR: /data\n-  redis:\n-    image: redis:7.2-alpine\n-    restart: unless-stopped\n-    volumes:\n-      - redis:/data\n   chrome:\n     image: gcr.io/zenika-hub/alpine-chrome:123\n     restart: unless-stopped\n@@ -37,24 +33,7 @@ services:\n       MEILI_NO_ANALYTICS: \"true\"\n     volumes:\n       - meilisearch:/meili_data\n-  workers:\n-    image: ghcr.io/hoarder-app/hoarder-workers:${KARAKEEP_VERSION:-release}\n-    restart: unless-stopped\n-    volumes:\n-      - data:/data\n-    env_file:\n-      - .env\n-    environment:\n-      REDIS_HOST: redis\n-      MEILI_ADDR: http://meilisearch:7700\n-      BROWSER_WEB_URL: http://chrome:9222\n-      DATA_DIR: /data\n-      # OPENAI_API_KEY: ...\n-    depends_on:\n-      web:\n-        condition: service_started\n\n volumes:\n-  redis:\n   meilisearch:\n   data:\n```\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/14-guides/02-search-query-language.md",
    "content": "# Search Query Language\n\nKarakeep provides a search query language to filter and find bookmarks. Here are all the supported qualifiers and how to use them:\n\n## Basic Syntax\n\n- Use spaces to separate multiple conditions (implicit AND)\n- Use `and`/`or` keywords for explicit boolean logic\n- Prefix qualifiers with `-` to negate them\n- Use parentheses `()` for grouping conditions (note that groups can't be negated)\n\n## Qualifiers\n\nHere's a comprehensive table of all supported qualifiers:\n\n| Qualifier                        | Description                                                                                                                                                                                               | Example Usage                                |\n| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |\n| `is:fav`                         | Favorited bookmarks                                                                                                                                                                                       | `is:fav`                                     |\n| `is:archived`                    | Archived bookmarks                                                                                                                                                                                        | `-is:archived`                               |\n| `is:tagged`                      | Bookmarks that has one or more tags                                                                                                                                                                       | `is:tagged`                                  |\n| `is:inlist`                      | Bookmarks that are in one or more lists                                                                                                                                                                   | `is:inlist`                                  |\n| `is:link`, `is:text`, `is:media` | Bookmarks that are of type link, text or media                                                                                                                                                            | `is:link`                                    |\n| `url:<value>`                    | Match bookmarks with URL substring                                                                                                                                                                        | `url:example.com`                            |\n| `title:<value>`                  | Match bookmarks with title substring                                                                                                               | `title:example`                              |\n|                                  | Supports quoted strings for titles with spaces                                                                                                   | `title:\"my title\"`                           |\n| `#<tag>`                         | Match bookmarks with specific tag                                                                                                                                                                         | `#important`                                 |\n|                                  | Supports quoted strings for tags with spaces                                                                                                                                                              | `#\"work in progress\"`                        |\n| `list:<name>`                    | Match bookmarks in specific list                                                                                                                                                                          | `list:reading`                               |\n|                                  | Supports quoted strings for list names with spaces                                                                                                                                                        | `list:\"to review\"`                           |\n| `after:<date>`                   | Bookmarks created on or after date (YYYY-MM-DD)                                                                                                                                                           | `after:2023-01-01`                           |\n| `before:<date>`                  | Bookmarks created on or before date (YYYY-MM-DD)                                                                                                                                                          | `before:2023-12-31`                          |\n| `feed:<name>`                    | Bookmarks imported from a particular rss feed                                                                                                                                                             | `feed:Hackernews`                            |\n| `age:<time-range>`               | Match bookmarks based on how long ago they were created. Use `<` or `>` to indicate the maximum / minimum age of the bookmarks. Supported units: `d` (days), `w` (weeks), `m` (months), `y` (years). | `age:<1d` `age:>2w` `age:<6m` `age:>3y` |\n\n### Examples\n\n```plaintext\n# Find favorited bookmarks from 2023 that are tagged \"important\"\nis:fav after:2023-01-01 before:2023-12-31 #important\n\n# Find archived bookmarks that are either in \"reading\" list or tagged \"work\"\nis:archived and (list:reading or #work)\n\n# Find bookmarks that are not tagged or not in any list\n-is:tagged or -is:inlist\n# Find bookmarks with \"React\" in the title\ntitle:React\n```\n\n## Combining Conditions\n\nYou can combine multiple conditions using boolean logic:\n\n```plaintext\n# Find favorited bookmarks from 2023 that are tagged \"important\"\nis:fav after:2023-01-01 before:2023-12-31 #important\n\n# Find archived bookmarks that are either in \"reading\" list or tagged \"work\"\nis:archived and (list:reading or #work)\n\n# Find bookmarks that are not favorited and not archived\n-is:fav -is:archived\n```\n\n## Text Search\n\nAny text not part of a qualifier will be treated as a full-text search:\n\n```plaintext\n# Search for \"machine learning\" in bookmark content\nmachine learning\n\n# Combine text search with qualifiers\nmachine learning is:fav\n```\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/14-guides/03-singlefile.md",
    "content": "# Using Karakeep with SingleFile Extension\n\nKarakeep supports being a destination for the [SingleFile extension](https://github.com/gildas-lormeau/SingleFile). This has the benefit of allowing you to use the singlefile extension to hoard links as you're seeing them in the browser. This is perfect for websites that don't like to get crawled, has annoying cookie banner or require authentication.\n\n## Setup\n\n1. Install the [SingleFile extension](https://github.com/gildas-lormeau/SingleFile).\n2. In the extension settings, select `Destinations`.\n3. Select `upload to a REST Form API`.\n4. In the URL, insert the address: `https://YOUR_SERVER_ADDRESS/api/v1/bookmarks/singlefile`.\n5. In the `authorization token` field, paste an API key that you can get from your karakeep settings.\n6. Set `data field name` to `file`.\n7. Set `URL field name` to `url`.\n8. (Optional) Add `&ifexists=MODE` to the URL where MODE is one of `skip`, `overwrite`, `overwrite-recrawl`, `append`, or `append-recrawl`. See \"Handling Existing Bookmarks\" section below for details.\n\nNow, go to any page and click the singlefile extension icon. Once it's done with the upload, the bookmark should show up in your karakeep instance. Note that the singlefile extension doesn't show any progress on the upload. Given that archives are typically large, it might take 30+ seconds until the upload is done and starts showing up in Karakeep.\n\n## Handling Existing Bookmarks\n\nWhen uploading a page that already exists in your archive (same URL), you can control the behavior by setting the `ifexists` query parameter in the upload URL. The available modes are:\n\n- `skip` (default): If the bookmark already exists, skip creating a new one\n- `overwrite`: Replace existing precrawled archive (only the most recent archive is kept)\n- `overwrite-recrawl`: Replace existing archive and queue a recrawl to update content\n- `append`: Add new archive version alongside existing ones\n- `append-recrawl`: Add new archive and queue a recrawl\n\nTo use these modes, append `?ifexists=MODE` to your upload URL, replacing `MODE` with your desired behavior.\n\nFor example:  \n`https://YOUR_SERVER_ADDRESS/api/v1/bookmarks/singlefile?ifexists=overwrite`\n\n\n## Recommended settings\n\nIn the singlefile extension, you probably will want to change the following settings for better experience:\n* Stylesheets > compress CSS content: on\n* Stylesheets > group duplicate stylesheets together: on\n* HTML content > remove frames: on\n\nAlso, you most likely will want to change the default `MAX_ASSET_SIZE_MB` in karakeep to something higher, for example `100`.\n\n:::info\nCurrently, we don't support screenshots for singlefile uploads, but this will change in the future.\n:::\n\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/14-guides/04-hoarder-to-karakeep-migration.md",
    "content": "# Hoarder to Karakeep Migration\n\nHoarder is rebranding to Karakeep. Due to github limitations, the old docker image might not be getting new updates after the rebranding. You might need to update your docker image to point to the new karakeep image instead by applying the following change in the docker compose file.\n\n```diff\ndiff --git a/docker/docker-compose.yml b/docker/docker-compose.yml\nindex cdfc908..6297563 100644\n--- a/docker/docker-compose.yml\n+++ b/docker/docker-compose.yml\n@@ -1,7 +1,7 @@\n version: \"3.8\"\n services:\n   web:\n-    image: ghcr.io/hoarder-app/hoarder:${HOARDER_VERSION:-release}\n+    image: ghcr.io/karakeep-app/karakeep:${HOARDER_VERSION:-release}\n```\n\nYou can also change the `HOARDER_VERSION` environment variable but if you do so remember to change it in the `.env` file as well.\n\n## Migrating a Baremetal Installation\n\nIf you previously used the [Debian/Ubuntu install script](https://docs.karakeep.app/Installation/debuntu) to install Hoarder, there is an option to migrate your installation to Karakeep.\n\n```bash\nbash karakeep-linux.sh migrate\n```\n\nThis will migrate your installation with no user input required. After the migration, the script will also check for an update.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/14-guides/05-different-ai-providers.md",
    "content": "# Configuring different AI Providers\n\nKarakeep uses LLM providers for AI tagging and summarization. We support OpenAI-compatible providers and ollama. This guide will show you how to configure different providers.\n\n## OpenAI\n\nIf you want to use OpenAI itself, you just need to pass in the OPENAI_API_KEY environment variable.\n\n```\nOPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\n# You can change the default models by uncommenting the following lines, and choosing your model.\n# INFERENCE_TEXT_MODEL=gpt-4.1-mini\n# INFERENCE_IMAGE_MODEL=gpt-4o-mini\n```\n\n## Ollama\n\nOllama is a local LLM provider that you can use to run your own LLM server. You'll need to pass ollama's address to karakeep and you need to ensure that it's accessible from within the karakeep container (e.g. no localhost addresses).\n\n```\n# MAKE SURE YOU DON'T HAVE OPENAI_API_KEY set, otherwise it takes precedence.\n\nOLLAMA_BASE_URL=http://ollama.mylab.com:11434\n\n# Make sure to pull the models in ollama first. Example models:\nINFERENCE_TEXT_MODEL=gemma3\nINFERENCE_IMAGE_MODEL=llava\n\n# If the model you're using doesn't support structured output, you also need:\n# INFERENCE_OUTPUT_SCHEMA=plain\n```\n\n## Gemini\n\nGemini has an OpenAI-compatible API. You need to get an api key from Google AI Studio.\n\n```\n\nOPENAI_BASE_URL=https://generativelanguage.googleapis.com/v1beta\nOPENAI_API_KEY=YOUR_API_KEY\n\n# Example models:\nINFERENCE_TEXT_MODEL=gemini-2.0-flash\nINFERENCE_IMAGE_MODEL=gemini-2.0-flash\n```\n\n## OpenRouter\n\n```\nOPENAI_BASE_URL=https://openrouter.ai/api/v1\nOPENAI_API_KEY=YOUR_API_KEY\n\n# Example models:\nINFERENCE_TEXT_MODEL=meta-llama/llama-4-scout\nINFERENCE_IMAGE_MODEL=meta-llama/llama-4-scout\n```\n\n## Perplexity\n\n```\nOPENAI_BASE_URL: https://api.perplexity.ai\nOPENAI_API_KEY: Your Perplexity API Key\nINFERENCE_TEXT_MODEL: sonar-pro\nINFERENCE_IMAGE_MODEL: sonar-pro\n```\n\n## Azure\n\nAzure has an OpenAI-compatible API.\n\nYou can get your API key from the Overview page of the Azure AI Foundry Portal or via \"Keys + Endpoints\" on the resource in the Azure Portal.\n\n:::warning\nThe [model name is the deployment name](https://learn.microsoft.com/en-us/azure/ai-foundry/openai/how-to/switching-endpoints#keyword-argument-for-model) you specified when deploying the model, which may differ from the base model name.\n:::\n\n```\n# Deployed via Azure AI Foundry:\nOPENAI_BASE_URL=https://{your-azure-ai-foundry-resource-name}.cognitiveservices.azure.com/openai/v1/\n\n# Deployed via Azure OpenAI Service:\nOPENAI_BASE_URL=https://{your-azure-openai-resource-name}.openai.azure.com/openai/v1/\n\nOPENAI_API_KEY=YOUR_API_KEY\nINFERENCE_TEXT_MODEL=YOUR_DEPLOYMENT_NAME\nINFERENCE_IMAGE_MODEL=YOUR_DEPLOYMENT_NAME\n```\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/14-guides/06-server-migration.md",
    "content": "# Migrating Between Servers\n\nThis guide explains how to migrate all of your data from one Karakeep server to another using the official CLI.\n\n## What the command does\n\nThe migration copies user-owned data from a source server to a destination server in this order:\n\n- User settings\n- Lists (preserving hierarchy and settings)\n- RSS feeds\n- AI prompts (custom prompts and their enabled state)\n- Webhooks (URL and events)\n- Tags (ensures tags by name exist)\n- Rule engine rules (IDs remapped to destination equivalents)\n- Bookmarks (links, text, and assets)\n  - After creation, attaches the correct tags and adds to the correct lists\n\nNotes:\n- Webhook tokens cannot be read via the API, so tokens are not migrated. Re‑add them on the destination if needed.\n- Asset bookmarks are migrated by downloading the original asset and re‑uploading it to the destination. Only images and PDFs are supported for asset bookmarks.\n- Link bookmarks on the destination may be de‑duplicated if the same URL already exists.\n\n## Prerequisites\n\n- Install the CLI:\n  - NPM: `npm install -g @karakeep/cli`\n  - Docker: `docker run --rm ghcr.io/karakeep-app/karakeep-cli:release --help`\n- Collect API keys and base URLs for both servers:\n  - Source: `--server-addr`, `--api-key`\n  - Destination: `--dest-server`, `--dest-api-key`\n\n## Quick start\n\n```\nkarakeep --server-addr https://src.example.com --api-key <SOURCE_API_KEY> migrate \\\n  --dest-server https://dest.example.com \\\n  --dest-api-key <DEST_API_KEY>\n```\n\nThe command is long‑running and shows live progress for each phase. You will be prompted for confirmation; pass `--yes` to skip the prompt.\n\n### Options\n\n- `--server-addr <url>`: Source server base URL\n- `--api-key <key>`: API key for the source server\n- `--dest-server <url>`: Destination server base URL\n- `--dest-api-key <key>`: API key for the destination server\n- `--batch-size <n>`: Page size for bookmark migration (default 50, max 100)\n- `-y`, `--yes`: Skip the confirmation prompt\n\n## What to expect\n\n- Lists are recreated parent‑first and retain their hierarchy.\n- Feeds, prompts, webhooks, and tags are recreated by value.\n- Rules are recreated after IDs (tags, lists, feeds) are remapped to their corresponding destination IDs.\n- After each bookmark is created, the command attaches the correct tags and adds it to the correct lists.\n\n## Caveats and tips\n\n- Webhook auth tokens must be re‑entered on the destination after migration.\n- If your destination already contains data, duplicate links may be de‑duplicated; tags and list membership are still applied to the existing bookmark.\n\n## Troubleshooting\n\n- If the command exits early, you can re‑run it, but note:\n  - Tags and lists that already exist are reused.\n  - Link de‑duplication avoids duplicate link bookmarks. Notes and assets will get re-created.\n  - Rules, webhooks, rss feeds will get re-created and you'll have to manually clean them up afterwards.\n  - The progress log indicates how far it got.\n- Use a smaller `--batch-size` if your source or destination is under heavy load.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/14-guides/_category_.json",
    "content": "{\n    \"label\": \"Guides\",\n}\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/_category_.json",
    "content": "{ \"label\": \"API\" }\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/add-a-bookmark-to-a-list.api.mdx",
    "content": "---\nid: add-a-bookmark-to-a-list\ntitle: \"Add a bookmark to a list\"\ndescription: \"Add the bookmarks to a list\"\nsidebar_label: \"Add a bookmark to a list\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy9VE1v00AQ/SurOYG01C0qovItHJAKCFWQikPkw8SexNvY3u3uuE2w9r+jsU2ctKFcEL7Yu56PN2/eTAcFhdwbx8Y2kMKsKBSXpJbWbmr0m6DYKlSVCQwaGNcB0gV8MYEDZBoC5a03vIN00cGS0JOftVxCushipsGhx5qYfOgNQl5SjZB2wDtHkEJgb5o1aKAt1q6SK0OmqLa7df14d/Xebn++K7fMNr+S7IZ7E8l+XUDU4Om+NZ4KSNm3pKHBWgyqwUCDkZIccglR/7P0H0ZqXoSwnIyOYGTiEZxtAgVB8vb8Ul7HTfhqVW4bpobVm6NuqEcMCouC+tSXp3yFHGX95NJYVivbNgJkjCpe6FxlchSv5C6I6wmC7PKOcmm889aRZzOAzm1Bz2mMGmoKAden/h0xtRgiTPZZlEfOXNpCyGr7rEJZCon0MyTd0NaY7LWZdBPNEUSN/uG31lpfQQodFoWnEGKCziQPF6DhAb3BZTVUMv4eaFxhWzGkUDK7kCYJ+93ZBj1uiNwZOgf6CdfzktQYQdlV36nPo70asECM8WBMvgvBQ+bDYdmzJZmljt5MVNQbgR4/PlpfoyD89GPeU2qalRV3qXqAdHF2fnZ+INY9ntnN9Un8s5trtbL+GLwUGzU4G7jGXhqjrGU94KStw91wFLqbpPa3lTJUzrTlxFVoGkncN68b+7/o5zmAhnQ/2PtYcnswa5mG0gYWp65bYqBbX8Uo1/cteVlT2aSAXieFCfJdQLrCKtALdbz6Ngr4tfoT7vESm10vtKqVE2jY0G5aS7KK/mPWA3ZiFjWUhAX5vvbBYJbn5PjA9dlqEAnvR/Pmdg4a8Fi4T4TaRz8Jq+sGi7ndUBPjHiXLWQDG+As6ID3T\nsidebar_class_name: \"put api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Add a bookmark to a list\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"put\"}\n  path={\"/lists/{listId}/bookmarks/{bookmarkId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nAdd the bookmarks to a list\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"ListId\"},\"required\":true,\"name\":\"listId\",\"in\":\"path\"},{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"204\":{\"description\":\"No content - the bookmark was added\"},\"404\":{\"description\":\"List or bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/attach-asset.api.mdx",
    "content": "---\nid: attach-asset\ntitle: \"Attach asset\"\ndescription: \"Attach a new asset to a bookmark\"\nsidebar_label: \"Attach asset\"\nhide_title: true\nhide_table_of_contents: true\napi: eJztVcGO2zYQ/RVhTg3ArnaLFA10c4oW3RZoF1kHPRg+jMWxxTVFMiS1tivw34Oh5JUde4McesxFkEaP5Lw3b4Y9SAq1Vy4qa6CCWYxYNwUWhnYFhkCxiLbAYmXttkW/BQERNwGqBbwfQwGWAgLVnVfxANWihxWhJz/rYgPVYpmWAhx6bCmSDxkQ6oZahKqHeHAEFYToldmAANpj6zSHFCmp94dNu3t694vd//dzs4/R1u84AxUz5JjBvYQkwNOnTnmSUEXfkQCDLYNWE0iAYo4OYwOcFa+gEN9beeBczpWYN3QiQJYFBNTWRDKR4eicVjUyvHwKvOYKMbt6ojqCAOetIx8VBf6r5CX5JCCfN8/RK9KYrmXdtTLbP2Krfx1TERBqT2RCY/kj7/F4GlmhMeTvW9wQCFh3Wj/ghma+btQzR56VJMu4UakZ75BTptrjTpOcsF0g/9Fpi5JYz85sjd0ZWKazAiyY4CmdZUoDIjhrwqDBT7d3r6iexSY5yP9d9NdFF7BWmv7ORr/M3XRa44o7hRvi2yr09vbtZVGOfVYYG4u17Yz8/4pSW3kl+SSgpRBYvMt/XxDJO0z4TCSvj42V3O425GO56ysoj4KHsp9GQyqzEoHrSv75OKc6r6GCHqX0FEIq0any+Y6rh16xtJnC+HvQbY2djlBBE6MLVVlGf7jZosctkbtB50Bcc/ywQ2HXRWyo+GvEF0MukFI6GbGPrOxw8umgfZGJT87+ZBgPwAxiq+WX361vkTP889951pIr9mGahL8dJ/DQMJOdTvrkoh0Sj9a15UUs3sDs7ub25vZkXL/Qmj3cX5Vh9nBfrK0/14A1SyJXscVsrXGwH6+qsXHOtusne37TlTawirSPpdOoDJ+Yi9+Pvlm8NCp7pDq7VEbrLAU0bLVqAX2/wkAfvU6Jw5868nwzLifjZHtJFfhdQrVGHegrHH74MBr+TfFaumMQzSH7U3f8BQK2dDi/BRPPjYZQks9ZDICxkj/mAk8bXLR2EscVs7omF7+KXZ604cM/j/M874bLts19Dx53IPIzJ2sz9+ztHOtBo9l0eQ7AsCdbFs8d/4XDM6urcvT9gJjbLZmUXtSJ/M3CpPQZ2d8imQ==\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Attach asset\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/bookmarks/{bookmarkId}/assets\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nAttach a new asset to a bookmark\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The asset to attach\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"unknown\"]}},\"required\":[\"id\",\"assetType\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"201\":{\"description\":\"The attached asset\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}}},\"404\":{\"description\":\"Bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/attach-tags-to-a-bookmark.api.mdx",
    "content": "---\nid: attach-tags-to-a-bookmark\ntitle: \"Attach tags to a bookmark\"\ndescription: \"Attach tags to a bookmark\"\nsidebar_label: \"Attach tags to a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVU2P0zAQ/SvRnEAyTUEgUG4FgbQgwQqKOFQ9TJNp420SG3uytET572icr5aWFUL0UCXO83jmzZvnBjLyqdOWtakggQUzpnnEuPMRmwijjTH7Et0eFMgiJCt43S95WCvwlNZO8xGSVQMbQkduUXMOyWrdrhVYdFgSk/MB4NOcSoSkAT5aggQ8O13tQAEdsLSFLGnSWXE47sofd69emsPPF/mB2aSvJAPNATJkcJNBq8DR91o7yiBhV5OCCksBbSaQAi3FWeQcJCvZQZ5fm+wouZxTsMxpqj/QMQMFqamYKhY4WlvoFAUe33nZc6Uws7mjlEGBdcaSY00+fA0cjih0Do+SHlPp/2r3TXbJXht68zGUffFNfqccrboc1sO6t6byXfhn8/l1OgrtOTLbng7KhJ9IZ/7/8TJE/gtu/k0yy0DdBRfjuT0fz+fPLykY5BZVhqOtqavs/xWemuxa1xSU5D3urnb0vIQQYcKv+46XxLnJRPXGh2NF/AnEw1j4uJkmpI2DKGSc3f0wrLUrIIEGs8yR922MVsf3T0HBPTqNm6LvXPe5Y22LdcGQQM5sfRLH7I6zPTrcE9kZWgvqirr6CCIwzin60OOjLhfp2YnPfBFeu5NP3WYkSU6WOgJMXCCAQPUP74wrUTJ8/20ZmJR+fZ7s4O2gqWFUV+PUTcIbh21syboVi9ka2Sf8dcU9nc1n8xMNjpUtbm+uMrG4vYm2xp3TILS1KrSxxKCt3uAe8uqz2M0k1ocNvqOQ6cCxLVBXcm5QQdPLZzW6qoglObPYzlYU5KK3ZAVNs0FPX13RtrL8vSYnt8R60k9gN9NenjNItlh4eiD1R5971T+O/pTsYB3VMci0qOUNFOzpeH4jhI7lhBm5kEUHeNOd9WQpYaYAF/PdqmHHIk3J8oPY9cks3n76shQt9hdPGYYfHP4AFf5DsibUHiQe1hoosNrVwQygiynKxXPh/yb0TrnX6GiaDrE0e6radmSH5V2IadtfmYTHdA==\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Attach tags to a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/bookmarks/{bookmarkId}/tags\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nAttach tags to a bookmark\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The tags to attach.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"tagId\":{\"type\":\"string\"},\"tagName\":{\"type\":\"string\"}}}}},\"required\":[\"tags\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The list of attached tag ids\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"attached\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"TagId\"}}},\"required\":[\"attached\"]}}}},\"404\":{\"description\":\"Bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/create-a-new-bookmark.api.mdx",
    "content": "---\nid: create-a-new-bookmark\ntitle: \"Create a new bookmark\"\ndescription: \"Create a new bookmark\"\nsidebar_label: \"Create a new bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJztWktv2zgQ/ivGnLWxU+zJN6fYYrOvBpsUezB8GElji7VEqiTlxBX03xdD0pacSrHdBMUuYAQIFHGGM5znR01qSMkkWpRWKAlTeK8JLY1wJOlxFCu1LlCvIQKLKwPTOdyEVwYWERhKKi3sFqbzGmJCTXpW2Qym80WziEDTl4qMvVHpFqb1M0EPGe33H1k1SpxgiCBR0pK0zIFlmYsEmWP82TBbDSbJqEC3mucfl0603ZYEU1DxZ0osRFBqVZK2ggzTWWFzcg+ezFgt5AoikFWeY8xrVlcUQYFPf5Bc8QGuJ5NJEwHqJBMbSjvcsVI5oYQmgiVuFJ9/aF0q2yO3icBURYF627vm7ZDO7HGNHTU+5ndaKO+GbzlIVgX7LVePzK90gTksmghEUSpt78kYoeRt2q+nqnTSa7rdtlgKiOCRYnZczs+FikXOfqQnS5I3hwiMkKucln5BGwM7+bBomiaqQUk6zZdu9YVjCrl2x6t03ke3ZANYmEKlBZ+w1ORMSOnMu7rXEo2PZaHZ03O/6kUsWPnXqWzpyTqV3cOwGz71nWhAtd2er9UNjSGvnHt6OEIuClyx+DJdtkwDscXB8BcWAwly9olb/Vqxi4Z/Gs9hSiWNP/W7yeRIPcJcE6bbET0JY805Relle4t+W7yQ8w1nVCqW4uSScGK5e3V1s7haCbm6t2grc1xeGyWmShJyNWCJIq+0ixiSKXMt9tVRfHUmfvvt+6tyj3kGq3Qf7WClHFLzDSrnrivvhaLWuGUKS4X57miUQ1mJ1mKSUXrzYqdBPktWFSi5uB8mq0gh7H+wm6PrpNiP6QZn5cpBsTiB3pXC3gI2RDwbKpZ94ZZoImkyZc/hWlZ5fseSfN6fw/pNnzyHeSNSUmcpihuRnGjozBb5+zZ0jiMmT3uOOruTnyYAK5spfZpVqzgXJqPTqFO0dBc4TlOcOf4M3eMEhv8uzDlR1f817BnsKqdWESO+dreVVRGTfiEFvtvM/Vjr1Vav5Fqqx56u4RgYygVxb9/yTnIzd5NfO9WmW4Z3lrjvvolRStK3ITqeVV8IdZHpAux0NQl6ii1noSH9qcwVpsQ9tLXVWSHV147bsy+aXoIWnR5g0Q5+PACLz5FhP6AL2KUNxr1vF/u2vP/YAB7Ev5tcH8HtK2XDh4T0AtovoP0C2i+g/QLaL6D9AtovoP0C2i+g/QLafzxo/7nvY/sNpqMwHXw7nJ6otB8CFWQMu/PoLMHt0NK72YHjt5lKYQqlcvqWyCNCGMf7WSiPQvWGtHH4xwEXqDFNNRnTjLEU4801xw5qwY516oZlb5wlVjmPpTJrSzMdj63eXq1R45qovMKyhKjn2hN2GKnlyGY0+j3Qj7wubP/OjPaeregldye1e5OwZJcdTMaXBkfEge4ePuwGZ7/98+Dsxt75ux3w/vKERRlgWQiEfTy3VxaPpruXFP/Gw/uWY4/i21ed61b35cH4M0w5vxltdjYOkD8g+XB2BzkD4Gxpe2eDnbAScqn4vOx375Trq8nVBNpU2Htkdnfb68HZ3e1oqfSh+9jdjDOUsYzHp3UA94Oz+Wcwd59Ow8N8f2zuvOMyR+GuhgFu++CeQ9wd9Gcc+NM51HWMhntb0/DrLxWxk+aLNrTd7D+CjDBlhDSvYU3smVB/fwodaIN55d3wLN+5I3mOWZJQaV+kXXRy8+7j/YMry/6fDQpXDEAjBwT/ngJEoJyRXBK4dzXkKFeVKw7g92wCGuzenw9ToXvHQbntaFjXnuJBrUk2DUThKJb/BjeL/Bepno+R\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Create a new bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/bookmarks\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nCreate a new bookmark\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The bookmark to create\",\"content\":{\"application/json\":{\"schema\":{\"allOf\":[{\"type\":\"object\",\"properties\":{\"title\":{\"type\":\"string\",\"nullable\":true,\"maxLength\":1000},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"note\":{\"type\":\"string\"},\"summary\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\",\"nullable\":true},\"crawlPriority\":{\"type\":\"string\",\"enum\":[\"low\",\"normal\"]},\"importSessionId\":{\"type\":\"string\"},\"source\":{\"type\":\"string\",\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]}}},{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\",\"format\":\"uri\"},\"precrawledArchiveId\":{\"type\":\"string\"}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\"}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\"}},\"required\":[\"type\",\"assetType\",\"assetId\"]}]}]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The bookmark already exists\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"attachedBy\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\"]}},\"required\":[\"id\",\"name\",\"attachedBy\"]}},\"content\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true},\"description\":{\"type\":\"string\",\"nullable\":true},\"imageUrl\":{\"type\":\"string\",\"nullable\":true},\"imageAssetId\":{\"type\":\"string\",\"nullable\":true},\"screenshotAssetId\":{\"type\":\"string\",\"nullable\":true},\"fullPageArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"precrawledArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"videoAssetId\":{\"type\":\"string\",\"nullable\":true},\"favicon\":{\"type\":\"string\",\"nullable\":true},\"htmlContent\":{\"type\":\"string\",\"nullable\":true},\"contentAssetId\":{\"type\":\"string\",\"nullable\":true},\"crawledAt\":{\"type\":\"string\",\"nullable\":true},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\",\"nullable\":true},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true},\"size\":{\"type\":\"number\",\"nullable\":true},\"content\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"assetType\",\"assetId\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"unknown\"]}},\"required\":[\"type\"]}]},\"assets\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"tags\",\"content\",\"assets\"],\"title\":\"Bookmark\"}}}},\"201\":{\"description\":\"The bookmark got created\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"attachedBy\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\"]}},\"required\":[\"id\",\"name\",\"attachedBy\"]}},\"content\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true},\"description\":{\"type\":\"string\",\"nullable\":true},\"imageUrl\":{\"type\":\"string\",\"nullable\":true},\"imageAssetId\":{\"type\":\"string\",\"nullable\":true},\"screenshotAssetId\":{\"type\":\"string\",\"nullable\":true},\"fullPageArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"precrawledArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"videoAssetId\":{\"type\":\"string\",\"nullable\":true},\"favicon\":{\"type\":\"string\",\"nullable\":true},\"htmlContent\":{\"type\":\"string\",\"nullable\":true},\"contentAssetId\":{\"type\":\"string\",\"nullable\":true},\"crawledAt\":{\"type\":\"string\",\"nullable\":true},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\",\"nullable\":true},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true},\"size\":{\"type\":\"number\",\"nullable\":true},\"content\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"assetType\",\"assetId\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"unknown\"]}},\"required\":[\"type\"]}]},\"assets\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"tags\",\"content\",\"assets\"],\"title\":\"Bookmark\"}}}},\"400\":{\"description\":\"Bad request\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/create-a-new-highlight.api.mdx",
    "content": "---\nid: create-a-new-highlight\ntitle: \"Create a new highlight\"\ndescription: \"Create a new highlight\"\nsidebar_label: \"Create a new highlight\"\nhide_title: true\nhide_table_of_contents: true\napi: eJztVj1v2zAQ/SvCzaztFJm4uUWLph0SNC46GB7O0tlSLJEMSSUxBP734ig5khwl6JChQzdbd7yPd+8e2UBGLrWF8YVWIOGzJfSUYKLoMcmLfV4W+9yDAI97B3IN307fHGwEOEprW/gjyHUDW0JLdln7HOR6EzYCLN3X5PwnnR1BNmepVjn1GRKvkzTmBgGpVp6U5yNoTFmkyEfmd47PNeDSnCrkX/5oCCTo7R2lXKSx2pD1BTm2brU+VGgPV9nA13lbqD0EAc6j9de7nSM/sKu62pJlO6nsDWuqS21fxuVjdcVAHaks9SMwCBkI2FsiBQK2ZU2MXEY7rEsP8uQYBHh68lMhVV2WuC0JpLc1BQFKe/oLx9BOoOAK5HqIx7j7Ya9dFV2OTQhtFGe0ci2sHxcX08Ns55eNaPN/lO8zSgHFdOu1I/sKKt08lhOFvAM1YkXP+YfZNgJ84bn0Xi2gJdLlYvGSO59wwJmk04z3406qswmEg4CKnMP9lO0MnRih99+cermc6KUDMlHaJztdq+xfbySe97nOQILREXiDLOEwz3utZ6m3D2RdVPraliChwSyz5FyYoynmDxcg4AFtwayN9XbmFqTTiuTeGyfnc2+PswNaPBCZGRoDYkJRugiJ3iU+p+RH55+0tTCnBnfQLcPYycXgJnrGhDNzH9ENZOfEexx/fNW2Qq7w++9VBI7H87O/wL48YWVKOlejfmNHIrQYic7iWWR6LWkVYrDwUQgGUy3UTnM2Rr2F5GK2mC2g365nPJY3V5P4LW+ukp22Y/AY7CDirCuMBFQYAXn17h8Fbno6v/FaaCHnHuemxEJFqWLWNB271pCPXhI5U0+uoWm26OiXLUPgz/c1WX5dbHpuxceFgJwwIxvpeKAjF9OW9WHFudmd5Vm+3LggTieWaUrGv+m7GWzHzfXtiunSvWaquI5gMd4M+AgSQICOMEUWxm8NlKj2dVxPaGMyuXDMzTMuxq46E6rjoMKmaT1W+kAqBBBdK57/Q+CN/gPpI19E\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Create a new highlight\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/highlights\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nCreate a new highlight\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The highlight to create\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarkId\":{\"type\":\"string\"},\"startOffset\":{\"type\":\"number\"},\"endOffset\":{\"type\":\"number\"},\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"],\"default\":\"yellow\"},\"text\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"bookmarkId\",\"startOffset\",\"endOffset\",\"text\",\"note\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"201\":{\"description\":\"The created highlight\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarkId\":{\"type\":\"string\"},\"startOffset\":{\"type\":\"number\"},\"endOffset\":{\"type\":\"number\"},\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"],\"default\":\"yellow\"},\"text\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\",\"nullable\":true},\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"}},\"required\":[\"bookmarkId\",\"startOffset\",\"endOffset\",\"text\",\"note\",\"id\",\"userId\",\"createdAt\"],\"title\":\"Highlight\"}}}},\"400\":{\"description\":\"Bad highlight request\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}},\"404\":{\"description\":\"Bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/create-a-new-list.api.mdx",
    "content": "---\nid: create-a-new-list\ntitle: \"Create a new list\"\ndescription: \"Create a new list\"\nsidebar_label: \"Create a new list\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy1VU1v2zAM/SsGz1qSDtvFt7TYgG4FVqwZdghyYGw2UStLqiS3DQz994GyEzup12JAdwliiZ/vPVINlOQLJ22QRkMOF44wUIaZpqdMSR9AQMCNh3wJV9IHDysBnoraybCDfNnAmtCRm9dhC/lyFVcCHD3U5MO5KXeQNycJFltKcbNgsiIlAwGF0YF0YGu0VskC2Xp659mlAV9sqUL+F3aWIAezvqOCS7POWHJBkudbjRUNrHxwUm9AQCX1FekNV3gmoMLnw9dsFsVxga96z468PydvWYy5RdEdvIxHuq4Yzgp1jQoE+ApdYFxLusVaBcj3d1HAQ01u91ZTUYBFRzpclmOmulYK14ogD66mGFuKpKOS60iodX2sYmyvvTXat6h+nJ2N09jSV+5l8k4kypEWovgLt2+zd9L7K3z9A4L/ndwX+Wy9VrIYmK6NUYQaTtmUJYgjSgeNHcKsBAQZOHwaamhZ/zSbvST6HMusG+j347gw5TiZFXmPm7G7kzZThN4+6Tb5h60pIQdrUr0WeT5gqtLq4s3lHsn5tLhqpyCHBsvSkfdxilZOH89AwCM6yeCnUrvrFpg9g9sQrM+n0+B2k3t0eE9kJ2gtiJEx6SJk5jYLW8q+d/ZZWwtjP1ipN4xgm3m4WA9wcGbuI5mxDJIRiO7PV+Mq5Aq//V4kzJiZn/0+/vKMlVXUL8tecseFH47bcem/uzoO+u4k3Bv0YzTgVepbw0kZ/DbB2WQ2mUGvwwMs8+vLURjn15fZrXHHGDLmPB3Ghwr1oK2xd+xkUxy0PP7otX0Geg5Tq1BqzpMk03SqWoLaP4hbVlu+hKZZo6dfTsXIxx02y1WvqfRGCtgSluSSDO+J4btoi/mw4LRsrmpO/2LIoth7zIuCbHjVdjUYiOsfNwuWSfcoV2kCweETiPSbAwgwCZykvnTWgEK9qdNEQhuTRYXHmjzRYOqqu0K9G1TYNK3FwtyTjhFE10rgb4g8xH8AqpnrCg==\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Create a new list\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/lists\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nCreate a new list\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The list to create\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\",\"minLength\":1,\"maxLength\":100},\"description\":{\"type\":\"string\",\"minLength\":0,\"maxLength\":500},\"icon\":{\"type\":\"string\"},\"type\":{\"type\":\"string\",\"enum\":[\"manual\",\"smart\"],\"default\":\"manual\"},\"query\":{\"type\":\"string\",\"minLength\":1},\"parentId\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"name\",\"icon\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"201\":{\"description\":\"The created list\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"description\":{\"type\":\"string\",\"nullable\":true},\"icon\":{\"type\":\"string\"},\"parentId\":{\"type\":\"string\",\"nullable\":true},\"type\":{\"type\":\"string\",\"enum\":[\"manual\",\"smart\"],\"default\":\"manual\"},\"query\":{\"type\":\"string\",\"nullable\":true},\"public\":{\"type\":\"boolean\"}},\"required\":[\"id\",\"name\",\"icon\",\"parentId\",\"public\"],\"title\":\"List\"}}}},\"400\":{\"description\":\"Bad request\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/create-a-new-tag.api.mdx",
    "content": "---\nid: create-a-new-tag\ntitle: \"Create a new tag\"\ndescription: \"Create a new tag\"\nsidebar_label: \"Create a new tag\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVD1v2zAQ/SvCzayVdNTmFi2QdmjQuOhgaDhLZ4uxRDLkKYkh8L8XR6qxkhpFhy6STN7Hu/feeYKWQuO1Y20NVPDREzIVWBh6KhgPoIDxEKDawkbetYJAzeg1n6DaTrAj9OTXI3dQbetYK/D0MFLgD7Y9QTW9Kb/pqGiRsWBbNLkVdySNiifN3QoUNNYwGZZcdK7XDUpueR+kwASh6WhA+eKTI6jA7u6pYVDgvHXkWVOQW4MDLaICe20OEGMGqD21MlOKqmPM58FZE3L6+6vry+gz6nbm5j+B1e0FqOofZ9AtqMUgUcFA3NkWKnA2pGYo8kCZlBQB/SP5kPQbfQ8VTNi2nkKIJTpdPl6Dgkf0Gnd9xjdfZ0b2OPYMFXTMLlRlyf60OqLHI5FboXOgLtA2VyjsPgn+dY4vMhaIMS6cdSe05c5Lf70QIZ1ljhQG1RwEav74bP2AgvDLz01iS+T4frblp2ccXE9nlywo12Zv5UIIyuivV1erK1kDzZIDL9DXtzcXR13f3hR761/PKbxElQQZ0CxaX1i4VyWns8cuLmdmhOmZS9ejNtIliTrNsm/zAtcKOnFDtYVp2mGgH76PUY4fRvKyzPVZ9LTLCjrClnzyyZFOAiBDebeRrhLej9L9D+tH9Ttj3TTk+K+x9cKwt9/uNqLj/Ocx2FZyPD6BSs8KQIFN1CR7pLMJejSHEQ8Sm2uK6vjaNG9Mkqaar9CcFginKUds7JFMjKDmUVh+Q5Ql+wUehMct\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Create a new tag\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/tags\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nCreate a new tag\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The data to create the tag with.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"}},\"required\":[\"name\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"201\":{\"description\":\"The created tag\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"}},\"required\":[\"id\",\"name\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/delete-a-bookmark.api.mdx",
    "content": "---\nid: delete-a-bookmark\ntitle: \"Delete a bookmark\"\ndescription: \"Delete bookmark by its id\"\nsidebar_label: \"Delete a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJx9U01v2zAM/SsCTxugxe3QYYNvGdoB3Yah2DLsEORA20ysxrZUiW6TGfrvA23nq83qi/XxKD4+PnZQUMi9cWxsAylcU0VMKrN2XaNfq2yrDAdlCtDAuAqQzuHzeBlgoSFQ3nrDW0jnHWSEnvy05RLS+SIuNDj0WBOTDz0g5CXVCGkHvHUEKQT2plmBBtpg7So5MmSKarNd1U/3nz7azd8P5YbZ5p+EgeEesmNwW0DU4OmhNZ4KSNm3pKHBWkDZAaTBSHEOuQRh5Sk42wQKwuT9xZX8TnX4YVVuG6aG1TvF5ZEiTxhU0avUJ786F73jpxrLamnbRiiM7wkanatMjoJO7oOEnJHGZveUM2hw3jrybAa6uS3opYBRQ00h4Orc3YlG8+GFA34R5ZM9l7aAFIbqJLHolUKyqz0k3UHUCNJ8/7hrbesrSKHDovAUQkzQmeTxEjQ8ojeYVQP78XqQbIltxZBCyexCmiTst5M1elwTuQk6B/qZrrOS1PiCssu+L99GvBq4QIzxyJW/RNQh87E39wpJZqmjh4lnehDocfHF+hqF4dc/s15G0yythEvVA6XLycXk4siaez7Tu9uz/Kd3t2pp/Sl5KTZqcDZwjb0dRhOP84h7/z1/szv46vXhHQpm2nDiKjSN5Ot71o19nu8nJoCG9Gh8FhpKG1ggXZdhoN++ilGOH1ryMvqLQ5t7MxQmyLqAdIlVoFc4v/k5OvOt+h/L8RCbbe+mqpUdaFjT9nTM4yJqKAkL8j2LATDNc3J8FPpi+sQxe/df33y/md2ABjy1yjNr9AnOMuu6ATGza2pi3BNl2QvHGP8BgxHeWQ==\nsidebar_class_name: \"delete api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Delete a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"delete\"}\n  path={\"/bookmarks/{bookmarkId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nDelete bookmark by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"204\":{\"description\":\"No content - the bookmark was deleted\"},\"404\":{\"description\":\"Bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/delete-a-highlight.api.mdx",
    "content": "---\nid: delete-a-highlight\ntitle: \"Delete a highlight\"\ndescription: \"Delete highlight by its id\"\nsidebar_label: \"Delete a highlight\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVcFu2zAM/RWDpw0QknTosMK3AO2wbANWbBl2CHJQLCZWY0uqRLfJDP37QNtJnNbtdtgpjvkkPj4+0jUoDJnXjrQ1kMI1FkiY5HqTF3qTU7LaJ5pCohUIILkJkC7g0yEaYCkgYFZ5TXtIFzWsUHr004pySBfLuBTgpJclEvrQAEKWYykhrYH2DiGFQF6bDQjAnSxdwa80alXs9pvy8e7qg939fp/viGx2xRQ0NZAjhZmCKMDjfaU9KkjJVyjAyJJReQ8lQHOBTlIOzMtjcNYEDMzl3WTCP+dazHNMVKOHOgkCAjJrCA3xAelcoTPJB8Z3gU8NVGhXd5jxQeetQ0+6zbmydltKv52p52pEAYGkp2/rdUDqxU1VrtBzHI16JZrZwvpBlU1Vcg/3WBT2EVgH1mbjEQ0IWBUVclMVrmVVEKQHYBRAuKOhK01VFHLFbWHxowBjCf8JqIdLrwL6F1TJPEpCNR0gEs98sOjre65mX7uuqo5zw+iYv59tOWA9iJFzXk4un3vnCEqMpWRtK6P+n3Myqwb0jQJKDEFuhmJPtGluOOGXsS2lRMqtghRa23NiHpcUxkf/h3Hdm6rI0qJ/OEx35QtIoZZKeQwhjqXT44cLEPAgvebWN/y7cKvawWc5kQvpeEx+P9pKL7eIbiSdAzEwlt0NiV0nlGPypcMnLRduTG8x/WBZu5nrraejRpyZ62hgkHYgHobm4aP1pWSGn3/NGyG1WVs+zlW3lC5Gk9Gkt52OfKa3s0H+09tZsrb+nDwXGwU4G6iUjSG6NdYtZXm2hc4urU/W+ssKb0tm049dIbVp5o27Vne9Xpy2ZgABaX+HLgXkNhCD6nolA/70RYz8+r5Cz1+A5anVjSGUDvysIF3LIuArtN987/z5NnmJZ/dSmn3jKF5VKYCALe6fLPu4jAJylAp9Q6NFTLMMHfXOPhtCts1xCK5vvt7Mb0CAPPfLE380CQap1XWLmNstmhiPTIn/M8cY/wCwMJE0\nsidebar_class_name: \"delete api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Delete a highlight\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"delete\"}\n  path={\"/highlights/{highlightId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nDelete highlight by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"HighlightId\"},\"required\":true,\"name\":\"highlightId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The deleted highlight\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarkId\":{\"type\":\"string\"},\"startOffset\":{\"type\":\"number\"},\"endOffset\":{\"type\":\"number\"},\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"],\"default\":\"yellow\"},\"text\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\",\"nullable\":true},\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"}},\"required\":[\"bookmarkId\",\"startOffset\",\"endOffset\",\"text\",\"note\",\"id\",\"userId\",\"createdAt\"],\"title\":\"Highlight\"}}}},\"404\":{\"description\":\"Highlight not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/delete-a-list.api.mdx",
    "content": "---\nid: delete-a-list\ntitle: \"Delete a list\"\ndescription: \"Delete list by its id\"\nsidebar_label: \"Delete a list\"\nhide_title: true\nhide_table_of_contents: true\napi: eJx9U8tu2zAQ/BViTy3ARkmRooFuBpICaYMiaF30YPiwltYWY0lkyFViV+C/F0upfiROfbFIDjmzs7M9lBQKbxwb20IO11QTk6pNYLXYKsNBmRI0MK4C5DO4M4EDzDUEKjpveAv5rIcFoSc/6biCfDaPcw0OPTbE5EMChKKiBiHvgbeOIIfA3rQr0EAbbFwtW4ZMWW+2q+b54eqz3fz5VG2YbXEl7IYTRNhvS4gaPD12xlMJOfuONLTYCKAeABqMFOOQKxA1noKzbaAgCj6eX8rfcd3frSpsy9Sy+qC4IrWwdt2gX6tnDKpMriTiy1O3RZdqLaul7VqhH98SJDpXmwIFmT0EgZ+wwy4eqGDQ4Lx15NkMUgtb0mvTooaGQsDVqbMjb2bDC3v8PMpP1lzZEnIYKhNi8SqHTBwMWT8YGUEa7Z/+tbHzNeTQY1l6CiFm6Ez2dAEantAbXNSD6vF4sGmJXc2QQ8XsQp5l7Ldna/S4JnJn6BzoF15OK1LjC8ouUy++jXg1aIEY40ECf4qZA/NhDnfOCLPUkWCQjyDQ48cX6xsUhV9/T5N9pl1auS5VD5Iuzs7Pzg9iuNMzub89qX9yf6uW1h+Ll2KjBmcDN5hiMIZ2nDlMU/fyvX6fpbeHcyiUacOZq9G0wpN61Y99naXJCKAhH0dkrqGygeWo7xcY6JevY5Ttx468jPV839bU/NIE+S4hX2Id6D863/0YE/hevaVu3MR2m9JTd7ICDWva7sc4zqOGirAknxQMh5OiIMcH115NmKRjl/Drm7ub6Q1owONYvIhBIjipqu8HxNSuqY1xJ5JlLRpj/AuPmMsh\nsidebar_class_name: \"delete api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Delete a list\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"delete\"}\n  path={\"/lists/{listId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nDelete list by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"ListId\"},\"required\":true,\"name\":\"listId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"204\":{\"description\":\"No content - the bookmark was deleted\"},\"404\":{\"description\":\"List not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/delete-a-tag.api.mdx",
    "content": "---\nid: delete-a-tag\ntitle: \"Delete a tag\"\ndescription: \"Delete tag by its id\"\nsidebar_label: \"Delete a tag\"\nhide_title: true\nhide_table_of_contents: true\napi: eJx9U01v2zAM/SsCTxugxe3QYYNvAdoB3Yah2DLsEOTA2EysxrZUiW6TGfrvA2UvH206X2SJT+Lj42MPJYXCG8fGtpDDNdXEpBjXarlThoMyJWhgXAfI5zCTdaEhUNF5wzvI5z0sCT35accV5PNFXGhw6LEhJh8SIBQVNQh5D7xzBDkE9qZdgwbaYuNqOTJkynq7WzdP958+2u2fD9WW2RafJLnhBJnh+raEqMHTQ2c8lZCz70hDi43EOcU1GCnEIVcgXDwFZ9tAQfK/v7iS5bTm71YVtmVqWb1TXJFaWrtp0G/UEwZVJkVS3qtzt2e4Vq1ltbJdK9nHpwSIztWmQAFm90HQZ7Swy3sqGDQ4bx15NgPTwpb0UrGooaEQcH0udqLMfHjhgF9E+WTPlS0hh6EwSSxS5ZBJk7M+qRhBeuwf/3Ww8zXk0GNZegohZuhM9ngJGh7RG1zWA+cxPGi0wq5myKFidiHPMva7yQY9bojcBJ0D/VzIitT4grKr1IivI14NXCDGeGS+nyLlkPnYgntdJLPUkWCQjyDQ489n6xsUhl9+z5J4pl1ZuS5VD5QuJxeTiyMH7vlM727P8p/e3aqV9afkpdiowdnADSYTjIYdhw1l3J4/1x+M9OpQDmUybTlzNZpWsqRO9WNP58PgasiH2VhoqGxgCfT9EgP98nWMcvzQkZdpXhxamhpfmiD/JeQrrAP9h+SbH6P33qrXuI2H2O6Sc+pOdqBhQ7v9+MZF1FARluQTgSE2LQpyfHTrxWiJMfbWvr75djO7AQ146ohnDkgJzpLq+wExsxtqYzxwlL1wjPEvTTHFIQ==\nsidebar_class_name: \"delete api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Delete a tag\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"delete\"}\n  path={\"/tags/{tagId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nDelete tag by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"TagId\"},\"required\":true,\"name\":\"tagId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"204\":{\"description\":\"No content - the bookmark was deleted\"},\"404\":{\"description\":\"Tag not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/detach-asset.api.mdx",
    "content": "---\nid: detach-asset\ntitle: \"Detach asset\"\ndescription: \"Detach an asset from a bookmark\"\nsidebar_label: \"Detach asset\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy9VE1v2zAM/SsCTxugxe3QYYVvGdoB3Yah2DLsEPjA2HTsxrZUiW6TGfrvA23nq82KHYblEkt6oh4fH9lBRj51peXSNBDDFTGmhcJGoffEKnemVqgWxqxqdCvQwLj0EM/hw7jlIdHgKW1dyRuI5x0sCB25acsFxPMkJBosOqyJyfke4NOCaoS4A95Yghg8u7JZggZaY20r2SqpzKr1Zlk/3l2+N+tf74o1s0kvhUHJPWTL4CaDoMHRfVs6yiBm15KGBmsBLfYgDaWkaJELCPqf0ZiKUC9ywBFxRCARuLem8eSFw9uzC/k7LsdXo1LTMDWs3owVeUSvsr5KlCnfpil5n7dVtRECF6eCbHVSjWGVm7YRJmNYQaO1VZmioKM7L1dOaGMWd5QyaLDOWHJcDqxTk9FzBYOGmrzH5amzI53mQ4Q9PgnykzUXJoMYMqqIBdHLFkO0raiPun1xQ9Sr46NuFDuAuNI9bD3Xugpi6DDLHHkfIrRl9HAOGh7QlbiohnTG40HDHNuKIYaC2fo4ithtJit0uCKyE7QW9BOhZwWpMYIyueKC1OcRrwYuEEI4aJfvovLw8mHT7CSTlyWPHiZm7kGgx4+PxtUoDD/9nPW6lk1u5LpkPVA6n5xNzg7MuuMzvb05yX96e6Ny447JS7JBgzWea+z9MTp7Oy5E86fhur3H/masDBkzrTmyFZaNPNgXrRsrP9/1sgcN8VFjD8WX7W2vJRoK41mudd0CPf1wVQiyfd+Sk0GV7GvfOyQrvXxnEOdYeXohm1ffRv++Vn9iPm5is+ktVrWyAg0r2hwPJRlE//HlrTwhCRoKwoxcn/xwOk1Tsnxw79loEPfuWvPq+sv17FrkP7btE5v2D5yk1XUDYmZW1ISwY8myFo4h/Abov0RX\nsidebar_class_name: \"delete api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Detach asset\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"delete\"}\n  path={\"/bookmarks/{bookmarkId}/assets/{assetId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nDetach an asset from a bookmark\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"},{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"AssetId\"},\"required\":true,\"name\":\"assetId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"204\":{\"description\":\"No content - asset was detached successfully\"},\"404\":{\"description\":\"Bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/detach-tags-from-a-bookmark.api.mdx",
    "content": "---\nid: detach-tags-from-a-bookmark\ntitle: \"Detach tags from a bookmark\"\ndescription: \"Detach tags from a bookmark\"\nsidebar_label: \"Detach tags from a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVV2v0zgQ/SvRPLGStykrVovydoG70gWEEBTxUPVhGk8b3yaxsSeXdqP8dzTOR1taLmhFH6rEOR7PnDlz3IKmkHvj2NgaMnhFjHmRMG5DsvG2SjBZW7ur0O9AgSxDtoQXw1KAlYJAeeMNHyBbtrAm9ORvGi4gW666lQKHHiti8iECQl5QhZC1wAdHkEFgb+otKKA9Vq6UJUNGl/vDtvp6//wfu//v72LPbPPnkoHhCBkzuNPQKfD0pTGeNGTsG1JQYyWg9RGkwEh5DrkAyUp2UOAXVh8kl3MSFgX1DLBNdCRkBgpyWzPVLHB0rjQ5Cjy9D7LnSmF2fU85gwLnrSPPhkL8GjmcUOg9HiQ9pir80u47fcleF3vzLpZ98U1+pxwt+xxW43pwtg59+L/m8+t0lCZwYjcDHaSFn8To8Pt4GSP/Ajf/TzKLSN0FF9O5Ax/P5s8uKRjlltSWk41tav37Cs+tvtY1BRWFgNurHT0vIUY44ldDxyviwmrIQFNJLIgo/wzScTBC2h5npEujLGSg/cM4ro0vIYMWtfYUQpeiM+nDU1DwgN7guuxLGD73vG2wKRkyKJhdyNKU/WG2Q487IjdD50Bd0dcQQSTGBSVvBnzS5yJdO3Gaj8Jsf/Kp30w0yclSR4SJD0QQqOHhX+srlAxff15ELqVjH46GcDuqahzW5TR3R+lN4zY1ZdWJyWys7BP++uKezuaz+YkKp8pu3t9dZeLm/V2ysf6cBqGtU+Bs4AqjugaLe9yvz6K3R8H+zOZ7Gpn2nLoSTS1nRyW0g4SWk7eKYLIzo+3NRUFhAwuybdcY6JMvu06WvzTk5a5YHTUUGdYmyLOGbINloEeSf/Jh0P4fyY+SHQ2kPkSplo28gYIdHc7vhdi1glCTj1n0gJf9WX8uJMwxwMWUd2rccZPn5PhR7OpkIl/dvr1d3IoihwuoiiYAHr+Civ8xXRurj0KPay2UWG+baArQRxX94rn8v5N7r99rhLRtj1jYHdVdN/HD8i7UdN03RJTKrQ==\nsidebar_class_name: \"delete api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Detach tags from a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"delete\"}\n  path={\"/bookmarks/{bookmarkId}/tags\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nDetach tags from a bookmark\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The tags to detach.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"tagId\":{\"type\":\"string\"},\"tagName\":{\"type\":\"string\"}}}}},\"required\":[\"tags\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The list of detached tag ids\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"detached\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"TagId\"}}},\"required\":[\"detached\"]}}}},\"404\":{\"description\":\"Bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/get-a-single-asset.api.mdx",
    "content": "---\nid: get-a-single-asset\ntitle: \"Get a single asset\"\ndescription: \"Get asset by its id\"\nsidebar_label: \"Get a single asset\"\nhide_title: true\nhide_table_of_contents: true\napi: eJx9U02P0zAQ/SvWnECymi4SYuVbhWC1cFlBEYeqB7eZNqaJ7bUnpSHyf0fjeEu72uUUf7zMezPveYQa4zYYT8ZZUHCHJHSMSGIzCENRmBokkN5HUCtY8E2EtYSI2z4YGkCtRtigDhgWPTWgVuu0luB10B0ShpgBcdtgp0GNQINHUBApGLsHCXjSnW/5yKCp29Ow737/uv3gTn/eNycit71lekMZkunva0gSAj72JmANikKPEqzuGKELQoLhbrymBlhPwOidjRhZw7v5nD/XjefaYussoaWZ+DgtBOsVJoqam+mMxZoHQw2WKfH9DFKS0CE1rgYFeySQE7WCKsNiNRZlCXh24fg0mT60oGDUdR0wxlRpb6rjDUg46mD0pp0Ul+tJ9U73LYGChshHVVUUhtlBB31A9DPtPchnrS1Z7VRBuF0W/7XgxaQFUkoXpn5nuybmS2vP7jEz95FhoAoIZFl8dqHTrPDLz2UejbE7x79z15Okm9l8Nr9w9qxn8XD/ov7Fw73YuXAtnptNEryL1GnLDCUGOcUiGrtvi1HPi45QrH4981OvhCeqfKuNZaps11jMXU1xiyBBPQVvLaFxkfhyHDc64o/QpsTHjz0Gfi7rf97mBNQm8roGtdNtxP/ofPOthP6teE1fOdR2yBFqe96BhAMOF68jrS/zevdpCRL0tcXPLM1KXyw+jhNi6Q5oUzpzEe+ZKaW/hvt+aA==\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get a single asset\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/assets/{assetId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet asset by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"AssetId\"},\"required\":true,\"name\":\"assetId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Asset content. Content type is determined by the asset type.\"}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/get-a-single-bookmark.api.mdx",
    "content": "---\nid: get-a-single-bookmark\ntitle: \"Get a single bookmark\"\ndescription: \"Get bookmark by its id\"\nsidebar_label: \"Get a single bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzNWN9v3DYM/lcMvmwDjFw6dFhxb2mxddmwNlhS7CG4B9qiz8rJkivJl1wO/t8Lyj8vcRInDbA9nc8ixY+fSIr0HgS51MrSS6NhCR/JR4kxmwLtJkp2kfQukgJi8Lh2sLyE9+2ig1UMjtLKSr+D5eUeEkJL9qTyOSwvV/UqhhItFuTJuiDg0pwKhOUe/K4kWILzVuo1xEA3WJSKX0mSQt3s1sX11btfzc3tL/mN9yZ9xwikDyIdglMBdQyWvlbSkoCltxXFoLFgoWQQikGyZyX6HOp4AkZijCLUEIOgDCvlu70OqTnNIkc+8iYKqz1NP7goNdqT9tG1VCpKKJI6VZUgEUkd+ZwiS6402tFR9Ml4iiOfy0EpRc06Cu2aoszYyJmC+t3d0aGXGSr3/4HW891u+6FR7Dj/WpHdAYdCZ8Yx7z8fH/PPoQ+fkytKGafPhwgU6JGttIBYC8tSyRRZa3HlWHXiSE3YDGIorSnJetkYluJ+9NUxpJbQkzjxk6uFETKT08sx6EopTDgwmfq6D9MZkmjTXG5JTIRiHUOGW8PJ9dC6x/Va6vW5R1+5p+3FQLoqOINdlabkHLAJqSpLTBNpwVqrOgZXFQVaeRsofv3ttfHz6Glw7ObJmsqmM7YdYGIpIYZrSji6FD8XJpGKQjXypB1HZQxO6rWirFmwwS1ZlMb64EtTFHujaC3uWMJT4V4cjU1GTSyg95jmJN5PctI7xr7kVYEaVvVB6biEUMnD/ge7BblRihlNn7NQsh93oFl9GIqSehN4qqya9Gh+rhzUihnyssA1fZky+5DwiXPkTyeOZCrcUkukXW78c7SySqkzttTk/XNUS0upxWtF4gXKWynIPAsobmU6k+jcF+rDEDpPyrdh9hw4nefzDGDlc2PnsVolSrqc5kkL9HTWaswDzhp/t7fHDIU7yRqkm9xZcd/yfbno6aatWfwwlYxNDZ2XM9NQOxvfixU5OALY8HTxhHhIXzYiskFpKrY4tKWiT5MF9sFbZW4VcfJ2vK2uioTsIynwYpoHUgZfX4H1Sm+0uZ64NYLCqu6pff0rb9Yx823yx6jajMtwx8T5+E2CWpM9baPjTvWFti6yXNtthpoEE8WWs9CR/VIqg4L4Dh24elZITV3Hg++relJg6E4PetFR/3jQLN7tDKcburZ3GYKxP9vV/UkL6oDs7fHb+217JxNp46PMVFq8XrueGjHdCRXkHJ/q/bU7BIYdBnlmuA76PjcClrBuDpxHwyUs+sFmsR/mx5oZJLvtptjQy8AehbDkXL3AUi62bzic0Eo+6wC9XW74aodKyL0v3XKx8HZ3tEGLG6LyCMsS7s5zFzlF7Q6RycKc9lcrHzVY+EhGA/g5M9pYHo/hPT1sOSQMi/EcEYQ49sPD78YWyAj//PcicCh1ZlidvW4gvTk6PjoeTeE9npOz00n8J2enYWQ8AM/O8sVrnOcGdbnv5kf+7oBR023349/dffdDYD3yoaLxmO+iRalQhmGpbUCbg77sM54TYDn6VLCKITfOs8h+n6Dj2l/X/LqZZPn8hXR8yg+M4mOI/+FUPknChnZTk/oWVcXCYU7vgviZrv74T5t0P0UPWe8uDL0b2+xQjQ4hXDQ5oeC+7HLfCpykKZVjuPcKC8PvE/vjbxfQ9ILj6fkw6scTziGs/b6RuDAb0nXdo/T8nwHW9Tfb7Kd7\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get a single bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/bookmarks/{bookmarkId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet bookmark by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"},{\"schema\":{\"type\":\"boolean\",\"default\":true,\"description\":\"If set to true, bookmark's content will be included in the response. Note, this content can be large for some bookmarks.\"},\"required\":false,\"description\":\"If set to true, bookmark's content will be included in the response. Note, this content can be large for some bookmarks.\",\"name\":\"includeContent\",\"in\":\"query\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with bookmark data.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"attachedBy\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\"]}},\"required\":[\"id\",\"name\",\"attachedBy\"]}},\"content\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true},\"description\":{\"type\":\"string\",\"nullable\":true},\"imageUrl\":{\"type\":\"string\",\"nullable\":true},\"imageAssetId\":{\"type\":\"string\",\"nullable\":true},\"screenshotAssetId\":{\"type\":\"string\",\"nullable\":true},\"fullPageArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"precrawledArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"videoAssetId\":{\"type\":\"string\",\"nullable\":true},\"favicon\":{\"type\":\"string\",\"nullable\":true},\"htmlContent\":{\"type\":\"string\",\"nullable\":true},\"contentAssetId\":{\"type\":\"string\",\"nullable\":true},\"crawledAt\":{\"type\":\"string\",\"nullable\":true},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\",\"nullable\":true},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true},\"size\":{\"type\":\"number\",\"nullable\":true},\"content\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"assetType\",\"assetId\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"unknown\"]}},\"required\":[\"type\"]}]},\"assets\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"tags\",\"content\",\"assets\"],\"title\":\"Bookmark\"}}}},\"404\":{\"description\":\"Bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/get-a-single-highlight.api.mdx",
    "content": "---\nid: get-a-single-highlight\ntitle: \"Get a single highlight\"\ndescription: \"Get highlight by its id\"\nsidebar_label: \"Get a single highlight\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVcFu2zAM/RWDpw0QknTosMK3HLau22HD1mGHIAfFYmw1tqRKdJvM0L8PVNzESd1ih53imE/U4+Mj3YHCUHjtSFsDOVwjZZUuq1qXFWWrXaYpZFqBAJJlgHwBn5+iAZYCAhat17SDfNHBCqVHP2+pgnyxjEsBTnrZIKEPCRCKChsJeQe0cwg5BPLalCAAt7JxNb/SqFW93ZXN493VB7v9877aEtniiiloSpADhRsFUYDH+1Z7VJCTb1GAkQ2jqgFKgObqnKQKmJfH4KwJGJjLu9mMf06F+La6w4KyR03VQBAlSU5AQGENoSE+Jp2rdSH52PQu8NmROm3KBgKctw496f3NK2s3jfSbG/VckyggkPT0bb0OSIO4aZsVeo6jUa9EC1tbP6q1aRvu5A7r2j4Cq8EKlR7RgIBV3SK3VuFatjVB/gSMAgi3NJbStHUtV9wcbkEUYCzhPwH1eOltQP+CKoVHSajmI0TiiRsWQ31P1Rxq11fVc06MDvcPb1uOGBBi5DsvZ5fPHXQAZcZStratUf/POYVVI/pGAQ2GIMux2Jk2KcMRv4z7UhqkyirIoUzSpInJYXoYgTDtBoMVWVf0D08D3voacuikUh5DiFPp9PThAgQ8SK+574l8H95L9mSyisiFfDolv5tspJcbRDeRzoE40/W2wqzPkNl1RhVmX3t8tufCXRnspp+saT9wgw11EIhv5joSDPIexJOQHj5Z30hm+OX3bVJRm7Xl41z1ntLFZDaZDRbUgc/8+80o//n3m2xt/Sl5LjYKcDZQI5Mb+k3GS1lmQZuyxuMyOk/cHb312hrf18yWn7paapOmjdvW9c1eHDdnAAH5cI8uBVQ2EIO6biUD/vJ1jPz6vkXPX4HlsdfJEUoHflaQr2Ud8BXOb3707nybvcSzfynNLlmKF1UOIGCDu7OFH5dRQIVSoU809oh5UaCjwdlnI8i+OYzA9cdbECBP3XLmjpR9lFfX7RG3doMmxgNN4v9MMMa/V8aQOg==\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get a single highlight\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/highlights/{highlightId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet highlight by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"HighlightId\"},\"required\":true,\"name\":\"highlightId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with highlight data.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarkId\":{\"type\":\"string\"},\"startOffset\":{\"type\":\"number\"},\"endOffset\":{\"type\":\"number\"},\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"],\"default\":\"yellow\"},\"text\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\",\"nullable\":true},\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"}},\"required\":[\"bookmarkId\",\"startOffset\",\"endOffset\",\"text\",\"note\",\"id\",\"userId\",\"createdAt\"],\"title\":\"Highlight\"}}}},\"404\":{\"description\":\"Highlight not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/get-a-single-list.api.mdx",
    "content": "---\nid: get-a-single-list\ntitle: \"Get a single list\"\ndescription: \"Get list by its id\"\nsidebar_label: \"Get a single list\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVMFu2zAM/RWDpw0QknTosMK3Hrai24AVW4cdghwYm4nVypIq0W0yQ/8+UHbTpE2HHXayLVLke3zP7KGmWAXtWTsLJVwQF0ZHLpbbQnMsdA0KGNcRyjl81ZEjLBREqrqgeQvlvIclYaBw3nED5XyRFgo8BmyJKcScEKuGWoSyB956ghIiB23XoIA22HojR5p0bTbbdftwc/bBbX6/bzbMrjqT7ppzinS/rCEpCHTX6UA1lBw6UmCxlQQzJCjQwsQjNyBoAkXvbKQoCN7NZvI4JP1teUMVFw+am4F8jYwTUFA5y2RZbqD3RlcoN6Y3Ua4dIeZyIVDgg/MUWA9Ndf2SfHqEfSRwgO7l1GxnDC5lJkI/KdDVscSUhSArM/mXKkPCEZFs14r6LdoODSiILQYWG9S0ws4wlI+xpOCuo7D9p36+Wxpd7aUunTOEFtKBxHPIHszDGqnuEduVWRwaBVKSMqez05dyS7ywjouV62z9/2SuXH1cz5ZixPWx2DOmucJT/iINLFrixtVQwppyV3F2CVOxapz2g+2TCEPh/vGn64KBEnqs60Axpil6Pb0/AQX3GLTokCGP4WFGj2I2zD6W0ymH7eQWA94S+Ql6D8+cCdcNFWOFwq0Kbqj4MuYXAxaRYW9f/JBJDp33t8ZuLNJZeOQ0cUROAjW+fHKhRUH4+dd1np22KyfXhfUA6WQym8z2lsYOz/nV5VH851eXxcqFQ/BCVhzqIreYPTCuGFmPWERt14byqnhes38y0yu7dGDKtOGpN6itNMpi9aOw87zIIigox422UNC4yBLq+yVG+hlMSnI8/m3zxZOuWf1aR3mvoVyhifQXkG++j/57W7yGbjxEu832MZ18gYJb2j5t3bRIChrCmkJGMATPq4o871178X+JPXb+vvh4DQrw0BTPTJCrH4XU90PGtbslm9IOIcu3AEzpD17EW8M=\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get a single list\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/lists/{listId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet list by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"ListId\"},\"required\":true,\"name\":\"listId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with list data.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"description\":{\"type\":\"string\",\"nullable\":true},\"icon\":{\"type\":\"string\"},\"parentId\":{\"type\":\"string\",\"nullable\":true},\"type\":{\"type\":\"string\",\"enum\":[\"manual\",\"smart\"],\"default\":\"manual\"},\"query\":{\"type\":\"string\",\"nullable\":true},\"public\":{\"type\":\"boolean\"}},\"required\":[\"id\",\"name\",\"icon\",\"parentId\",\"public\"],\"title\":\"List\"}}}},\"404\":{\"description\":\"List not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/get-a-single-tag.api.mdx",
    "content": "---\nid: get-a-single-tag\ntitle: \"Get a single tag\"\ndescription: \"Get tag by its id\"\nsidebar_label: \"Get a single tag\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVMFu2zAM/RWBpw0QkmzosMK3FNiKbocVW4YdghwYm7HV2JYq0W0yw/8+UHbTJPW6y06WRYp85HtkCxmF1BvHxtaQwDWxYszVeq8MB2Uy0MCYB0iWsJDvSkOgtPGG95AsW1gTevLzhgtIlqtupcGhx4qYfIgOIS2oQkha4L0jSCCwN3UOGmiHlSvlypDJyt0+rx7vLj/a3e8PxY7ZppeS3HB0WWB+k0GnwdN9YzxlkLBvSEONldg52jUYqcIhFyBYPAVn60BB8r+fzeRzWvC39R2lrB4NF6o0gVWGjBPQkNqaqWZ5gc6VJkV5Mb0L8mykLBsDgQbnrSPPpk9qspeld0+oxwxNdWXttkK/DUcOdVOtyZ87XO3nzJgWlC2i27/AoBkNWTQV1iOWrjvp9xKiHCL0M6CvwlqdsDiEvZhdvCRjgbmqLauNbers/3GQ2my82RWFgPmY7azwGOHZf9X1RVTEhc0ggZxiVpFdAlMZmGkbFdmBzIt/eJqGxpeQQItZ5imEborOTB/egYYH9AbX5cBTb+4btMGmZEigYHYhmU7Z7ydb9LglchN0DvR5FwtSQwRlN4oLUl8Hf9VjEQ6OBvmH9LHPfDzOh6ZIZqkjukEyOIEeDp+tr1AQfvm1iJ0z9cbKc6m6h/RuMpvMjqb5gGd+ezOKf357ozbWn4KXYjsNzgYeBDsMv2wtVMHUeUmyvs5Dts9KGt9wfZ1MO566Ek0taSJV7UDqst+CGpJ+0aw0FDawGNp2jYF++rLr5Pq+IS+rcfXMaWQ+M0HOGSQbLAO9gvDN90F5b9XfsA2XWO+jdMpG/kDDlvaHXditZLQJM/IRQG+bpyk5Pnr1YrBEGQdhX39agAY81cMZ/zH6KKK27T0Wdkt11z0DlH8B2HV/ANg2NwA=\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get a single tag\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/tags/{tagId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet tag by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"TagId\"},\"required\":true,\"name\":\"tagId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with list data.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"numBookmarks\":{\"type\":\"number\"},\"numBookmarksByAttachedType\":{\"type\":\"object\",\"properties\":{\"ai\":{\"type\":\"number\"},\"human\":{\"type\":\"number\"}}}},\"required\":[\"id\",\"name\",\"numBookmarks\",\"numBookmarksByAttachedType\"],\"title\":\"Tag\"}}}},\"404\":{\"description\":\"Tag not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/get-all-bookmarks.api.mdx",
    "content": "---\nid: get-all-bookmarks\ntitle: \"Get all bookmarks\"\ndescription: \"Get all bookmarks\"\nsidebar_label: \"Get all bookmarks\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzNGMtu20bwV4i59EJYTo+6OUGbukUTA3HQg6HDiByKGy13md2hHEXgvxezpEjKXttUYqA9idqd92tn5gA5+cypmpU1sIT3xAlqnayt3Vboth5SYNx4WN7B2+FslYKnrHGK97C8O8Ca0JG7ariE5d2qXaVQo8OKmJwPAD4rqUJYHoD3NcES1tZqQgNtCo6+NspRDssCtacUDFYCgi4r1Y5ySEGJaF8bcnto0x+jVuDOisCz6Hl2ymwgBTJNJaqjzyANphLdcyqw0QzL7uQZrt46/uhycjOYmqZak3uOmlaV4nPEZ8VaDt41zttnSWcdxHxTT6zArqH0QRxdF4knTtgm4XaIp198klnDZDi5VxJnlCiT6SanPFEm4ZISR762xtNF8sEypQmXakTK0AiORrehpLAu8baiMVovojr+T0QbjN2TfdchPjD6ShTo2Hix+6+Xl/JzqsPH9RfKRE4uT9M1yZFRWPVSCSrWtVYZCuriixf8iF9toAgp1M7W5Fh13Mc6MIKic7gXqZkq/zIJlT8OzTaFzBEy5Vccva1srgoVv07BNFrjWkJbXNgOgT4Dcqgq0eoxqRLRe8bNRpnNJ0Zu/Mv8xgLimywjL9W0QKUbR2ImMrlgrdoUfFNV6NT34KXXJ28szzNPJ8d+HqxtXDaD7KSM1gpSuKe1BKiW78qulRZp6RuT8RLdKXhlNpqK7sIFtVRVW8dBl+45euVo7DIzcoHMmJWUv43aZFBMdCmbCg2s2pMSdCcce/on1ALcJEutoY9FeCyfV6C7fVoUrcw22KlxOqrR/Fw5qTkz4FWFG/ocY/sU8JX3xNcRl8TCLXNExpeWz8EqGq1vhFOX9+eg1o4yh/ea8h9A3qmc7FmC4k5lMw1dcqXfjaHzInwfZueIc9R8HgNsuLRunlWbtVa+pHnQOTLd9BjzBBeMv/vXYwbCg2QN0F3urKT/+blcZPrW1yz5iCVjV0Pn5Uxc1COPn5UVJTiCsOHr9gXwkL7CJC9GpFhsSWgrTR+iBfbJV2VuFfHq+5Rs30M/nQI/bObRKKOur2D1xmyNvY+8GgFh1Q6mff0nb5ab5TX5Y1JtpmX4aIlP05M1GkPuuo+OB9UX+roocH1jGWoSRIqtZKEn97nWFvMwtY22OiukYs/xqPuqjQKM3elJL5pOp9KTkfK0M4w3dH3vMgbj4NvVOKsdp2wQuQx94354O1vR6Qg/oTNhdYMbZUTNcbJv22CPiri0OSxh0zkHZbKHxZSkJ7c7jveh1YAD5rkj79sF1mqxeyPeRqdEwhCH/XU3zRwn6JK59svFgt3+YosOt0T1BdY1PBzbbktKegqJLcI49lcPn3SyiPCTzcQnGXH6KWaynxjMKJxDPAuYtPkBSEIzfPxuXYUi4Z//3AZfKFNYQRetO5HeXFxeXE7G7EGeq5vrqPxXN9dhMjwRXpSVd9F6lv5xeTiOibFdzIOWbKhq8cVNp6i8EItaowojTN8Wdi6dRskqhdJ6lsPDYY1eanDbynE3mYqjc+XFneNovaX96apmh7oRpmF78AT4SeLMQZiuUebAHxclc2CHzcfzwE8a/j9cJ0T922n1aMUwarca0zJs61IoCcW24uAO+yrLqJ5iPVohCJWhSLz/7Ra6HnA6NZ+m03SyQbOf0D4cOohbuyXTtnBUgeU/tFKg/wWnsCCK\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get all bookmarks\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/bookmarks\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet all bookmarks\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"boolean\"},\"required\":false,\"name\":\"archived\",\"in\":\"query\"},{\"schema\":{\"type\":\"boolean\"},\"required\":false,\"name\":\"favourited\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"enum\":[\"asc\",\"desc\"],\"default\":\"desc\"},\"required\":false,\"name\":\"sortOrder\",\"in\":\"query\"},{\"schema\":{\"type\":\"number\"},\"required\":false,\"name\":\"limit\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"title\":\"Cursor\"},\"required\":false,\"name\":\"cursor\",\"in\":\"query\"},{\"schema\":{\"type\":\"boolean\",\"default\":true,\"description\":\"If set to true, bookmark's content will be included in the response. Note, this content can be large for some bookmarks.\"},\"required\":false,\"description\":\"If set to true, bookmark's content will be included in the response. Note, this content can be large for some bookmarks.\",\"name\":\"includeContent\",\"in\":\"query\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with all bookmarks data.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarks\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"attachedBy\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\"]}},\"required\":[\"id\",\"name\",\"attachedBy\"]}},\"content\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true},\"description\":{\"type\":\"string\",\"nullable\":true},\"imageUrl\":{\"type\":\"string\",\"nullable\":true},\"imageAssetId\":{\"type\":\"string\",\"nullable\":true},\"screenshotAssetId\":{\"type\":\"string\",\"nullable\":true},\"fullPageArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"precrawledArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"videoAssetId\":{\"type\":\"string\",\"nullable\":true},\"favicon\":{\"type\":\"string\",\"nullable\":true},\"htmlContent\":{\"type\":\"string\",\"nullable\":true},\"contentAssetId\":{\"type\":\"string\",\"nullable\":true},\"crawledAt\":{\"type\":\"string\",\"nullable\":true},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\",\"nullable\":true},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true},\"size\":{\"type\":\"number\",\"nullable\":true},\"content\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"assetType\",\"assetId\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"unknown\"]}},\"required\":[\"type\"]}]},\"assets\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"tags\",\"content\",\"assets\"],\"title\":\"Bookmark\"}},\"nextCursor\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"bookmarks\",\"nextCursor\"],\"title\":\"PaginatedBookmarks\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/get-all-highlights.api.mdx",
    "content": "---\nid: get-all-highlights\ntitle: \"Get all highlights\"\ndescription: \"Get all highlights\"\nsidebar_label: \"Get all highlights\"\nhide_title: true\nhide_table_of_contents: true\napi: eJyNVU2P2zYQ/SvCnAl706NuRtGm2x6yQLfoYeHDWBpbjCmSGY42MQT+92Ik2ZY22qS+2KYe37z5euqhplSxjWKDhxI+khToXNHYU+PsqZEEBgRPCcoX+ON+uDeQqOrYygXKlx4OhEy866SB8mWf9wYiMrYkxGkApKqhFqHsQS6RoATftQdiyAaYvnSWqYbyiC6RAY+tIpxtrYABq8K+dMQXyGaFKQlbf1KdVpwe/NpxCj+krkbEknuv+BSDT5SU/peHB/1aFujT4TNVUny10rwpVFGj4AYMVMELedG7GKOzFerd7eekBCv6w0AJBiKHSCx2DD9rwR2LzHhR3UJt+jnHIYRzi3x+rL+vVzaQBFk+HY+JZLUz5OsfPK2CC7zWB/Jdq/NyIefCV9Cy1mDgxEQeDBxcRzpANR2xcwLlFZgNCH2TNUrfOYcH7a5wR9mAD0L/C2jXU+8S8TtVqZhQqN6tCMmLmXqZ13dZzXntpqwmzYOiW/x5tP19gm+bNkT09E2mmf55xm8ULhZ5RjQL9oQn61XDbL+zfgy0JE2ooYTTkEdE3W/YLkgT8et1yzt2UEKPdc2UUt5itNvXD2DgFdmqyGEqp8fjcl1noBGJqdxuhS+bMzKeieIGYwTzZgOfGyomhiIcC2mo+GvCF6MWVT8zqL914aZ9mNnUrZIaWfMYYFBOIB3U4cfvgVtUhX/++zy0w/pj0Oua9Sjpw+Zh8zAzoJue3dPjqv7d02NxDLwUr8lmAzEkaXGwismtVk15QdrfTecdCx9T1UHcRofWDzug3eqnri5GZW+gCUn0tO8PmOgfdjnr8eiV2uvaJu3o3VvPdJm59ivqmpcwePY72JsN38H7+6wMbxIDDWFNPIQcb+2qiuI8xHcuqyy30f342zMYwGXP3/R4YL9arL/MuPt+RDyHM/mc4Spd9D/kfc75P+nqe/Y=\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get all highlights\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/highlights\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet all highlights\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"number\"},\"required\":false,\"name\":\"limit\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"title\":\"Cursor\"},\"required\":false,\"name\":\"cursor\",\"in\":\"query\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with all highlights data.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"highlights\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"bookmarkId\":{\"type\":\"string\"},\"startOffset\":{\"type\":\"number\"},\"endOffset\":{\"type\":\"number\"},\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"],\"default\":\"yellow\"},\"text\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\",\"nullable\":true},\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"}},\"required\":[\"bookmarkId\",\"startOffset\",\"endOffset\",\"text\",\"note\",\"id\",\"userId\",\"createdAt\"],\"title\":\"Highlight\"}},\"nextCursor\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"highlights\",\"nextCursor\"],\"title\":\"PaginatedHighlights\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/get-all-lists.api.mdx",
    "content": "---\nid: get-all-lists\ntitle: \"Get all lists\"\ndescription: \"Get all lists\"\nsidebar_label: \"Get all lists\"\nhide_title: true\nhide_table_of_contents: true\napi: eJyNVE1v2zAM/SsGz0Kc7ehbDlvRbcAKLMMOQQ6MzdRqZEmV6G6Gof8+UHY+G2A7WRYfP98TR2go1kF71s5CBQ/EBRpTGB05ggLG5wjVBr7l/62CSHUfNA9QbUbYEQYKq55bqDbbtFUQKHpnI0WoRvi4XMrnOsP33QvVXPzW3J4zFQ0yLkBB7SyTZXFD742uUdzKlyi+I8S6pQ7lxIMnqMDlaKDAB+cpsJ4yT+WfYRgCDqBAM3Xx3+66ucBEDto+Q1JgsaO7hqsOb+0KbG8M7gxBxaGnpEDX94BJgcdAlh/vpL8TZQK8B5LtO+GsQ9ujAQWxw8BCXkN77A1DdbQlBa89heG/8vl+Z3R9Ad05ZwgtpCTEv/Y6UCOJdQPzsOZWLxo7hdkqYM0SPosL0m2UicNtStnSEbeugQqeKfOFojkojzqNFN4oxKzKPhioYMSmCRRjKtHr8u0DKHjDoKWjzPFsngR6HEvL7GNVlhyGxQEDHoj8Ar2HG45h3VIxRyjcvuCWiq8zvphqkYYu3ssPUe6U+fLVnGYpmaWPDJPZZhCo+fDZhQ6lwi+/1nne2u6duEvXU0kfFsvFEs5TPdWzenq8W//q6bHYu3BdvDQrXLvIHWaRTqp/txluNH96uO9XyNQg0x8uvUFtJX7maJxpPHGtoHWR5WIcdxjpZzApyfUs0s32TGLeOApawoZC5v1AA1SwqmvynNk2fX77t3tEaDnJ6eHTGhTgNRk3w8/Rj5vEDhexx3FCrN2BbEqg5iJY/iGJeP8CO0jVRg==\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get all lists\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/lists\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet all lists\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with all lists data.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"lists\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"description\":{\"type\":\"string\",\"nullable\":true},\"icon\":{\"type\":\"string\"},\"parentId\":{\"type\":\"string\",\"nullable\":true},\"type\":{\"type\":\"string\",\"enum\":[\"manual\",\"smart\"],\"default\":\"manual\"},\"query\":{\"type\":\"string\",\"nullable\":true},\"public\":{\"type\":\"boolean\"}},\"required\":[\"id\",\"name\",\"icon\",\"parentId\",\"public\"],\"title\":\"List\"}}},\"required\":[\"lists\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/get-all-tags.api.mdx",
    "content": "---\nid: get-all-tags\ntitle: \"Get all tags\"\ndescription: \"Get all tags\"\nsidebar_label: \"Get all tags\"\nhide_title: true\nhide_table_of_contents: true\napi: eJydVU1v2zAM/SsGz0LS7uhbWmxFt8MKLMMOgQ+MzcRqbEmV6KyB4f8+0HK+0zTYSXFMPj6ST88tFBRyrx1rayCFJ+IEqyphXAZQ0B/pDKZyZgoC5Y3XvIF01sKc0JOfNFxCOsu6TIFDjzUx+dAHhLykGiFtgTeOIIXAXpsldAo8vTXaUwHpAqtACgzWEiHHozWM2kh9LZzeGvIb6NTHgArINLUQ7WEUNAGXcnqqaI0mJyFf0AKbiiEdXl+hEazn/ymPGhSUTY0GFBhrCLIrVZAZ85KKh83tta7A5Y0P1t8AZZp6ThJomqrCeUWQsm/oCnSla306kEzCg7MmUBDwL3d3chzr6ef8lXJO/moud7pKCmQcgYLcGibDkoXOVTpHyRq/Bkm9wNv2YKDAeevIs46Fo0h3Ueg99gNlqsPn2bq4OOTY+KUXTf1g7apGvwrnMz0JeNhMhh1P+7DPyKC+CBkVdf6m6452NpNm1PYOHBG9SitTwJpFBnLRQUANvfNj1NMFuZ/o5oTE4B0HEFkXqdbEpS0ghSX1vaNYB4yHhEB+vbWOxleQQotF4SmEboxOj9f3oGCNXkvpOK74Ospue7lLZhfS8Zj9ZrRCjysiN0LnQJ1oc1pSMiAkdpFwScmPIT6JXGTCB673SwQZKx96324+Uln66MMgHYJADT++WV+jMPz+Z9oPWZuFlXTpOlK6H92N7mC/jh2fycvzRf6Tl+dkYf0xeWm2U+Bs4EE3wzU+cfcjuHZ/Hc++ArE9pnceuwq1EfR+Q+2ww2HpmYLSBpbntp1joN++6jr5O3qGbLbQQfa3t5gVbc6Nf41VIzV7A/sgZTDpW0KPnPaWhJ2X3hK8dcd9bLYXav9tVFASFuT7CcSkSZ6TO8w6s0BB2d2Yp69TUIDHgjsRWI++dUFz2GrbxoipXZHpOtgyZ3mGTu7nPyWsuhQ=\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get all tags\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/tags\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet all tags\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\"},\"required\":false,\"name\":\"nameContains\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"enum\":[\"name\",\"usage\",\"relevance\"],\"default\":\"usage\"},\"required\":false,\"name\":\"sort\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\",\"none\"]},\"required\":false,\"name\":\"attachedBy\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\"},\"required\":false,\"name\":\"cursor\",\"in\":\"query\"},{\"schema\":{\"type\":\"number\",\"nullable\":true},\"required\":false,\"name\":\"limit\",\"in\":\"query\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with all tags data.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"numBookmarks\":{\"type\":\"number\"},\"numBookmarksByAttachedType\":{\"type\":\"object\",\"properties\":{\"ai\":{\"type\":\"number\"},\"human\":{\"type\":\"number\"}}}},\"required\":[\"id\",\"name\",\"numBookmarks\",\"numBookmarksByAttachedType\"],\"title\":\"Tag\"}},\"nextCursor\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"tags\",\"nextCursor\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/get-bookmarks-in-the-list.api.mdx",
    "content": "---\nid: get-bookmarks-in-the-list\ntitle: \"Get bookmarks in the list\"\ndescription: \"Get bookmarks in the list\"\nsidebar_label: \"Get bookmarks in the list\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzNGMtu20bwV4i9tAUIyylSNODNCdrUfcRG46AHQ4cRORTXWu4yu0PZisB/L2aXIimZkunEQHuyzJ33e2YrMnSplRVJo0Ui3iNFC2NWJdiVi6SOqMBISUciFgRLJ5Jb8ad05MQ8Fg7T2kraiOR2KxYIFu1FTYVIbufNPBYVWCiR0DoP4NICSxDJVtCmQpEIR1bqpYgFPkBZKf4kUWbqYbMs7+/e/GwevvxUPBCZ9A1zl+RBmPtlJppYWPxcS4uZSMjWGAsNJQOoABALyQpVQIVo4pPsdV2yXuBSEXt7sHIZ5lArEkn4sscvB+V6hs5YurIZ2h3PzzXazThTXZcLtKeoKVlKmkCpE39nmHe1deYk6TRAPE17YYxC0GJghWDi/WC5zCOHFJGJ/GsXON+5KDWaUFN0L5WKFhhJnao6w2wXUhZdZbTDs+iDIYwjKmSPlIJmHAV2iVFubORMiX1Yno3q+D8RrTN2S/ZdQDww+pwVCGwc2/3H83P+s6/D1eIOU5aTCp+CUQYEzKEVhjGgqpRMgTFmd47RRtxpPCERi8qaCi3JwLSTegAK1sKGhSUs3dMkZPY4IptYpBaBMLug0dfSZDKX48+x0LVSsOCIZs81XXxPgASbFnKN2UgkN7HIYW24XB17J1gupV5+JKDaPc2vrxuuTlN0TjALqWqLbCbUGWPNm1i4uizByi/eSy9PXhuaZp4gx2YarKltOoHsoHpWUsTiHhccoIp/l2YhFfr6TqgdB3UsnNRLhXl4sF4tWVbGktcltJgXjsaQkCMPQARpgdnbUZt0irEuRV2CFvNmr/LcMseW/h41DzfIUqPxKvdN8LQC4fW4KErqlbdTbdWoRtNzZa/UTICXJSzx0xjbY8AXziE34knhllpE7QpDz8HKa6WumVPI++egVhZTC/cKs69AXssMzbMEhbVMJxq6oFK960PnSfg2zJ4jzk7zaQygpsLYaVatF0q6AqdBZ0B43WJME5wx/mq7xwSEg2T10CF35jz2fFsuEj60NYt/jCVjqKHTcmZc1B2Pb5UVODi8sP7XzRPgPn2ZSZb3SGOxxaEtFX4YLbBHu8rUKuLklyHZdnQ+ngJfbebeKL2uL2D1Wq+0uR/pGh5h3nSmffmWN8nN3E1+G1SbYRneWeLj8MsCtEZ72UbHQfUVbV1kuHaw9DVJjBRbzkKH9lOlDGTIPbS31bNCaqwd97rPm1GAfjrdm0UH8+PesHg4GY4PdO3s0gdj59t5v6K9bQ0jWC6ND9TubM9WtB/d9+gMWF3DUmpW820H2nhzvD5//XjV4KU60oai3NQ6e7kVIzXZ+OhVonMcRo/fDjT1FHp4dmnj8akwmUjEMkQYb/mJmPGa5GbbcAJoZkMzObTr3SnCj09iC1lm0blmBpWcrV9xBIOVbHUvfPscrLU7BhRElUtmM7KbsxVYWCFWZ1BV4nADvSkwailEJveb5R8tfBRkYYcMrigf2abtZja4pXQGYs4+RxmMVxcPxOnmf/xqbAks4e//3HgrSp0bRmetg0ivzs7PzgcXg06ei+vLUfkvri/9krsnPCvLvd444pk42e423lO3o4Nxswuv0wenoDh3wVmlQPo1rR19g8dv/bWH3Zt0Z5/e6fNYFMYRg223C3DceZqGP4c1nEMhk44d3t8RVrg5uOmsQdUsh7+VHIHfXW2mwHZnmNPAR032H942Rj0StHp07+i1m/eJNW7zo6p+/3dbCn6IjnHf9U29GfLs/RKuhdxXCgR2KEsQHi/SFKuhqI9KHYvelZr3v9yIMA4PDwj7WThc8vZF2m4DxI1ZoW6aTkLi/1nApvkXsx9wOw==\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get bookmarks in the list\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/lists/{listId}/bookmarks\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet bookmarks in the list\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"ListId\"},\"required\":true,\"name\":\"listId\",\"in\":\"path\"},{\"schema\":{\"type\":\"string\",\"enum\":[\"asc\",\"desc\"],\"default\":\"desc\"},\"required\":false,\"name\":\"sortOrder\",\"in\":\"query\"},{\"schema\":{\"type\":\"number\"},\"required\":false,\"name\":\"limit\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"title\":\"Cursor\"},\"required\":false,\"name\":\"cursor\",\"in\":\"query\"},{\"schema\":{\"type\":\"boolean\",\"default\":true,\"description\":\"If set to true, bookmark's content will be included in the response. Note, this content can be large for some bookmarks.\"},\"required\":false,\"description\":\"If set to true, bookmark's content will be included in the response. Note, this content can be large for some bookmarks.\",\"name\":\"includeContent\",\"in\":\"query\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with list data.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarks\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"attachedBy\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\"]}},\"required\":[\"id\",\"name\",\"attachedBy\"]}},\"content\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true},\"description\":{\"type\":\"string\",\"nullable\":true},\"imageUrl\":{\"type\":\"string\",\"nullable\":true},\"imageAssetId\":{\"type\":\"string\",\"nullable\":true},\"screenshotAssetId\":{\"type\":\"string\",\"nullable\":true},\"fullPageArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"precrawledArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"videoAssetId\":{\"type\":\"string\",\"nullable\":true},\"favicon\":{\"type\":\"string\",\"nullable\":true},\"htmlContent\":{\"type\":\"string\",\"nullable\":true},\"contentAssetId\":{\"type\":\"string\",\"nullable\":true},\"crawledAt\":{\"type\":\"string\",\"nullable\":true},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\",\"nullable\":true},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true},\"size\":{\"type\":\"number\",\"nullable\":true},\"content\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"assetType\",\"assetId\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"unknown\"]}},\"required\":[\"type\"]}]},\"assets\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"tags\",\"content\",\"assets\"],\"title\":\"Bookmark\"}},\"nextCursor\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"bookmarks\",\"nextCursor\"],\"title\":\"PaginatedBookmarks\"}}}},\"404\":{\"description\":\"List not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/get-bookmarks-with-the-tag.api.mdx",
    "content": "---\nid: get-bookmarks-with-the-tag\ntitle: \"Get bookmarks with the tag\"\ndescription: \"Get bookmarks with the tag\"\nsidebar_label: \"Get bookmarks with the tag\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzNWF9v2zYQ/yoCX7YBQpwOHVb4LQ22LhvWBquLPQR+OEsniTFFquTJiWvouxdHypLsyInSBtie4oj398e7493tRIousbIiabSYi3dI0cqYdQl27aI7SUVEBUYEuYgFQe7E/EYs+O8yFg6T2kraivnNTqwQLNqLmgoxv1k2y1hUYKFEQus8gUsKLEHMd4K2FYq5cGSlZrl4D2Wl+JNEmar7bV7e3b751dx/+aW4JzLJG1YuyZMsIL9KRRMLi59raTEVc7I1xkJDyefkz2Mh2Z0KqBBN/KhyXZfsFLhExB4Ndi3FDGpFYh6+HKjLQLlenzOWPtgU7V7n5xrtdlyprssV2sekKVlKmiCpM38Py2VtnXlUdBIonpa9MkYhaDFAISB8GCpXWeSQIjKRP+3C5gcXJUYTaorupFLRCiOpE1WnmEZS+3Cy6CqjHZ5F7w1hHFEhe6YENPMosDlGmbGRMyX2QXk26uP/xLQO7FbsZWA8An3JDgQ1jnH/+fyc/xz68GF1iwmFFFTSUZQCAWtojWEOqColE2CO2a1jtpHrNF6QiEVlTYWWZFDaWT0gBWthy8YSlu5pETJ9GJFNLBKLQJhe0OhpaVKZyfHjWOhaKVhxRPPNNV18T6AEmxRyg+lIJDexyGBjuFidOifIc6nzjwRUu6f19XXD1UmCzglWIVVtkWFCnTLXsomFq8sSrPzib+nlxWtD0+AJdmyn0ZraJhPEDqpnJUUs7nDFAar4d2lWUqGv7oTacVDHwkmdK8zCgfVuybIylrwv4X154WgMCTlyAESQFJi+HcWkc4x9KeoStFg2B5XnhjW28g+kebpBlhqNHzL/BD7uQDg9bYqSeu1xqq0a9Wh6rhyUmgn0soQcP42pPUV84RzS1ciVjIVbYhG1Kww9hyurlbpmTSHvn8NaWUws3ClMv4F5I1M0zzIUNjKZCHRBpbrsQ+dJ+jbMnmPO3vNpCqCmwthpqNYrJV2B06hTILxuOaYZzhx/t6/HBIajZPXUIXeW3PZ8Xy4S3rc1i3+MJWOoodNyZtzUvY7vtRU4OLyx/tfiCXKfvqwkzXqmsdji0JYK348W2JOvytQq4uSXodi2dT6dAt8Mcw9K7+sLoF7rtTZ3I6+GZ1g2HbQv/+RNumZ+Tf4YVJthGd4j8XH4ZQVao71qo+Oo+oq2LjJd21j6miRGii1noUP7qVIGUuQ3tMfqWSE19hz3vi+bUYK+Oz3oRQf940GzeNwZjjd0be/SB2N3t8t+RHvbAiPYLo331M5sz3a0b90P5AxUXUMuNbv5tiNtPByvz18/HDUWkEfaUJSZWqcvN2EkJh3vvEp0jqPo4dmRo15CT8832nh+Kkwq5iIPAcZD/lzM+ApmOz//N7MhRg7tZr+F8L2T2EGaWnSumUElZ5tXHL5gJUPuTW+PA1T7TUBBVLn5bEZ2e7YGC2vE6gyqShyPn4sCo1ZCZDI/Vv7V0kfBFr6NwQLlIyPajmWDNUoHD2v2CcpkPLd4Is41/+N3Y0tgC//8d+ExlDozzM5eB5NenZ2fnQ/WBZ09F9dXo/ZfXF/5CffAeHaWH3rjiBvi+W4/7j66NjpqNrvoemLZFFznR3BWKZB+Sms733DjN/ukm+93Pv2lL2NRGEdMtNutwPGz0zT8OczgHAqpdHzh/RJhjdujhc4GVM1W+EXJCfr9ymYKbbeDeZz4JGL/4WJj9D6CVw+WHb13yz6xxjE/6eqP/7SF4KfolPb9o6m3Q517q0JQ+He2QOD7ZAPC2UWSYDW09EGdY8u7OvPut4UIrfBweXCYhMMB79Ci3S5QLMwaddP0BvL/bGDTfAVlGWzq\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get bookmarks with the tag\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/tags/{tagId}/bookmarks\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet bookmarks with the tag\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"TagId\"},\"required\":true,\"name\":\"tagId\",\"in\":\"path\"},{\"schema\":{\"type\":\"string\",\"enum\":[\"asc\",\"desc\"],\"default\":\"desc\"},\"required\":false,\"name\":\"sortOrder\",\"in\":\"query\"},{\"schema\":{\"type\":\"number\"},\"required\":false,\"name\":\"limit\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"title\":\"Cursor\"},\"required\":false,\"name\":\"cursor\",\"in\":\"query\"},{\"schema\":{\"type\":\"boolean\",\"default\":true,\"description\":\"If set to true, bookmark's content will be included in the response. Note, this content can be large for some bookmarks.\"},\"required\":false,\"description\":\"If set to true, bookmark's content will be included in the response. Note, this content can be large for some bookmarks.\",\"name\":\"includeContent\",\"in\":\"query\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with list data.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarks\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"attachedBy\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\"]}},\"required\":[\"id\",\"name\",\"attachedBy\"]}},\"content\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true},\"description\":{\"type\":\"string\",\"nullable\":true},\"imageUrl\":{\"type\":\"string\",\"nullable\":true},\"imageAssetId\":{\"type\":\"string\",\"nullable\":true},\"screenshotAssetId\":{\"type\":\"string\",\"nullable\":true},\"fullPageArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"precrawledArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"videoAssetId\":{\"type\":\"string\",\"nullable\":true},\"favicon\":{\"type\":\"string\",\"nullable\":true},\"htmlContent\":{\"type\":\"string\",\"nullable\":true},\"contentAssetId\":{\"type\":\"string\",\"nullable\":true},\"crawledAt\":{\"type\":\"string\",\"nullable\":true},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\",\"nullable\":true},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true},\"size\":{\"type\":\"number\",\"nullable\":true},\"content\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"assetType\",\"assetId\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"unknown\"]}},\"required\":[\"type\"]}]},\"assets\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"tags\",\"content\",\"assets\"],\"title\":\"Bookmark\"}},\"nextCursor\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"bookmarks\",\"nextCursor\"],\"title\":\"PaginatedBookmarks\"}}}},\"404\":{\"description\":\"Tag not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/get-current-user-info.api.mdx",
    "content": "---\nid: get-current-user-info\ntitle: \"Get current user info\"\ndescription: \"Returns info about the current user\"\nsidebar_label: \"Get current user info\"\nhide_title: true\nhide_table_of_contents: true\napi: eJyNUz1v2zAQ/SvCmwnL7ajNQxukHRq0DjoYHs7SOWIsiQx5SmsI/O/FUUJiJx2iRRJ5H+/eezeh4VgH68W6ARV+soxhiIUdjq6ggxulkJaLegyBBynGyAEGQg8R1Q73kUPE3iByPQYrZ1S7CQemwGEzSotqt097g8DRuyFyRDXh83qtr+u+Pw6PXEvxx0qbmxQNCa1gULtBeBDNIO87W5NmlI9R0ybEuuWe9EvOnlHB5UIw8MF5DmLnpra5iIkS7PCAZDBQz+8vDIax6+jQMSoJIycD7sl2H4rsXE2dEnMRfXCuYxqQknLxNNrAjfJnG1wm7JM+Bj1L6xpUeOA8CSmTKJWXWPYM5Ts8K/VK9xg6VJioaQLHmErytnz+BINnClahZQKW65n5I42doEIr4mNVlhLOqxMFOjH7FXkP80aebcvFUqFwx2yJ70t8MWNBSunCCL9Ul7nzpR1eGNHOOkcOU4ZyEMzy8dWFnhTht9/bzJr6UdN16hnSp9V6tVYvWlH28YJnc3f7X/ybu9vi6MI1eB02GXgXpafsqNkSuGG5cn3eiLd1p1d7fnRz5vGF/0rpO7KDds8KTovOO2SdoTbQ1WpdFD2dpgNFvg9dSnr8NHLQddu/6py3zaBlatR9uwknPqPCpq7ZSzZEN2r3d4ukyr2Y7ubLFgZ0rdcbfXL15YqG80XtaZojtu7EQ0owCwjRfyS1+D9TYIbk\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get current user info\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/users/me\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nReturns info about the current user\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with user data.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\",\"nullable\":true},\"email\":{\"type\":\"string\",\"nullable\":true},\"localUser\":{\"type\":\"boolean\"}},\"required\":[\"id\",\"localUser\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/get-current-user-stats.api.mdx",
    "content": "---\nid: get-current-user-stats\ntitle: \"Get current user stats\"\ndescription: \"Returns stats about the current user\"\nsidebar_label: \"Get current user stats\"\nhide_title: true\nhide_table_of_contents: true\napi: eJylVkuPm0AM/ivIZxSyPXJL1cduH2rVZlVVUQ4OccI0MENnTFqK+O+VB5IAG7S77WnZ+PXZ/mxPDVtyiVUFK6Mhhi/EpdUucIzsAtyYkgNOKUhKa0lzUDqyEALj3kG8gntH1sE6BEdJaRVXEK9q2BBasouSU4hX62YdgiVXGO3IQVzDi/lc/gwDf9r8oISDX4pTH6RFMIMQEqOZNIsJFkWmEhST6IcTuxpcklKO8sVVQRCD8Z4ghMKagiyrNqou85fGHHK0B9fT1mW+IQtNKF9v8GisYppUWNgkVUfaTsmXvi7XZR+U40nhrdqnmdqnExqbE/KX1dLLHss2U/pw1RPTb74qQOfomqSR7v0slZWsV63jzs3JaC1+TfHK5Kh0PwG0FisIQTHl7nHQW++gp+fYKr0XdIkp9RPQdR5O+gIsx993bfibucfJmC0E9lf1h6Yr8bDSz0yGh9aPp9Jhm4A1StRLT776luumRxel94uE1dEP5qOAU+W+EU3QJlXuo9Ey0RPS74T2OnerW1Pafy9kOrS+OH4iJ7z9hREe0SusPu1GyT6XrFj9D6qtj3UGNW7vqRf9yvfqfK7qMBk/iLi/d7j/D+ZqzJ/H3BF4bz85gmPl/loeLeHhyr0s2N46HS/Ph6tysJoezP9o2q+PTq+o0ipJISdOzRZi2JMvIMpkQCSXy0U5Rf56gdxFe5QTKWextBnEUON2a8m5JsJCRccbCOGIVuEma6vfidsLucMyY4ghZS5cHEVsq9kBLR6IihkWBYSjM7pMKeg8BGbnT/f7Tj9osUDTNL2D/VXOZxu5f7bPPZbIkodXg7hTkkL5jzfG5igI331beiIovTNiLlm3kG5m89lcaqg4Ew9nPIvPd1fxLz7fBTtjh+Al2SaEwjjOUV9oCm+JB6+T4FT6geP68ox48hunLYBcuqjI5Kw0YdvDuuv3Cny/QeggNfKB1yGkxrFI63qDju5t1jTy88+SrDyQ1peO+/dRCCnhlqwnyYEqiGGRJFSwp0ZW+hEev3ykh2cSvn29FCYPOzfqlPd+Wgi66vmu61ZjaQ6kmwbCDgTL/9AI5f8C4yOBiw==\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get current user stats\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/users/me/stats\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nReturns stats about the current user\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with user stats.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"numBookmarks\":{\"type\":\"number\"},\"numFavorites\":{\"type\":\"number\"},\"numArchived\":{\"type\":\"number\"},\"numTags\":{\"type\":\"number\"},\"numLists\":{\"type\":\"number\"},\"numHighlights\":{\"type\":\"number\"},\"bookmarksByType\":{\"type\":\"object\",\"properties\":{\"link\":{\"type\":\"number\"},\"text\":{\"type\":\"number\"},\"asset\":{\"type\":\"number\"}},\"required\":[\"link\",\"text\",\"asset\"]},\"topDomains\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"domain\":{\"type\":\"string\"},\"count\":{\"type\":\"number\"}},\"required\":[\"domain\",\"count\"]},\"maxItems\":10},\"totalAssetSize\":{\"type\":\"number\"},\"assetsByType\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\"},\"count\":{\"type\":\"number\"},\"totalSize\":{\"type\":\"number\"}},\"required\":[\"type\",\"count\",\"totalSize\"]}},\"bookmarkingActivity\":{\"type\":\"object\",\"properties\":{\"thisWeek\":{\"type\":\"number\"},\"thisMonth\":{\"type\":\"number\"},\"thisYear\":{\"type\":\"number\"},\"byHour\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"hour\":{\"type\":\"number\"},\"count\":{\"type\":\"number\"}},\"required\":[\"hour\",\"count\"]}},\"byDayOfWeek\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"day\":{\"type\":\"number\"},\"count\":{\"type\":\"number\"}},\"required\":[\"day\",\"count\"]}}},\"required\":[\"thisWeek\",\"thisMonth\",\"thisYear\",\"byHour\",\"byDayOfWeek\"]},\"tagUsage\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"},\"count\":{\"type\":\"number\"}},\"required\":[\"name\",\"count\"]},\"maxItems\":10}},\"required\":[\"numBookmarks\",\"numFavorites\",\"numArchived\",\"numTags\",\"numLists\",\"numHighlights\",\"bookmarksByType\",\"topDomains\",\"totalAssetSize\",\"assetsByType\",\"bookmarkingActivity\",\"tagUsage\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/get-highlights-of-a-bookmark.api.mdx",
    "content": "---\nid: get-highlights-of-a-bookmark\ntitle: \"Get highlights of a bookmark\"\ndescription: \"Get highlights of a bookmark\"\nsidebar_label: \"Get highlights of a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVcGO2zYQ/RVhTi1ArJwiRQPdXKBNNzk0aLboYeEDLY0triWSGY42dgT+ezG0bEm7SjaHnCyTj8M3b94Me6gwlGQ8G2ehgLfIWW32dWP2NYfM7TKdbZ07tJoOoID1PkBxD78PSwE2CgKWHRk+QXHfwxY1Ia07rqG438SNAq9Jt8hIIQFCWWOroeiBTx6hgMBk7B4U4FG3vpElg6Zqjqd9+/nhzW/u+OXX+sjsyjfCwHCCXBjcVhAVEH7qDGEFBVOHCqxuBbQdQQqM5Oc11yCsCIN3NmAQJr+sVvIzl+KuxqwxgUWEURJQUDrLaFlOaO8bU2o5kT8EObaQoNs+YMmgwJPzSGzOl05ijlhNpE9ClrENL8eYJPhM0KggsCb+e7cLyJN927VbJNlHW31jt3SNo8VC2a4VF5ywadxnEDFF4D0hWlCwbToUX1S4013DUFyAUQHjkZdC2q5p9FYqKwWMCqxj/C6gWU69C0hfUaUk1IzVeoFInHnpfm6gqZpT7YasBs6J0fX+6W2b0b1/XWoP8emVE1tsYtp9vXr93J0X/2fWcbZzna1+nDNLVy1oHxW0GILeL+09SSJFGPEpkXSea1dBAfskW2rGAvKLyiHvR8FjPmu6gPR4GSEdNVBAr6uKMISYa2/yx1eg4FGTEXOkLIbts3QXJ9bMPhR5znS6OWjSB0R/o70HtdD9QwQZAFxj9n7AZ2cuUrrJ9Pso4g5dOZmBV6XkZskjwWQ2JZC0S/r401GrheG7/+6SnMbunByXrM+UXt2sblaTEXjls/5wu8h//eE22zmak5dkowLvArc62WIYli+M/ln4frTai0/GOX1pkdw32tjUnVLBfjDA2GZS6GLWc9NuUFC7wILv+60O+C81Mcrypw5JXp/N6IDkk8oE+a6g2Okm4Ddy+Omfwbw/Z1+jfBnQ9pSMJjOuAFBwwNP8pYmbqKBGXSElFmfAuizR8+ToswYVM10b5O0fd6BAzy30xDIp+iKtvj8j7twBbYxXliz/hWCM/wNdHrx1\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get highlights of a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/bookmarks/{bookmarkId}/highlights\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet highlights of a bookmark\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The list of highlights\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"highlights\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"bookmarkId\":{\"type\":\"string\"},\"startOffset\":{\"type\":\"number\"},\"endOffset\":{\"type\":\"number\"},\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"],\"default\":\"yellow\"},\"text\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\",\"nullable\":true},\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"}},\"required\":[\"bookmarkId\",\"startOffset\",\"endOffset\",\"text\",\"note\",\"id\",\"userId\",\"createdAt\"],\"title\":\"Highlight\"}}},\"required\":[\"highlights\"]}}}},\"404\":{\"description\":\"Bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/get-lists-of-a-bookmark.api.mdx",
    "content": "---\nid: get-lists-of-a-bookmark\ntitle: \"Get lists of a bookmark\"\ndescription: \"Get lists of a bookmark\"\nsidebar_label: \"Get lists of a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVE2P1DAM/SuVTyBF2wEtAvW2SIAWOCBYxGE0B0/rmWanTbKJu8xQ5b8jp52v3bJw4NSPPMfPz8/uoaJQeu1YWwMFfCDOGh04ZHaVYba0dtOi34ACxnWAYg5vx18BFgoClZ3XvINi3sOS0JO/6riGYr6ICwUOPbbE5EMChLKmFqHogXeOoIDAXps1KKAttq6RX5p01Wx36/bn7ZvXdvvrVb1ltuUbYaA5QfYMriuICjzdddpTBQX7jhQYbAW0PIIUaCnNIdcgrDwFZ02gIExezmbyOFfhpqakgohQ63Xd6HXNARSU1jAZlgh0rtElSkR+GyRsokC7vKWSQYHz1pFnPSRNCp/A0HvcCU+mNvw9XFePNYz7yicOzop7LL7pmgaXIq0oGBXocgoYUz/J8PVE+olbBsBEr03XipFaNB02oCC06FncVNEKu4ah2J9FBXcd+d0/5XPdstHlCXRpbUNoIJ65ZC7yjWKNpZ4UdrhmcfTbZx0Y4sNbhh4uYjq4nF0+dtHep5mxnK1sZ6r/56DSVtO9bikEXE+dPeCfbjjiUyEpnmtbQQFrSlllaArI9+MU8v44WTEfRJA94O/3U975Bgrosao8hRBzdDq/fwEK7tFr6VgqYDweVNu3vWZ2ochz9ruLDXrcELkLdA7UxICON8iMck3ZpxGfDVykYScL6pvoOmQ+XVMHkSSz1JFg4p0EAjW+vLe+RWH48cdNUlKblZVwqXqg9OJidjE72VIHPldfrif5X325zlbWn5OXYsXLNnCLyRHjPvvzYn4w3QeDPbXLh6KZtpy7BrWRnKlv/djx+WGBSnuLs206Ol9BbQMLtO+XGOi7b2KU3+PIzhfHlidjVDrIewXFCptATzB/9nU06vPsT2z3m9PskrOaTr5AwYZ259s/LqKCmrAin1gMgKuyJMcnoY+GUdxzGIYP725AAZ575oFH0u2TtPp+QNzYDZkYDyxZvoVgjL8BtGuT5g==\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get lists of a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/bookmarks/{bookmarkId}/lists\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet lists of a bookmark\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The list of highlights\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"lists\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"description\":{\"type\":\"string\",\"nullable\":true},\"icon\":{\"type\":\"string\"},\"parentId\":{\"type\":\"string\",\"nullable\":true},\"type\":{\"type\":\"string\",\"enum\":[\"manual\",\"smart\"],\"default\":\"manual\"},\"query\":{\"type\":\"string\",\"nullable\":true},\"public\":{\"type\":\"boolean\"}},\"required\":[\"id\",\"name\",\"icon\",\"parentId\",\"public\"],\"title\":\"List\"}}},\"required\":[\"lists\"]}}}},\"404\":{\"description\":\"Bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/karakeep-api.info.mdx",
    "content": "---\nid: karakeep-api\ntitle: \"Karakeep API\"\ndescription: \"The API for the Karakeep app\"\nsidebar_label: Introduction\nsidebar_position: 0\nhide_title: true\ncustom_edit_url: null\n---\n\nimport ApiLogo from \"@theme/ApiLogo\";\nimport Heading from \"@theme/Heading\";\nimport SchemaTabs from \"@theme/SchemaTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Export from \"@theme/ApiExplorer/Export\";\n\n<span\n  className={\"theme-doc-version-badge badge badge--secondary\"}\n  children={\"Version: 1.0.0\"}\n>\n</span>\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Karakeep API\"}\n>\n</Heading>\n\n\n\nThe API for the Karakeep app\n\n<div\n  style={{\"marginBottom\":\"2rem\"}}\n>\n  <Heading\n    id={\"authentication\"}\n    as={\"h2\"}\n    className={\"openapi-tabs__heading\"}\n    children={\"Authentication\"}\n  >\n  </Heading><SchemaTabs\n    className={\"openapi-tabs__security-schemes\"}\n  >\n    <TabItem\n      label={\"HTTP: Bearer Auth\"}\n      value={\"bearerAuth\"}\n    >\n      \n      \n      \n      \n      <div>\n        <table>\n          <tbody>\n            <tr>\n              <th>\n                Security Scheme Type:\n              </th><td>\n                http\n              </td>\n            </tr><tr>\n              <th>\n                HTTP Authorization Scheme:\n              </th><td>\n                bearer\n              </td>\n            </tr><tr>\n              <th>\n                Bearer format:\n              </th><td>\n                JWT\n              </td>\n            </tr>\n          </tbody>\n        </table>\n      </div>\n    </TabItem>\n  </SchemaTabs>\n</div>\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/remove-a-bookmark-from-a-list.api.mdx",
    "content": "---\nid: remove-a-bookmark-from-a-list\ntitle: \"Remove a bookmark from a list\"\ndescription: \"Remove the bookmarks from a list\"\nsidebar_label: \"Remove a bookmark from a list\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzNVMFu2zAM/RWBpw3Q4nTosMK3DOuAbsVQdBl2CHJgbCZWY1uqJKfJDP37QNmNkzYrdhiGnWzLj+Lj4yNbyMllVhmvdA0p3FKlNyR8QWKh9bpCu3ZiaXUlUJTKeZDgceUgncG1ct7BXIKjrLHK7yCdtbAgtGQnjS8gnc3DXIJBixV5si4CXFZQhZC24HeGIAXnrapXIIG2WJmSjxSpvNzuVtXD3cV7vf35rth6r7MLzq58hHD2qxyCBEv3jbKUQ+ptQxJqrBhQdgAJiusy6AsI8q+l/9CL8yKFxQA6ojHnCGd07cgxk7fjc34cd+KrFpmuPdVevDnqh3hAJzDPKaY+H4+fxz6yE1hawnwnau2Fqh872N/LcWhMqTLkuOTOcfAJifTijjIONFYbsl51tDOd03Mhg4SKnMPVqX9HWs26Gwb8PIQQSzohB/dbaDuowCUtdVPn/3tBMd4XOocUcirJMyIaIYWEO+KStjNrSPYzl7SDeQLwjNnN4wQ1toQUWsxzS86FBI1KNmcgYYNW4aLsiul/d0ousSk9pFB4b1yaJN7uRmu0uCYyIzQG5BO5pwWJ/gahl9F/X3q86LhACOFg+L+xxl3mwxWwF4wzcx0RxrMRQSD7l0/aVsgMP/+YRlVVvdQczlV3lM5G49H4YAT3fCY3Vyf5T26uxFLbY/JcbJBgtPMVRnf0w9pvPhwcdrz2ju5vB8v90crsNPC09YkpUdVMIbax7Z0wi/vKgYR0v7j2t/HpwS6ZSyi08xzUtgt09N2WIfDxfUOW1/B88EJ0TK4cv+eQLrF09EIxr257N78Wv+PdH2K9i5YrG/4CCWvaDWuXV+0/zHqgTpgHCQVhTjbW3gEmWUbGH4Q+2xNs5v2cfry8vpxeggQ8dvET18YEJ5m1bYeY6jXVIeyJev5mjiH8AkTPj4c=\nsidebar_class_name: \"delete api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Remove a bookmark from a list\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"delete\"}\n  path={\"/lists/{listId}/bookmarks/{bookmarkId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nRemove the bookmarks from a list\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"ListId\"},\"required\":true,\"name\":\"listId\",\"in\":\"path\"},{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"204\":{\"description\":\"No content - the bookmark was added\"},\"400\":{\"description\":\"Bookmark already not in list\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}},\"404\":{\"description\":\"List or bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/replace-asset.api.mdx",
    "content": "---\nid: replace-asset\ntitle: \"Replace asset\"\ndescription: \"Replace an existing asset with a new one\"\nsidebar_label: \"Replace asset\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy9VE2P4zYM/SsCT11AO54ttujCt2yxBaYFisE0ix6CHBibiTWxJa1EzyQ19N8Lys7XTDpAgUUvgSOR1HuPjxygplgF49k4CyU8kG+xIoVW0c5ENnajMEZi9Wy4UagsPStnCTQwbiKUC/js3LbDsI2w1BCp6oPhPZSLAVaEgcKs5wbKxTItNXgM2BFTiDkgVg11COUAvPcEJUQOxm5AA+2w860cGTJ1u9tvuufHTz+73d8/NTtmV30SBIZzyAHBXQ1JQ6BvvQlUQ8mhJw0WOwlanYI0GOHqkRtI+rvBmIlOb2LAKeICwHIMp8ifXb0XFJctmTeUVR/bwE6FqUfSEdBQOctkWRLR+9ZUKInFY5TsK+Tc6pEqBg0+OE+BDcWcO2F7pUK64LM4Bi5TGq+idzaORX68/fiawB9OTRjV+4OZMB5o1Cr2VUUxrvu23Yt4H68VOfRYWcdq7Xpbfz/qlavpCm8NHcWIm2t3LzTJFU7xWZqcz42rpdN9flX6XUJxsGIshpMrU5GlicUwCZxAxik8HYalDy2UMGBdB4oxFehN8fQBNDxhMLhqpzaO16OAa+xbhhIaZh/LouCwv9liwC2Rv0HvQV/x2lRBubXihtTvU7wasUBK6WzO/xSJx5fPp/2ol7wsPHKYTGEOAj19/OpCh4Lwt7/mWVRp3cNpGr4c5u/Mn2f9MXbt5E40Ggl8uLm9uT2bySP62f3dVbaz+zu1duGSqkiTNHgXucNspWmAj+tRwLysN5z8+J/26KgU044L36Kx8nRu9jA5ZnFcXhE0lBebbDSNHB/nUkPjIkvaMKww0tfQpiTH33oKspmXJ89kZ9UmyncN5RrbSG/Q+uFhMv079W/Ip0O0+2zNtpd/oGFL+8stLJv3f3z5IE9aJg0NYU0hkx9vfxkfej+XGqfsV1tFQI8Zs6oiz2/GLs82wP3Xubh+2vBd3jgQ8Bl0/s1AXeadhymfDdCi3fR5A8FYUmYEL0fsxUhlUlelGIYxYu62ZFM6KsPyX3RJ6R8BPcUh\nsidebar_class_name: \"put api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Replace asset\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"put\"}\n  path={\"/bookmarks/{bookmarkId}/assets/{assetId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nReplace an existing asset with a new one\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"},{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"AssetId\"},\"required\":true,\"name\":\"assetId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The new asset to replace with\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"assetId\":{\"type\":\"string\"}},\"required\":[\"assetId\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"204\":{\"description\":\"No content - asset was replaced successfully\"},\"404\":{\"description\":\"Bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/search-bookmarks.api.mdx",
    "content": "---\nid: search-bookmarks\ntitle: \"Search bookmarks\"\ndescription: \"Search bookmarks\"\nsidebar_label: \"Search bookmarks\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzNWEtv4zYQ/ivCXNoCQpzt0Tdv0EdadBM0WfQQ6DCWRhbXFKmQlBOvof9eDClLsiMn8m6A9hRHnMc3T85wBxnZ1IjKCa1gDneEJi2ipdbrEs3aQgwOVxbmD/Cx+5bEYCmtjXBbmD/sYEloyCxqV8D8IWmSGCo0WJIjYz2BTQsqEeY7cNuKYA7WGaFW0MRg6LEWhjKYO1NTDApLJniEGAQDeqzJbKGJTwuJgVRdMkK0KcTeIGDBkjaoUmK4GeVYSwfzwecD5TlK22u32rgbk5GZgELV5ZLMa9KkKIU7xx4nnOQPV7Wx+lXRaaB4W/ZSa0moYOCK4O/D8F/nkSUXOR350y4PfrBRqpUj5aInIWW0pEioVNYZZZFQkSsoMmQrrSxdRJ+0ozhyheiZUlTMI9GsKMq1iawuqc+yi1Eb/yfQOme3Yq8C45HTEzYgqLHs958vL/nPoQ03yy+UMk5XeGA2VJshW0vnVbWomBWrSooUmXX2xTL/SFy1lwgxVEZXZJwI2vv67UnRGNwyakelfVuEyEbrNTWEjrKFGz0tdSZyMX4cg6qlxCWnNoew6RJ9AiX7SWwoG0npJoYcN5rb0alzh6uVUKs7h662b+vrO4qt05Qsd8EchawNsZtIZcyVNDHYuizRiK8+Su8vXmk3zT0Bx3Yara5NOkHsoK9WAmJ4oiUnqOTfpV4KyWjp2ZGynN0xWKFWkvJwYLxZoqy0cd6WcI28czaGyhw5QOcwLSj7OOqTzjC2pahLVJA0By3ogTW28g+kebpBlWpFN7m/5F43IJyehiKFWns/1UaOWjS9Vg56zgR6UeKKPo+pPUW8sJbc9UhIxtItNUTKFtqdw5XXUt6yplD357BWhlKDT5Kyb2DeiIz0WUBxI9KJji5cKa/61HmTvk2zc+DsLZ+mAGtXaDPNq/VSClvQNOoMHd22HNOAM8df7e0xgeGoWD11qJ2E55/vq0VHz23P4h9jxRh66LSaGYe61/G9WJGTw4P1v+7fIPfly0qyvGcayy1ObSHp02iDPXmrTO0iVnwdim1n6NMl8M1u7p3S2/oOXq/VWumnkVvDMyRN59r3v/ImhZlvk98H3WbYhveeuBt+WaJSZK7b7DjqvtD2RaZrB0vfk2Ck2XIVWjKfK6kxI75De1+dlVJj13Fve9KMEvTT6cEsOpgfD4bF48lwfKBrZ5c+GbvYJv2utt+OgXEpenbt8na2ocPVeyBnoOoWV0Kxmf1G3jTeHyW5Qmcwh1UIDvJGDrNO5CwsHGwnmc1+O/cTB+wwywxZ28ywErPNBw46GsFAfTq2x2Gp2W/ThXOVnc9mzmwv1mhwTVRdYFXB8fZ2X1DUSoh07pefP1v6KGBhGwYPC3e86bTLzOB5ofMma/ZpzWQ87XsizlD/41dtSmSEf/xz70MiVK6Zna0OkD5cXF5cDrbtDs/i9noU/+L22i+IB+DZWL4etXU8Rs53+21x5CXlaDDretvoq0swk6+JWSVR+D2mnQ1DXA9TpY1sEkOhrePT3W6Jljty0/DnsKdyvDNhOaonFu0hrh//bhPzp+gUpDVt2+eaDcqaz/0DxAsdgW74sDKFfv90MoW2ewt5nfiksf/hA8Mrnn3x6NBbl/QV6t/dYigI2bcc5MC9SFOqhlwvHhVYStc2fvvlHsJUONyjDytruOug2g5k73aB4l6vSTUN7E1w/D803LL/BR6EDps=\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Search bookmarks\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/bookmarks/search\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nSearch bookmarks\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\"},\"required\":true,\"name\":\"q\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"enum\":[\"asc\",\"desc\",\"relevance\"],\"default\":\"relevance\"},\"required\":false,\"name\":\"sortOrder\",\"in\":\"query\"},{\"schema\":{\"type\":\"number\"},\"required\":false,\"name\":\"limit\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"title\":\"Cursor\"},\"required\":false,\"name\":\"cursor\",\"in\":\"query\"},{\"schema\":{\"type\":\"boolean\",\"default\":true,\"description\":\"If set to true, bookmark's content will be included in the response. Note, this content can be large for some bookmarks.\"},\"required\":false,\"description\":\"If set to true, bookmark's content will be included in the response. Note, this content can be large for some bookmarks.\",\"name\":\"includeContent\",\"in\":\"query\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with the search results.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarks\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"attachedBy\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\"]}},\"required\":[\"id\",\"name\",\"attachedBy\"]}},\"content\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true},\"description\":{\"type\":\"string\",\"nullable\":true},\"imageUrl\":{\"type\":\"string\",\"nullable\":true},\"imageAssetId\":{\"type\":\"string\",\"nullable\":true},\"screenshotAssetId\":{\"type\":\"string\",\"nullable\":true},\"fullPageArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"precrawledArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"videoAssetId\":{\"type\":\"string\",\"nullable\":true},\"favicon\":{\"type\":\"string\",\"nullable\":true},\"htmlContent\":{\"type\":\"string\",\"nullable\":true},\"contentAssetId\":{\"type\":\"string\",\"nullable\":true},\"crawledAt\":{\"type\":\"string\",\"nullable\":true},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\",\"nullable\":true},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true},\"size\":{\"type\":\"number\",\"nullable\":true},\"content\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"assetType\",\"assetId\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"unknown\"]}},\"required\":[\"type\"]}]},\"assets\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"tags\",\"content\",\"assets\"],\"title\":\"Bookmark\"}},\"nextCursor\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"bookmarks\",\"nextCursor\"],\"title\":\"PaginatedBookmarks\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/sidebar.ts",
    "content": "import type { SidebarsConfig } from \"@docusaurus/plugin-content-docs\";\n\nconst sidebar: SidebarsConfig = {\n  apisidebar: [\n    {\n      type: \"doc\",\n      id: \"api/karakeep-api\",\n    },\n    {\n      type: \"category\",\n      label: \"Bookmarks\",\n      items: [\n        {\n          type: \"doc\",\n          id: \"api/get-all-bookmarks\",\n          label: \"Get all bookmarks\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/create-a-new-bookmark\",\n          label: \"Create a new bookmark\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/search-bookmarks\",\n          label: \"Search bookmarks\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-a-single-bookmark\",\n          label: \"Get a single bookmark\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/delete-a-bookmark\",\n          label: \"Delete a bookmark\",\n          className: \"api-method delete\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/update-a-bookmark\",\n          label: \"Update a bookmark\",\n          className: \"api-method patch\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/summarize-a-bookmark\",\n          label: \"Summarize a bookmark\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/attach-tags-to-a-bookmark\",\n          label: \"Attach tags to a bookmark\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/detach-tags-from-a-bookmark\",\n          label: \"Detach tags from a bookmark\",\n          className: \"api-method delete\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-lists-of-a-bookmark\",\n          label: \"Get lists of a bookmark\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-highlights-of-a-bookmark\",\n          label: \"Get highlights of a bookmark\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/attach-asset\",\n          label: \"Attach asset\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/replace-asset\",\n          label: \"Replace asset\",\n          className: \"api-method put\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/detach-asset\",\n          label: \"Detach asset\",\n          className: \"api-method delete\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"Lists\",\n      items: [\n        {\n          type: \"doc\",\n          id: \"api/get-all-lists\",\n          label: \"Get all lists\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/create-a-new-list\",\n          label: \"Create a new list\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-a-single-list\",\n          label: \"Get a single list\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/delete-a-list\",\n          label: \"Delete a list\",\n          className: \"api-method delete\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/update-a-list\",\n          label: \"Update a list\",\n          className: \"api-method patch\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-bookmarks-in-the-list\",\n          label: \"Get bookmarks in the list\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/add-a-bookmark-to-a-list\",\n          label: \"Add a bookmark to a list\",\n          className: \"api-method put\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/remove-a-bookmark-from-a-list\",\n          label: \"Remove a bookmark from a list\",\n          className: \"api-method delete\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"Tags\",\n      items: [\n        {\n          type: \"doc\",\n          id: \"api/get-all-tags\",\n          label: \"Get all tags\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/create-a-new-tag\",\n          label: \"Create a new tag\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-a-single-tag\",\n          label: \"Get a single tag\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/delete-a-tag\",\n          label: \"Delete a tag\",\n          className: \"api-method delete\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/update-a-tag\",\n          label: \"Update a tag\",\n          className: \"api-method patch\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-bookmarks-with-the-tag\",\n          label: \"Get bookmarks with the tag\",\n          className: \"api-method get\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"Highlights\",\n      items: [\n        {\n          type: \"doc\",\n          id: \"api/get-all-highlights\",\n          label: \"Get all highlights\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/create-a-new-highlight\",\n          label: \"Create a new highlight\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-a-single-highlight\",\n          label: \"Get a single highlight\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/delete-a-highlight\",\n          label: \"Delete a highlight\",\n          className: \"api-method delete\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/update-a-highlight\",\n          label: \"Update a highlight\",\n          className: \"api-method patch\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"Users\",\n      items: [\n        {\n          type: \"doc\",\n          id: \"api/get-current-user-info\",\n          label: \"Get current user info\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-current-user-stats\",\n          label: \"Get current user stats\",\n          className: \"api-method get\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"Assets\",\n      items: [\n        {\n          type: \"doc\",\n          id: \"api/upload-a-new-asset\",\n          label: \"Upload a new asset\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-a-single-asset\",\n          label: \"Get a single asset\",\n          className: \"api-method get\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"Admin\",\n      items: [\n        {\n          type: \"doc\",\n          id: \"api/update-user\",\n          label: \"Update user\",\n          className: \"api-method put\",\n        },\n      ],\n    },\n  ],\n};\n\nexport default sidebar.apisidebar;\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/summarize-a-bookmark.api.mdx",
    "content": "---\nid: summarize-a-bookmark\ntitle: \"Summarize a bookmark\"\ndescription: \"Attaches a summary to the bookmark and returns the updated record.\"\nsidebar_label: \"Summarize a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVU2P4zYM/SsGT1tASLLFFrvwLT0UmPbQQWeKPQQ50DYda2JLWonOJGvovy8o53PG285hTnFEinx8fKQGqCiUXjvW1kAOS2YsGwoZZqHvOvSHjG3GDWWFtdsO/TZDU2WeuPcmJEPvKmSSs9L6agYKGDcB8hX8frwSYK0gUNl7zQfIVwMUhJ78sucG8tU6rhU49NgRkw/JIZQNdQj5AHxwBDkE9tpsQAHtsXOtHGnSVbs/bLrnpy+f7f77b82e2ZZfBIHm5HJCcFdBVODpW689VZCz70mBwU6ciouTAi0sOOQGBJWn4KwJFATJr4uF/NwS9njFwJmiZ83NiT9QUFrDZFguo3OtLlEuz5+CRJio1RZPVDIocN468qzH/Lp6zUdUUHqS7EuetHa20rWeNiswfdtiIVQJI/FM3Bs80ZeN3tE1psLaltBI2hp3Vtr9MzvjZqPN5oGR+/D/+RSQ6TvRVOjLkkIASaHb3pPQRKaSW+uoYGRdf08Uv394Y/lt9Jy6/yZf2/vyDWEvMNFpUPBMhairle/OFrqlNB9MJog4FQRtNi3Vo8GnsnTnrGdYx5t5WIm4rqV0I5yrZt909mUbp9lfxyjJPi0+vZ6e03xmxnJW295U7zcupa0mOJWRoBBwM2V7wUmKcPFPhaT73NhK1oQNKa1sixzmp/EP8+GyUuL8xImECuR3pxXX+xZyGLCqPIUQ5+j0fPcRFOzQa2l6quJoHqmrsW8ZcmiYXcjnc/aH2RY9boncDJ0DNbGdjhEyW6d1/dfRPxuxQIzxajs/CLlj5usdfWZKMksdyU1GOjmBOn78YX2HgvDPr4+JTm1qK9el6hHSx9litrha0Wc8y/u7SfzL+7ustv4WvBQbVepAh0kWx2X+cGI7w/M+fhl2uEjsnR68kRymPc9dizqtuNTf4aiO1fmVkTHJb56ci0DWChrRVL6CYSgw0L++jVGOv/Uky2S1vsgjiajSQb4ryGtsA/1HoR/+OSr7l+xniI+HaA5JhW0v/0DBlg63z2SUVdgQVuQTitFhWZbk+Orqq+kVpZ2n5/7vh0dZLbcCeyGoFH4S1zCMHo92SybGM0yW/4Iwxh/uYBE2\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Summarize a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/bookmarks/{bookmarkId}/summarize\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nAttaches a summary to the bookmark and returns the updated record.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The updated bookmark with summary\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\"]}}}},\"404\":{\"description\":\"Bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/update-a-bookmark.api.mdx",
    "content": "---\nid: update-a-bookmark\ntitle: \"Update a bookmark\"\ndescription: \"Update bookmark by its id\"\nsidebar_label: \"Update a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVk2P2zYQ/SvCnFpAtb1Figa6OUGLbj/QRbJBD4YPI3FkcS2RCkl5rRj678VQsj52lY2Krk4iOSSHb+bNmwsIsomRpZNaQQSfSoGOgljrY4HmGMR1IJ0NpIAQHB4sRDt41y1a2IdgKamMdDVEuwvEhIbMtnIZRLt9sw+hRIMFOTLWG9gkowIhuoCrS4IIrDNSHSAEOmNR5jwlSYr8XB+Kx4e3P+vzl5+ys3M6ecseSOdNrh7cCmhCMPS5koYERM5UFILCgo3iwSgEyY8r0WXAXvEOsu6dFjX7MoXgPqNAoMPA6aDycKyCv1VeBy6jIJWUCxvUugoeUbnBJlBEgocxBaXRJylIrCCERCtHyvE1WJa5TJCvWT9YvmsGEB0/UOIghNLokoyTZP1ek2TyxG/sLWOtc0LFCKR40hyFr63bqijQ1HO4qyrPMWZUGbwmBKUdPTdsevC/dUQIBZ7/JHXgJLjZbDZNCIkhdCS2bpEHlcnn7FJtCnQQQWUk+zOJ2oJjsXKZNotMyyrOpc1omTWH/67bIRbv+EsLmcqFGxydl2GH1pJ7P+TcNzb4j/lgS61sm2o/bjbzpGgTXfTF4fWyW86g8HLaNCEUHYALs2ph+jKG/4trDg8HqQ4fHbrKLqELqargsmqrJCFrOdNR5pUhhomU4F37nsTyi4f49Y+f5/0MPP+lmFhdmWRR0bi6iaWEEB4p5uzK+b/QsczJS4QjZTkfQ7BSHXJK2wXjnyWLUhsH+2YiCTvw2jWk0iRxRsGeRPZpGOfR37fsebN585wwV4kKlHZBqislXo8uiRbzJboga/Ewt/YEE3/CYL/v6kBBLtOiVcok43tZMSNYXylv15dBVhuGhczpKu6+bsMFhTBkbbPGUq5PNxDCCY3kWLdK1i63iKVY5VzSM+dKG63XztSrIxo8EpUrLEsIZ+pQd0KgUy/Jf3T2QesLNE0z6ks+MqbtzePupAeIb+Z3eDNmsjeCsPv59So6v/9z71HkWH0Y2odfrm3LWKLbjB4XinamZ86IBp51w/ja4fQToxo4TLZI98MpRP30VfCGmZGujXZP5Wu6MKjUyEcvRqN7JpozSkepUs3IcIa0vt2sNqvN6Jl97LZ3t7Ox3t7dBqk200BzYrBKa+sK9MzpWr6ue8WxRD1pE3oKvtzqtsnBL12XOUpf2bu+pOXEru8vuTpEo2ZzH0KmrWOTyyVGS59M3jQ8/bkijv5uP1DCE0dIy/8CohRzSy/4/N2HjsTfB1/zsptEVXvm5RWPIIQj1dOmuOGqnxEKTofdpTPo4vjDPR8zHPCsXDXhdcc2Sah0L9ruR6Xlbnv//jfmV9d8F76YgcFHruT42Hqr/eM9bf3cBXJUh8oXN2gPbbqebizDU/L6Z83icbm0Fvf6SKppengcjxmZpvkXS5iHXA==\nsidebar_class_name: \"patch api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Update a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"patch\"}\n  path={\"/bookmarks/{bookmarkId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nUpdate bookmark by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The data to update. Only the fields you want to update need to be provided.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"summary\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true,\"maxLength\":1000},\"createdAt\":{\"type\":\"string\",\"nullable\":true},\"url\":{\"type\":\"string\",\"format\":\"uri\"},\"description\":{\"type\":\"string\",\"nullable\":true},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true},\"text\":{\"type\":\"string\",\"nullable\":true},\"assetContent\":{\"type\":\"string\",\"nullable\":true}}}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The updated bookmark\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\"]}}}},\"404\":{\"description\":\"Bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/update-a-highlight.api.mdx",
    "content": "---\nid: update-a-highlight\ntitle: \"Update a highlight\"\ndescription: \"Update highlight by its id\"\nsidebar_label: \"Update a highlight\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVcGO2zYQ/RVhTi3A2k6RooFubtAi2x6ySB30YOhAi2OLa4lkyNGuVYH/Xgwl2/Kusi3Q+CRrnmbePL4Z9qAwlF470tZADp+dkoRZpQ9VrQ8VZbsu0xQyrUAAyUOAfAsfztEAhYCAZes1dZBve9ih9OjXLVWQb4tYCHDSywYJfUiAUFbYSMh7oM4h5BDIa3MAAXiSjav5lUat6lN3aJ4e3v1sT3//VJ2IbPmOKWhKkAuFOwVRgMcvrfaoICffogAjG0ZVE5QAzQ06SRUwL/4EA/1iVcdsbmXYVJgpSTIjm7VJkkX20dRdRhVme421Clln2+xJGrpiMoOo+O8OM+fto1aoFiCgtIbQEJeRztW6lFxm+RC41owkdveAJYEA561DTxoDR0tbWz+rnGkbPpcO69o+AffG/R48ogEBu7pFKGL6cSw4a8KQ8sfVar75oSF1NcK3a2Nn7bGR/ninXvYSBQSSnj7u9wFpEjdts0PPcTTqlej/00iAwr1sa4L8DIwCCE80l9K0dS13bEc2XRRgLOF/Aur51tuA/iuqlB75PNYzROKN/7dTfW/VnGo3djVyTowu9afVipmRg8FIb1dvX3rnAsqMpWxvW6O+5QCoGX2jgAZDkIe52DNtUoYrvhhnokGqrBq2Q1lxXd4SOSwv9g/LfrJMIiuL/vG81FpfQw69VMpjCHEpnV4+vgEBj9JrPvlEfwwPop1tVhG5kC+X5LvFUXp5RHQL6RyImakcM2R2nxbRHyM+G7jwuUz28Z+s6jhyk618kYgrcx8JBvkI4llID79Z30hm+Ptfm6Qjn9an69L89byuL4tpMjLa7C1HWKGB/pvFarGaLPAL9/X93Wyv6/u7bG/9baMsTBTgbKBGJu+Mm368t+TNwrpJ2l9d+C+33CAPz8fS1VKbNJp8wv3oi+31YgkgIJ9eM4WAygZiUN/vZMDPvo6RX39p0fMlWVxtkcyjdOBnBfle1gFfof3dp9HK32df4zm+lKZL7uOtlgMIOGL37D6MRRRQoVToE40B8X4o9sOG81wzvJjaKM5frMsSHb2KLSYTdr/evP/AJhvv3SbNNHiZdrJ8Guja1H3ybnrXQy3NoU0zDkNStqS8dfQzB6e2ZgXp+wGxsUc0MV70If7PysT4D5kaH2M=\nsidebar_class_name: \"patch api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Update a highlight\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"patch\"}\n  path={\"/highlights/{highlightId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nUpdate highlight by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"HighlightId\"},\"required\":true,\"name\":\"highlightId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The data to update. Only the fields you want to update need to be provided.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"]}}}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The updated highlight\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarkId\":{\"type\":\"string\"},\"startOffset\":{\"type\":\"number\"},\"endOffset\":{\"type\":\"number\"},\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"],\"default\":\"yellow\"},\"text\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\",\"nullable\":true},\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"}},\"required\":[\"bookmarkId\",\"startOffset\",\"endOffset\",\"text\",\"note\",\"id\",\"userId\",\"createdAt\"],\"title\":\"Highlight\"}}}},\"404\":{\"description\":\"Highlight not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/update-a-list.api.mdx",
    "content": "---\nid: update-a-list\ntitle: \"Update a list\"\ndescription: \"Update list by its id\"\nsidebar_label: \"Update a list\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVk1v4zYQ/SvEnFqAtZ1iF13o5i5aNO0CDbZe9GDoMBbHFhOJ1JJUYlXQf18MJVtyrGRRID4YIjmaj/feDNWCIp85XQVtDSTwpVIYSBTaB7FrhA5eaAUSAh48JFv4pH3wkErwlNVOhwaSbQs7QkduXYcckm3apRIqdFhSIOejgc9yKhGSFkJTESTgg9PmABLoiGVV8JYmrYpjcyif7j/8Yo//vc+PIdjsA0fXIZpw9FsFnQRHX2vtSEESXE0SDJZsUPQGEjQXU2HIgbNha/LhV6sazuGy5E1OQmFAEayoY/kL8bcpGhFyEntNhfKisbV4QhNGG2GIFC93JCpnH7UitQAJmTWBTOAwWFWFzpDDLO89x5oBwu7uKQsgoXK2Ihc0eT7t67mGq9TmE5kDA30jocTjebVadfKysuu3TV0UuGMoe9Qm3lYX3t5Hbzqbc9NFdskw0N+N0Un4WpNrvlcLO613hc4mhjtrC0IDXfwxi76yxvcA/bxazVPZ06OihN+ODz1TaydfoOn/EvFmUPcGM01m6pK7t0RTYwESfIkucBsr2mNdBEhOZ68RdhXvFcqmLbqFOEMiWEOpk8LObtLLRoee9Xerd9dE87kwNoi9rY16O5ozq+b5LMl7PMydPas0ehjt00G7JYXcqn4mZXksn3UPS5apX7b94OqYGnKPp7FZuwISaFEpR953S6z08vEGJDyi08xETHo47lE60ZmHUPlkuQyuWTygwweiaoFVBXKmZwYPwu7j0PtrsBd9LkzEZOL/w1j2kadz/wwMR+Y6ohlrIhqBHB5+t65EzvDPfzcRPebo8zigfztdCKchOOrvMvHzdt8743psmXFv0PTEaJDuqf/M3nJIhr53f7NYLVaTu+cMyvrudhbE9d2t2Ft3iSAjzo1ifSjRTIoaLlo8zalnA+Os55dv5B7tQMewrArUhuNEwbSDuLbxOvQgIRnuxVRCbn3go7bdoacvrug63h7w2aajtqIClfb8rCDZY+HplTx/+Dx0wY/ipeyGTTRNlHBR8wokPFAz3t1d2knICRW5mEF/+LGP89OGXYwvX/V6J09vrLOMqvCqbTrpy7v15uMfLNLhG6GMkwAcPoGM/zFTGwuP2o97LRRoDnWcDNA7ZUnjZUc864BY1iwWbdtbbOwDma47QxN4zch03Tdsok9Y\nsidebar_class_name: \"patch api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Update a list\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"patch\"}\n  path={\"/lists/{listId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nUpdate list by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"ListId\"},\"required\":true,\"name\":\"listId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The data to update. Only the fields you want to update need to be provided.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\",\"minLength\":1,\"maxLength\":100},\"description\":{\"type\":\"string\",\"nullable\":true,\"minLength\":0,\"maxLength\":500},\"icon\":{\"type\":\"string\"},\"parentId\":{\"type\":\"string\",\"nullable\":true},\"query\":{\"type\":\"string\",\"minLength\":1},\"public\":{\"type\":\"boolean\"}}}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The updated list\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"description\":{\"type\":\"string\",\"nullable\":true},\"icon\":{\"type\":\"string\"},\"parentId\":{\"type\":\"string\",\"nullable\":true},\"type\":{\"type\":\"string\",\"enum\":[\"manual\",\"smart\"],\"default\":\"manual\"},\"query\":{\"type\":\"string\",\"nullable\":true},\"public\":{\"type\":\"boolean\"}},\"required\":[\"id\",\"name\",\"icon\",\"parentId\",\"public\"],\"title\":\"List\"}}}},\"404\":{\"description\":\"List not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/update-a-tag.api.mdx",
    "content": "---\nid: update-a-tag\ntitle: \"Update a tag\"\ndescription: \"Update tag by its id\"\nsidebar_label: \"Update a tag\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVMGO2zYQ/RViTgnA2k6RooFubpCg2x66SB3kYOgwFscWdyWRIUe7VgX9ezCkdu3deoMc4oNtcZ44b2bemxEMxSpYz9Z1UMBnb5BJMR7UblCWo7IGNDAeIhRb2MhvqSFS1QfLAxTbEXaEgcK65xqKbTmVGjwGbIkpxASIVU0tQjECD56ggMjBdgfQQEdsfSNHlqxpjsOhvb9597s7/vdbfWR21TtJbjlBNni4MjBpCPS1t4EMFBx60tBhK3FOcQ1WCvHINQgXAVPkP5wZhMHTcjc1KYOMip3qU+kL9U/XDIprUntLjYlqcL26x45PGNURGXnckfLB3VlDZgEaKtcxdSxp0PvGVihpljdRcl1og9vdUMWgwQfnKbClKNFczvNmTekj9UTvupihv65Wl4vKRI0M8ucRs+YCLf0i3/NBbSHpKEHLXMjb1dsL3PGgOsdq7/rO/DzmlTOXKGpoKUY8/AD9dMMJX87TaIlrZ7Leqlryiu4KWIpjlmOS5ARimHD3YIc+NFDAiMYEinFaorfLuzeg4Q6DxV2TKc/h3KI99g1DATWzj8VyyWFY3GLAWyK/QO9BX9DAfINy+yTnv2e8ylxEUGdO/lc6mTOf+/mxLZJZ6kgwKGYQ6PnPRxdaFIZ/fdmk3smEPp2s9+HB6A/yPpuB7fZOAtKgzP7NYrVYnTn/kfr6+upiqevrK7V34Wmd0pdJg3eRW+zOUs9LDmd3PLluPEnuxWWYO8J05KVv0HaSJQ11nMe/zQtTQ5F3UqmhdpElMI47jPQ5NNMkx197CrJFy9P0k0aMjfLfQLHHJtJ3SL76NKv0tXqJ23yI3ZBE1vTyBBpuaXhcm1M5aagJDYVEIMfe5zS/bOSG07v/s+KkH95YVxV5/i62PLPN9Xrz/k9R0bye22RUCHgPOn0noi7VncSZzkZosDv0ybiQLxXN4VPJPpNoKutiK8YxIzbulrppOnVGnqUz0/QN/t1xKA==\nsidebar_class_name: \"patch api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Update a tag\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"patch\"}\n  path={\"/tags/{tagId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nUpdate tag by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"TagId\"},\"required\":true,\"name\":\"tagId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The data to update. Only the fields you want to update need to be provided.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"}}}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The updated tag\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"}},\"required\":[\"id\",\"name\"]}}}},\"404\":{\"description\":\"Tag not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/update-user.api.mdx",
    "content": "---\nid: update-user\ntitle: \"Update user\"\ndescription: \"Update a user's role, bookmark quota, or storage quota. Admin access required.\"\nsidebar_label: \"Update user\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzFVk1v4zYQ/SvEXNoCjD92txfdvF+A20u6TdCDYRRjcWxxLZEMSSXrCvzvi6Fkx7GdPRRZRAdbIofDN28eZ9iBolB67aK2Bgq4dQojCRRtIP9LEN7WJMXK2m2DfivuWhtRCutFiNbjhvqRkZipRhuBZUkhCE93rfakRiAh4iZAsYBsAEsJgcrW67iDYtHBitCTn7WxgmKxTEsJDj02FMmHbBDKihqEooO4cwQFhOi12YA8wX1TkZh/FHYtYkUZvIhWtDkakEDfsHE1r+epf6dv3kKSsMcJRfQtSTDY7E3mCiRo9uwwVsDI2JpCfG/VjvGU1kQykV/RuVqXyFDGXwPjuQDcrr5SGUGC89aRj5oCzzLBl8Ij0zbMG4MBCdjTlyTsc/EXE3+0UptIm2xr2rrGFbvtw2q00Q17myQJQ97+5+qVtw+B/AePD7U2m0+GLdWRn5W1NaE585PSacZuOUd9goTCiMdZ2tMyhH0W9HQymZyG8vtk/yR+OF/BWRN6mt9MJvz3LAQlQpvFu27regfypdI7eL3AUHoiwMXBctmjf3cJ8HtUYpChuBJzc4+1VkIb18bMIR/MEo2xcc+sfTBikNALRUTeW3+u2NNwerNDMNML7BtsY2W9/o+UuBJcBMjEAdahhrw28LfnwD9bv9JKkWHUl+rea2N+94zUWRdr25pXBZgkNBQrq7i2ttkjV9gCxvmwj1mtYdz1RTgBNwx/v28Hra+hgA6V8hRCGqPT4/spSLhHr7naZFDDdM/CGts6QgFVjC4U43H0u9EWPW6J3Aidu9hKBg/7fvLnYC96LJBSOupkfzNX/c7H/ezACe/McWQzrgDZiKtafvlsfYOM8I9/bjJxnIMvj83m0wtVRe5na8tumM4+1uloMppwl9Yxez4EOrueXyRmdj0Xa+ufssIsJgnOhthgls/QSYfbxFCAnng7aqA/4dLREx/pWxy7GrVheFk73aC2xYHErDeQUAxtfymhsiGySdetMNCtr1Pi4buWPF9blo9yy6JUOgxtcI11oB8E+uuXAeRv4jmUwyCaXVZ13fIXSNjS7vFqkvgiUBEq8hlBP/mh3+fqhl08Lj473knuV8zKklz8oe3y6Lhe395k1fUXoMYqXuLxAWT+zThtDru/2vBYBzWaTYsbtu1dssbx6RE5ORI5qItMdF1vcWO3ZFI6EBP5m3lJ6Tty27rt\nsidebar_class_name: \"put api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Update user\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"put\"}\n  path={\"/admin/users/{userId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nUpdate a user's role, bookmark quota, or storage quota. Admin access required.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The ID of the user to update\",\"example\":\"user_123\"},\"required\":true,\"name\":\"userId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"role\":{\"type\":\"string\",\"enum\":[\"user\",\"admin\"]},\"bookmarkQuota\":{\"type\":\"integer\",\"nullable\":true,\"minimum\":0},\"storageQuota\":{\"type\":\"integer\",\"nullable\":true,\"minimum\":0},\"browserCrawlingEnabled\":{\"type\":\"boolean\",\"nullable\":true}},\"description\":\"User update data\",\"example\":{\"role\":\"admin\",\"bookmarkQuota\":1000,\"storageQuota\":5000000000}}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"User updated successfully\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"success\":{\"type\":\"boolean\"}},\"required\":[\"success\"]}}}},\"400\":{\"description\":\"Bad request - Invalid input data or cannot update own user\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"error\":{\"type\":\"string\"}},\"required\":[\"error\"]}}}},\"401\":{\"description\":\"Unauthorized - Authentication required\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"error\":{\"type\":\"string\"}},\"required\":[\"error\"]}}}},\"403\":{\"description\":\"Forbidden - Admin access required\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"error\":{\"type\":\"string\"}},\"required\":[\"error\"]}}}},\"404\":{\"description\":\"User not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"error\":{\"type\":\"string\"}},\"required\":[\"error\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.28.0/api/upload-a-new-asset.api.mdx",
    "content": "---\nid: upload-a-new-asset\ntitle: \"Upload a new asset\"\ndescription: \"Upload a new asset\"\nsidebar_label: \"Upload a new asset\"\nhide_title: true\nhide_table_of_contents: true\napi: eJyNVE1v2zAM/SsGz26c7uhbtqFANmAL0BQ7BDnQNlOrsS1VotNlhv77QMlNnDQDdpMlfrzH98wBKnKlVYaV7iCHJ9NorBJMOnpL0DliSIHx2UG+gYV8O9im4KjsreIj5JsBCkJLdtFzDflm67cpWHrtyfFnXR0hH65arGtKKmRMWCelJWRKuKbYLHlTXM8ghVJ3TB1Ldts3rAxaznbatneSKteurKkNJz4aghx08UKlwDVWG7KsyMnrTjUUohTLAR5UQ9K6oKQPZKkC7yNmZakSoiFn6328d0Z3Lhb7NJ9/JPSVGFXjEix0z4FLpFWdBjhhg8Y0qkRJzV6c5P8/lVBuWU0CHVvVPYM/tViH+xvvTv2ZPnR9W5CVB+H6A9tbWVdTeW9/2WwsPSm0TU/DDo4BHyfZEte6ghyMdoEcimMgw2grcZU9kHXBVL1tIIcBq8qScz5Do7LDPaRwQKuwaMaJxOeoyQ77hiGHmtm4PMvYHmd7tLgnMjM0BtIbThwrJHoXlPs+xicRi0Cf2P1RhIqdp6Y/DU46C48QBvkYBOl4eNC2RUH47dc6TFd1Oy3pwjpCup/NZ3M4D/CEZ7Fa3sS/WC2TnbaX4IWsT8OcWwwW64LCt3/vi6LD2az/WAaRKtNvzkyDqpNOQa1hVHR0SlgUtUidb2AYCnT0ZBvv5fq1JyvLY3vWM+yOFGrCimywwJ6OkMOXCOduNNsBm17639oKPn1PWpQlGZ6Ef/jtRNaTIVc/H9ei0rivWl1JjhQOddPzMYLES9mvZA7gxyfsjhMUwxAj1npPnfeQjnBZvsHLxvkLeuzpPg==\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Upload a new asset\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/assets\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nUpload a new asset\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The data to create the asset with.\",\"content\":{\"multipart/form-data\":{\"schema\":{\"type\":\"object\",\"properties\":{\"file\":{\"title\":\"File to be uploaded\"}},\"required\":[\"file\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Details about the created asset\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"assetId\":{\"type\":\"string\"},\"contentType\":{\"type\":\"string\"},\"size\":{\"type\":\"number\"},\"fileName\":{\"type\":\"string\"}},\"required\":[\"assetId\",\"contentType\",\"size\",\"fileName\"],\"title\":\"Asset\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/01-intro.md",
    "content": "---\nslug: /\n---\n\n# Introduction\n\nKarakeep (previously Hoarder) is an open source \"Bookmark Everything\" app that uses AI for automatically tagging the content you throw at it. The app is built with self-hosting as a first class citizen.\n\n![Screenshot](https://raw.githubusercontent.com/karakeep-app/karakeep/main/screenshots/homepage.png)\n\n\n## Features\n\n- 🔗 Bookmark links, take simple notes and store images and pdfs.\n- ⬇️ Automatic fetching for link titles, descriptions and images.\n- 📋 Sort your bookmarks into lists.\n- 🔎 Full text search of all the content stored.\n- ✨ AI-based (aka chatgpt) automatic tagging and summarization. With supports for local models using ollama!\n- 🤖 Rule-based engine for customized management.\n- 🎆 OCR for extracting text from images.\n- 🔖 [Chrome plugin](https://chromewebstore.google.com/detail/karakeep/kgcjekpmcjjogibpjebkhaanilehneje) and [Firefox addon](https://addons.mozilla.org/en-US/firefox/addon/karakeep/) for quick bookmarking.\n- 📱 An [iOS app](https://apps.apple.com/us/app/karakeep-app/id6479258022), and an [Android app](https://play.google.com/store/apps/details?id=app.hoarder.hoardermobile&pcampaignid=web_share).\n- 📰 Auto hoarding from RSS feeds.\n- 🔌 REST API and multiple clients.\n- 🌐 Multi-language support.\n- 🖍️ Mark and store highlights from your hoarded content.\n- 🗄️ Full page archival (using [monolith](https://github.com/Y2Z/monolith)) to protect against link rot.\n- ▶️ Auto video archiving using [yt-dlp](https://github.com/yt-dlp/yt-dlp).\n- ☑️ Bulk actions support.\n- 🔐 SSO support.\n- 🌙 Dark mode support.\n- 💾 Self-hosting first.\n- ⬇️ Bookmark importers from Chrome, Pocket, Linkwarden, Omnivore, Tab Session Manager.\n- 🔄 Automatic sync with browser bookmarks via [floccus](https://floccus.org/).\n- [Planned] Offline reading on mobile, semantic search across bookmarks, ...\n\n**⚠️ This app is under heavy development.**\n\n\n## Demo\n\nYou can access the demo at [https://try.karakeep.app](https://try.karakeep.app). Login with the following creds:\n\n```\nemail: demo@karakeep.app\npassword: demodemo\n```\n\nThe demo is seeded with some content, but it's in read-only mode to prevent abuse.\n\n## About the name\n\nThe name Karakeep is inspired by the Arabic word \"كراكيب\" (karakeeb), a colloquial term commonly used to refer to miscellaneous clutter, odds and ends, or items that may seem disorganized but often hold personal value or hidden usefulness. It evokes the image of a messy drawer or forgotten box, full of stuff you can't quite throw away—because somehow, it matters (or more likely, because you're a hoarder!).\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/02-installation/01-docker.md",
    "content": "# Docker Compose [Recommended]\n\n### Requirements\n\n- Docker\n- Docker Compose\n\n### 1. Create a new directory\n\nCreate a new directory to host the compose file and env variables.\n\nThis is where you’ll place the `docker-compose.yml` file from the next step and the environment variables.\n\nFor example you could make a new directory called \"karakeep-app\" with the following command:\n```\nmkdir karakeep-app\n```\n\n\n### 2. Download the compose file\n\nDownload the docker compose file provided [here](https://github.com/karakeep-app/karakeep/blob/main/docker/docker-compose.yml) directly into your new directory.\n\n```\nwget https://raw.githubusercontent.com/karakeep-app/karakeep/main/docker/docker-compose.yml\n```\n\n### 3. Populate the environment variables\n\nTo configure the app, create a `.env` file in the directory and add this minimal env file:\n\n```\nKARAKEEP_VERSION=release\nNEXTAUTH_SECRET=super_random_string\nMEILI_MASTER_KEY=another_random_string\nNEXTAUTH_URL=http://localhost:3000\n```\n\nYou **should** change the random strings. You can use `openssl rand -base64 36` in a seperate terminal window to generate the random strings. You should also change the `NEXTAUTH_URL` variable to point to your server address.\n\nUsing `KARAKEEP_VERSION=release` will pull the latest stable version. You might want to pin the version instead to control the upgrades (e.g. `KARAKEEP_VERSION=0.10.0`). Check the latest versions [here](https://github.com/karakeep-app/karakeep/pkgs/container/karakeep).\n\nPersistent storage and the wiring between the different services is already taken care of in the docker compose file.\n\nKeep in mind that every time you change the `.env` file, you'll need to re-run `docker compose up`.\n\nIf you want more config params, check the config documentation [here](/configuration).\n\n### 4. Setup OpenAI\n\nTo enable automatic tagging, you'll need to configure OpenAI. This is optional though but highly recommended.\n\n- Follow [OpenAI's help](https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key) to get an API key.\n- Add the OpenAI API key to the env file:\n\n```\nOPENAI_API_KEY=<key>\n```\n\nLearn more about the costs of using openai [here](/openai).\n\n<details>\n    <summary>If you want to use Ollama (https://ollama.com/) instead for local inference.</summary>\n\n    **Note:** The quality of the tags you'll get will depend on the quality of the model you choose.\n\n    - Make sure ollama is running.\n    - Set the `OLLAMA_BASE_URL` env variable to the address of the ollama API.\n    - Set `INFERENCE_TEXT_MODEL` to the model you want to use for text inference in ollama (for example: `llama3.1`)\n    - Set `INFERENCE_IMAGE_MODEL` to the model you want to use for image inference in ollama (for example: `llava`)\n    - Make sure that you `ollama pull`-ed the models that you want to use.\n    - You might want to tune the `INFERENCE_CONTEXT_LENGTH` as the default is quite small. The larger the value, the better the quality of the tags, but the more expensive the inference will be.\n\n</details>\n\n### 5. Start the service\n\nStart the service by running:\n\n```\ndocker compose up -d\n```\n\nThen visit `http://localhost:3000` and you should be greeted with the Sign In page.\n\n### [Optional] 6. Enable optional features\n\nCheck the [configuration docs](/configuration) for extra features to enable such as full page archival, full page screenshots, inference languages, etc.\n\n### [Optional] 7. Setup quick sharing extensions\n\nGo to the [quick sharing page](/quick-sharing) to install the mobile apps and the browser extensions. Those will help you hoard things faster!\n\n## Updating\n\nUpdating Karakeep will depend on what you used for the `KARAKEEP_VERSION` env variable.\n\n- If you pinned the app to a specific version, bump the version and re-run `docker compose up -d`. This should pull the new version for you.\n- If you used `KARAKEEP_VERSION=release`, you'll need to force docker to pull the latest version by running `docker compose up --pull always -d`.\n\nNote that if you want to upgrade/migrate `Meilisearch` versions, refer to the [troubleshooting](/troubleshooting) page.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/02-installation/02-unraid.md",
    "content": "# Unraid\n\n## Docker Compose Manager Plugin (Recommended)\n\nYou can use [Docker Compose Manager](https://forums.unraid.net/topic/114415-plugin-docker-compose-manager/) plugin to deploy Karakeep using the official docker compose file provided [here](https://github.com/karakeep-app/karakeep/blob/main/docker/docker-compose.yml). After creating the stack, you'll need to setup some env variables similar to that from the docker compose installation docs [here](/installation/docker#3-populate-the-environment-variables).\n\n## Community Apps\n\n:::info\nThe community application template is maintained by the community.\n:::\n\nKarakeep can be installed on Unraid using the community application plugins. Karakeep is a multi-container service, and because unraid doesn't natively support that, you'll have to install the different pieces as separate applications and wire them manually together.\n\nHere's a high level overview of the services you'll need:\n\n- **Karakeep** ([Support post](https://forums.unraid.net/topic/165108-support-collectathon-karakeep/)): Karakeep's main web app.\n- **Browserless** ([Support post](https://forums.unraid.net/topic/130163-support-template-masterwishxbrowserless/)): The chrome headless service used for fetching the content. Karakeep's official docker compose doesn't use browserless, but it's currently the only headless chrome service available on unraid, so you'll have to use it.\n- **MeiliSearch** ([Support post](https://forums.unraid.net/topic/164847-support-collectathon-meilisearch/)): The search engine used by Karakeep. It's optional but highly recommended. If you don't have it set up, search will be disabled.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/02-installation/03-archlinux.md",
    "content": "# Arch Linux\n\n## Installation\n\n> [Karakeep on AUR](https://aur.archlinux.org/packages/karakeep) is not maintained by the karakeep official.\n\n1. Install karakeep\n\n    ```shell\n    paru -S karakeep\n    ```\n\n2. (**Optional**) Install optional dependencies\n\n    ```shell\n    # karakeep-cli: karakeep cli tool\n    paru -S karakeep-cli\n\n    # ollama: for automatic tagging\n    sudo pacman -S ollama\n\n    # yt-dlp: for download video\n    sudo pacman -S yt-dlp\n    ```\n\n    You can use Open-AI instead of `ollama`. If you use `ollama`, you need to download the ollama model. Please refer to: [https://ollama.com/library](https://ollama.com/library).\n\n3. Set up\n\n    Environment variables can be set in `/etc/karakeep/karakeep.env` according to [configuration page](/configuration). **The environment variables that are not specified in `/etc/karakeep/karakeep.env` need to be added by yourself.**\n\n4. Enable service\n\n    ```shell\n    sudo systemctl enable --now karakeep.target\n    ```\n\n    Then visit `http://localhost:3000` and you should be greated with the sign in page.\n\n## Services and Ports\n\n`karakeep.target` include 3 services: `karakeep-web.service`, `karakeep-works.service`, `karakeep-browser.service`.\n\n- `karakeep-web.service`: Provide karakeep webui service, uses `3000` port by default.\n\n- `karakeep-workers.service`: Provide karakeep workers service, no port.\n\n- `karakeep-browser.service`: Provide browser headless service, uses `9222` port by default.\n\nNow `karakeep` depends on `meilisearch`, and `karakeep-workers.service` wants `meilisearch.service`, starting `karakeep.target` will start `meilisearch.service` at the same time.\n\n## How to Migrate from Hoarder to Karakeep\n\nThe PKGBUILD has been fully updated to replace all references to `hoarder` with `karakeep`. If you want to preserve your existing `hoarder` data during the upgrade, please follow the steps below:\n\n**1. Stop the old services**\n\n```shell\nsudo systemctl stop hoarder-web.service hoarder-worker.service hoarder-browser.service\nsudo systemctl disable --now hoarder.target\n```\n\n**2. Uninstall Hoarder**  \nAfter uninstalling, you can manually remove the old `hoarder` user and group if needed.\n```shell\nparu -R hoarder\n```\n\n**3. Rename the old data directory**\n```shell\nsudo mv /var/lib/hoarder /var/lib/karakeep\n```\n\n**4. Install Karakeep**\n```shell\nparu -S karakeep\n```\n\n**5. Fix ownership of the data directory**\n```shell\nsudo chown -R karakeep:karakeep /var/lib/karakeep\n```\n\n**6. Set Karakeep**  \nEdit `/etc/karakeep/karakeep.env` according to [configuration page](/configuration). **The environment variables that are not specified in `/etc/karakeep/karakeep.env` need to be added by yourself.**\n\nOr you can copy old hoarder env file to karakeep:\n```shell\nsudo cp -f /etc/hoarder/hoarder.env /etc/karakeep/karakeep.env\n```\n\n**7. Start Karakeep**\n```shell\nsudo systemctl enable --now karakeep.target\n```\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/02-installation/04-kubernetes.md",
    "content": "# Kubernetes\n\n### Requirements\n\n- A kubernetes cluster\n- kubectl\n- kustomize\n\n### 1. Get the deployment manifests\n\nYou can clone the repository and copy the `/kubernetes` directory into another directory of your choice.\n\n### 2. Populate the environment variables and secrets\n\nTo configure the app, copy the `.env_sample` to `.env` and change to your specific needs.\n\nYou should also change the `NEXTAUTH_URL` variable to point to your server address.\n\nUsing `KARAKEEP_VERSION=release` will pull the latest stable version. You might want to pin the version instead to control the upgrades (e.g. `KARAKEEP_VERSION=0.10.0`). Check the latest versions [here](https://github.com/karakeep-app/karakeep/pkgs/container/karakeep).\n\nTo see all available configuration options check the [documentation](https://docs.karakeep.app/configuration).\n\nTo configure the neccessary secrets for the application copy the `.secrets_sample` file to `.secrets` and change the sample secrets to your generated secrets.\n\n> Note: You **should** change the random strings. You can use `openssl rand -base64 36` to generate the random strings. \n\n### 3. Setup OpenAI\n\nTo enable automatic tagging, you'll need to configure OpenAI. This is optional though but highly recommended.\n\n- Follow [OpenAI's help](https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key) to get an API key.\n- Add the OpenAI API key to the `.env` file:\n\n```\nOPENAI_API_KEY=<key>\n```\n\nLearn more about the costs of using openai [here](/openai).\n\n<details>\n    <summary>[EXPERIMENTAL] If you want to use Ollama (https://ollama.com/) instead for local inference.</summary>\n\n    **Note:** The quality of the tags you'll get will depend on the quality of the model you choose. Running local models is a recent addition and not as battle tested as using openai, so proceed with care (and potentially expect a bunch of inference failures).\n\n    - Make sure ollama is running.\n    - Set the `OLLAMA_BASE_URL` env variable to the address of the ollama API.\n    - Set `INFERENCE_TEXT_MODEL` to the model you want to use for text inference in ollama (for example: `mistral`)\n    - Set `INFERENCE_IMAGE_MODEL` to the model you want to use for image inference in ollama (for example: `llava`)\n    - Make sure that you `ollama pull`-ed the models that you want to use.\n\n\n</details>\n\n### 4. Deploy the service\n\nDeploy the service by running:\n\n```\nmake deploy\n```\n\n### 5. Access the service\n\n#### via LoadBalancer IP\n\nBy default, these manifests expose the application as a LoadBalancer Service. You can run `kubectl get services` to identify the IP of the loadbalancer for your service.\n\nThen visit `http://<loadbalancer-ip>:3000` and you should be greated with the Sign In page.\n\n> Note: Depending on your setup you might want to expose the service via an Ingress, or have a different means to access it.\n\n#### Via Ingress\n\nIf you want to use an ingress, you can customize the sample ingress in the kubernetes folder and change the host to the DNS name of your choice.\n\nAfter that you have to configure the web service to the type ClusterIP so it is only reachable via the ingress.\n\nIf you have already deployed the service you can patch the web service to the type ClusterIP with the following command:\n\n` kubectl -n karakeep patch service web -p '{\"spec\":{\"type\":\"ClusterIP\"}}' `\n\nAfterwards you can apply the ingress and access the service via your chosen URL.\n\n#### Setting up HTTPS access to the Service\n\nTo access karakeep securely you can configure the ingress to use a preconfigured TLS certificate. This requires that you already have the needed files, namely your .crt and .key file, on hand.\n\nAfter you have deployed the karakeep manifests you can deploy your certificate for karakeep in the `karakeep` namespace with this example command. You can name the secret however you want. But be aware that the secret name in the ingress definition has to match the secret name.\n\n` $ kubectl --namespace karakeep create secret tls karakeep-web-tls --cert=/path/to/crt --key=/path/to/key `\n\nIf the secret is successfully created you can now configure the Ingress to use TLS via this changes to the spec:\n\n```` yaml\n spec:\n  tls:\n  - hosts:\n      - karakeep.example.com\n    secretName: karakeep-web-tls\n````\n\n> Note: Be aware that the hosts have to match between the tls spec and the HTTP spec.\n\n### [Optional] 6. Setup quick sharing extensions\n\nGo to the [quick sharing page](/quick-sharing) to install the mobile apps and the browser extensions. Those will help you hoard things faster!\n\n## Updating\n\nEdit the `KARAKEEP_VERSION` variable in the `kustomization.yaml` file and run `make clean deploy`.\n\nIf you have chosen `release` as the image tag you can also destroy the web pod, since the deployment has an ImagePullPolicy set to always the pod always pulls the image from the registry, this way we can ensure that the newest release image is pulled.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/02-installation/05-pikapods.md",
    "content": "# PikaPods [Paid Hosting]\n\n:::info\nNote: PikaPods shares some of its revenue from hosting Karakeep with the maintainer of this project.\n:::\n\n[PikaPods](https://www.pikapods.com/) offers managed paid hosting for many open source apps, including Karakeep.\nServer administration, updates, migrations and backups are all taken care of, which makes it well suited\nfor less technical users. As of Nov 2024, running Karakeep there will cost you ~$3 per month.\n\n### Requirements\n\n- A _PikaPods_ account. Can be created for free [here](https://www.pikapods.com/register). You get an initial welcome credit of $5.\n\n### 1. Choose app\n\nChoose _Karakeep_ from their [list of apps](https://www.pikapods.com/apps) or use this [direct link](https://www.pikapods.com/pods?run=hoarder). This will either\nopen a new dialog to add a new _Karakeep_ pod or ask you to log in.\n\n### 2. Add settings\n\nThere are a few settings to configure in the dialog:\n\n- **Basics**: Give the pod a name and choose a region that's near you.\n- **Env Vars**: Here you can disable signups or set an OpenAI API key. All settings are optional.\n- **Resources**: The resources your _Karakeep_ pod can use. The defaults are fine, unless you have a very large collection.\n\n### 3. Start pod and add user\n\nAfter hitting _Add pod_ it will take about a minute for the app to fully start. After this you can visit\nthe pod's URL and add an initial user under _Sign Up_. After this you may want to disable further sign-ups\nby setting the pod's `DISABLE_SIGNUPS` _Env Var_ to `true`.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/02-installation/06-debuntu.md",
    "content": "# Debian 12/Ubuntu 24.04\n\n:::warning\nThis script is a stripped-down version of those found in the [Proxmox Community Scripts](https://github.com/community-scripts/ProxmoxVE) repo. It has been adapted to work on baremetal Debian 12 or Ubuntu 24.04 installs **only**. Any other use is not supported and you use this script at your own risk.\n:::\n\n### Requirements\n\n- **Debian 12** (Buster) or\n- **Ubuntu 24.04** (Noble Numbat)\n\nThe script will download and install all dependencies (except for Ollama), install Karakeep, do a basic configuration of Karakeep and Meilisearch (the search app used by Karakeep), and create and enable the systemd service files needed to run Karakeep on startup. Karakeep and Meilisearch are run in the context of their low-privilege user environments for more security.\n\nThe script functions as an update script in addition to an installer. See **[Updating](#updating)**.\n\n### 1. Download the script from the [Karakeep repository](https://github.com/karakeep-app/karakeep/blob/main/karakeep-linux.sh)\n\n```\nwget https://raw.githubusercontent.com/karakeep-app/karakeep/main/karakeep-linux.sh\n```\n\n### 2. Run the script\n\n> This script must be run as `root`, or as a user with `sudo` privileges.\n\n    If this is a fresh install, then run the installer by using the following command:\n\n    ```shell\n    bash karakeep-linux.sh install\n    ```\n\n### 3. Create an account/sign in\n\n    Then visit `http://localhost:3000` and you should be greated with the Sign In page.\n\n## Updating\n\n> This script must be run as `root`, or as a user with `sudo` privileges.\n\n    If Karakeep has previously been installed using this script, then run the updater like so:\n\n    ```shell\n     bash karakeep-linux.sh update\n    ```\n\n## Services and Ports\n\n`karakeep.target` includes 4 services: `meilisearch.service`, `karakeep-web.service`, `karakeep-workers.service`, `karakeep-browser.service`.\n\n- `meilisearch.service`: Provides full-text search, Karakeep Workers service connects to it, uses port `7700` by default.\n\n- `karakeep-web.service`: Provides the karakeep web service, uses `3000` port by default.\n\n- `karakeep-workers.service`: Provides the karakeep workers service, no port.\n\n- `karakeep-browser.service`: Provides the headless browser service, uses `9222` port by default.\n\n## Configuration, ENV file, database locations\n\nDuring installation, the script created a configuration file for `meilisearch`, an `ENV` file for Karakeep, and located config paths and database paths separate from the installation path of Karakeep, so as to allow for easier updating. Their names/locations are as follows:\n\n- `/etc/meilisearch.toml` - a basic configuration for meilisearch, that contains configs for the database location, disabling analytics, and using a master key, which prevents unauthorized connections.\n- `/var/lib/meilisearch` - Meilisearch DB location.\n- `/etc/karakeep/karakeep.env` - The Karakeep `ENV` file. Edit this file to configure Karakeep beyond the default. The web service and the workers service need to be restarted after editing this file:\n\n    ```shell\n    sudo systemctl restart karakeep-workers karakeep-web\n    ```\n\n- `/var/lib/karakeep` - The Karakeep database location. If you delete the contents of this folder you will lose all your data.\n\n## Still Running Hoarder?\n\nThere is a way to upgrade. Please see [Guides > Hoarder to Karakeep Migration](https://docs.karakeep.app/Guides/hoarder-to-karakeep-migration)\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/02-installation/07-minimal-install.md",
    "content": "# Minimal Installation\n\n:::warning\nUnless necessary, prefer the [full installation](/installation/docker) to leverage all the features of Karakeep. You'll be sacrificing a lot of functionality if you go with the minimal installation route.\n:::\n\nKarakeep's default installation has a dependency on Meilisearch for the full text search, Chrome for crawling and OpenAI/Ollama for AI tagging. You can however run Karakeep without those dependencies if you're willing to sacrifice those features.\n\n- If you run without meilisearch, the search functionality will be completely disabled.\n- If you run without chrome, crawling will still work, but you'll lose ability to take screenshots of websites and websites with javascript content won't get crawled correctly.\n- If you don't setup OpenAI/Ollama, AI tagging will be disabled.\n\nThose features are important for leveraging Karakeep's full potential, but if you're running in constrained environments, you can use the following minimal docker compose to skip all those dependencies:\n\n```yaml\nservices:\n  web:\n    image: ghcr.io/karakeep-app/karakeep:release\n    restart: unless-stopped\n    volumes:\n      - data:/data\n    ports:\n      - 3000:3000\n    environment:\n      DATA_DIR: /data\n      NEXTAUTH_SECRET: super_random_string\nvolumes:\n  data:\n```\n\nOr just with the following docker command:\n\n```base\ndocker run -d \\\n  --restart unless-stopped \\\n  -v data:/data \\\n  -p 3000:3000 \\\n  -e DATA_DIR=/data \\\n  -e NEXTAUTH_SECRET=super_random_string \\\n  ghcr.io/karakeep-app/karakeep:release\n```\n\n:::warning\nYou **MUST** change the `super_random_string` to a true random string which you can generate with `openssl rand -hex 32`.\n:::\n\nCheck the [configuration docs](/configuration) for extra features to enable such as full page archival, full page screenshots, inference languages, etc.\n\n\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/02-installation/08-truenas.md",
    "content": "# TrueNAS\n\nKarakeep is available directly from TrueNAS's app catalog ([link](https://apps.truenas.com/catalog/karakeep/)).\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/02-installation/_category_.json",
    "content": "{\n    \"label\": \"Installation\",\n}\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/03-configuration.md",
    "content": "# Configuration\n\nThe app is mainly configured by environment variables. All the used environment variables are listed in [packages/shared/config.ts](https://github.com/karakeep-app/karakeep/blob/main/packages/shared/config.ts). The most important ones are:\n\n| Name                                   | Required                              | Default         | Description                                                                                                                                                                                                                                                                                                             |\n| -------------------------------------- | ------------------------------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| PORT                                   | No                                    | 3000            | The port on which the web server will listen. DON'T CHANGE THIS IF YOU'RE USING DOCKER, instead changed the docker bound external port.                                                                                                                                                                                 |\n| WORKERS_PORT                           | No                                    | 0 (Random Port) | The port on which the worker will export its prometheus metrics on `/metrics`. By default it's a random unused port. If you want to utilize those metrics, fix the port to a value (and export it in docker if you're using docker).                                                                                    |\n| WORKERS_HOST                           | No                                    | 127.0.0.1       | Host to listen to for requests to WORKERS_PORT. You will need to set this if running in a container, since localhost will not be reachable from outside                                                                                                                                                                 |\n| WORKERS_ENABLED_WORKERS                | No                                    | Not set         | Comma separated list of worker names to enable. If set, only these workers will run. Valid values: crawler,inference,search,adminMaintenance,video,feed,assetPreprocessing,webhook,ruleEngine.                                                                                                                          |\n| WORKERS_DISABLED_WORKERS               | No                                    | Not set         | Comma separated list of worker names to disable. Takes precedence over `WORKERS_ENABLED_WORKERS`.                                                                                                                                                                                                                       |\n| LOG_LEVEL                              | No                                    | debug            | The application log level as defined in the [winston documentation](https://github.com/winstonjs/winston?tab=readme-ov-file#logging-levels). You may want to set this to `notice` or `warning` when running Karakeep in a production environment.                                                            |\n| DATA_DIR                               | Yes                                   | Not set         | The path for the persistent data directory. This is where the db lives. Assets are stored here by default unless `ASSETS_DIR` is set.                                                                                                                                                                                   |\n| ASSETS_DIR                             | No                                    | Not set         | The path where crawled assets will be stored. If not set, defaults to `${DATA_DIR}/assets`.                                                                                                                                                                                                                             |\n| NEXTAUTH_URL                           | Yes                                   | Not set         | Should point to the address of your server. The app will function without it, but will redirect you to wrong addresses on signout for example.                                                                                                                                                                          |\n| NEXTAUTH_SECRET                        | Yes                                   | Not set         | Random string used to sign the JWT tokens. Generate one with `openssl rand -base64 36`.                                                                                                                                                                                                                                 |\n| MEILI_ADDR                             | No                                    | Not set         | The address of meilisearch. If not set, Search will be disabled. E.g. (`http://meilisearch:7700`)                                                                                                                                                                                                                       |\n| MEILI_MASTER_KEY                       | Only in Prod and if search is enabled | Not set         | The master key configured for meilisearch. Not needed in development environment. Generate one with `openssl rand -base64 36 \\| tr -dc 'A-Za-z0-9'`                                                                                                                                                                     |\n| MAX_ASSET_SIZE_MB                      | No                                    | 50              | Sets the maximum allowed asset size (in MB) to be uploaded                                                                                                                                                                                                                                                              |\n| DISABLE_NEW_RELEASE_CHECK              | No                                    | false           | If set to true, latest release check will be disabled in the admin panel.                                                                                                                                                                                                                                               |\n| PROMETHEUS_AUTH_TOKEN                  | No                                    | Random          | Enable a prometheus metrics endpoint at `/api/metrics`. This endpoint will require this token being passed in the Authorization header as a Bearer token. If not set, a new random token is generated everytime at startup. This cannot contain any special characters or you may encounter a 400 Bad Request response. |\n| RATE_LIMITING_ENABLED                  | No                                    | false           | If set to true, API rate limiting will be enabled.                                                                                                                                                                                                                                                                      |\n| CRAWLER_DOMAIN_RATE_LIMIT_WINDOW_MS    | No                                    | Not set         | Time window in milliseconds for per-domain crawler rate limiting.                                                                                                                                                                                                                                                       |\n| CRAWLER_DOMAIN_RATE_LIMIT_MAX_REQUESTS | No                                    | Not set         | Maximum crawler requests allowed per domain inside the configured window.                                                                                                                                                                                                                                               |\n| DB_WAL_MODE                            | No                                    | false           | Enables WAL mode for the sqlite database. This should improve the performance of the database. There's no reason why you shouldn't set this to true unless you're running the db on a network attached drive. This will become the default at some time in the future.                                                  |\n| SEARCH_NUM_WORKERS                     | No                                    | 1               | Number of concurrent workers for search indexing tasks. Increase this if you have a high volume of content being indexed for search.                                                                                                                                                                                    |\n| SEARCH_JOB_TIMEOUT_SEC                 | No                                    | 30              | How long to wait for a search indexing job to finish before timing out. Increase this if you have large bookmarks with extensive content that takes longer to index.                                                                                                                                                    |\n| WEBHOOK_NUM_WORKERS                    | No                                    | 1               | Number of concurrent workers for webhook delivery. Increase this if you have multiple webhook endpoints or high webhook traffic.                                                                                                                                                                                        |\n| ASSET_PREPROCESSING_NUM_WORKERS        | No                                    | 1               | Number of concurrent workers for asset preprocessing tasks (image processing, OCR, etc.). Increase this if you have many images or documents that need processing.                                                                                                                                                      |\n| RULE_ENGINE_NUM_WORKERS                | No                                    | 1               | Number of concurrent workers for rule engine processing. Increase this if you have complex automation rules that need to be processed quickly.                                                                                                                                                                          |\n\n## Asset Storage\n\nKarakeep supports two storage backends for assets: local filesystem (default) and S3-compatible object storage. S3 storage is automatically detected when an S3 endpoint is passed.\n\n| Name                             | Required          | Default | Description                                                                                               |\n| -------------------------------- | ----------------- | ------- | --------------------------------------------------------------------------------------------------------- |\n| ASSET_STORE_S3_ENDPOINT          | No                | Not set | The S3 endpoint URL. Required for S3-compatible services like MinIO. **Setting this enables S3 storage**. |\n| ASSET_STORE_S3_REGION            | No                | Not set | The S3 region to use.                                                                                     |\n| ASSET_STORE_S3_BUCKET            | Yes when using S3 | Not set | The S3 bucket name where assets will be stored.                                                           |\n| ASSET_STORE_S3_ACCESS_KEY_ID     | Yes when using S3 | Not set | The S3 access key ID for authentication.                                                                  |\n| ASSET_STORE_S3_SECRET_ACCESS_KEY | Yes when using S3 | Not set | The S3 secret access key for authentication.                                                              |\n| ASSET_STORE_S3_FORCE_PATH_STYLE  | No                | false   | Whether to force path-style URLs for S3 requests. Set to true for MinIO and other S3-compatible services. |\n\n:::info\nWhen using S3 storage, make sure the bucket exists and the provided credentials have the necessary permissions to read, write, and delete objects in the bucket.\n:::\n\n:::warning\nSwitching between storage backends after data has been stored will require manual migration of existing assets. Plan your storage backend choice carefully before deploying.\n:::\n\n## Authentication / Signup\n\nBy default, Karakeep uses the database to store users, but it is possible to also use OAuth.\nThe flags need to be provided to the `web` container.\n\n:::info\nOnly OIDC compliant OAuth providers are supported! For information on how to set it up, consult the documentation of your provider.\n:::\n\n:::info\nWhen setting up OAuth, the allowed redirect URLs configured at the provider should be set to `<KARAKEEP_ADDRESS>/api/auth/callback/custom` where `<KARAKEEP_ADDRESS>` is the address you configured in `NEXTAUTH_URL` (for example: `https://try.karakeep.app/api/auth/callback/custom`).\n:::\n\n| Name                                        | Required | Default                | Description                                                                                                                                                                                           |\n| ------------------------------------------- | -------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| DISABLE_SIGNUPS                             | No       | false                  | If enabled, no new signups will be allowed and the signup button will be disabled in the UI                                                                                                           |\n| DISABLE_PASSWORD_AUTH                       | No       | false                  | If enabled, only signups and logins using OAuth are allowed and the signup button and login form for local accounts will be disabled in the UI                                                        |\n| EMAIL_VERIFICATION_REQUIRED                 | No       | false                  | Whether email verification is required during user signup. If enabled, users must verify their email address before they can use their account. If you enable this, you must configure SMTP settings. |\n| OAUTH_WELLKNOWN_URL                         | No       | Not set                | The \"wellknown Url\" for openid-configuration as provided by the OAuth provider                                                                                                                        |\n| OAUTH_CLIENT_SECRET                         | No       | Not set                | The \"Client Secret\" as provided by the OAuth provider                                                                                                                                                 |\n| OAUTH_CLIENT_ID                             | No       | Not set                | The \"Client ID\" as provided by the OAuth provider                                                                                                                                                     |\n| OAUTH_SCOPE                                 | No       | \"openid email profile\" | \"Full list of scopes to request (space delimited)\"                                                                                                                                                    |\n| OAUTH_PROVIDER_NAME                         | No       | \"Custom Provider\"      | The name of your provider. Will be shown on the signup page as \"Sign in with `<name>`\"                                                                                                                |\n| OAUTH_ALLOW_DANGEROUS_EMAIL_ACCOUNT_LINKING | No       | false                  | Whether existing accounts in karakeep stored in the database should automatically be linked with your OAuth account. Only enable it if you trust the OAuth provider!                                  |\n| OAUTH_TIMEOUT                               | No       | 3500                   | The wait time in milliseconds for the OAuth provider response. Increase this if you are having `outgoing request timed out` errors                                                                    |\n\nFor more information on `OAUTH_ALLOW_DANGEROUS_EMAIL_ACCOUNT_LINKING`, check the [next-auth.js documentation](https://next-auth.js.org/configuration/providers/oauth#allowdangerousemailaccountlinking-option).\n\n## Inference Configs (For automatic tagging)\n\nEither `OPENAI_API_KEY` or `OLLAMA_BASE_URL` need to be set for automatic tagging to be enabled. Otherwise, automatic tagging will be skipped.\n\n:::warning\n\n- The quality of the tags you'll get will depend on the quality of the model you choose.\n- You might want to tune the `INFERENCE_CONTEXT_LENGTH` as the default is quite small. The larger the value, the better the quality of the tags, but the more expensive the inference will be (money-wise on OpenAI and resource-wise on ollama).\n  :::\n\n| Name                                 | Required | Default                | Description                                                                                                                                                                                                                                                                                                                                                                           |\n| ------------------------------------ | -------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| OPENAI_API_KEY                       | No       | Not set                | The OpenAI key used for automatic tagging. More on that in [here](/openai).                                                                                                                                                                                                                                                                                                           |\n| OPENAI_BASE_URL                      | No       | Not set                | If you just want to use OpenAI you don't need to pass this variable. If, however, you want to use some other openai compatible API (e.g. azure openai service), set this to the url of the API.                                                                                                                                                                                       |\n| OLLAMA_BASE_URL                      | No       | Not set                | If you want to use ollama for local inference, set the address of ollama API here.                                                                                                                                                                                                                                                                                                    |\n| OLLAMA_KEEP_ALIVE                    | No       | Not set                | Controls how long the model will stay loaded into memory following the request (example value: \"5m\").                                                                                                                                                                                                                                                                                 |\n| INFERENCE_TEXT_MODEL                 | No       | gpt-4.1-mini           | The model to use for text inference. You'll need to change this to some other model if you're using ollama.                                                                                                                                                                                                                                                                           |\n| INFERENCE_IMAGE_MODEL                | No       | gpt-4o-mini            | The model to use for image inference. You'll need to change this to some other model if you're using ollama and that model needs to support vision APIs (e.g. llava).                                                                                                                                                                                                                 |\n| EMBEDDING_TEXT_MODEL                 | No       | text-embedding-3-small | The model to be used for generating embeddings for the text.                                                                                                                                                                                                                                                                                                                          |\n| INFERENCE_CONTEXT_LENGTH             | No       | 2048                   | The max number of tokens that we'll pass to the inference model. If your content is larger than this size, it'll be truncated to fit. The larger this value, the more of the content will be used in tag inference, but the more expensive the inference will be (money-wise on openAI and resource-wise on ollama). Check the model you're using for its max supported content size. |\n| INFERENCE_MAX_OUTPUT_TOKENS          | No       | 2048                   | The maximum number of tokens that the inference model is allowed to generate in its response. This controls the length of AI-generated content like tags and summaries. Increase this if you need longer responses, but be aware that higher values will increase costs (for OpenAI) and processing time.                                                                             |\n| INFERENCE_USE_MAX_COMPLETION_TOKENS  | No       | false                  | \\[OpenAI Only\\] Whether to use the newer `max_completion_tokens` parameter instead of the deprecated `max_tokens` parameter. Set to `true` if using GPT-5 or o-series models which require this. Will become the default in a future release.                                                                                                                                         |\n| INFERENCE_LANG                       | No       | english                | The language in which the tags will be generated.                                                                                                                                                                                                                                                                                                                                     |\n| INFERENCE_NUM_WORKERS                | No       | 1                      | Number of concurrent workers for AI inference tasks (tagging and summarization). Increase this if you have multiple AI inference requests and want to process them in parallel.                                                                                                                                                                                                       |\n| INFERENCE_ENABLE_AUTO_TAGGING        | No       | true                   | Whether automatic AI tagging is enabled or disabled.                                                                                                                                                                                                                                                                                                                                  |\n| INFERENCE_ENABLE_AUTO_SUMMARIZATION  | No       | false                  | Whether automatic AI summarization is enabled or disabled.                                                                                                                                                                                                                                                                                                                            |\n| INFERENCE_JOB_TIMEOUT_SEC            | No       | 30                     | How long to wait for the inference job to finish before timing out. If you're running ollama without powerful GPUs, you might want to increase the timeout a bit.                                                                                                                                                                                                                     |\n| INFERENCE_FETCH_TIMEOUT_SEC          | No       | 300                    | \\[Ollama Only\\] The timeout of the fetch request to the ollama server. If your inference requests take longer than the default 5mins, you might want to increase this timeout.                                                                                                                                                                                                        |\n| INFERENCE_SUPPORTS_STRUCTURED_OUTPUT | No       | Not set                | \\[DEPRECATED\\] Whether the inference model supports structured output or not. Use INFERENCE_OUTPUT_SCHEMA instead. Setting this to true translates to INFERENCE_OUTPUT_SCHEMA=structured, and to false translates to INFERENCE_OUTPUT_SCHEMA=plain.                                                                                                                                   |\n| INFERENCE_OUTPUT_SCHEMA              | No       | structured             | Possible values are \"structured\", \"json\", \"plain\". Structured is the preferred option, but if your model doesn't support it, you can use \"json\" if your model supports JSON mode, otherwise \"plain\" which should be supported by all the models but the model might not output the data in the correct format.                                                                        |\n\n:::info\n\n- You can append additional instructions to the prompt used for automatic tagging, in the `AI Settings` (in the `User Settings` screen)\n- You can use the placeholders `$tags`, `$aiTags`, `$userTags` in the prompt. These placeholders will be replaced with all tags, ai generated tags or human created tags when automatic tagging is performed (e.g. `[karakeep, computer, ai]`)\n  :::\n\n## Crawler Configs\n\n| Name                                     | Required | Default   | Description                                                                                                                                                                                                                                                                                                                                                                   |\n| ---------------------------------------- | -------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| CRAWLER_NUM_WORKERS                      | No       | 1         | Number of allowed concurrent crawling jobs. By default, we're only doing one crawling request at a time to avoid consuming a lot of resources.                                                                                                                                                                                                                                |\n| BROWSER_WEB_URL                          | No       | Not set   | The browser's http debugging address. The worker will talk to this endpoint to resolve the debugging console's websocket address. If you already have the websocket address, use `BROWSER_WEBSOCKET_URL` instead. If neither `BROWSER_WEB_URL` nor `BROWSER_WEBSOCKET_URL` are set, the worker will use plain http requests skipping screenshotting and javascript execution. |\n| BROWSER_WEBSOCKET_URL                    | No       | Not set   | The websocket address of browser's debugging console. If you want to use [browserless](https://browserless.io), use their websocket address here. If neither `BROWSER_WEB_URL` nor `BROWSER_WEBSOCKET_URL` are set, the worker will use plain http requests skipping screenshotting and javascript execution.                                                                 |\n| BROWSER_CONNECT_ONDEMAND                 | No       | false     | If set to false, the crawler will proactively connect to the browser instance and always maintain an active connection. If set to true, the browser will be launched on demand only whenever a crawling is requested. Set to true if you're using a service that provides you with browser instances on demand.                                                               |\n| CRAWLER_DOWNLOAD_BANNER_IMAGE            | No       | true      | Whether to cache the banner image used in the cards locally or fetch it each time directly from the website. Caching it consumes more storage space, but is more resilient against link rot and rate limits from websites.                                                                                                                                                    |\n| CRAWLER_STORE_SCREENSHOT                 | No       | true      | Whether to store a screenshot from the crawled website or not. Screenshots act as a fallback for when we fail to extract an image from a website. You can also view the stored screenshots for any link.                                                                                                                                                                      |\n| CRAWLER_FULL_PAGE_SCREENSHOT             | No       | false     | Whether to store a screenshot of the full page or not. Disabled by default, as it can lead to much higher disk usage. If disabled, the screenshot will only include the visible part of the page                                                                                                                                                                              |\n| CRAWLER_SCREENSHOT_TIMEOUT_SEC           | No       | 5         | How long to wait for the screenshot finish before timing out. If you are capturing full-page screenshots of long webpages, consider increasing this value.                                                                                                                                                                                                                    |\n| CRAWLER_FULL_PAGE_ARCHIVE                | No       | false     | Whether to store a full local copy of the page or not. Disabled by default, as it can lead to much higher disk usage. If disabled, only the readable text of the page is archived.                                                                                                                                                                                            |\n| CRAWLER_JOB_TIMEOUT_SEC                  | No       | 60        | How long to wait for the crawler job to finish before timing out. If you have a slow internet connection or a low powered device, you might want to bump this up a bit                                                                                                                                                                                                        |\n| CRAWLER_NAVIGATE_TIMEOUT_SEC             | No       | 30        | How long to spend navigating to the page (along with its redirects). Increase this if you have a slow internet connection                                                                                                                                                                                                                                                     |\n| CRAWLER_VIDEO_DOWNLOAD                   | No       | false     | Whether to download videos from the page or not (using yt-dlp)                                                                                                                                                                                                                                                                                                                |\n| CRAWLER_VIDEO_DOWNLOAD_MAX_SIZE          | No       | 50        | The maximum file size for the downloaded video. The quality will be chosen accordingly. Use -1 to disable the limit.                                                                                                                                                                                                                                                          |\n| CRAWLER_VIDEO_DOWNLOAD_TIMEOUT_SEC       | No       | 600       | How long to wait for the video download to finish                                                                                                                                                                                                                                                                                                                             |\n| CRAWLER_ENABLE_ADBLOCKER                 | No       | true      | Whether to enable an adblocker in the crawler or not. If you're facing troubles downloading the adblocking lists on worker startup, you can disable this.                                                                                                                                                                                                                     |\n| CRAWLER_YTDLP_ARGS                       | No       | []        | Include additional yt-dlp arguments to be passed at crawl time separated by %%: https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#general-options                                                                                                                                                                                                                           |\n| BROWSER_COOKIE_PATH                      | No       | Not set   | Path to a JSON file containing cookies to be loaded into the browser context. The file should be an array of cookie objects, each with name and value (required), and optional fields like domain, path, expires, httpOnly, secure, and sameSite (e.g., `[{\"name\": \"session\", \"value\": \"xxx\", \"domain\": \".example.com\"}`]).                                                   |\n| HTML_CONTENT_SIZE_INLINE_THRESHOLD_BYTES | No       | 5 \\* 1024 | The thresholds in bytes after which larger assets will be stored in the assetdb (folder/s3) instead of inline in the database.                                                                                                                                                                                                                                                |\n\n<details>\n\n  <summary>More info on BROWSER_COOKIE_PATH</summary>\n\nBROWSER_COOKIE_PATH specifies the path to a JSON file containing cookies to be loaded into the browser context for crawling.\n\nThe JSON file must be an array of cookie objects, each with:\n\n- name: The cookie name (required).\n- value: The cookie value (required).\n- Optional fields: domain, path, expires, httpOnly, secure, sameSite (values: \"Strict\", \"Lax\", or \"None\").\n\nExample JSON file:\n\n```json\n[\n  {\n    \"name\": \"session\",\n    \"value\": \"xxx\",\n    \"domain\": \".example.com\",\n    \"path\": \"/\",\n    \"expires\": 1735689600,\n    \"httpOnly\": true,\n    \"secure\": true,\n    \"sameSite\": \"Lax\"\n  }\n]\n```\n\n</details>\n\n## OCR Configs\n\nKarakeep uses [tesseract.js](https://github.com/naptha/tesseract.js) to extract text from images.\n\n| Name                     | Required | Default   | Description                                                                                                                                                                                                                               |\n| ------------------------ | -------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| OCR_CACHE_DIR            | No       | $TEMP_DIR | The dir where tesseract will download its models. By default, those models are not persisted and stored in the OS' temp dir.                                                                                                              |\n| OCR_LANGS                | No       | eng       | Comma separated list of the language codes that you want tesseract to support. You can find the language codes [here](https://tesseract-ocr.github.io/tessdoc/Data-Files-in-different-versions.html). Set to empty string to disable OCR. |\n| OCR_CONFIDENCE_THRESHOLD | No       | 50        | A number between 0 and 100 indicating the minimum acceptable confidence from tessaract. If tessaract's confidence is lower than this value, extracted text won't be stored.                                                               |\n\n## Webhook Configs\n\nYou can use webhooks to trigger actions when bookmarks are created, changed or crawled.\n\n| Name                | Required | Default | Description                                       |\n| ------------------- | -------- | ------- | ------------------------------------------------- |\n| WEBHOOK_TIMEOUT_SEC | No       | 5       | The timeout for the webhook request in seconds.   |\n| WEBHOOK_RETRY_TIMES | No       | 3       | The number of times to retry the webhook request. |\n\n:::info\n\n- The WEBHOOK_TOKEN is used for authentication. It will appear in the Authorization header as Bearer token.\n  ```\n  Authorization: Bearer <WEBHOOK_TOKEN>\n  ```\n- The webhook will be triggered with the job id (used for idempotence), bookmark id, bookmark type, the user id, the url and the operation in JSON format in the body.\n\n  ```json\n  {\n    \"jobId\": \"123\",\n    \"type\": \"link\",\n    \"bookmarkId\": \"exampleBookmarkId\",\n    \"userId\": \"exampleUserId\",\n    \"url\": \"https://example.com\",\n    \"operation\": \"crawled\"\n  }\n  ```\n\n  :::\n\n## SMTP Configuration\n\nKarakeep can send emails for various purposes such as email verification during signup. Configure these settings to enable email functionality.\n\n| Name          | Required | Default | Description                                                                                     |\n| ------------- | -------- | ------- | ----------------------------------------------------------------------------------------------- |\n| SMTP_HOST     | No       | Not set | The SMTP server hostname or IP address. Required if you want to enable email functionality.     |\n| SMTP_PORT     | No       | 587     | The SMTP server port. Common values are 587 (STARTTLS), 465 (SSL/TLS), or 25 (unencrypted).     |\n| SMTP_SECURE   | No       | false   | Whether to use SSL/TLS encryption. Set to true for port 465, false for port 587 with STARTTLS.  |\n| SMTP_USER     | No       | Not set | The username for SMTP authentication. Usually your email address.                               |\n| SMTP_PASSWORD | No       | Not set | The password for SMTP authentication. For services like Gmail, use an app-specific password.    |\n| SMTP_FROM     | No       | Not set | The \"from\" email address that will appear in sent emails. This should be a valid email address. |\n\n## Proxy Configuration\n\nIf your Karakeep instance needs to connect through a proxy server, you can configure the following settings:\n\n| Name                               | Required | Default | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |\n| ---------------------------------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| CRAWLER_HTTP_PROXY                 | No       | Not set | HTTP proxy server URL for outgoing HTTP requests (e.g., `http://proxy.example.com:8080`). You can pass multiple comma separated proxies and the used one will be chosen at random. The proxy is used for crawling, RSS feed fetches and webhooks.                                                                                                                                                                                                                                                                           |\n| CRAWLER_HTTPS_PROXY                | No       | Not set | HTTPS proxy server URL for outgoing HTTPS requests (e.g., `http://proxy.example.com:8080`). You can pass multiple comma separated proxies and the used one will be chosen at random. The proxy is used for crawling, RSS feed fetches and webhooks.                                                                                                                                                                                                                                                                         |\n| CRAWLER_NO_PROXY                   | No       | Not set | Comma-separated list of hostnames/IPs that should bypass the proxy (e.g., `localhost,127.0.0.1,.local`)                                                                                                                                                                                                                                                                                                                                                                                                                     |\n| CRAWLER_ALLOWED_INTERNAL_HOSTNAMES | No       | Not set | By default, Karakeep blocks worker-initiated requests whose DNS resolves to private, loopback, or link-local IP addresses. Use this to allowlist specific hostnames for internal access (e.g., `internal.company.com,.local`). Supports domain wildcards by prefixing with a dot (e.g., `.internal.company.com`). Passing `.` allowlists all domains. Note: Internal IP validation is bypassed when a proxy is configured for the URL as the local DNS resolver won't necessarily be the same as the one used by the proxy. |\n\n:::info\nThese proxy settings will be used by the crawler and other components that make outgoing HTTP requests.\n:::\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/04-screenshots.md",
    "content": "# Screenshots\n\n## Homepage\n\n![Homepage](/img/screenshots/homepage.png)\n\n## Homepage (Dark Mode)\n\n![Homepage](/img/screenshots/homepage-dark.png)\n\n## Tags\n\n![All Tags](/img/screenshots/all-tags.png)\n\n## Lists\n\n![All Lists](/img/screenshots/all-lists.png)\n\n## Bookmark Preview\n\n![Bookmark Preview](/img/screenshots/bookmark-preview.png)\n\n## Settings\n\n![Settings](/img/screenshots/settings.png)\n\n## Admin Panel\n\n![Ammin](/img/screenshots/admin.png)\n\n\n## iOS Sharing\n\n<img src=\"/img/screenshots/share-sheet.png\" width=\"400px\"  />\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/05-quick-sharing.md",
    "content": "# Quick Sharing Extensions\n\nThe whole point of Karakeep is making it easy to hoard the content. That's why there are a couple of \n\n## Mobile Apps\n\n<img src=\"/img/quick-sharing/mobile.png\" alt=\"mobile screenshot\" width=\"300\"/>\n\n\n- **iOS app**: [App Store Link](https://apps.apple.com/us/app/karakeep-app/id6479258022).\n- **Android App**: [Play Store link](https://play.google.com/store/apps/details?id=app.hoarder.hoardermobile&pcampaignid=web_share).\n\n## Browser Extensions\n\n<img src=\"/img/quick-sharing/extension.png\" alt=\"mobile screenshot\" width=\"300\"/>\n\n- **Chrome extension**: [here](https://chromewebstore.google.com/detail/karakeep/kgcjekpmcjjogibpjebkhaanilehneje).\n- **Firefox addon**: [here](https://addons.mozilla.org/en-US/firefox/addon/karakeep/).\n\n- ## Community Extensions\n- **Safari extension**: [App Store Link](https://apps.apple.com/us/app/karakeeper-bookmarker/id6746722790).  For macOS and iOS to allow a simple way to add your bookmarks to your self hosted karakeep instance.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/06-openai.md",
    "content": "# OpenAI Costs\n\nThis service uses OpenAI for automatic tagging. This means that you'll incur some costs if automatic tagging is enabled. There are two type of inferences that we do:\n\n## Text Tagging\n\nFor text tagging, we use the `gpt-4.1-mini` model. This model is [extremely cheap](https://openai.com/api/pricing). Cost per inference varies depending on the content size per article. Though, roughly, You'll be able to generate tags for almost 3000+ bookmarks for less than $1.\n\n## Image Tagging\n\nFor image uploads, we use the `gpt-4o-mini` model for extracting tags from the image. You can learn more about the costs of using this model [here](https://platform.openai.com/docs/guides/images?api-mode=chat#calculating-costs). To lower the costs, we're using the low resolution mode (fixed number of tokens regardless of image size). You'll be able to run inference for 1000+ images for less than a $1.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/07-development/01-setup.md",
    "content": "# Setup\n\n## Quick Start\n\nFor the fastest way to get started with development, use the one-command setup script:\n\n```bash\n./start-dev.sh\n```\n\nThis script will automatically:\n- Start Meilisearch in Docker (on port 7700)\n- Start headless Chrome in Docker (on port 9222) \n- Install dependencies with `pnpm install` if needed\n- Start both the web app and workers in parallel\n- Provide cleanup when you stop with Ctrl+C\n\n**Prerequisites:**\n- Docker installed and running\n- pnpm installed (see manual setup below for installation instructions)\n\nThe script will output the running services:\n- Web app: http://localhost:3000\n- Meilisearch: http://localhost:7700  \n- Chrome debugger: http://localhost:9222\n\nPress Ctrl+C to stop all services and clean up Docker containers.\n\n## Manual Setup\n\nKarakeep uses `node` version 22. To install it, you can use `nvm` [^1]\n\n```\n$ nvm install  22\n```\n\nVerify node version using this command:\n```\n$ node --version\nv22.14.0\n```\n\nKarakeep also makes use of `corepack`[^2]. If you have `node` installed, then `corepack` should already be\ninstalled on your machine, and you don't need to do anything. To verify the `corepack` is installed run:\n\n```\n$ command -v corepack\n/home/<user>/.nvm/versions/node/v22.14.0/bin/corepack\n```\n\nTo enable `corepack` run the following command:\n\n```\n$ corepack enable\n```\n\nThen, from the root of the repository, install the packages and dependencies using:\n\n```\n$ pnpm install\n```\n\nOutput of a successful `pnpm install` run should look something like:\n\n```\nScope: all 20 workspace projects\nLockfile is up to date, resolution step is skipped\nPackages: +3129\n+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\nProgress: resolved 0, reused 2699, downloaded 0, added 3129, done\n\ndevDependencies:\n+ @karakeep/prettier-config 0.1.0 <- tooling/prettier\n\n. prepare$ husky\n└─ Done in 45ms\nDone in 5.5s\n```\n\nYou can now continue with the rest of this documentation.\n\n### First Setup\n\n- You'll need to prepare the environment variables for the dev env.\n- Easiest would be to set it up once in the root of the repo and then symlink it in each app directory (e.g. `/apps/web`, `/apps/workers`) and also `/packages/db`.\n- Start by copying the template by `cp .env.sample .env`.\n- The most important env variables to set are:\n  - `DATA_DIR`: Where the database and assets will be stored. This is the only required env variable. You can use an absolute path so that all apps point to the same dir.\n  - `NEXTAUTH_SECRET`: Random string used to sign the JWT tokens. Generate one with `openssl rand -base64 36`. Logging in will not work if this is missing!\n  - `MEILI_ADDR`: If not set, search will be disabled. You can set it to `http://127.0.0.1:7700` if you run meilisearch using the command below.\n  - `OPENAI_API_KEY`: If you want to enable auto tag inference in the dev env.\n- run `pnpm run db:migrate` in the root of the repo to set up the database.\n\n### Dependencies\n\n#### Meilisearch\n\nMeilisearch is the provider for the full text search (and at some point embeddings search too). You can get it running with `docker run -p 7700:7700 getmeili/meilisearch:v1.13.3`.\n\nMount persistent volume if you want to keep index data across restarts. You can trigger a re-index for the entire items collection in the admin panel in the web app.\n\n#### Chrome\n\nThe worker app will automatically start headless chrome on startup for crawling pages. You don't need to do anything there.\n\n### Web App\n\n- Run `pnpm web` in the root of the repo.\n- Go to `http://localhost:3000`.\n\n> NOTE: The web app kinda works without any dependencies. However, search won't work unless meilisearch is running. Also, new items added won't get crawled/indexed unless workers are running.\n\n### Workers\n\n- Run `pnpm workers` in the root of the repo.\n\n### Mobile App (iOS & Android)\n\n#### Prerequisites\n\nTo build and run the mobile app locally, you'll need:\n\n- **For iOS development**: \n  - macOS computer\n  - Xcode installed from the App Store\n  - iOS Simulator (comes with Xcode)\n\n- **For Android development**:\n  - Android Studio installed\n  - Android SDK configured\n  - Android Emulator or physical device\n\nFor detailed setup instructions, refer to the [Expo documentation](https://docs.expo.dev/guides/local-app-development/).\n\n#### Running the app\n\n- `cd apps/mobile`\n- `pnpm exec expo prebuild --no-install` to build the app.\n\n**For iOS:**\n- `pnpm exec expo run:ios`\n- The app will be installed and started in the simulator.\n\n**Troubleshooting iOS Setup:**\nIf you encounter an error like `xcrun: error: SDK \"iphoneos\" cannot be located`, you may need to set the correct Xcode developer directory:\n```bash\nsudo xcode-select -s /Applications/Xcode.app/Contents/Developer\n```\n\n**For Android:**\n- Start the Android emulator or connect a physical device.\n- `pnpm exec expo run:android`\n- The app will be installed and started on the emulator/device.\n\nChanging the code will hot reload the app. However, installing new packages requires restarting the expo server.\n\n### Browser Extension\n\n- `cd apps/browser-extension`\n- `pnpm dev`\n- This will generate a `dist` package\n- Go to extension settings in chrome and enable developer mode.\n- Press `Load unpacked` and point it to the `dist` directory.\n- The plugin will pop up in the plugin list.\n\nIn dev mode, opening and closing the plugin menu should reload the code.\n\n\n## Docker Dev Env\n\nIf the manual setup is too much hassle for you. You can use a docker based dev environment by running `docker compose -f docker/docker-compose.dev.yml up` in the root of the repo. This setup wasn't super reliable for me though.\n\n\n[^1]: [nvm](https://github.com/nvm-sh/nvm) is a node version manager. You can install it following [these\ninstructions](https://github.com/nvm-sh/nvm?tab=readme-ov-file#installing-and-updating).\n\n[^2]: [corepack](https://nodejs.org/api/corepack.html) is an experimental tool to help with managing versions of your\npackage managers.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/07-development/02-directories.md",
    "content": "# Directory Structure\n\n## Apps\n\n| Directory                | Description                                            |\n| ------------------------ | ------------------------------------------------------ |\n| `apps/web`               | The main web app                                       |\n| `apps/workers`           | The background workers logic                           |\n| `apps/mobile`            | The react native based mobile app                      |\n| `apps/browser-extension` | The browser extension                                  |\n| `apps/landing`           | The landing page of [karakeep.app](https://karakeep.app) |\n\n## Shared Packages\n\n| Directory         | Description                                                                  |\n| ----------------- | ---------------------------------------------------------------------------- |\n| `packages/db`     | The database schema and migrations                                           |\n| `packages/trpc`   | Where most of the business logic lies built as TRPC routes                   |\n| `packages/shared` | Some shared code between the different apps (e.g. loggers, configs, assetdb) |\n\n## Toolings\n\n| Directory            | Description             |\n| -------------------- | ----------------------- |\n| `tooling/typescript` | The shared tsconfigs    |\n| `tooling/eslint`     | ESlint configs          |\n| `tooling/prettier`   | Prettier configs        |\n| `tooling/tailwind`   | Shared tailwind configs |\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/07-development/03-database.md",
    "content": "# Database Migrations\n\n- The database schema lives in `packages/db/schema.ts`.\n- Changing the schema, requires a migration.\n- You can generate the migration by running `pnpm run db:generate --name description_of_schema_change` in the root dir.\n- You can then apply the migration by running `pnpm run db:migrate`.\n\n## Drizzle Studio\n\nYou can start the drizzle studio by running `pnpm run db:studio` in the root of the repo.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/07-development/04-architecture.md",
    "content": "# Architecture\n\n![Architecture Diagram](/img/architecture/arch.png)\n\n- Webapp: NextJS based using sqlite for data storage.\n- Workers: Consume the jobs from sqlite based job queue and executes them, there are three job types:\n  1. Crawling: Fetches the content of links using a headless chrome browser running in the workers container.\n  2. OpenAI: Uses OpenAI APIs to infer the tags of the content.\n  3. Indexing: Indexes the content in meilisearch for faster retrieval during search.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/07-development/_category_.json",
    "content": "{\n    \"label\": \"Development\",\n}\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/08-security-considerations.md",
    "content": "# Security Considerations\n\nIf you're going to give app access to untrusted users, there's some security considerations that you'll need to be aware of given how the crawler works. The crawler is basically running a browser to fetch the content of the bookmarks. Any untrusted user can submit bookmarks to be crawled from your server and they'll be able to see the crawling result. This can be abused in multiple ways:\n\n1. Untrusted users can submit crawl requests to websites that you don't want to be coming out of your IPs.\n2. Crawling user controlled websites can expose your origin IP (and location) even if your service is hosted behind cloudflare for example.\n3. The crawling requests will be coming out from your own network, which untrusted users can leverage to crawl internal non-internet exposed endpoints.\n\nTo mitigate those risks, you can do one of the following:\n\n1. Limit access to trusted users\n2. Let the browser traffic go through some VPN with restricted network policies.\n3. Host the browser container outside of your network.\n4. Use a hosted browser as a service (e.g. [browserless](https://browserless.io)). Note: I've never used them before.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/09-command-line.md",
    "content": "# Command Line Tool (CLI)\n\nKarakeep comes with a simple CLI for those users who want to do more advanced manipulation.\n\n## Features\n\n- Manipulate bookmarks, lists and tags\n- Mass import/export of bookmarks\n\n## Installation (NPM)\n\n```\nnpm install -g @karakeep/cli\n```\n\n\n## Installation (Docker)\n\n```\ndocker run --rm ghcr.io/karakeep-app/karakeep-cli:release --help\n```\n\n## Usage\n\n```\nkarakeep\n```\n\n```\nUsage: karakeep [options] [command]\n\nA CLI interface to interact with the karakeep api\n\nOptions:\n  --api-key <key>       the API key to interact with the API (env: KARAKEEP_API_KEY)\n  --server-addr <addr>  the address of the server to connect to (env: KARAKEEP_SERVER_ADDR)\n  -V, --version         output the version number\n  -h, --help            display help for command\n\nCommands:\n  bookmarks             manipulating bookmarks\n  lists                 manipulating lists\n  tags                  manipulating tags\n  whoami                returns info about the owner of this API key\n  help [command]        display help for command\n```\n\nAnd some of the subcommands:\n\n```\nkarakeep bookmarks\n```\n\n```\nUsage: karakeep bookmarks [options] [command]\n\nManipulating bookmarks\n\nOptions:\n  -h, --help             display help for command\n\nCommands:\n  add [options]          creates a new bookmark\n  get <id>               fetch information about a bookmark\n  update [options] <id>  updates bookmark\n  list [options]         list all bookmarks\n  delete <id>            delete a bookmark\n  help [command]         display help for command\n\n```\n\n```\nkarakeep lists\n```\n\n```\nUsage: karakeep lists [options] [command]\n\nManipulating lists\n\nOptions:\n  -h, --help                 display help for command\n\nCommands:\n  list                       lists all lists\n  delete <id>                deletes a list\n  add-bookmark [options]     add a bookmark to list\n  remove-bookmark [options]  remove a bookmark from list\n  help [command]             display help for command\n```\n\n## Obtaining an API Key\n\nTo use the CLI, you'll need to get an API key from your karakeep settings. You can validate that it's working by running:\n\n```\nkarakeep --api-key <key> --server-addr <addr> whoami\n```\n\nFor example:\n\n```\nkarakeep --api-key mysupersecretkey --server-addr https://try.karakeep.app whoami\n{\n  id: 'j29gnbzxxd01q74j2lu88tnb',\n  name: 'Test User',\n  email: 'test@gmail.com'\n}\n```\n\n\n## Other clients\n\nThere also exists a **non-official**, community-maintained, python package called [karakeep-python-api](https://github.com/thiswillbeyourgithub/karakeep_python_api) that can be accessed from the CLI, but is **not** official.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/09-mcp.md",
    "content": "# Model Context Protocol Server (MCP)\n\nKarakeep comes with a Model Context Protocol server that can be used to interact with it through LLMs.\n\n## Supported Tools\n\n- Searching bookmarks\n- Adding and removing bookmarks from lists\n- Attaching and detaching tags to bookmarks\n- Creating new lists\n- Creating text and URL bookmarks\n\n\n## Usage with Claude Desktop\n\nFrom NPM:\n\n```json\n{\n  \"mcpServers\": {\n    \"karakeep\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"@karakeep/mcp\"\n      ],\n      \"env\": {\n        \"KARAKEEP_API_ADDR\": \"https://<YOUR_SERVER_ADDR>\",\n        \"KARAKEEP_API_KEY\": \"<YOUR_TOKEN>\"\n      }\n    }\n  }\n}\n```\n\nFrom Docker:\n\n```json\n{\n  \"mcpServers\": {\n    \"karakeep\": {\n      \"command\": \"docker\",\n      \"args\": [\n        \"run\",\n        \"-e\",\n        \"KARAKEEP_API_ADDR=https://<YOUR_SERVER_ADDR>\",\n        \"-e\",\n        \"KARAKEEP_API_KEY=<YOUR_TOKEN>\",\n        \"ghcr.io/karakeep-app/karakeep-mcp:latest\"\n      ]\n    }\n  }\n}\n```\n\n\n### Demo\n\n#### Search\n![mcp-1](/img/mcp-1.gif)\n\n#### Adding Text Bookmarks\n![mcp-2](/img/mcp-2.gif)\n\n#### Adding URL Bookmarks\n![mcp-2](/img/mcp-3.gif)\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/10-import.md",
    "content": "# Importing Bookmarks\n\n\nKarakeep supports importing bookmarks using the Netscape HTML Format, Pocket's new CSV format & Omnivore's JSONs. Titles, tags and addition date will be preserved during the import. An automatically created list will contain all the imported bookmarks.\n\n:::info\nAll the URLs in the bookmarks file will be added automatically, you will not be able to pick and choose which bookmarks to import!\n:::\n\n## Import from Chrome\n\n- Open Chrome and go to `chrome://bookmarks`\n- Click on the three dots on the top right corner and choose `Export bookmarks`\n- This will download an html file with all of your bookmarks.\n- To import the bookmark file, go to the settings and click \"Import Bookmarks from HTML file\".\n\n## Import from Firefox\n- Open Firefox and click on the menu button (☰) in the top right corner.\n- Navigate to Bookmarks > Manage bookmarks (or press Ctrl + Shift + O / Cmd + Shift + O to open the Bookmarks Library).\n- In the Bookmarks Library, click the Import and Backup button at the top. Select Export Bookmarks to HTML... to save your bookmarks as an HTML file.\n- To import a bookmark file, go back to the Import and Backup menu, then select Import Bookmarks from HTML... and choose your saved HTML file.\n\n## Import from Pocket\n\n- Go to the [Pocket export page](https://getpocket.com/export) and follow the instructions to export your bookmarks.\n- Pocket after a couple of minutes will mail you a zip file with all the bookmarks.\n- Unzip the file and you'll get a CSV file.\n- To import the bookmark file, go to the settings and click \"Import Bookmarks from Pocket export\".\n\n## Import from Omnivore\n\n- Follow Omnivore's [documentation](https://docs.omnivore.app/using/exporting.html) to export your bookmarks.\n- This will give you a zip file with all your data.\n- The zip file contains a lot of JSONs in the format `metadata_*.json`. You can either import every JSON file manually, or merge the JSONs into a single JSON file and import that.\n- To  merge the JSONs into a single JSON file, you can use the following command in the unzipped directory: `jq -r '.[]' metadata_*.json | jq -s > omnivore.json` and then import the `omnivore.json` file. You'll need to have the [jq](https://github.com/jqlang/jq) tool installed.\n\n## Import using the CLI\n\n:::warning\nImporting bookmarks using the CLI requires some technical knowledge and might not be very straightforward for non-technical users. Don't hesitate to ask questions in github discussions or discord though.\n:::\n\nIf you can get your bookmarks in a text file with one link per line, you can use the following command to import them using the [karakeep cli](https://docs.karakeep.app/command-line):\n\n```\nwhile IFS= read -r url; do\n    karakeep --api-key \"<KEY>\" --server-addr \"<SERVER_ADDR>\" bookmarks add --link \"$url\"\ndone < all_links.txt\n```\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/11-FAQ.md",
    "content": "# Frequently Asked Questions (FAQ)\r\n\r\n## User Management\r\n\r\n### Lost password\r\n\r\n#### If you are not an administrator\r\n\r\nAdministrators can reset the password of any user. Contact an administrator to reset the password for you.\r\n\r\n- Navigate to the `Admin Settings` page\r\n- Find the user in the `Users List`\r\n- In the `Actions` column, there is a button to reset the password\r\n- Enter a new password and press `Reset`\r\n- The new password is now set\r\n- If required, you can change your password again (so the admin does not know your password) in the `User Settings`\r\n\r\n#### If you are an administrator\r\n\r\nIf you are an administrator and lost your password, you have to reset the password in the database.\r\n\r\nTo reset the password:\r\n\r\n- Acquire some kind of tools that helps you to connect to the database:\r\n  - `sqlite3` on Linux: run `apt-get install sqlite3` (depending on your package manager)\r\n  - e.g. `dbeaver` on Windows\r\n- Shut down Karakeep\r\n- Connect to the `db.db` database, which is located in the `data` directory you have mounted to your docker container:\r\n  - by e.g. running `sqlite3 db.db` (in your `data` directory)\r\n  - or going through e.g. the `dbeaver` UI to locate the file in the data directory and connecting to it\r\n- Update the password in the database by running:\r\n  - `update user set password='$2a$10$5u40XUq/cD/TmLdCOyZ82ePENE6hpkbodJhsp7.e/BgZssUO5DDTa', salt='' where email='<YOUR_EMAIL_HERE>';`\r\n  - (don't forget to put your email address into the command)\r\n- The new password for your user is now `adminadmin`.\r\n- Start Karakeep again\r\n- Log in with your email address and the password `adminadmin` and change the password to whatever you want in the `User Settings`\r\n\r\n### Adding another administrator\r\n\r\nBy default, the first user to sign up gets promoted to administrator automatically.\r\n\r\nIn case you want to grant those permissions to another user:\r\n\r\n- Navigate to the `Admin Settings` page\r\n- Find the user in the `Users List`\r\n- In the `Actions` column, there is a button to change the Role\r\n- Change the Role to `Admin`\r\n- Press `Change`\r\n- The new administrator has to log out and log in again to get the new role assigned\r\n\r\n### Adding new users, when signups are disabled\r\n\r\nAdministrators can create new accounts any time:\r\n\r\n- Navigate to the `Admin Settings` page\r\n- Go to the `Users List`\r\n- Press the `Create User` Button.\r\n- Enter the information for the user\r\n- Press `create`\r\n- The new user can now log in\r\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/12-troubleshooting.md",
    "content": "# Troubleshooting\n\n## SqliteError: no such table: user\n\nThis usually means that there's something wrong with the database setup (more concretely, it means that the database is not initialized). This can be caused by multiple problems:\n1. **Wiped DATA_DIR:** Your `DATA_DIR` got wiped (or the backing storage dir changed). If you did this intentionally, restart the container so that it can re-initalize the database.\n2. **Missing DATA_DIR**: You're not using the default docker compose file, and you forgot to configure the `DATA_DIR` env var. This will result into the database getting set up in a different directory than the one used by the service.\n\n## Chrome Failed to Read DnsConfig\n\nIf you see this error in the logs of the chrome container, it's a benign error and you can safely ignore it. Whatever problems you're having, is unrelated to this error.\n\n## AI Tagging not working (when using OpenAI)\n\nCheck the logs of the container and this will usually tell you what's wrong. Common problems are:\n1. Typo in the env variable `OPENAI_API_KEY` name resulting into logs saying something like \"skipping inference as it's not configured\".\n2. You forgot to call `docker compose up` after configuring open ai.\n3. OpenAI requires pre-charging the account with credits before using it, otherwise you'll get an error like \"insufficient funds\".\n\n## AI Tagging not working (when using Ollama)\n\nCheck the logs of the container and this will usually tell you what's wrong. Common problems are:\n1. Typo in the env variable `OLLAMA_BASE_URL` name resulting into logs saying something like \"skipping inference as it's not configured\".\n2. You forgot to call `docker compose up` after configuring ollama.\n3. You didn't change the `INFERENCE_TEXT_MODEL` env variable, resulting into karakeep attempting to use gpt models with ollama which won't work.\n4. Ollama server is not reachable by the karakeep container. This can be caused by:\n    1. Ollama server being in a different docker network than the karakeep container.\n    2. You're using `localhost` as the `OLLAMA_BASE_URL` instead of the actual address of the ollama server. `localhost` points to the container itself, not the docker host. Check this [stackoverflow answer](https://stackoverflow.com/questions/24319662/from-inside-of-a-docker-container-how-do-i-connect-to-the-localhost-of-the-mach) to find how to correctly point to the docker host address instead.\n\n## Crawling not working\n\nCheck the logs of the container and this will usually tell you what's wrong. Common problems are:\n1. You changed the name of the chrome container but didn't change the `BROWSER_WEB_URL` env variable.\n\n## Upgrading Meilisearch - Migrating the Meilisearch db version\n\n[Meilisearch](https://www.meilisearch.com/) is the database used by karakeep for searching in your bookmarks. The version used by karakeep is `1.13.3` and it is advised not to upgrade it without good reasons. If you do, you might see errors like `Your database version (1.11.1) is incompatible with your current engine version (1.13.3). To migrate data between Meilisearch versions, please follow our guide on https://www.meilisearch.com/docs/learn/update_and_migration/updating.`.\n\nLuckily we can easily workaround this:\n1. Stop the Meilisearch container.\n2. Inside the Meilisearch volume bound to `/meili_data`, erase/rename the folder called `data.ms`.\n3. Launch Meilisearch again.\n4. Login to karakeep as administrator and go to (as of v0.24.1) `Admin Settings > Background Jobs` then click on `Reindex All Bookmarks`.\n5. When the reindexing has finished, Meilisearch should be working as usual.\n\nIf you run into issues, the official documentation can be found [there](https://www.meilisearch.com/docs/learn/update_and_migration/updating).\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/13-community-projects.md",
    "content": "# Community Projects\n\nThis page lists community projects that are built around Karakeep, but not officially supported by the development team.\n\n:::warning\nThis list comes with no guarantees about security, performance, reliability, or accuracy. Use at your own risk.\n:::\n\n### Raycast Extension\n\n_By [@luolei](https://github.com/foru17)._\n\nA user-friendly Raycast extension that seamlessly integrates with Karakeep, bringing powerful bookmark management to your fingertips. Quickly save, search, and organize your bookmarks, texts, and images—all through Raycast's intuitive interface.\n\nGet it [here](https://www.raycast.com/luolei/karakeep).\n\n### Alfred Workflow\n\n_By [@yinan-c](https://github.com/yinan-c)_\n\nAn Alfred workflow to quickly hoard stuff or access your hoarded bookmarks!\n\nGet it [here](https://www.alfredforum.com/topic/22528-hoarder-workflow-for-self-hosted-bookmark-management/).\n\n### Obsidian Plugin\n\n_By [@jhofker](https://github.com/jhofker)_\n\nAn Obsidian plugin that syncs your Karakeep bookmarks with Obsidian, creating markdown notes for each bookmark in a designated folder.\n\nGet it [here](https://github.com/jhofker/obsidian-hoarder/), or install it directly from Obsidian's community plugin store ([link](https://obsidian.md/plugins?id=hoarder-sync)).\n\n### Telegram Bot\n\n_By [@Madh93](https://github.com/Madh93)_\n\nA Telegram Bot for saving bookmarks to Karakeep directly through Telegram.\n\nGet it [here](https://github.com/Madh93/karakeepbot).\n\n### Hoarder's Pipette\n\n_By [@DanSnow](https://github.com/DanSnow)_\n\nA chrome extension that injects karakeep's bookmarks into your search results.\n\nGet it [here](https://dansnow.github.io/hoarder-pipette/guides/installation/).\n\n### Karakeep-Python-API\n\n_By [@thiswillbeyourgithub](https://github.com/thiswillbeyourgithub/)_\n\nA python package to simplify access to the karakeep API. Can be used as a library or from the CLI. Aims for feature completeness and high test coverage but do check its feature matrix before relying too much on it.\n\nIts repository also hosts the [Community Script](https://github.com/thiswillbeyourgithub/karakeep_python_api/tree/main/community_scripts), for example:\n\n| Community Script | Description | Documentation |\n|----------------|-------------|---------------|\n| **Karakeep-Time-Tagger** | Automatically adds time-to-read tags (`0-5m`, `5-10m`, etc.) to bookmarks based on content length analysis. Includes systemd service and timer files for automated periodic execution. | [`Link`](https://github.com/thiswillbeyourgithub/karakeep_python_api/tree/main/community_scripts/karakeep-time-tagger) |\n| **Karakeep-List-To-Tag** | Converts a Karakeep list into tags by adding a specified tag to all bookmarks within that list. | [`Link`](https://github.com/thiswillbeyourgithub/karakeep_python_api/tree/main/community_scripts/karakeep-list-to-tag) |\n| **Omnivore2Karakeep-Highlights** | Imports highlights from Omnivore export data to Karakeep, with intelligent position detection and bookmark matching. Supports dry-run mode for testing. | [`Link`](https://github.com/thiswillbeyourgithub/karakeep_python_api/tree/main/community_scripts/omnivore2karakeep-highlights) |\n\n\nGet it [here](https://github.com/thiswillbeyourgithub/karakeep_python_api).\n\n### FreshRSS_to_Karakeep\n\n_By [@thiswillbeyourgithub](https://github.com/thiswillbeyourgithub/)_\n\nA python script to automatically create Karakeep bookmarks from your [FreshRSS](https://github.com/FreshRSS/FreshRSS) *favourites/saved* RSS item. Made to be called periodically. Based on the community project `Karakeep-Python-API` above, by the same author.\n\nGet it [here](https://github.com/thiswillbeyourgithub/freshrss_to_karakeep).\n\n### karakeep-sync\n_By [@sidoshi](https://github.com/sidoshi/)_\n\nSync links from Hacker News upvotes, Reddit Saves to Karakeep for centralized bookmark management.\n\nGet it [here](https://github.com/sidoshi/karakeep-sync)\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/14-guides/01-legacy-container-upgrade.md",
    "content": "# Legacy Container Upgrade\n\nKarakeep's 0.16 release consolidated the web and worker containers into a single container and also dropped the need for the redis container. The legacy containers will stop being supported soon, to upgrade to the new container do the following:\n\n1. Remove the redis container and its volume if it had one.\n2. Move the environment variables that you've set exclusively to the `workers` container to the `web` container.\n3. Delete the `workers` container.\n4. Rename the web container image from `hoarder-app/hoarder-web` to `hoarder-app/hoarder`.\n\n```diff\ndiff --git a/docker/docker-compose.yml b/docker/docker-compose.yml\nindex cdfc908..6297563 100644\n--- a/docker/docker-compose.yml\n+++ b/docker/docker-compose.yml\n@@ -1,7 +1,7 @@\n version: \"3.8\"\n services:\n   web:\n-    image: ghcr.io/hoarder-app/hoarder-web:${KARAKEEP_VERSION:-release}\n+    image: ghcr.io/karakeep-app/karakeep:${KARAKEEP_VERSION:-release}\n     restart: unless-stopped\n     volumes:\n       - data:/data\n@@ -10,14 +10,10 @@ services:\n     env_file:\n       - .env\n     environment:\n-      REDIS_HOST: redis\n       MEILI_ADDR: http://meilisearch:7700\n+      BROWSER_WEB_URL: http://chrome:9222\n+      # OPENAI_API_KEY: ...\n       DATA_DIR: /data\n-  redis:\n-    image: redis:7.2-alpine\n-    restart: unless-stopped\n-    volumes:\n-      - redis:/data\n   chrome:\n     image: gcr.io/zenika-hub/alpine-chrome:123\n     restart: unless-stopped\n@@ -37,24 +33,7 @@ services:\n       MEILI_NO_ANALYTICS: \"true\"\n     volumes:\n       - meilisearch:/meili_data\n-  workers:\n-    image: ghcr.io/hoarder-app/hoarder-workers:${KARAKEEP_VERSION:-release}\n-    restart: unless-stopped\n-    volumes:\n-      - data:/data\n-    env_file:\n-      - .env\n-    environment:\n-      REDIS_HOST: redis\n-      MEILI_ADDR: http://meilisearch:7700\n-      BROWSER_WEB_URL: http://chrome:9222\n-      DATA_DIR: /data\n-      # OPENAI_API_KEY: ...\n-    depends_on:\n-      web:\n-        condition: service_started\n\n volumes:\n-  redis:\n   meilisearch:\n   data:\n```\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/14-guides/02-search-query-language.md",
    "content": "# Search Query Language\n\nKarakeep provides a search query language to filter and find bookmarks. Here are all the supported qualifiers and how to use them:\n\n## Basic Syntax\n\n- Use spaces to separate multiple conditions (implicit AND)\n- Use `and`/`or` keywords for explicit boolean logic\n- Prefix qualifiers with `-` to negate them\n- Use parentheses `()` for grouping conditions (note that groups can't be negated)\n\n## Qualifiers\n\nHere's a comprehensive table of all supported qualifiers:\n\n| Qualifier                        | Description                                                                                                                                                                                               | Example Usage                                |\n| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |\n| `is:fav`                         | Favorited bookmarks                                                                                                                                                                                       | `is:fav`                                     |\n| `is:archived`                    | Archived bookmarks                                                                                                                                                                                        | `-is:archived`                               |\n| `is:tagged`                      | Bookmarks that has one or more tags                                                                                                                                                                       | `is:tagged`                                  |\n| `is:inlist`                      | Bookmarks that are in one or more lists                                                                                                                                                                   | `is:inlist`                                  |\n| `is:link`, `is:text`, `is:media` | Bookmarks that are of type link, text or media                                                                                                                                                            | `is:link`                                    |\n| `url:<value>`                    | Match bookmarks with URL substring                                                                                                                                                                        | `url:example.com`                            |\n| `title:<value>`                  | Match bookmarks with title substring                                                                                                               | `title:example`                              |\n|                                  | Supports quoted strings for titles with spaces                                                                                                   | `title:\"my title\"`                           |\n| `#<tag>`                         | Match bookmarks with specific tag                                                                                                                                                                         | `#important`                                 |\n|                                  | Supports quoted strings for tags with spaces                                                                                                                                                              | `#\"work in progress\"`                        |\n| `list:<name>`                    | Match bookmarks in specific list                                                                                                                                                                          | `list:reading`                               |\n|                                  | Supports quoted strings for list names with spaces                                                                                                                                                        | `list:\"to review\"`                           |\n| `after:<date>`                   | Bookmarks created on or after date (YYYY-MM-DD)                                                                                                                                                           | `after:2023-01-01`                           |\n| `before:<date>`                  | Bookmarks created on or before date (YYYY-MM-DD)                                                                                                                                                          | `before:2023-12-31`                          |\n| `feed:<name>`                    | Bookmarks imported from a particular rss feed                                                                                                                                                             | `feed:Hackernews`                            |\n| `age:<time-range>`               | Match bookmarks based on how long ago they were created. Use `<` or `>` to indicate the maximum / minimum age of the bookmarks. Supported units: `d` (days), `w` (weeks), `m` (months), `y` (years). | `age:<1d` `age:>2w` `age:<6m` `age:>3y` |\n\n### Examples\n\n```plaintext\n# Find favorited bookmarks from 2023 that are tagged \"important\"\nis:fav after:2023-01-01 before:2023-12-31 #important\n\n# Find archived bookmarks that are either in \"reading\" list or tagged \"work\"\nis:archived and (list:reading or #work)\n\n# Find bookmarks that are not tagged or not in any list\n-is:tagged or -is:inlist\n# Find bookmarks with \"React\" in the title\ntitle:React\n```\n\n## Combining Conditions\n\nYou can combine multiple conditions using boolean logic:\n\n```plaintext\n# Find favorited bookmarks from 2023 that are tagged \"important\"\nis:fav after:2023-01-01 before:2023-12-31 #important\n\n# Find archived bookmarks that are either in \"reading\" list or tagged \"work\"\nis:archived and (list:reading or #work)\n\n# Find bookmarks that are not favorited and not archived\n-is:fav -is:archived\n```\n\n## Text Search\n\nAny text not part of a qualifier will be treated as a full-text search:\n\n```plaintext\n# Search for \"machine learning\" in bookmark content\nmachine learning\n\n# Combine text search with qualifiers\nmachine learning is:fav\n```\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/14-guides/03-singlefile.md",
    "content": "# Using Karakeep with SingleFile Extension\n\nKarakeep supports being a destination for the [SingleFile extension](https://github.com/gildas-lormeau/SingleFile). This has the benefit of allowing you to use the singlefile extension to hoard links as you're seeing them in the browser. This is perfect for websites that don't like to get crawled, has annoying cookie banner or require authentication.\n\n## Setup\n\n1. Install the [SingleFile extension](https://github.com/gildas-lormeau/SingleFile).\n2. In the extension settings, select `Destinations`.\n3. Select `upload to a REST Form API`.\n4. In the URL, insert the address: `https://YOUR_SERVER_ADDRESS/api/v1/bookmarks/singlefile`.\n5. In the `authorization token` field, paste an API key that you can get from your karakeep settings.\n6. Set `data field name` to `file`.\n7. Set `URL field name` to `url`.\n8. (Optional) Add `&ifexists=MODE` to the URL where MODE is one of `skip`, `overwrite`, `overwrite-recrawl`, `append`, or `append-recrawl`. See \"Handling Existing Bookmarks\" section below for details.\n\nNow, go to any page and click the singlefile extension icon. Once it's done with the upload, the bookmark should show up in your karakeep instance. Note that the singlefile extension doesn't show any progress on the upload. Given that archives are typically large, it might take 30+ seconds until the upload is done and starts showing up in Karakeep.\n\n## Handling Existing Bookmarks\n\nWhen uploading a page that already exists in your archive (same URL), you can control the behavior by setting the `ifexists` query parameter in the upload URL. The available modes are:\n\n- `skip` (default): If the bookmark already exists, skip creating a new one\n- `overwrite`: Replace existing precrawled archive (only the most recent archive is kept)\n- `overwrite-recrawl`: Replace existing archive and queue a recrawl to update content\n- `append`: Add new archive version alongside existing ones\n- `append-recrawl`: Add new archive and queue a recrawl\n\nTo use these modes, append `?ifexists=MODE` to your upload URL, replacing `MODE` with your desired behavior.\n\nFor example:  \n`https://YOUR_SERVER_ADDRESS/api/v1/bookmarks/singlefile?ifexists=overwrite`\n\n\n## Recommended settings\n\nIn the singlefile extension, you probably will want to change the following settings for better experience:\n* Stylesheets > compress CSS content: on\n* Stylesheets > group duplicate stylesheets together: on\n* HTML content > remove frames: on\n\nAlso, you most likely will want to change the default `MAX_ASSET_SIZE_MB` in karakeep to something higher, for example `100`.\n\n:::info\nCurrently, we don't support screenshots for singlefile uploads, but this will change in the future.\n:::\n\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/14-guides/04-hoarder-to-karakeep-migration.md",
    "content": "# Hoarder to Karakeep Migration\n\nHoarder is rebranding to Karakeep. Due to github limitations, the old docker image might not be getting new updates after the rebranding. You might need to update your docker image to point to the new karakeep image instead by applying the following change in the docker compose file.\n\n```diff\ndiff --git a/docker/docker-compose.yml b/docker/docker-compose.yml\nindex cdfc908..6297563 100644\n--- a/docker/docker-compose.yml\n+++ b/docker/docker-compose.yml\n@@ -1,7 +1,7 @@\n version: \"3.8\"\n services:\n   web:\n-    image: ghcr.io/hoarder-app/hoarder:${HOARDER_VERSION:-release}\n+    image: ghcr.io/karakeep-app/karakeep:${HOARDER_VERSION:-release}\n```\n\nYou can also change the `HOARDER_VERSION` environment variable but if you do so remember to change it in the `.env` file as well.\n\n## Migrating a Baremetal Installation\n\nIf you previously used the [Debian/Ubuntu install script](https://docs.karakeep.app/Installation/debuntu) to install Hoarder, there is an option to migrate your installation to Karakeep.\n\n```bash\nbash karakeep-linux.sh migrate\n```\n\nThis will migrate your installation with no user input required. After the migration, the script will also check for an update.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/14-guides/05-different-ai-providers.md",
    "content": "# Configuring different AI Providers\n\nKarakeep uses LLM providers for AI tagging and summarization. We support OpenAI-compatible providers and ollama. This guide will show you how to configure different providers.\n\n## OpenAI\n\nIf you want to use OpenAI itself, you just need to pass in the OPENAI_API_KEY environment variable.\n\n```\nOPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\n# You can change the default models by uncommenting the following lines, and choosing your model.\n# INFERENCE_TEXT_MODEL=gpt-4.1-mini\n# INFERENCE_IMAGE_MODEL=gpt-4o-mini\n```\n\n## Ollama\n\nOllama is a local LLM provider that you can use to run your own LLM server. You'll need to pass ollama's address to karakeep and you need to ensure that it's accessible from within the karakeep container (e.g. no localhost addresses).\n\n```\n# MAKE SURE YOU DON'T HAVE OPENAI_API_KEY set, otherwise it takes precedence.\n\nOLLAMA_BASE_URL=http://ollama.mylab.com:11434\n\n# Make sure to pull the models in ollama first. Example models:\nINFERENCE_TEXT_MODEL=gemma3\nINFERENCE_IMAGE_MODEL=llava\n\n# If the model you're using doesn't support structured output, you also need:\n# INFERENCE_OUTPUT_SCHEMA=plain\n```\n\n## Gemini\n\nGemini has an OpenAI-compatible API. You need to get an api key from Google AI Studio.\n\n```\n\nOPENAI_BASE_URL=https://generativelanguage.googleapis.com/v1beta\nOPENAI_API_KEY=YOUR_API_KEY\n\n# Example models:\nINFERENCE_TEXT_MODEL=gemini-2.0-flash\nINFERENCE_IMAGE_MODEL=gemini-2.0-flash\n```\n\n## OpenRouter\n\n```\nOPENAI_BASE_URL=https://openrouter.ai/api/v1\nOPENAI_API_KEY=YOUR_API_KEY\n\n# Example models:\nINFERENCE_TEXT_MODEL=meta-llama/llama-4-scout\nINFERENCE_IMAGE_MODEL=meta-llama/llama-4-scout\n```\n\n## Perplexity\n\n```\nOPENAI_BASE_URL: https://api.perplexity.ai\nOPENAI_API_KEY: Your Perplexity API Key\nINFERENCE_TEXT_MODEL: sonar-pro\nINFERENCE_IMAGE_MODEL: sonar-pro\n```\n\n## Azure\n\nAzure has an OpenAI-compatible API.\n\nYou can get your API key from the Overview page of the Azure AI Foundry Portal or via \"Keys + Endpoints\" on the resource in the Azure Portal.\n\n:::warning\nThe [model name is the deployment name](https://learn.microsoft.com/en-us/azure/ai-foundry/openai/how-to/switching-endpoints#keyword-argument-for-model) you specified when deploying the model, which may differ from the base model name.\n:::\n\n```\n# Deployed via Azure AI Foundry:\nOPENAI_BASE_URL=https://{your-azure-ai-foundry-resource-name}.cognitiveservices.azure.com/openai/v1/\n\n# Deployed via Azure OpenAI Service:\nOPENAI_BASE_URL=https://{your-azure-openai-resource-name}.openai.azure.com/openai/v1/\n\nOPENAI_API_KEY=YOUR_API_KEY\nINFERENCE_TEXT_MODEL=YOUR_DEPLOYMENT_NAME\nINFERENCE_IMAGE_MODEL=YOUR_DEPLOYMENT_NAME\n```\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/14-guides/06-server-migration.md",
    "content": "# Migrating Between Servers\n\nThis guide explains how to migrate all of your data from one Karakeep server to another using the official CLI.\n\n## What the command does\n\nThe migration copies user-owned data from a source server to a destination server in this order:\n\n- User settings\n- Lists (preserving hierarchy and settings)\n- RSS feeds\n- AI prompts (custom prompts and their enabled state)\n- Webhooks (URL and events)\n- Tags (ensures tags by name exist)\n- Rule engine rules (IDs remapped to destination equivalents)\n- Bookmarks (links, text, and assets)\n  - After creation, attaches the correct tags and adds to the correct lists\n\nNotes:\n- Webhook tokens cannot be read via the API, so tokens are not migrated. Re‑add them on the destination if needed.\n- Asset bookmarks are migrated by downloading the original asset and re‑uploading it to the destination. Only images and PDFs are supported for asset bookmarks.\n- Link bookmarks on the destination may be de‑duplicated if the same URL already exists.\n\n## Prerequisites\n\n- Install the CLI:\n  - NPM: `npm install -g @karakeep/cli`\n  - Docker: `docker run --rm ghcr.io/karakeep-app/karakeep-cli:release --help`\n- Collect API keys and base URLs for both servers:\n  - Source: `--server-addr`, `--api-key`\n  - Destination: `--dest-server`, `--dest-api-key`\n\n## Quick start\n\n```\nkarakeep --server-addr https://src.example.com --api-key <SOURCE_API_KEY> migrate \\\n  --dest-server https://dest.example.com \\\n  --dest-api-key <DEST_API_KEY>\n```\n\nThe command is long‑running and shows live progress for each phase. You will be prompted for confirmation; pass `--yes` to skip the prompt.\n\n### Options\n\n- `--server-addr <url>`: Source server base URL\n- `--api-key <key>`: API key for the source server\n- `--dest-server <url>`: Destination server base URL\n- `--dest-api-key <key>`: API key for the destination server\n- `--batch-size <n>`: Page size for bookmark migration (default 50, max 100)\n- `-y`, `--yes`: Skip the confirmation prompt\n\n## What to expect\n\n- Lists are recreated parent‑first and retain their hierarchy.\n- Feeds, prompts, webhooks, and tags are recreated by value.\n- Rules are recreated after IDs (tags, lists, feeds) are remapped to their corresponding destination IDs.\n- After each bookmark is created, the command attaches the correct tags and adds it to the correct lists.\n\n## Caveats and tips\n\n- Webhook auth tokens must be re‑entered on the destination after migration.\n- If your destination already contains data, duplicate links may be de‑duplicated; tags and list membership are still applied to the existing bookmark.\n\n## Troubleshooting\n\n- If the command exits early, you can re‑run it, but note:\n  - Tags and lists that already exist are reused.\n  - Link de‑duplication avoids duplicate link bookmarks. Notes and assets will get re-created.\n  - Rules, webhooks, rss feeds will get re-created and you'll have to manually clean them up afterwards.\n  - The progress log indicates how far it got.\n- Use a smaller `--batch-size` if your source or destination is under heavy load.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/14-guides/_category_.json",
    "content": "{\n    \"label\": \"Guides\",\n}\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/_category_.json",
    "content": "{ \"label\": \"API\" }\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/add-a-bookmark-to-a-list.api.mdx",
    "content": "---\nid: add-a-bookmark-to-a-list\ntitle: \"Add a bookmark to a list\"\ndescription: \"Add the bookmarks to a list\"\nsidebar_label: \"Add a bookmark to a list\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy9VE1v00AQ/SurOYG01C0qovItHJAKCFWQikPkw8SexNvY3u3uuE2w9r+jsU2ctKFcEL7Yu56PN2/eTAcFhdwbx8Y2kMKsKBSXpJbWbmr0m6DYKlSVCQwaGNcB0gV8MYEDZBoC5a03vIN00cGS0JOftVxCushipsGhx5qYfOgNQl5SjZB2wDtHkEJgb5o1aKAt1q6SK0OmqLa7df14d/Xebn++K7fMNr+S7IZ7E8l+XUDU4Om+NZ4KSNm3pKHBWgyqwUCDkZIccglR/7P0H0ZqXoSwnIyOYGTiEZxtAgVB8vb8Ul7HTfhqVW4bpobVm6NuqEcMCouC+tSXp3yFHGX95NJYVivbNgJkjCpe6FxlchSv5C6I6wmC7PKOcmm889aRZzOAzm1Bz2mMGmoKAden/h0xtRgiTPZZlEfOXNpCyGr7rEJZCon0MyTd0NaY7LWZdBPNEUSN/uG31lpfQQodFoWnEGKCziQPF6DhAb3BZTVUMv4eaFxhWzGkUDK7kCYJ+93ZBj1uiNwZOgf6CdfzktQYQdlV36nPo70asECM8WBMvgvBQ+bDYdmzJZmljt5MVNQbgR4/PlpfoyD89GPeU2qalRV3qXqAdHF2fnZ+INY9ntnN9Un8s5trtbL+GLwUGzU4G7jGXhqjrGU94KStw91wFLqbpPa3lTJUzrTlxFVoGkncN68b+7/o5zmAhnQ/2PtYcnswa5mG0gYWp65bYqBbX8Uo1/cteVlT2aSAXieFCfJdQLrCKtALdbz6Ngr4tfoT7vESm10vtKqVE2jY0G5aS7KK/mPWA3ZiFjWUhAX5vvbBYJbn5PjA9dlqEAnvR/Pmdg4a8Fi4T4TaRz8Jq+sGi7ndUBPjHiXLWQDG+As6ID3T\nsidebar_class_name: \"put api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Add a bookmark to a list\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"put\"}\n  path={\"/lists/{listId}/bookmarks/{bookmarkId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nAdd the bookmarks to a list\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"ListId\"},\"required\":true,\"name\":\"listId\",\"in\":\"path\"},{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"204\":{\"description\":\"No content - the bookmark was added\"},\"404\":{\"description\":\"List or bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/attach-asset.api.mdx",
    "content": "---\nid: attach-asset\ntitle: \"Attach asset\"\ndescription: \"Attach a new asset to a bookmark\"\nsidebar_label: \"Attach asset\"\nhide_title: true\nhide_table_of_contents: true\napi: eJztVcGO2zYQ/RVhTg3ArnaLFA10c4oW3RZoF1kHPRg+jMWxxTVFMiS1tivw34Oh5JUde4McesxFkEaP5Lw3b4Y9SAq1Vy4qa6CCWYxYNwUWhnYFhkCxiLbAYmXttkW/BQERNwGqBbwfQwGWAgLVnVfxANWihxWhJz/rYgPVYpmWAhx6bCmSDxkQ6oZahKqHeHAEFYToldmAANpj6zSHFCmp94dNu3t694vd//dzs4/R1u84AxUz5JjBvYQkwNOnTnmSUEXfkQCDLYNWE0iAYo4OYwOcFa+gEN9beeBczpWYN3QiQJYFBNTWRDKR4eicVjUyvHwKvOYKMbt6ojqCAOetIx8VBf6r5CX5JCCfN8/RK9KYrmXdtTLbP2Krfx1TERBqT2RCY/kj7/F4GlmhMeTvW9wQCFh3Wj/ghma+btQzR56VJMu4UakZ75BTptrjTpOcsF0g/9Fpi5JYz85sjd0ZWKazAiyY4CmdZUoDIjhrwqDBT7d3r6iexSY5yP9d9NdFF7BWmv7ORr/M3XRa44o7hRvi2yr09vbtZVGOfVYYG4u17Yz8/4pSW3kl+SSgpRBYvMt/XxDJO0z4TCSvj42V3O425GO56ysoj4KHsp9GQyqzEoHrSv75OKc6r6GCHqX0FEIq0any+Y6rh16xtJnC+HvQbY2djlBBE6MLVVlGf7jZosctkbtB50Bcc/ywQ2HXRWyo+GvEF0MukFI6GbGPrOxw8umgfZGJT87+ZBgPwAxiq+WX361vkTP889951pIr9mGahL8dJ/DQMJOdTvrkoh0Sj9a15UUs3sDs7ub25vZkXL/Qmj3cX5Vh9nBfrK0/14A1SyJXscVsrXGwH6+qsXHOtusne37TlTawirSPpdOoDJ+Yi9+Pvlm8NCp7pDq7VEbrLAU0bLVqAX2/wkAfvU6Jw5868nwzLifjZHtJFfhdQrVGHegrHH74MBr+TfFaumMQzSH7U3f8BQK2dDi/BRPPjYZQks9ZDICxkj/mAk8bXLR2EscVs7omF7+KXZ604cM/j/M874bLts19Dx53IPIzJ2sz9+ztHOtBo9l0eQ7AsCdbFs8d/4XDM6urcvT9gJjbLZmUXtSJ/M3CpPQZ2d8imQ==\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Attach asset\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/bookmarks/{bookmarkId}/assets\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nAttach a new asset to a bookmark\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The asset to attach\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"unknown\"]}},\"required\":[\"id\",\"assetType\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"201\":{\"description\":\"The attached asset\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}}},\"404\":{\"description\":\"Bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/attach-tags-to-a-bookmark.api.mdx",
    "content": "---\nid: attach-tags-to-a-bookmark\ntitle: \"Attach tags to a bookmark\"\ndescription: \"Attach tags to a bookmark\"\nsidebar_label: \"Attach tags to a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVU2P0zAQ/SvRnEAyTUEgUG4FgbQgwQqKOFQ9TJNp420SG3uytET572icr5aWFUL0UCXO83jmzZvnBjLyqdOWtakggQUzpnnEuPMRmwijjTH7Et0eFMgiJCt43S95WCvwlNZO8xGSVQMbQkduUXMOyWrdrhVYdFgSk/MB4NOcSoSkAT5aggQ8O13tQAEdsLSFLGnSWXE47sofd69emsPPF/mB2aSvJAPNATJkcJNBq8DR91o7yiBhV5OCCksBbSaQAi3FWeQcJCvZQZ5fm+wouZxTsMxpqj/QMQMFqamYKhY4WlvoFAUe33nZc6Uws7mjlEGBdcaSY00+fA0cjih0Do+SHlPp/2r3TXbJXht68zGUffFNfqccrboc1sO6t6byXfhn8/l1OgrtOTLbng7KhJ9IZ/7/8TJE/gtu/k0yy0DdBRfjuT0fz+fPLykY5BZVhqOtqavs/xWemuxa1xSU5D3urnb0vIQQYcKv+46XxLnJRPXGh2NF/AnEw1j4uJkmpI2DKGSc3f0wrLUrIIEGs8yR922MVsf3T0HBPTqNm6LvXPe5Y22LdcGQQM5sfRLH7I6zPTrcE9kZWgvqirr6CCIwzin60OOjLhfp2YnPfBFeu5NP3WYkSU6WOgJMXCCAQPUP74wrUTJ8/20ZmJR+fZ7s4O2gqWFUV+PUTcIbh21syboVi9ka2Sf8dcU9nc1n8xMNjpUtbm+uMrG4vYm2xp3TILS1KrSxxKCt3uAe8uqz2M0k1ocNvqOQ6cCxLVBXcm5QQdPLZzW6qoglObPYzlYU5KK3ZAVNs0FPX13RtrL8vSYnt8R60k9gN9NenjNItlh4eiD1R5971T+O/pTsYB3VMci0qOUNFOzpeH4jhI7lhBm5kEUHeNOd9WQpYaYAF/PdqmHHIk3J8oPY9cks3n76shQt9hdPGYYfHP4AFf5DsibUHiQe1hoosNrVwQygiynKxXPh/yb0TrnX6GiaDrE0e6radmSH5V2IadtfmYTHdA==\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Attach tags to a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/bookmarks/{bookmarkId}/tags\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nAttach tags to a bookmark\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The tags to attach.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"tagId\":{\"type\":\"string\"},\"tagName\":{\"type\":\"string\"}}}}},\"required\":[\"tags\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The list of attached tag ids\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"attached\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"TagId\"}}},\"required\":[\"attached\"]}}}},\"404\":{\"description\":\"Bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/create-a-new-bookmark.api.mdx",
    "content": "---\nid: create-a-new-bookmark\ntitle: \"Create a new bookmark\"\ndescription: \"Create a new bookmark\"\nsidebar_label: \"Create a new bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJztWktv2zgQ/ivGnLWxU+zJN6fYYrOvBpsUezB8GEtji7VEqiTlxBX03xdD0pacSrHcBIsuYBQoFHGGnPd89KiChEysRWGFkjCF95rQ0ghHkh5HS6U2OeoNRGBxbWA6h5vwysAiAkNxqYXdwXRewZJQk56VNoXpfFEvItD0pSRjb1Syg2n17KCHlA77j6waxe5giCBW0pK0zIFFkYkYmWP82TBbBSZOKUe3mmUfV+5ouysIpqCWnym2EEGhVUHaCjJMZ4XNyD14MmO1kGuIQJZZhktes7qkCHJ8+oPkmhW4nkwmdQSo41RsKWlxL5XKCCXUEaxwq1j/vnWpbMe5dQSmzHPUu841b4dkZk9L7KjxMbvTQnk3fMtBsszZb5l6ZH6lc8xgUUcg8kJpe0/GCCVvk245VanjTtPtt8VCQASPtGTHZfycq6XI2I/0ZEny5hCBEXKd0covaGNgfz4s6rqOKlCShvnSrb6gppAbp16psy66FRvAwhRKLVjDQpMzISUz7+pOS9Q+loVmT8/9qj9iwcK/TmRLT9aJ7B763fCpS6Me0fZ7vlY2NIa8cO7p4QS5yHHNxxfJqmHqiS0Ohr8w70mQszVu5GuOXdT8r/YcplDSeK3fTSYn6hFmmjDZjehJGGvOKUov21t02+KFnK85oxKxEoNLwsBy9+rqZnG9FnJ9b9GW5vR5TZSYMo7J1YAViqzULmJIJsy1OFRH8dWZ+O23767KHebprdJdtL2Vsk/MN6icEZSGdE9++YZ9WECtccfMlnLz3YEq+xIWrcU4peTmxSaErGZa5ii57h/nsUgg7H+0m6NrZd9/0yjOSqOjOjKA3lXJztrWRzzrq6NdkRhrImlSZc/hWpVZdscn+ZJwDus3LfQc5q1ISJ0lKG5FPNDQqc2z903onAZTnvYccfaaDzsAS5sqPcyq5TITJqVh1AlaugscwwRnjj9DYxnA8OMioIGi/q8RUW/DGVpFjPja3laW+ZL0Cynw3WbuhmGvtnopN1I9dnQNx8AoLxz39i1vkJu5m/zaqjbtMry3xH37zRKlJH0bouNZ9YVQF5kuIFJXk6Cj2IIHAZ+KTGFC3EMbW50VUl3tuNF9UXcSNMD1CKa2oOURjnwOGrux3gHWBBDTROXByYtDfz78IAEe6L+bXJ/A9mtlw48NyQXYX4D9BdhfgP0F2F+A/QXYX4D9BdhfgP0F2P+YwP7nrh/tbzAZhSnj22H5WCXdWCgnY9ivJ2cSboeG3s0gHL9NVQJTKJSTt0AeNcJ4eZip8khVb0kbB4QcgoEKk0STMfUYCzHeXnMQoRbsYSduWPbGWWGZ8XgrtbYw0/HY6t3VBjVuiIorLAqIOq5GYYeRWo1sSqPfA/3Iy8L2b81679mK/uT2xPdgEj7ZpQmT8cXCEXHEu4cP+wHcb/88OLuxd/5uBsW/PGFeBHwWAuEQ2M21xiPu9kXGv/FXgIbjgPSbV60rWfvl0Rg1TEu/GZG2Ng7XgoD2g+4Oewbk2dB2zhhbYSXkSrG+7HfvlOurydUEmlQ4eGR2d9vpwdnd7Wil9LH72N0MOJSxDMynVUD5vTP+Z3j3kE79HwV4tbkFj4sMhbs+Btztg3sOy/YHAykH/nQOVbVEw02urvn1l5LYSfNFE9ruG4IIUsKEodK8gg2xZ0Ih/im0oi1mpXfDs3zn1uQ5ZnFMhX2RdtHKzbuP9w+uPvuPFnJXDEAjBwT/PwWIQDkjuSRw7yrIUK5LVxzA71kHWNi+Yx+nQvuyg3LXkrCqPMWD2pCsa4iCKpb/BjfT/Bc8h6eb\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Create a new bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/bookmarks\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nCreate a new bookmark\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The bookmark to create\",\"content\":{\"application/json\":{\"schema\":{\"allOf\":[{\"type\":\"object\",\"properties\":{\"title\":{\"type\":\"string\",\"nullable\":true,\"maxLength\":1000},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"note\":{\"type\":\"string\"},\"summary\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\",\"nullable\":true},\"crawlPriority\":{\"type\":\"string\",\"enum\":[\"low\",\"normal\"]},\"importSessionId\":{\"type\":\"string\"},\"source\":{\"type\":\"string\",\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]}}},{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\",\"format\":\"uri\"},\"precrawledArchiveId\":{\"type\":\"string\"}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\"}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\"}},\"required\":[\"type\",\"assetType\",\"assetId\"]}]}]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The bookmark already exists\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"userId\":{\"type\":\"string\"},\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"attachedBy\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\"]}},\"required\":[\"id\",\"name\",\"attachedBy\"]}},\"content\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true},\"description\":{\"type\":\"string\",\"nullable\":true},\"imageUrl\":{\"type\":\"string\",\"nullable\":true},\"imageAssetId\":{\"type\":\"string\",\"nullable\":true},\"screenshotAssetId\":{\"type\":\"string\",\"nullable\":true},\"fullPageArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"precrawledArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"videoAssetId\":{\"type\":\"string\",\"nullable\":true},\"favicon\":{\"type\":\"string\",\"nullable\":true},\"htmlContent\":{\"type\":\"string\",\"nullable\":true},\"contentAssetId\":{\"type\":\"string\",\"nullable\":true},\"crawledAt\":{\"type\":\"string\",\"nullable\":true},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\",\"nullable\":true},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true},\"size\":{\"type\":\"number\",\"nullable\":true},\"content\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"assetType\",\"assetId\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"unknown\"]}},\"required\":[\"type\"]}]},\"assets\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"userId\",\"tags\",\"content\",\"assets\"],\"title\":\"Bookmark\"}}}},\"201\":{\"description\":\"The bookmark got created\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"userId\":{\"type\":\"string\"},\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"attachedBy\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\"]}},\"required\":[\"id\",\"name\",\"attachedBy\"]}},\"content\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true},\"description\":{\"type\":\"string\",\"nullable\":true},\"imageUrl\":{\"type\":\"string\",\"nullable\":true},\"imageAssetId\":{\"type\":\"string\",\"nullable\":true},\"screenshotAssetId\":{\"type\":\"string\",\"nullable\":true},\"fullPageArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"precrawledArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"videoAssetId\":{\"type\":\"string\",\"nullable\":true},\"favicon\":{\"type\":\"string\",\"nullable\":true},\"htmlContent\":{\"type\":\"string\",\"nullable\":true},\"contentAssetId\":{\"type\":\"string\",\"nullable\":true},\"crawledAt\":{\"type\":\"string\",\"nullable\":true},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\",\"nullable\":true},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true},\"size\":{\"type\":\"number\",\"nullable\":true},\"content\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"assetType\",\"assetId\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"unknown\"]}},\"required\":[\"type\"]}]},\"assets\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"userId\",\"tags\",\"content\",\"assets\"],\"title\":\"Bookmark\"}}}},\"400\":{\"description\":\"Bad request\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/create-a-new-highlight.api.mdx",
    "content": "---\nid: create-a-new-highlight\ntitle: \"Create a new highlight\"\ndescription: \"Create a new highlight\"\nsidebar_label: \"Create a new highlight\"\nhide_title: true\nhide_table_of_contents: true\napi: eJztVj1v2zAQ/SvCzaztFJm4uUWLph0SNC46GB7O0tlSLJEMSSUxBP734ig5khwl6JChQzdbd7yPd+8e2UBGLrWF8YVWIOGzJfSUYKLoMcmLfV4W+9yDAI97B3IN307fHGwEOEprW/gjyHUDW0JLdln7HOR6EzYCLN3X5PwnnR1BNmepVjn1GRKvkzTmBgGpVp6U5yNoTFmkyEfmd47PNeDSnCrkX/5oCCTo7R2lXKSx2pD1BTm2brU+VGgPV9nA13lbqD0EAc6j9de7nSM/sKu62pJlO6nsDWuqS21fxuVjdcVAHaks9SMwCBkI2FsiBQK2ZU2MXEY7rEsP8uQYBHh68lMhVV2WuC0JpLc1BQFKe/oLx9BOoOAK5HqIx7j7Ya9dFV2OTQhtFGe0ci2sHxcX08Ns55eNaPN/lO8zSgHFdOu1I/sKKt08lhOFvAM1YkXP+YfZNgJ84bn0Xi2gJdLlYvGSO59wwJmk04z3406qswmEg4CKnMP9lO0MnRih99+cermc6KUDMlHaJztdq+xfbySe97nOQILREXiDLOEwz3utZ6m3D2RdVPraliChwSyz5FyYoynmDxcg4AFtwayN9XbmFqTTiuTeGyfnc2+PswNaPBCZGRoDYkJRugiJ3iU+p+RH55+0tTCnBnfQLcPYycXgJnrGhDNzH9ENZOfEexx/fNW2Qq7w++9VBI7H87O/wL48YWVKOlejfmNHIrQYic7iWWR6LWkVYrDwUQgGUy3UTnM2Rr2F5GK2mC2g365nPJY3V5P4LW+ukp22Y/AY7CDirCuMBFQYAXn17h8Fbno6v/FaaCHnHuemxEJFqWLWNB271pCPXhI5U0+uoWm26OiXLUPgz/c1WX5dbHpuxceFgJwwIxvpeKAjF9OW9WHFudmd5Vm+3LggTieWaUrGv+m7GWzHzfXtiunSvWaquI5gMd4M+AgSQICOMEUWxm8NlKj2dVxPaGMyuXDMzTMuxq46E6rjoMKmaT1W+kAqBBBdK57/Q+CN/gPpI19E\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Create a new highlight\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/highlights\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nCreate a new highlight\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The highlight to create\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarkId\":{\"type\":\"string\"},\"startOffset\":{\"type\":\"number\"},\"endOffset\":{\"type\":\"number\"},\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"],\"default\":\"yellow\"},\"text\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"bookmarkId\",\"startOffset\",\"endOffset\",\"text\",\"note\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"201\":{\"description\":\"The created highlight\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarkId\":{\"type\":\"string\"},\"startOffset\":{\"type\":\"number\"},\"endOffset\":{\"type\":\"number\"},\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"],\"default\":\"yellow\"},\"text\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\",\"nullable\":true},\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"}},\"required\":[\"bookmarkId\",\"startOffset\",\"endOffset\",\"text\",\"note\",\"id\",\"userId\",\"createdAt\"],\"title\":\"Highlight\"}}}},\"400\":{\"description\":\"Bad highlight request\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}},\"404\":{\"description\":\"Bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/create-a-new-list.api.mdx",
    "content": "---\nid: create-a-new-list\ntitle: \"Create a new list\"\ndescription: \"Create a new list\"\nsidebar_label: \"Create a new list\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy1VsFu2zAM/RWDZ61Jh+3iW1dsQLcCK7YMOwQ5MDYba5UlVZLbBob+faDkJE7qtRiwXQJLIsXHx0cqPdTkKydtkEZDCZeOMFCBhabHQkkfQEDAjYdyCdfSBw8rAZ6qzsmwhXLZw5rQkbvoQgPlchVXAhzdd+TDB1NvoexPAiwaSvcWwRRVCgYCKqMD6cDWaK2SFbL17Jdnlx581VCL/BW2lqAEs/5FFUOzzlhyQZLnU40tjax8cFJvQEAr9TXpDSM8F9Di0341n0dxDPBF7/mR9/vkLasptyiGjef3ke5aprNF3aECAb5FF5jXmm6xUwHK3VkUcN+R276WVBRg0ZEOV/WUqe6UwrUiKIPrKMZcIumoZhyJtSGPVYz52FujfWb17fx8uoy5fPVOJv+oiHIihSj+UNvXq3eS+wv1+gsG/3txn8Wz3VrJamS6NkYRar6lQX9p2N44DMb5aavOk/tm1IuozaMmx+taBsMfD5Ie084Qf3UqHlmDOFLQiMe91wTEEZ6VgCADA0sTBrIE383nz1X3AetimC7/TnCVqaeV1ZL3uJk6OyEh3XCwT02U/ENjaijBmoTXIjcrzFSaozxG3QNxuZY9dE5BCT3WtSPv4wytnD2ccwXQSVZCgjocZ2J2cmpCsL6czYLbnt2hwzsie4bWgpjo2eGGwtwWoaHiy2BfZCzM/Wi+f2cGc+TxlN/TwZE5j2TGaktGIIaPT8a1yAg//1wkzrgy3w6Pw8cnbG0WZO7ugyCPge+3c+8e1gOOfbMN/XQwOPT0qK5S3xoOyuTnAOdn87M5HHS4p+Xi5mqSxoubq+LWuGMOmXNuVeNDi3qU1tSjejK29lqefoFznoGewswqlLmhWTL9oKolqN3r3LDayiX0/Ro9/XAqRt4euFmuDppKD7aAhrAml2R4R0zfZQbzZsFh2Vx1HP5Zk0Wx87ioKrLhRdvVqCFuvn5fsEyGfwht6kBw+Agi/ZYAAkwiJ6kv7fWgUG+61JGQ72RR4bEmTzSYshqOUG9HCPs+WyzMHekYQQypBF5D5Cb+DVD4G/I=\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Create a new list\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/lists\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nCreate a new list\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The list to create\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\",\"minLength\":1,\"maxLength\":100},\"description\":{\"type\":\"string\",\"minLength\":0,\"maxLength\":500},\"icon\":{\"type\":\"string\"},\"type\":{\"type\":\"string\",\"enum\":[\"manual\",\"smart\"],\"default\":\"manual\"},\"query\":{\"type\":\"string\",\"minLength\":1},\"parentId\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"name\",\"icon\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"201\":{\"description\":\"The created list\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"description\":{\"type\":\"string\",\"nullable\":true},\"icon\":{\"type\":\"string\"},\"parentId\":{\"type\":\"string\",\"nullable\":true},\"type\":{\"type\":\"string\",\"enum\":[\"manual\",\"smart\"],\"default\":\"manual\"},\"query\":{\"type\":\"string\",\"nullable\":true},\"public\":{\"type\":\"boolean\"},\"hasCollaborators\":{\"type\":\"boolean\"},\"userRole\":{\"type\":\"string\",\"enum\":[\"owner\",\"editor\",\"viewer\",\"public\"]}},\"required\":[\"id\",\"name\",\"icon\",\"parentId\",\"public\",\"hasCollaborators\",\"userRole\"],\"title\":\"List\"}}}},\"400\":{\"description\":\"Bad request\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/create-a-new-tag.api.mdx",
    "content": "---\nid: create-a-new-tag\ntitle: \"Create a new tag\"\ndescription: \"Create a new tag\"\nsidebar_label: \"Create a new tag\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVD1v2zAQ/SvCzayVdNTmFi2QdmjQuOhgaDhLZ4uxRDLkKYkh8L8XR6qxkhpFhy6STN7Hu/feeYKWQuO1Y20NVPDREzIVWBh6KhgPoIDxEKDawkbetYJAzeg1n6DaTrAj9OTXI3dQbetYK/D0MFLgD7Y9QTW9Kb/pqGiRsWBbNLkVdySNiifN3QoUNNYwGZZcdK7XDUpueR+kwASh6WhA+eKTI6jA7u6pYVDgvHXkWVOQW4MDLaICe20OEGMGqD21MlOKqmPM58FZE3L6+6vry+gz6nbm5j+B1e0FqOofZ9AtqMUgUcFA3NkWKnA2pGYo8kCZlBQB/SP5kPQbfQ8VTNi2nkKIJTpdPl6Dgkf0Gnd9xjdfZ0b2OPYMFXTMLlRlyf60OqLHI5FboXOgLtA2VyjsPgn+dY4vMhaIMS6cdSe05c5Lf70QIZ1ljhQG1RwEav74bP2AgvDLz01iS+T4frblp2ccXE9nlywo12Zv5UIIyuivV1erK1kDzZIDL9DXtzcXR13f3hR761/PKbxElQQZ0CxaX1i4VyWns8cuLmdmhOmZS9ejNtIliTrNsm/zAtcKOnFDtYVp2mGgH76PUY4fRvKyzPVZ9LTLCjrClnzyyZFOAiBDebeRrhLej9L9D+tH9Ttj3TTk+K+x9cKwt9/uNqLj/Ocx2FZyPD6BSs8KQIFN1CR7pLMJejSHEQ8Sm2uK6vjaNG9Mkqaar9CcFginKUds7JFMjKDmUVh+Q5Ql+wUehMct\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Create a new tag\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/tags\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nCreate a new tag\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The data to create the tag with.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"}},\"required\":[\"name\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"201\":{\"description\":\"The created tag\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"}},\"required\":[\"id\",\"name\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/delete-a-backup.api.mdx",
    "content": "---\nid: delete-a-backup\ntitle: \"Delete a backup\"\ndescription: \"Delete backup by its id\"\nsidebar_label: \"Delete a backup\"\nhide_title: true\nhide_table_of_contents: true\napi: eJx9U8Fu2zAM/RWBpw3Q4nbosMG3DO2AbsNQbBl2CHJgbCZWY1uqRLfJDP37QNlNkzarL5bEJ/Hx8bGHkkLhjWNjW8jhkmpiUkssNp1Ty50yHJQpQQPjOkA+h88pFGChIVDRecM7yOc9LAk9+WnHFeTzRVxocOixISYfEiAUFTUIeQ+8cwQ5BPamXYMG2mLjajkyZMp6u1s3D7efPtrt3w/VltkWnyS/4QQZ8l+XEDV4uuuMpxJy9h1paLERyPIRosFIUQ65AmHkKTjbBgrC4v3ZhfyO6/9hVWFbppbVO8XVXokHDKpM2qTEF6fuDsxUa1mtbNdK+vEtwaJztSlQsNltkAsnJLHLWyoYNDhvHXk2A9XClvRSuKihoRBwfSp2pM58eOEJv4jyyZ4rW0IOQ22SWLTKIRvqDln/KGYEabi/f2xn52vIocey9BRCzNCZ7P4cNNyjN7isB+ZjeBBrhV3NkEPF7EKeZex3kw163BC5CToH+pmis4rU+IKyq9SPbyNeDVwgxnjgxF8i6JD50I97dSSz1JFg4pQEAj0uvljfoDD8+meWJDTtysp1qXqgdD45m5wd2HHPZ3pzfZL/9OZaraw/Ji/FRg3OBm4wWWG07jh/OPru+Yv9k6NeG9WhWKYtZ65G00qu1K9+7O98nJEAGvL9uCw0VDawhPt+iYF++zpGOb7ryMuYL57am0xQmiDrEvIV1oFeYfvm5+jGt+p/DMdDbHfJRXUnO9Cwod3hUMdF1FARluQThyE8LQpyfHDxxbyJT/Z+v7z6fjW7Ag14bJBnhkgJTvLq+wExsxtqY9zTZNkLxxj/AVwN0vk=\nsidebar_class_name: \"delete api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Delete a backup\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"delete\"}\n  path={\"/backups/{backupId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nDelete backup by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BackupId\"},\"required\":true,\"name\":\"backupId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"204\":{\"description\":\"No content - the backup was deleted\"},\"404\":{\"description\":\"Backup not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/delete-a-bookmark.api.mdx",
    "content": "---\nid: delete-a-bookmark\ntitle: \"Delete a bookmark\"\ndescription: \"Delete bookmark by its id\"\nsidebar_label: \"Delete a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJx9U01v2zAM/SsCTxugxe3QYYNvGdoB3Yah2DLsEORA20ysxrZUiW6TGfrvA23nq83qi/XxKD4+PnZQUMi9cWxsAylcU0VMKrN2XaNfq2yrDAdlCtDAuAqQzuHzeBlgoSFQ3nrDW0jnHWSEnvy05RLS+SIuNDj0WBOTDz0g5CXVCGkHvHUEKQT2plmBBtpg7So5MmSKarNd1U/3nz7azd8P5YbZ5p+EgeEesmNwW0DU4OmhNZ4KSNm3pKHBWkDZAaTBSHEOuQRh5Sk42wQKwuT9xZX8TnX4YVVuG6aG1TvF5ZEiTxhU0avUJ786F73jpxrLamnbRiiM7wkanatMjoJO7oOEnJHGZveUM2hw3jrybAa6uS3opYBRQ00h4Orc3YlG8+GFA34R5ZM9l7aAFIbqJLHolUKyqz0k3UHUCNJ8/7hrbesrSKHDovAUQkzQmeTxEjQ8ojeYVQP78XqQbIltxZBCyexCmiTst5M1elwTuQk6B/qZrrOS1PiCssu+L99GvBq4QIzxyJW/RNQh87E39wpJZqmjh4lnehDocfHF+hqF4dc/s15G0yythEvVA6XLycXk4siaez7Tu9uz/Kd3t2pp/Sl5KTZqcDZwjb0dRhOP84h7/z1/szv46vXhHQpm2nDiKjSN5Ot71o19nu8nJoCG9Gh8FhpKG1ggXZdhoN++ilGOH1ryMvqLQ5t7MxQmyLqAdIlVoFc4v/k5OvOt+h/L8RCbbe+mqpUdaFjT9nTM4yJqKAkL8j2LATDNc3J8FPpi+sQxe/df33y/md2ABjy1yjNr9AnOMuu6ATGza2pi3BNl2QvHGP8BgxHeWQ==\nsidebar_class_name: \"delete api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Delete a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"delete\"}\n  path={\"/bookmarks/{bookmarkId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nDelete bookmark by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"204\":{\"description\":\"No content - the bookmark was deleted\"},\"404\":{\"description\":\"Bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/delete-a-highlight.api.mdx",
    "content": "---\nid: delete-a-highlight\ntitle: \"Delete a highlight\"\ndescription: \"Delete highlight by its id\"\nsidebar_label: \"Delete a highlight\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVcFu2zAM/RWDpw0QknTosMK3AO2wbANWbBl2CHJQLCZWY0uqRLfJDP37QNtJnNbtdtgpjvkkPj4+0jUoDJnXjrQ1kMI1FkiY5HqTF3qTU7LaJ5pCohUIILkJkC7g0yEaYCkgYFZ5TXtIFzWsUHr004pySBfLuBTgpJclEvrQAEKWYykhrYH2DiGFQF6bDQjAnSxdwa80alXs9pvy8e7qg939fp/viGx2xRQ0NZAjhZmCKMDjfaU9KkjJVyjAyJJReQ8lQHOBTlIOzMtjcNYEDMzl3WTCP+dazHNMVKOHOgkCAjJrCA3xAelcoTPJB8Z3gU8NVGhXd5jxQeetQ0+6zbmydltKv52p52pEAYGkp2/rdUDqxU1VrtBzHI16JZrZwvpBlU1Vcg/3WBT2EVgH1mbjEQ0IWBUVclMVrmVVEKQHYBRAuKOhK01VFHLFbWHxowBjCf8JqIdLrwL6F1TJPEpCNR0gEs98sOjre65mX7uuqo5zw+iYv59tOWA9iJFzXk4un3vnCEqMpWRtK6P+n3Myqwb0jQJKDEFuhmJPtGluOOGXsS2lRMqtghRa23NiHpcUxkf/h3Hdm6rI0qJ/OEx35QtIoZZKeQwhjqXT44cLEPAgvebWN/y7cKvawWc5kQvpeEx+P9pKL7eIbiSdAzEwlt0NiV0nlGPypcMnLRduTG8x/WBZu5nrraejRpyZ62hgkHYgHobm4aP1pWSGn3/NGyG1WVs+zlW3lC5Gk9Gkt52OfKa3s0H+09tZsrb+nDwXGwU4G6iUjSG6NdYtZXm2hc4urU/W+ssKb0tm049dIbVp5o27Vne9Xpy2ZgABaX+HLgXkNhCD6nolA/70RYz8+r5Cz1+A5anVjSGUDvysIF3LIuArtN987/z5NnmJZ/dSmn3jKF5VKYCALe6fLPu4jAJylAp9Q6NFTLMMHfXOPhtCts1xCK5vvt7Mb0CAPPfLE380CQap1XWLmNstmhiPTIn/M8cY/wCwMJE0\nsidebar_class_name: \"delete api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Delete a highlight\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"delete\"}\n  path={\"/highlights/{highlightId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nDelete highlight by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"HighlightId\"},\"required\":true,\"name\":\"highlightId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The deleted highlight\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarkId\":{\"type\":\"string\"},\"startOffset\":{\"type\":\"number\"},\"endOffset\":{\"type\":\"number\"},\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"],\"default\":\"yellow\"},\"text\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\",\"nullable\":true},\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"}},\"required\":[\"bookmarkId\",\"startOffset\",\"endOffset\",\"text\",\"note\",\"id\",\"userId\",\"createdAt\"],\"title\":\"Highlight\"}}}},\"404\":{\"description\":\"Highlight not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/delete-a-list.api.mdx",
    "content": "---\nid: delete-a-list\ntitle: \"Delete a list\"\ndescription: \"Delete list by its id\"\nsidebar_label: \"Delete a list\"\nhide_title: true\nhide_table_of_contents: true\napi: eJx9U8tu2zAQ/BViTy3ARkmRooFuBpICaYMiaF30YPiwltYWY0lkyFViV+C/F0upfiROfbFIDjmzs7M9lBQKbxwb20IO11QTk6pNYLXYKsNBmRI0MK4C5DO4M4EDzDUEKjpveAv5rIcFoSc/6biCfDaPcw0OPTbE5EMChKKiBiHvgbeOIIfA3rQr0EAbbFwtW4ZMWW+2q+b54eqz3fz5VG2YbXEl7IYTRNhvS4gaPD12xlMJOfuONLTYCKAeABqMFOOQKxA1noKzbaAgCj6eX8rfcd3frSpsy9Sy+qC4IrWwdt2gX6tnDKpMriTiy1O3RZdqLaul7VqhH98SJDpXmwIFmT0EgZ+wwy4eqGDQ4Lx15NkMUgtb0mvTooaGQsDVqbMjb2bDC3v8PMpP1lzZEnIYKhNi8SqHTBwMWT8YGUEa7Z/+tbHzNeTQY1l6CiFm6Ez2dAEantAbXNSD6vF4sGmJXc2QQ8XsQp5l7Ldna/S4JnJn6BzoF15OK1LjC8ouUy++jXg1aIEY40ECf4qZA/NhDnfOCLPUkWCQjyDQ48cX6xsUhV9/T5N9pl1auS5VD5Iuzs7Pzg9iuNMzub89qX9yf6uW1h+Ll2KjBmcDN5hiMIZ2nDlMU/fyvX6fpbeHcyiUacOZq9G0wpN61Y99naXJCKAhH0dkrqGygeWo7xcY6JevY5Ttx468jPV839bU/NIE+S4hX2Id6D863/0YE/hevaVu3MR2m9JTd7ICDWva7sc4zqOGirAknxQMh5OiIMcH115NmKRjl/Drm7ub6Q1owONYvIhBIjipqu8HxNSuqY1xJ5JlLRpj/AuPmMsh\nsidebar_class_name: \"delete api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Delete a list\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"delete\"}\n  path={\"/lists/{listId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nDelete list by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"ListId\"},\"required\":true,\"name\":\"listId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"204\":{\"description\":\"No content - the bookmark was deleted\"},\"404\":{\"description\":\"List not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/delete-a-tag.api.mdx",
    "content": "---\nid: delete-a-tag\ntitle: \"Delete a tag\"\ndescription: \"Delete tag by its id\"\nsidebar_label: \"Delete a tag\"\nhide_title: true\nhide_table_of_contents: true\napi: eJx9U01v2zAM/SsCTxugxe3QYYNvAdoB3Yah2DLsEOTA2EysxrZUiW6TGfrvA2UvH206X2SJT+Lj42MPJYXCG8fGtpDDNdXEpBjXarlThoMyJWhgXAfI5zCTdaEhUNF5wzvI5z0sCT35accV5PNFXGhw6LEhJh8SIBQVNQh5D7xzBDkE9qZdgwbaYuNqOTJkynq7WzdP958+2u2fD9WW2RafJLnhBJnh+raEqMHTQ2c8lZCz70hDi43EOcU1GCnEIVcgXDwFZ9tAQfK/v7iS5bTm71YVtmVqWb1TXJFaWrtp0G/UEwZVJkVS3qtzt2e4Vq1ltbJdK9nHpwSIztWmQAFm90HQZ7Swy3sqGDQ4bx15NgPTwpb0UrGooaEQcH0udqLMfHjhgF9E+WTPlS0hh6EwSSxS5ZBJk7M+qRhBeuwf/3Ww8zXk0GNZegohZuhM9ngJGh7RG1zWA+cxPGi0wq5myKFidiHPMva7yQY9bojcBJ0D/VzIitT4grKr1IivI14NXCDGeGS+nyLlkPnYgntdJLPUkWCQjyDQ489n6xsUhl9+z5J4pl1ZuS5VD5QuJxeTiyMH7vlM727P8p/e3aqV9afkpdiowdnADSYTjIYdhw1l3J4/1x+M9OpQDmUybTlzNZpWsqRO9WNP58PgasiH2VhoqGxgCfT9EgP98nWMcvzQkZdpXhxamhpfmiD/JeQrrAP9h+SbH6P33qrXuI2H2O6Sc+pOdqBhQ7v9+MZF1FARluQTgSE2LQpyfHTrxWiJMfbWvr75djO7AQ146ohnDkgJzpLq+wExsxtqYzxwlL1wjPEvTTHFIQ==\nsidebar_class_name: \"delete api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Delete a tag\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"delete\"}\n  path={\"/tags/{tagId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nDelete tag by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"TagId\"},\"required\":true,\"name\":\"tagId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"204\":{\"description\":\"No content - the bookmark was deleted\"},\"404\":{\"description\":\"Tag not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/detach-asset.api.mdx",
    "content": "---\nid: detach-asset\ntitle: \"Detach asset\"\ndescription: \"Detach an asset from a bookmark\"\nsidebar_label: \"Detach asset\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy9VE1v2zAM/SsCTxugxe3QYYVvGdoB3Yah2DLsEPjA2HTsxrZUiW6TGfrvA23nq82KHYblEkt6oh4fH9lBRj51peXSNBDDFTGmhcJGoffEKnemVqgWxqxqdCvQwLj0EM/hw7jlIdHgKW1dyRuI5x0sCB25acsFxPMkJBosOqyJyfke4NOCaoS4A95Yghg8u7JZggZaY20r2SqpzKr1Zlk/3l2+N+tf74o1s0kvhUHJPWTL4CaDoMHRfVs6yiBm15KGBmsBLfYgDaWkaJELCPqf0ZiKUC9ywBFxRCARuLem8eSFw9uzC/k7LsdXo1LTMDWs3owVeUSvsr5KlCnfpil5n7dVtRECF6eCbHVSjWGVm7YRJmNYQaO1VZmioKM7L1dOaGMWd5QyaLDOWHJcDqxTk9FzBYOGmrzH5amzI53mQ4Q9PgnykzUXJoMYMqqIBdHLFkO0raiPun1xQ9Sr46NuFDuAuNI9bD3Xugpi6DDLHHkfIrRl9HAOGh7QlbiohnTG40HDHNuKIYaC2fo4ithtJit0uCKyE7QW9BOhZwWpMYIyueKC1OcRrwYuEEI4aJfvovLw8mHT7CSTlyWPHiZm7kGgx4+PxtUoDD/9nPW6lk1u5LpkPVA6n5xNzg7MuuMzvb05yX96e6Ny447JS7JBgzWea+z9MTp7Oy5E86fhur3H/masDBkzrTmyFZaNPNgXrRsrP9/1sgcN8VFjD8WX7W2vJRoK41mudd0CPf1wVQiyfd+Sk0GV7GvfOyQrvXxnEOdYeXohm1ffRv++Vn9iPm5is+ktVrWyAg0r2hwPJRlE//HlrTwhCRoKwoxcn/xwOk1Tsnxw79loEPfuWvPq+sv17FrkP7btE5v2D5yk1XUDYmZW1ISwY8myFo4h/Abov0RX\nsidebar_class_name: \"delete api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Detach asset\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"delete\"}\n  path={\"/bookmarks/{bookmarkId}/assets/{assetId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nDetach an asset from a bookmark\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"},{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"AssetId\"},\"required\":true,\"name\":\"assetId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"204\":{\"description\":\"No content - asset was detached successfully\"},\"404\":{\"description\":\"Bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/detach-tags-from-a-bookmark.api.mdx",
    "content": "---\nid: detach-tags-from-a-bookmark\ntitle: \"Detach tags from a bookmark\"\ndescription: \"Detach tags from a bookmark\"\nsidebar_label: \"Detach tags from a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVV2v0zgQ/SvRPLGStykrVovydoG70gWEEBTxUPVhGk8b3yaxsSeXdqP8dzTOR1taLmhFH6rEOR7PnDlz3IKmkHvj2NgaMnhFjHmRMG5DsvG2SjBZW7ur0O9AgSxDtoQXw1KAlYJAeeMNHyBbtrAm9ORvGi4gW666lQKHHiti8iECQl5QhZC1wAdHkEFgb+otKKA9Vq6UJUNGl/vDtvp6//wfu//v72LPbPPnkoHhCBkzuNPQKfD0pTGeNGTsG1JQYyWg9RGkwEh5DrkAyUp2UOAXVh8kl3MSFgX1DLBNdCRkBgpyWzPVLHB0rjQ5Cjy9D7LnSmF2fU85gwLnrSPPhkL8GjmcUOg9HiQ9pir80u47fcleF3vzLpZ98U1+pxwt+xxW43pwtg59+L/m8+t0lCZwYjcDHaSFn8To8Pt4GSP/Ajf/TzKLSN0FF9O5Ax/P5s8uKRjlltSWk41tav37Cs+tvtY1BRWFgNurHT0vIUY44ldDxyviwmrIQFNJLIgo/wzScTBC2h5npEujLGSg/cM4ro0vIYMWtfYUQpeiM+nDU1DwgN7guuxLGD73vG2wKRkyKJhdyNKU/WG2Q487IjdD50Bd0dcQQSTGBSVvBnzS5yJdO3Gaj8Jsf/Kp30w0yclSR4SJD0QQqOHhX+srlAxff15ELqVjH46GcDuqahzW5TR3R+lN4zY1ZdWJyWys7BP++uKezuaz+YkKp8pu3t9dZeLm/V2ysf6cBqGtU+Bs4AqjugaLe9yvz6K3R8H+zOZ7Gpn2nLoSTS1nRyW0g4SWk7eKYLIzo+3NRUFhAwuybdcY6JMvu06WvzTk5a5YHTUUGdYmyLOGbINloEeSf/Jh0P4fyY+SHQ2kPkSplo28gYIdHc7vhdi1glCTj1n0gJf9WX8uJMwxwMWUd2rccZPn5PhR7OpkIl/dvr1d3IoihwuoiiYAHr+Civ8xXRurj0KPay2UWG+baArQRxX94rn8v5N7r99rhLRtj1jYHdVdN/HD8i7UdN03RJTKrQ==\nsidebar_class_name: \"delete api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Detach tags from a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"delete\"}\n  path={\"/bookmarks/{bookmarkId}/tags\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nDetach tags from a bookmark\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The tags to detach.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"tagId\":{\"type\":\"string\"},\"tagName\":{\"type\":\"string\"}}}}},\"required\":[\"tags\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The list of detached tag ids\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"detached\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"TagId\"}}},\"required\":[\"detached\"]}}}},\"404\":{\"description\":\"Bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/download-a-backup.api.mdx",
    "content": "---\nid: download-a-backup\ntitle: \"Download a backup\"\ndescription: \"Download backup file\"\nsidebar_label: \"Download a backup\"\nhide_title: true\nhide_table_of_contents: true\napi: eJx9VE1v2zAM/SsGTy0gxOnQYYVvKfaBbpdiy7BD4ANjM7Ea21IlOk1q6L8PtJ3PJTvZpijyvcdHt5CTz5y2rE0NCXw2b3VpMI/mmK0aGy10SaCAcekhmcFjF/WQKvCUNU7zFpJZC3NCR27ScAHJLA2pAosOK2JyvkvwWUEVQtICby1BAp6drpeggDZY2VJCmnRebrbL6u3l4ZPZvH8sNswme5D+mruUvv9TDkGBo9dGO8ohYdeQghorSZnvUhRoYWSRCxBEjrw1tScvKD6Mx/I4Jf944BzdvGsbocsKvaZbUJCZmqlmuYTWljpDuRS/ayuhPb0QQlBwP76/Wr02HC1MU+fXi754uXBBNDN/oYxBgXXGkmPdk8lMTv9KGxRU5D0uL52d6DfrKxzy09DzqIgLk0MCS+q6ipQJxL3EPm53Woc4H3wDYgy33o29cSUk0GKeO/I+xGh1vL4DBWt0Gudlj3847iVbYFMyJFAwW5/EMbvtaIUOV0R2hNaCOtN1WlA0VIjMIuKCoh9DftRjgRDCkWN/iax952Pf7jWSzsKjSxNHdUmghpevxlUoCL//mXZC6nph5Lqw7iHdjcaj8ZFt93gmz08X8U+en6KFcafghWxQYI3nCjtDDBbfLykOa3pesz046+pG91yZNhzbEnUtrbpxtcOcZ8MqeVCQHG3VftSpgsJ4lsy2naOn364MQcKvDTn5MaSHQXd2yLWX9xySBZae/oP65ufgztvoGtghiPW281PZyBcoWNH2+DcQ0qCgIMzJdRj640mWkeWji+dLLYbZ2//blykowFObnNmiK34RU9v2GVOzojqEPUSWb8EXwl+BY+PE\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Download a backup\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/backups/{backupId}/download\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nDownload backup file\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BackupId\"},\"required\":true,\"name\":\"backupId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Backup file (zip archive)\",\"content\":{\"application/zip\":{\"schema\":{}}}},\"404\":{\"description\":\"Backup not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/get-a-single-asset.api.mdx",
    "content": "---\nid: get-a-single-asset\ntitle: \"Get a single asset\"\ndescription: \"Get asset by its id\"\nsidebar_label: \"Get a single asset\"\nhide_title: true\nhide_table_of_contents: true\napi: eJx9U02P0zAQ/SvWnECymi4SYuVbhWC1cFlBEYeqB7eZNqaJ7bUnpSHyf0fjeEu72uUUf7zMezPveYQa4zYYT8ZZUHCHJHSMSGIzCENRmBokkN5HUCtY8E2EtYSI2z4YGkCtRtigDhgWPTWgVuu0luB10B0ShpgBcdtgp0GNQINHUBApGLsHCXjSnW/5yKCp29Ow737/uv3gTn/eNycit71lekMZkunva0gSAj72JmANikKPEqzuGKELQoLhbrymBlhPwOidjRhZw7v5nD/XjefaYussoaWZ+DgtBOsVJoqam+mMxZoHQw2WKfH9DFKS0CE1rgYFeySQE7WCKsNiNRZlCXh24fg0mT60oGDUdR0wxlRpb6rjDUg46mD0pp0Ul+tJ9U73LYGChshHVVUUhtlBB31A9DPtPchnrS1Z7VRBuF0W/7XgxaQFUkoXpn5nuybmS2vP7jEz95FhoAoIZFl8dqHTrPDLz2UejbE7x79z15Okm9l8Nr9w9qxn8XD/ov7Fw73YuXAtnptNEryL1GnLDCUGOcUiGrtvi1HPi45QrH4981OvhCeqfKuNZaps11jMXU1xiyBBPQVvLaFxkfhyHDc64o/QpsTHjz0Gfi7rf97mBNQm8roGtdNtxP/ofPOthP6teE1fOdR2yBFqe96BhAMOF68jrS/zevdpCRL0tcXPLM1KXyw+jhNi6Q5oUzpzEe+ZKaW/hvt+aA==\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get a single asset\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/assets/{assetId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet asset by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"AssetId\"},\"required\":true,\"name\":\"assetId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Asset content. Content type is determined by the asset type.\"}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/get-a-single-backup.api.mdx",
    "content": "---\nid: get-a-single-backup\ntitle: \"Get a single backup\"\ndescription: \"Get backup by its id\"\nsidebar_label: \"Get a single backup\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVMGO2zYQ/RVhTi1A2E6RooFubpEG2yBo0G6Rg+HDSBpbXEskQw43dgT+ezGU1uv1aoMccrLMGc689+ZxBmgo1F471tZACe+IiwrrQ3RFdSo0h0I3oIBxH6DcwO85FGCrIFAdveYTlJsBKkJPfh25hXKzTVsFDj32xORDTgh1Sz1COQCfHEEJgb02e1BAR+xdJ0eadNMdT/v+y92b3+zx66/tkdnWb6S/5pwy9r9pICnw9DlqTw2U7CMpMNhLSvWQokALI4fcgiDyFJw1gYKg+GW1kp+n5P+u7qjm4ovm9kGEBhkXoKC2hsmw3EHnOl2j3FneBbk4Q8/mUqDAeevIsx7b6ua5BElBDORv5kMYAvFcTIGJXYeV6CICJAW1J2Rq1jxbKeivdBEwsa/IS6Cy9tCjP/xho+HZjMDIMcxOz8RejOHINONJiHVNIYCCHeoueoJtUkDeW/+BQsA9fQeXJ+PdQPbgJNKjJJeEJ3rXZM7QtylJ0der18/HPpqqMJaLnY2m+XHjrm0zwzYp6F9S4pp5rvCYn2nk+9zaBkrYU+4qHi9hOZo2LIeHR5BEAfL3D88w+g5KGLBpPIWQluj08v4VKLhHr0X/DHsKj0rtMHYMJbTMLpTLJfvT4oAeD0Rugc6BupLztqViqlDYXcEtFe+n/GLEAimliw3yr6g5dr7cI2dppLPwyGnywnOSzDp//Gl9j4Lwr0+3WT9tdlauC+sR0qvFarG6WCNnPOuPN7P41x9vip31T8EL2aTA2cA9Zh9MK0eWJhZBm31H0+K4rjo8WurFHTuyZTry0nWoTd4MMrBhGvBmWm7yuMrzntsqaG1gCQ9DhYH+811Kcvw5kpf9vH2cb3ZBo4N8N1DusAv0Dag//TN58efiJYTTIZpTtlEX5R8oONDpchsn2QItYUM+YxjD67omxxcXn702McrZ7e/e3sr7f2qPKzvk6rOghmHMuLUHMimdMbL8F4Ap/Q8pJ2jm\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get a single backup\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/backups/{backupId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet backup by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BackupId\"},\"required\":true,\"name\":\"backupId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with backup data.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"assetId\":{\"type\":\"string\",\"nullable\":true},\"createdAt\":{\"type\":\"string\"},\"size\":{\"type\":\"number\"},\"bookmarkCount\":{\"type\":\"number\"},\"status\":{\"type\":\"string\",\"enum\":[\"pending\",\"success\",\"failure\"]},\"errorMessage\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"userId\",\"assetId\",\"createdAt\",\"size\",\"bookmarkCount\",\"status\"]}}}},\"404\":{\"description\":\"Backup not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/get-a-single-bookmark.api.mdx",
    "content": "---\nid: get-a-single-bookmark\ntitle: \"Get a single bookmark\"\ndescription: \"Get bookmark by its id\"\nsidebar_label: \"Get a single bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzNWN9v3DYM/lcMvmwDjFw6dFhxb9di67JhbbCk2ENwD7RFn9WTJVeSL7ke/L8XlH9e4mucNsD2dD6LFD9+IinSBxDkUitLL42GJbwlHyXGbAu02yjZR9K7SAqIwePGwfIGXreLDtYxOEorK/0eljcHSAgt2VXlc1jerOt1DCVaLMiTdUHApTkVCMsD+H1JsATnrdQbiIHusCgVv5Ikhbrbb4rbj69+NXeff8nvvDfpK0YgfRDpEFwIqGOw9KmSlgQsva0oBo0FCyWDUAySPSvR51DHEzASYxShhhgEZVgp3+11TM1FFjnykTdRWO1p+sFFqdGetI9upVJRQpHUqaoEiUjqyOcUWXKl0Y7OonfGUxz5XA5KKWrWUWg3FGXGRs4U1O/uzo69zFC5/w+0nu922zeNYsf5p4rsHjgUOjOOef/5/Jx/jn14n3yklHH6fIhAgR7ZSguItbAslUyRtRYfHatOHKkJm0EMpTUlWS8bw1I8jL46htQSehIrP7laGCEzOb0cg66UwoQDk6mv+zCdIYk2zeWOxEQo1jFkuDOcXKfWPW42Um+uPPrKPW4vBtJVwRnsqjQl54BNSFVZYppIC9Za1zG4qijQys+B4uffXhs/j54Gx36erKlsOmPbASaWEmK4pYSjS/FzYRKpKFQjT9pxVMbgpN4oypoFG9ySRWmsD75UjuzFdEw19bJfQGtxz8qeCvfNgdok28QCeo9pTuL1JF29z+xmXhWoYV0fVZUbCEU+7H+0W5AbZZ/R9D4L1fzrDjSrp6EoqbcNhVZN8zc7jY7KyAx5WeCGPkyZPSW8co781DlPRWJqibTLjX+KVlYpdcmWmpLwFNXSUmrxVpH4BuWdFGSeBBR3Mp1JdO4L9WYInUfl2zB7CpzO83kGsPK5sfNYrRIlXU7zpAV6umw15gFnjb/bi2WGwr1kDdJN7qy5pfm+XPR015Sz8DCVjE15nZcz01A7G9+LFTk4AtjwdP2IeEhfNiKyQelExeYi/26ywJ68cOZWESc/j7fVVZGQ/UoKfDPNAymDr8/AeqW32txO3BpBYV331D7/lTfrmPk2+WNUbcZluGPiavwmQa3JXrTRca/6QlsXWa5tRENNgoliC00T8KFUBgXxHTpw9aSQmrqOB9/X9aTA0Lgetamj1vKoj7zfNE73en1b0zYxQ1T2h7x+OI1BHSC+PH/5sLXvZCJtfJSZSovna+lTI6ZbooKc4+N9uHaPybDDIM9U10Hf50bAEjbNyfP4uIRFP/wsDsOMWTOVZHfdpBuaGjigEJacqxdYysXuBccVWsmHHqC3yw1f7eAJufelWy4W3u7PtmhxS1SeYVnC/ZnvOqeo3SEyWZjl/mrlowYLH8loSL9iRhvL41G9p4cth8xhMZ41ghAnQXj43dgCGeGf/14HDqXODKuz1w2kF2fnZ+ejSb3Hs7q8mMS/urwIY+UReHaWb2DjPHeqy0M3Y/K3CYyajrwfEe/vexgC6ysfMxqP+VJalAplGKjaTrQ56Js+9TkBlqPPCesYcuM8ixwOCTq+BOqaXzfTLp+/kI5P+cS4Pob4H07ukyRsaT81ze9QVSwcZvkuiJ/o6o//tEn3U3TKendz6P3YZodqdAjhxskJBTdoN4dWYJWmVI7hPigsDL9P7Le/XUPTFI4n7OOoH486x7AOh0bi2mxJ13WP0vN/BljXXwC+YbOA\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get a single bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/bookmarks/{bookmarkId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet bookmark by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"},{\"schema\":{\"type\":\"boolean\",\"default\":true,\"description\":\"If set to true, bookmark's content will be included in the response. Note, this content can be large for some bookmarks.\"},\"required\":false,\"description\":\"If set to true, bookmark's content will be included in the response. Note, this content can be large for some bookmarks.\",\"name\":\"includeContent\",\"in\":\"query\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with bookmark data.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"userId\":{\"type\":\"string\"},\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"attachedBy\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\"]}},\"required\":[\"id\",\"name\",\"attachedBy\"]}},\"content\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true},\"description\":{\"type\":\"string\",\"nullable\":true},\"imageUrl\":{\"type\":\"string\",\"nullable\":true},\"imageAssetId\":{\"type\":\"string\",\"nullable\":true},\"screenshotAssetId\":{\"type\":\"string\",\"nullable\":true},\"fullPageArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"precrawledArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"videoAssetId\":{\"type\":\"string\",\"nullable\":true},\"favicon\":{\"type\":\"string\",\"nullable\":true},\"htmlContent\":{\"type\":\"string\",\"nullable\":true},\"contentAssetId\":{\"type\":\"string\",\"nullable\":true},\"crawledAt\":{\"type\":\"string\",\"nullable\":true},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\",\"nullable\":true},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true},\"size\":{\"type\":\"number\",\"nullable\":true},\"content\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"assetType\",\"assetId\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"unknown\"]}},\"required\":[\"type\"]}]},\"assets\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"userId\",\"tags\",\"content\",\"assets\"],\"title\":\"Bookmark\"}}}},\"404\":{\"description\":\"Bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/get-a-single-highlight.api.mdx",
    "content": "---\nid: get-a-single-highlight\ntitle: \"Get a single highlight\"\ndescription: \"Get highlight by its id\"\nsidebar_label: \"Get a single highlight\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVcFu2zAM/RWDpw0QknTosMK3HLau22HD1mGHIAfFYmw1tqRKdJvM0L8PVNzESd1ih53imE/U4+Mj3YHCUHjtSFsDOVwjZZUuq1qXFWWrXaYpZFqBAJJlgHwBn5+iAZYCAhat17SDfNHBCqVHP2+pgnyxjEsBTnrZIKEPCRCKChsJeQe0cwg5BPLalCAAt7JxNb/SqFW93ZXN493VB7v9877aEtniiiloSpADhRsFUYDH+1Z7VJCTb1GAkQ2jqgFKgObqnKQKmJfH4KwJGJjLu9mMf06F+La6w4KyR03VQBAlSU5AQGENoSE+Jp2rdSH52PQu8NmROm3KBgKctw496f3NK2s3jfSbG/VckyggkPT0bb0OSIO4aZsVeo6jUa9EC1tbP6q1aRvu5A7r2j4Cq8EKlR7RgIBV3SK3VuFatjVB/gSMAgi3NJbStHUtV9wcbkEUYCzhPwH1eOltQP+CKoVHSajmI0TiiRsWQ31P1Rxq11fVc06MDvcPb1uOGBBi5DsvZ5fPHXQAZcZStratUf/POYVVI/pGAQ2GIMux2Jk2KcMRv4z7UhqkyirIoUzSpInJYXoYgTDtBoMVWVf0D08D3voacuikUh5DiFPp9PThAgQ8SK+574l8H95L9mSyisiFfDolv5tspJcbRDeRzoE40/W2wqzPkNl1RhVmX3t8tufCXRnspp+saT9wgw11EIhv5joSDPIexJOQHj5Z30hm+OX3bVJRm7Xl41z1ntLFZDaZDRbUgc/8+80o//n3m2xt/Sl5LjYKcDZQI5Mb+k3GS1lmQZuyxuMyOk/cHb312hrf18yWn7paapOmjdvW9c1eHDdnAAH5cI8uBVQ2EIO6biUD/vJ1jPz6vkXPX4HlsdfJEUoHflaQr2Ud8BXOb3707nybvcSzfynNLlmKF1UOIGCDu7OFH5dRQIVSoU809oh5UaCjwdlnI8i+OYzA9cdbECBP3XLmjpR9lFfX7RG3doMmxgNN4v9MMMa/V8aQOg==\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get a single highlight\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/highlights/{highlightId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet highlight by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"HighlightId\"},\"required\":true,\"name\":\"highlightId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with highlight data.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarkId\":{\"type\":\"string\"},\"startOffset\":{\"type\":\"number\"},\"endOffset\":{\"type\":\"number\"},\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"],\"default\":\"yellow\"},\"text\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\",\"nullable\":true},\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"}},\"required\":[\"bookmarkId\",\"startOffset\",\"endOffset\",\"text\",\"note\",\"id\",\"userId\",\"createdAt\"],\"title\":\"Highlight\"}}}},\"404\":{\"description\":\"Highlight not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/get-a-single-list.api.mdx",
    "content": "---\nid: get-a-single-list\ntitle: \"Get a single list\"\ndescription: \"Get list by its id\"\nsidebar_label: \"Get a single list\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVUFv2zoM/isGTxtgJNmwhzf4Vjy8Dd0GbNg67BD4wFhMrFaWNIlukxn67wNlN01ab9hhp8jiJ/Ij+ZEZQFFsgvasnYUK3hIXRkcuNodCcyy0ghIYdxGqNXzQkSPUJURq+qD5ANV6gA1hoHDRcwvVuk51CR4DdsQUYgbEpqUOoRqAD56ggshB2x2UQHvsvJErTVqZ/WHX3V2//tftf/zT7pld81qia84QiX6pIJUQ6HuvAymoOPRUgsVOAGYElKAlE4/cgrAJFL2zkaIweLlayc950h8319Rwcae5HZNXyLiAEhpnmSzLC/Te6AblxfI6yrOZxFx2BCX44DwF1mNQrZ4mn+5pzxjO2D2tmu2NwY3URNJPJehmDphyI8hKTf7EywiYaZLtO+l+h7ZHAyXEDgOLDBRtsTcM1b0tlfC9p3D4o3i+3xjdnEA3zhlCK15ajP85wbuA7EKcR/WRwmdnfsva3VkK8q00OzncarrLN1P8Op0pag1Z8rk3U2VP6nh8NUPxhE99rlpISYK8Wr16qj2xF9ZxsXW9VX9Pc41T8+LqKEbczdke1SF7eMDXacyiI26dggp2lKPKmFWwlLmJy2GcwSQqoXB7vwH6YKCCAZUKFGNaotfL2xfSDAxaRJEpT+axRvfKapl9rJZLDofFDQa8IfIL9B4ejQlctVRMHgq3Lbil4v2EL0Yu0oaT5fVFKjlGPl1hx7JIZMkjw0R4GQTldHjjQofC8N23q1w7bbdOnkvWI6UXi9VidbLBjnwuPl3O8r/4dFlsXTgnL8nKuLjIHWYNTPtOdjUWUdudoby3HvscHsT0i8U+Zsq056U3qMepkmYNU2PXeauKuKtpvdYltC6ymIZhg5G+BpOSXE+jv64f+pq7r3SUs4JqiybSb0g++zzp73nxK3bTJdpDlo/p5QtKuKHDw19AqmWHECoKmcFovGga8nzy7Ml8iTyO+n77/xWUgOeieCSC7H2W0jCMiCt3QzalI0OWbyGY0k9EOoyc\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get a single list\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/lists/{listId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet list by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"ListId\"},\"required\":true,\"name\":\"listId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with list data.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"description\":{\"type\":\"string\",\"nullable\":true},\"icon\":{\"type\":\"string\"},\"parentId\":{\"type\":\"string\",\"nullable\":true},\"type\":{\"type\":\"string\",\"enum\":[\"manual\",\"smart\"],\"default\":\"manual\"},\"query\":{\"type\":\"string\",\"nullable\":true},\"public\":{\"type\":\"boolean\"},\"hasCollaborators\":{\"type\":\"boolean\"},\"userRole\":{\"type\":\"string\",\"enum\":[\"owner\",\"editor\",\"viewer\",\"public\"]}},\"required\":[\"id\",\"name\",\"icon\",\"parentId\",\"public\",\"hasCollaborators\",\"userRole\"],\"title\":\"List\"}}}},\"404\":{\"description\":\"List not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/get-a-single-tag.api.mdx",
    "content": "---\nid: get-a-single-tag\ntitle: \"Get a single tag\"\ndescription: \"Get tag by its id\"\nsidebar_label: \"Get a single tag\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVMFu2zAM/RWBpw0QkmzosMK3FNiKbocVW4YdghwYm7HV2JYq0W0yw/8+UHbTJPW6y06WRYp85HtkCxmF1BvHxtaQwDWxYszVeq8MB2Uy0MCYB0iWsJDvSkOgtPGG95AsW1gTevLzhgtIlqtupcGhx4qYfIgOIS2oQkha4L0jSCCwN3UOGmiHlSvlypDJyt0+rx7vLj/a3e8PxY7ZppeS3HB0WWB+k0GnwdN9YzxlkLBvSEONldg52jUYqcIhFyBYPAVn60BB8r+fzeRzWvC39R2lrB4NF6o0gVWGjBPQkNqaqWZ5gc6VJkV5Mb0L8mykLBsDgQbnrSPPpk9qspeld0+oxwxNdWXttkK/DUcOdVOtyZ87XO3nzJgWlC2i27/AoBkNWTQV1iOWrjvp9xKiHCL0M6CvwlqdsDiEvZhdvCRjgbmqLauNbers/3GQ2my82RWFgPmY7azwGOHZf9X1RVTEhc0ggZxiVpFdAlMZmGkbFdmBzIt/eJqGxpeQQItZ5imEborOTB/egYYH9AbX5cBTb+4btMGmZEigYHYhmU7Z7ydb9LglchN0DvR5FwtSQwRlN4oLUl8Hf9VjEQ6OBvmH9LHPfDzOh6ZIZqkjukEyOIEeDp+tr1AQfvm1iJ0z9cbKc6m6h/RuMpvMjqb5gGd+ezOKf357ozbWn4KXYjsNzgYeBDsMv2wtVMHUeUmyvs5Dts9KGt9wfZ1MO566Ek0taSJV7UDqst+CGpJ+0aw0FDawGNp2jYF++rLr5Pq+IS+rcfXMaWQ+M0HOGSQbLAO9gvDN90F5b9XfsA2XWO+jdMpG/kDDlvaHXditZLQJM/IRQG+bpyk5Pnr1YrBEGQdhX39agAY81cMZ/zH6KKK27T0Wdkt11z0DlH8B2HV/ANg2NwA=\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get a single tag\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/tags/{tagId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet tag by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"TagId\"},\"required\":true,\"name\":\"tagId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with list data.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"numBookmarks\":{\"type\":\"number\"},\"numBookmarksByAttachedType\":{\"type\":\"object\",\"properties\":{\"ai\":{\"type\":\"number\"},\"human\":{\"type\":\"number\"}}}},\"required\":[\"id\",\"name\",\"numBookmarks\",\"numBookmarksByAttachedType\"],\"title\":\"Tag\"}}}},\"404\":{\"description\":\"Tag not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/get-all-backups.api.mdx",
    "content": "---\nid: get-all-backups\ntitle: \"Get all backups\"\ndescription: \"Get all backups\"\nsidebar_label: \"Get all backups\"\nhide_title: true\nhide_table_of_contents: true\napi: eJyNVMGO00AM/ZXI51Hb5ZhbQYAWhFiJIg6rHtzEbWabzMx6PAslmn9HTrNtWirBqenY43nP79k91BQrtkGsd1DCR5IC27bYYLVPIYIBwV2E8hHejidrA5GqxFYOUD72sCFk4mWSBsrHdV4bYIrBu0gRyh7eLBb6c/nK180TVVL8tNJMXytqFJyBgco7ISd6EUNobYV6cf4U9XYPsWqoQ/2SQyAowQ/1wEBgH4jFHt9+JXFORGY8gAEr1MV/F7D1JCcKW7eDbCBF4vvbIYyR5FbMgEtti5uWoBROlA1UTChUL+VmpWh/0yTgUrch1sDG+32HvH/nk5ObGVFQUryFgVzqVMxArj6exFRVFFXoLdo2McE6GyBmz18oRtzRf3DJKvpzsky1Vrc1nJp0bsmU8EjvmswJ+jpf13zVUiMa60gaX0MJOxp0Q/UfzM++jcQvxHHwaOIWSuixrplizHMMdv5yBwZekK3yGJo1ho923WJqBUpoREIs53Phw2yPjHuiMMMQwFx5etVQMVYo/LaQhorPY35xxAI558n0fFMXj0adzNCp2fqy8hjSoByTtGfDxwfPHSrCTz9WoB2xbuv1urI+QrqbLWYLHWErKhWc8Cwf7m/iXz7cF1vPl+CVbDYQfJQOh/lzOAD6e1dcVOzPY3xrrRxJCv2SeWjRumGwVKd+FHOiuYHGR9Gjvt9gpO/c5qzHz4lY19D6LOWwhQw0hDXxoP6eDlDCsqooyKB5m4ZdcL1ZVJyTrT6+X6l3LyW5kmCo/rpZ3GFSu++PGSu/J5czmBGE6H/IauI/FkLfgQ==\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get all backups\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/backups\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet all backups\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with all backups data.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"backups\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"assetId\":{\"type\":\"string\",\"nullable\":true},\"createdAt\":{\"type\":\"string\"},\"size\":{\"type\":\"number\"},\"bookmarkCount\":{\"type\":\"number\"},\"status\":{\"type\":\"string\",\"enum\":[\"pending\",\"success\",\"failure\"]},\"errorMessage\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"userId\",\"assetId\",\"createdAt\",\"size\",\"bookmarkCount\",\"status\"]}}},\"required\":[\"backups\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/get-all-bookmarks.api.mdx",
    "content": "---\nid: get-all-bookmarks\ntitle: \"Get all bookmarks\"\ndescription: \"Get all bookmarks\"\nsidebar_label: \"Get all bookmarks\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzNGMtu4zbwV4S59CLE2R59yy7abVp0EyBZ9BD4MJZGFtcUqSUpJ15D/14MKUuyQydyNkB7skzO+8WZ2UFONjOidkIrmMNncglKmSy1Xldo1hZScLiyMH+Aj/3ZIgVLWWOE28L8YQdLQkPmqnElzB8W7SKFGg1W5MhYD2CzkiqE+Q7ctiaYw1JrSaigTcHQ90YYymFeoLSUgsKKQdBkpdhQDikIFu17Q2YLbfo2agVuNAs8iZ51RqgVpECqqVh1tBmk3lSse04FNtLBPJy8wNVq425MTmYCU9VUSzIvUZOiEu4c8Z1wkg8+NcbqF0lnAWK6qUdWcKah9CiOrovEkkucTvxtH0+/2CTTypFyyaPgOKNEqEw2OeWJUIkrKTFka60sXSRftKM0caUYkDJUjCPRrCgptEmsrmiI1ouojv8T0Xpjd2Q/BcQjoy9YgcDGst1/vbzkn0MdbpbfKGM5XXmYrkmODplVJxWjYl1LkSGjzr5Zxo/4VXuKkEJtdE3GicB9qAMDKBqDW5baUWVfJyHy56HZppAZQkf5lYveVjoXhYhfp6AaKXHJoc0ubPtAnwDZV5Vo9RhViei9w9VKqNWdQ9fY1/kNBcQ2WUaWq2mBQjaG2EykcsZatCnYpqrQiB/eS+9PXmk3zTxBju00WN2YbALZURmtBaTwSEsOUMnflV4KydLSkyNlObpTsEKtJBXhwni1RFVr47wujSVzHY+p8FK9c6CGpI1coHOYlZR/jJqr15nVLJsKFSzag+r0wBw7+gfUPNwogbWim8K/oy8rEG5PiyKFWgcTGhm33+Q0OihHE+BFhSv6GmN7CvjKWnIxP8ciMTNEypbanYNVNFLeMqdQEs5BrQ1lBh8l5W9A3oic9FmC4kZkEw1dukp+GkLnVfguzM4RZ6/5NAbYuFKbaVZtllLYkqZB5+jotsOYJjhj/N09LBMQjpLVQ4fcWXBr9HO56OgplDP/EUvGUF6n5Uxc1D2Pn5UVOTi8sP7r/hVwn77MJC8GpBMVm4v8l2iBPfngTK0iVvwYk+3a69Mp8GYzD0YZdH0HqzdqrfRj5NXwCIu2N+37P3mT3MyvyR+jajMuw3tL3I1PlqgUmesuOo6qL3R1keG6ntPXJIgUWwhNwNdaasz9QDfY6qyQij3Hg+6LNgowNK4HbWo6HlgPps3DpjHe6/VtTdfEDFHZO3kxzHP7SRxYQEVPrhvwztZ4POaP6IxY3eJKKNZ3mP7b1humIlfqHOawCl5Cnv5hNiZpyWz2KwDfc8AO89yQte0MazHbfGC3oxEsoQ/I7jpMPPspu3SutvPZzJntxRoNronqC6xrOB7t7ktKOgqJLvzI9lcHnwRZWPjR9uKOx6Bu0hntMHozMmcf2AzGo4AH4hj1H79rUyFL+Oc/994XQhWa0VnrINKHi8uLy9Eo3stzdXsdlf/q9tpPjwfCs7L8QGrruJGc7/ajZGxfc9Sb9eUtvtwJivJTMaslCj/mdP1hcOk4ShYplNo6Ptztlmi5GLctH4fplR2dC8vuHMbvNW0P1zkblA0z9RuGE+AHGTQFYbxqmQK/X6ZMge23Iy8DnzT8f7hyiPo3aPVsDTFotxjS0m/0UigJ2bbs4IB9lWVUj7GerRmYSl8kPv92D6EZHE/Wh+k0HnFQbUe0d7sAca/XpNoW9io4/g8tV+p/AcIULI8=\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get all bookmarks\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/bookmarks\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet all bookmarks\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"boolean\"},\"required\":false,\"name\":\"archived\",\"in\":\"query\"},{\"schema\":{\"type\":\"boolean\"},\"required\":false,\"name\":\"favourited\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"enum\":[\"asc\",\"desc\"],\"default\":\"desc\"},\"required\":false,\"name\":\"sortOrder\",\"in\":\"query\"},{\"schema\":{\"type\":\"number\"},\"required\":false,\"name\":\"limit\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"title\":\"Cursor\"},\"required\":false,\"name\":\"cursor\",\"in\":\"query\"},{\"schema\":{\"type\":\"boolean\",\"default\":true,\"description\":\"If set to true, bookmark's content will be included in the response. Note, this content can be large for some bookmarks.\"},\"required\":false,\"description\":\"If set to true, bookmark's content will be included in the response. Note, this content can be large for some bookmarks.\",\"name\":\"includeContent\",\"in\":\"query\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with all bookmarks data.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarks\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"userId\":{\"type\":\"string\"},\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"attachedBy\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\"]}},\"required\":[\"id\",\"name\",\"attachedBy\"]}},\"content\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true},\"description\":{\"type\":\"string\",\"nullable\":true},\"imageUrl\":{\"type\":\"string\",\"nullable\":true},\"imageAssetId\":{\"type\":\"string\",\"nullable\":true},\"screenshotAssetId\":{\"type\":\"string\",\"nullable\":true},\"fullPageArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"precrawledArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"videoAssetId\":{\"type\":\"string\",\"nullable\":true},\"favicon\":{\"type\":\"string\",\"nullable\":true},\"htmlContent\":{\"type\":\"string\",\"nullable\":true},\"contentAssetId\":{\"type\":\"string\",\"nullable\":true},\"crawledAt\":{\"type\":\"string\",\"nullable\":true},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\",\"nullable\":true},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true},\"size\":{\"type\":\"number\",\"nullable\":true},\"content\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"assetType\",\"assetId\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"unknown\"]}},\"required\":[\"type\"]}]},\"assets\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"userId\",\"tags\",\"content\",\"assets\"],\"title\":\"Bookmark\"}},\"nextCursor\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"bookmarks\",\"nextCursor\"],\"title\":\"PaginatedBookmarks\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/get-all-highlights.api.mdx",
    "content": "---\nid: get-all-highlights\ntitle: \"Get all highlights\"\ndescription: \"Get all highlights\"\nsidebar_label: \"Get all highlights\"\nhide_title: true\nhide_table_of_contents: true\napi: eJyNVU2P2zYQ/SvCnAl706NuRtGm2x6yQLfoYeHDWBpbjCmSGY42MQT+92Ik2ZY22qS+2KYe37z5euqhplSxjWKDhxI+khToXNHYU+PsqZEEBgRPCcoX+ON+uDeQqOrYygXKlx4OhEy866SB8mWf9wYiMrYkxGkApKqhFqHsQS6RoATftQdiyAaYvnSWqYbyiC6RAY+tIpxtrYABq8K+dMQXyGaFKQlbf1KdVpwe/NpxCj+krkbEknuv+BSDT5SU/peHB/1aFujT4TNVUny10rwpVFGj4AYMVMELedG7GKOzFerd7eekBCv6w0AJBiKHSCx2DD9rwR2LzHhR3UJt+jnHIYRzi3x+rL+vVzaQBFk+HY+JZLUz5OsfPK2CC7zWB/Jdq/NyIefCV9Cy1mDgxEQeDBxcRzpANR2xcwLlFZgNCH2TNUrfOYcH7a5wR9mAD0L/C2jXU+8S8TtVqZhQqN6tCMmLmXqZ13dZzXntpqwmzYOiW/x5tP19gm+bNkT09E2mmf55xm8ULhZ5RjQL9oQn61XDbL+zfgy0JE2ooYTTkEdE3W/YLkgT8et1yzt2UEKPdc2UUt5itNvXD2DgFdmqyGEqp8fjcl1noBGJqdxuhS+bMzKeieIGYwTzZgOfGyomhiIcC2mo+GvCF6MWVT8zqL914aZ9mNnUrZIaWfMYYFBOIB3U4cfvgVtUhX/++zy0w/pj0Oua9Sjpw+Zh8zAzoJue3dPjqv7d02NxDLwUr8lmAzEkaXGwismtVk15QdrfTecdCx9T1UHcRofWDzug3eqnri5GZW+gCUn0tO8PmOgfdjnr8eiV2uvaJu3o3VvPdJm59ivqmpcwePY72JsN38H7+6wMbxIDDWFNPIQcb+2qiuI8xHcuqyy30f342zMYwGXP3/R4YL9arL/MuPt+RDyHM/mc4Spd9D/kfc75P+nqe/Y=\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get all highlights\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/highlights\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet all highlights\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"number\"},\"required\":false,\"name\":\"limit\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"title\":\"Cursor\"},\"required\":false,\"name\":\"cursor\",\"in\":\"query\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with all highlights data.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"highlights\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"bookmarkId\":{\"type\":\"string\"},\"startOffset\":{\"type\":\"number\"},\"endOffset\":{\"type\":\"number\"},\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"],\"default\":\"yellow\"},\"text\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\",\"nullable\":true},\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"}},\"required\":[\"bookmarkId\",\"startOffset\",\"endOffset\",\"text\",\"note\",\"id\",\"userId\",\"createdAt\"],\"title\":\"Highlight\"}},\"nextCursor\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"highlights\",\"nextCursor\"],\"title\":\"PaginatedHighlights\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/get-all-lists.api.mdx",
    "content": "---\nid: get-all-lists\ntitle: \"Get all lists\"\ndescription: \"Get all lists\"\nsidebar_label: \"Get all lists\"\nhide_title: true\nhide_table_of_contents: true\napi: eJyNVMFu2zAM/RWDZyHJdvQtGLai24AVW4YdghwYm6nVyJIq0e0CQ/8+UHYSJw22nSxLj+QjH8keaopV0J61s1DCHXGBxhRGR46ggPExQrmGr/l/oyBS1QXNByjXPWwJA4Vlxw2U603aKAgUvbORIpQ9vF8s5HMZ4dv2iSouXjU350hFjYwzUFA5y2RZzNB7oysUs/lTFNseYtVQi3LigycowWVvoMAH5ymwHiIP9M8wDAEPoEAztfHf5rqeYCIHbR8hKbDY0s2Hiwyv3xXYzhjcGoKSQ0dJga5uAZMCj4Es398If8PLAHgLJNu1olmLtkMDCmKLgUW8mnbYGYby+JYUPHcUDv8Vz3dbo6sJdOucIbTipcH4wQneBWQX4m1UFyl8d+avrN2rpSD/tWYnhxdNr/lmjL9JSfrsudOBarHQNYzajJWd1PFkdYPihM9GAWsWYrnTIV3HGBpqk1J+aYkbV0MJj5SbB2UAYH4cmkjhhaQG6x66YKCEHus6UIxpjl7PX95JWhi0lDeXanwepuWoUcPsYzmfczjM9hhwT+Rn6D1cNRysGipGD4XbFdxQ8WXEFwMXSWgyvD9kjIbI0xE+iSKRJY8MEwkzCNR4+ORCi8Lw868VSD203Tkxl6wHSu9mi9kCzlU98Vk+3N/kv3y4L3YuXJKXZKXxXOQW88QMI/hmTV0N4GmLvN1nQ4JMv3nuDeqhLUWjfpTxpLWCxkWWi77fYqSfwaQk1+PErDdnEfP6U9AQ1hSy7ns6QAnLqiLPWW3T5UV0vdREllM73X1cgQK8FOOq+Nn7ca3Zw8R33w+IlduTTQnUSILlH5I07x+0owYu\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get all lists\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/lists\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet all lists\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with all lists data.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"lists\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"description\":{\"type\":\"string\",\"nullable\":true},\"icon\":{\"type\":\"string\"},\"parentId\":{\"type\":\"string\",\"nullable\":true},\"type\":{\"type\":\"string\",\"enum\":[\"manual\",\"smart\"],\"default\":\"manual\"},\"query\":{\"type\":\"string\",\"nullable\":true},\"public\":{\"type\":\"boolean\"},\"hasCollaborators\":{\"type\":\"boolean\"},\"userRole\":{\"type\":\"string\",\"enum\":[\"owner\",\"editor\",\"viewer\",\"public\"]}},\"required\":[\"id\",\"name\",\"icon\",\"parentId\",\"public\",\"hasCollaborators\",\"userRole\"],\"title\":\"List\"}}},\"required\":[\"lists\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/get-all-tags.api.mdx",
    "content": "---\nid: get-all-tags\ntitle: \"Get all tags\"\ndescription: \"Get all tags\"\nsidebar_label: \"Get all tags\"\nhide_title: true\nhide_table_of_contents: true\napi: eJydVU1v2zAM/SsGz0LS7uhbWmxFt8MKLMMOgQ+MzcRqbEmV6KyB4f8+0HK+0zTYSXFMPj6ST88tFBRyrx1rayCFJ+IEqyphXAZQ0B/pDKZyZgoC5Y3XvIF01sKc0JOfNFxCOsu6TIFDjzUx+dAHhLykGiFtgTeOIIXAXpsldAo8vTXaUwHpAqtACgzWEiHHozWM2kh9LZzeGvIb6NTHgArINLUQ7WEUNAGXcnqqaI0mJyFf0AKbiiEdXl+hEazn/ymPGhSUTY0GFBhrCLIrVZAZ85KKh83tta7A5Y0P1t8AZZp6ThJomqrCeUWQsm/oCnSla306kEzCg7MmUBDwL3d3chzr6ef8lXJO/moud7pKCmQcgYLcGibDkoXOVTpHyRq/Bkm9wNv2YKDAeevIs46Fo0h3Ueg99gNlqsPn2bq4OOTY+KUXTf1g7apGvwrnMz0JeNhMhh1P+7DPyKC+CBkVdf6m6452NpNm1PYOHBG9SitTwJpFBnLRQUANvfNj1NMFuZ/o5oTE4B0HEFkXqdbEpS0ghSX1vaNYB4yHhEB+vbWOxleQQotF4SmEboxOj9f3oGCNXkvpOK74Ospue7lLZhfS8Zj9ZrRCjysiN0LnQJ1oc1pSMiAkdpFwScmPIT6JXGTCB673SwQZKx96324+Uln66MMgHYJADT++WV+jMPz+Z9oPWZuFlXTpOlK6H92N7mC/jh2fycvzRf6Tl+dkYf0xeWm2U+Bs4EE3wzU+cfcjuHZ/Hc++ArE9pnceuwq1EfR+Q+2ww2HpmYLSBpbntp1joN++6jr5O3qGbLbQQfa3t5gVbc6Nf41VIzV7A/sgZTDpW0KPnPaWhJ2X3hK8dcd9bLYXav9tVFASFuT7CcSkSZ6TO8w6s0BB2d2Yp69TUIDHgjsRWI++dUFz2GrbxoipXZHpOtgyZ3mGTu7nPyWsuhQ=\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get all tags\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/tags\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet all tags\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\"},\"required\":false,\"name\":\"nameContains\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"enum\":[\"name\",\"usage\",\"relevance\"],\"default\":\"usage\"},\"required\":false,\"name\":\"sort\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\",\"none\"]},\"required\":false,\"name\":\"attachedBy\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\"},\"required\":false,\"name\":\"cursor\",\"in\":\"query\"},{\"schema\":{\"type\":\"number\",\"nullable\":true},\"required\":false,\"name\":\"limit\",\"in\":\"query\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with all tags data.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"numBookmarks\":{\"type\":\"number\"},\"numBookmarksByAttachedType\":{\"type\":\"object\",\"properties\":{\"ai\":{\"type\":\"number\"},\"human\":{\"type\":\"number\"}}}},\"required\":[\"id\",\"name\",\"numBookmarks\",\"numBookmarksByAttachedType\"],\"title\":\"Tag\"}},\"nextCursor\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"tags\",\"nextCursor\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/get-bookmarks-in-the-list.api.mdx",
    "content": "---\nid: get-bookmarks-in-the-list\ntitle: \"Get bookmarks in the list\"\ndescription: \"Get bookmarks in the list\"\nsidebar_label: \"Get bookmarks in the list\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzNWN1v2zYQ/1cEvmwDhDgdOqzQW1psXfbRBGuKPQR+oMWTxZgiVfLkxDX0vw9HypLs0I7SBtie4oj3+bvj8e62TIDLraxRGs0y9h4wWRizqrhduUTqBEtIlHTIUoZ86Vh2y/6UDh2bp8xB3liJG5bdbtkCuAV70WDJstt5O09ZzS2vAME6T+DyEirOsi3DTQ0sYw6t1EuWMnjgVa3okwQp1MNmWd3fvfnZPHz5qXxANPkb0i7Rk5D2S8HalFn43EgLgmVoG0iZ5hURqECQMkkO1RxL1qYn1eumIr+4y1nq8SDnBBS8Uciy8GVPX8GVGxQ6Y/HKCrA7nZ8bsJu4Ut1UC7CnpClZSZwgqTd/B8y7xjpzUnQeKJ6WvTBGAddshEKAeD9ZLovEASZoEn/aJ853LsmNRtCY3EulkgUkUueqESB2KWXB1UY7OEs+GIQ0wVIOTDnXxKO4XUJSGJs4U8GQlmdRH/8npvVgd2LfBcYD0OfkQFDjCPcfz8/pz74PV4s7yMlOLP0VTARHTho6Y4iD17WSOSeO2Z0jtkg4jRfEUlZbU4NFGZT2Vo9IubV8Q8YiVO5pEVI8zsg2ZbkFjiAuMHpaGSELGT9OmW6U4gvKaIpc2+f3BEpu81KuQUQyuU1ZwdeGytWxc+TLpdTLj8ixcU/rG+qGa/IcnGOkQqrGAsEEWhDXvE2Za6qKW/nFR+nlxWuD0+AJdmym0ZrG5hPEjqpnLVnK7mFBCarod2UWUoGv7wjaUVKnzEm9VFCEA+vdklVtLHpfGgf2Mp5T4fV54UQNdzVywBF5XoJ4G4Wr95ncLJuKazZv94rSLWns5O9J83SjC2w0XBX+fTztQDg9boqSehUgtCqO3+RrtFeFJtDLii/hU0ztMeIL5wBjcY5lYm4BtCsNPoeraJS6Jk2hJDyHtbaQW36vQHwF81oKMM8ylK9lPhHoEiv1bkidJ+m7NHuOOTvPpyngDZbGTkO1WSjpSphGLTjCdccxzXDi+Kt7WCYwHFxWTx3uzpw6om+7iwgPoZz5H7HLGMrrtDsTN3Wn41tt5ZQc3lj/6+YJcn99SYkoBqYjFZuK/IdogT364EytIk5+GYvtuurjV+CrYR5AGXx9AdQbvdLmPvJqeIZ520P78k/epDDTa/LbqNqMy/AOiY/jLwuuNdjLLjsOqi/r6iLRdT2nr0ksUmxZaAI+1cpwAfSGDlg9K6Viz/Hg+7yNEgyN616bOmot9/rIw6Yx3uv1bU3XxAxZ2Qd5PoxxbzuEGBmo4QG7ue7ZHg/t/Z6ckaprvpSa/H3bk7Yel9fnrx+PIzR4J9pgUphGi5cbQ3Ij4j1YBc5RPj0+O/DUSxjoKbat58fSCJaxZUg12gRkbEajlJttw5qgnY1hcmDXu3WF76PYlgthwbl2xms5W7+iVOZWEure+O44oLVbGJSItctmM7SbsxW3fAVQn/G6ZodT6k0JSSchMYWfPv/o6JNgCwVktGn5SJh209to39IDRJr9ZSUyGm88Ed07/+NXYytOFv7+z41HUerCEDt5HUx6dXZ+dj7aKvT2XFxfRu2/uL70g/Ce8eQsPfrGITXH2XY3FZ/aLx30nX16nV5KBcfpOZzViks/ynU9cIj4rd8IUXizfjU0BH2estI4JLLtdsEdPUFtS5/DqE6pIKSjgA+7hhVsDvY+a64assPvU47Q7zY7U2j7Vc1p4qOQ/Yf7j2hEglePdiKDd/PhYsUxP+rq9393peCH5Jj23QOqN2OdQ1zCRpEemBI4BZQsCIcXeQ712NRHpY5M70vN+19uWOiLx0uG/Vs4nvb2TdpuA8WNWYFu295CpP/JwLb9F46ffEA=\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get bookmarks in the list\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/lists/{listId}/bookmarks\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet bookmarks in the list\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"ListId\"},\"required\":true,\"name\":\"listId\",\"in\":\"path\"},{\"schema\":{\"type\":\"string\",\"enum\":[\"asc\",\"desc\"],\"default\":\"desc\"},\"required\":false,\"name\":\"sortOrder\",\"in\":\"query\"},{\"schema\":{\"type\":\"number\"},\"required\":false,\"name\":\"limit\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"title\":\"Cursor\"},\"required\":false,\"name\":\"cursor\",\"in\":\"query\"},{\"schema\":{\"type\":\"boolean\",\"default\":true,\"description\":\"If set to true, bookmark's content will be included in the response. Note, this content can be large for some bookmarks.\"},\"required\":false,\"description\":\"If set to true, bookmark's content will be included in the response. Note, this content can be large for some bookmarks.\",\"name\":\"includeContent\",\"in\":\"query\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with list data.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarks\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"userId\":{\"type\":\"string\"},\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"attachedBy\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\"]}},\"required\":[\"id\",\"name\",\"attachedBy\"]}},\"content\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true},\"description\":{\"type\":\"string\",\"nullable\":true},\"imageUrl\":{\"type\":\"string\",\"nullable\":true},\"imageAssetId\":{\"type\":\"string\",\"nullable\":true},\"screenshotAssetId\":{\"type\":\"string\",\"nullable\":true},\"fullPageArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"precrawledArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"videoAssetId\":{\"type\":\"string\",\"nullable\":true},\"favicon\":{\"type\":\"string\",\"nullable\":true},\"htmlContent\":{\"type\":\"string\",\"nullable\":true},\"contentAssetId\":{\"type\":\"string\",\"nullable\":true},\"crawledAt\":{\"type\":\"string\",\"nullable\":true},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\",\"nullable\":true},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true},\"size\":{\"type\":\"number\",\"nullable\":true},\"content\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"assetType\",\"assetId\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"unknown\"]}},\"required\":[\"type\"]}]},\"assets\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"userId\",\"tags\",\"content\",\"assets\"],\"title\":\"Bookmark\"}},\"nextCursor\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"bookmarks\",\"nextCursor\"],\"title\":\"PaginatedBookmarks\"}}}},\"404\":{\"description\":\"List not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/get-bookmarks-with-the-tag.api.mdx",
    "content": "---\nid: get-bookmarks-with-the-tag\ntitle: \"Get bookmarks with the tag\"\ndescription: \"Get bookmarks with the tag\"\nsidebar_label: \"Get bookmarks with the tag\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzNWN1v2zYQ/1cEvmwDhDgdOqzwWxpsXTasDVYXewj8cJZOEmOKVMmTE9fQ/14cKUuyIydKG2B7iiPe54/3xduJFF1iZUXSaDEX75CilTHrEuzaRXeSiogKjAhyEQuC3In5jVjw32UsHCa1lbQV85udWCFYtBc1FWJ+s2yWsajAQomE1nkClxRYgpjvBG0rFHPhyErNcvEeykrxJ4kyVffbvLy7ffOruf/yS3FPZJI3rFySJ1lAfpWKJhYWP9fSYirmZGuMhYaSz8mfx0KyOxVQIZr4UeW6LtkpcImIPRrsWooZ1IrEPHw5UJeBcr0+Zyx9sCnavc7PNdrtuFJdlyu0j0lTspQ0QVJn/h6Wy9o686joJFA8LXtljELQYoBCQPgwVK6yyCFFZCJ/2oXNDy5KjCbUFN1JpaIVRlInqk4xjaT24WTRVUY7PIveG8I4okL2TAlo5lFgc4wyYyNnSuyD8mzUx/+JaR3YrdjLwHgE+pIdCGoc4/7z+Tn/OfThw+oWEwopqKSjKAUC1tAawxxQVUomwByzW8dsI9dpvCARi8qaCi3JoLSzekAK1sKWjSUs3dMiZPowIptYJBaBML2g0dPSpDKT48ex0LVSsOKI5ptruvieQAk2KeQG05FIbmKRwcZwsTp1TpDnUucfCah2T+vr64arkwSdE6xCqtoiw4Q6Za5lEwtXlyVY+cXf0suL14amwRPs2E6jNbVNJogdVM9Kiljc4YoDVPHv0qykQl/dCbXjoI6FkzpXmIUD692SZWUseV9qh/ZqPKZC63nhQA25OnIARJAUmL4dhavzmd0s6hK0WDYHRemGNbbyD6R5ukECG40fMt8dH3cgnJ42RUm9DhBaNY7f5DQ6qEIT6GUJOX4aU3uK+MI5pLF7HovExCJqVxh6DldWK3XNmkJJeA5rZTGxcKcw/QbmjUzRPMtQ2MhkItAFleqyD50n6dswe445e8+nKYCaCmOnoVqvlHQFTqNOgfC65ZhmOHP83TaWCQxHyeqpQ+4seSL6vlwkvA/lzP8YS8ZQXqflzLipex3faytwcHhj/a/FE+Q+fVlJmvVMJyo2F/n3owX2ZMOZWkWc/DIU207Vp1Pgm2HuQel9fQHUa73W5m6ka3iGZdNB+/Itb9I1czf5Y1BthmV4j8TH4ZcVaI32qo2Oo+or2rrIdO3M6WuSGCm2IgwBnyplIEXuoT1WzwqpsXbc+75sRgn6wfVgTB2Mlgdz5PHQOD7rdWNNO8T0Udld8rJ/xr1tERJsoMZ7at91z/a4H+8P5AxUXUMuNfv7tiNtPC6vz18/fI4sII+0oSgztU5f7hWSmHR8BCvROQ6nh2dHjnoJPT1fbeP5qTCpmIs8RBovAuZixlcw2/kdQTMbYuTQbvabCj9EiR2kqUXnmhlUcrZ5xXEMVjLk3vT2OEC13xYURJWbz2Zkt2drsLBGrM6gqsTxE3VRYNRKiEzmn55/tfRRsIVvY7Bk+ciItk+3waqlg4c1+0xlMn7beCJOOv/jd2NLYAv//HfhMZQ6M8zOXgeTXp2dn50PVgqdPRfXV6P2X1xf+VfwgfHsLHd844gn4/lu/yR+dLV0NHV20fXEQiq4zt1wVimQ/iXXjsDhxm/2STff74X6S1/GojCOmGi3W4Hj/tM0/Dm80zkUUun4wvtFwxq3R0ufDaiarfDLlBP0+7XOFNpuT/M48UnE/sPlx+h9BK8eLER675Z9Yo1jftLVH/9pC8FP0Snt++6pt0Ode6tCUPiGWyDwfbIB4ewiSbAaWvqgzrHlXZ1599tChJl4uGA4TMLhS+/Qot0uUCzMGnXT9Aby/2xg03wF6Kp47w==\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get bookmarks with the tag\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/tags/{tagId}/bookmarks\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet bookmarks with the tag\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"TagId\"},\"required\":true,\"name\":\"tagId\",\"in\":\"path\"},{\"schema\":{\"type\":\"string\",\"enum\":[\"asc\",\"desc\"],\"default\":\"desc\"},\"required\":false,\"name\":\"sortOrder\",\"in\":\"query\"},{\"schema\":{\"type\":\"number\"},\"required\":false,\"name\":\"limit\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"title\":\"Cursor\"},\"required\":false,\"name\":\"cursor\",\"in\":\"query\"},{\"schema\":{\"type\":\"boolean\",\"default\":true,\"description\":\"If set to true, bookmark's content will be included in the response. Note, this content can be large for some bookmarks.\"},\"required\":false,\"description\":\"If set to true, bookmark's content will be included in the response. Note, this content can be large for some bookmarks.\",\"name\":\"includeContent\",\"in\":\"query\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with list data.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarks\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"userId\":{\"type\":\"string\"},\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"attachedBy\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\"]}},\"required\":[\"id\",\"name\",\"attachedBy\"]}},\"content\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true},\"description\":{\"type\":\"string\",\"nullable\":true},\"imageUrl\":{\"type\":\"string\",\"nullable\":true},\"imageAssetId\":{\"type\":\"string\",\"nullable\":true},\"screenshotAssetId\":{\"type\":\"string\",\"nullable\":true},\"fullPageArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"precrawledArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"videoAssetId\":{\"type\":\"string\",\"nullable\":true},\"favicon\":{\"type\":\"string\",\"nullable\":true},\"htmlContent\":{\"type\":\"string\",\"nullable\":true},\"contentAssetId\":{\"type\":\"string\",\"nullable\":true},\"crawledAt\":{\"type\":\"string\",\"nullable\":true},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\",\"nullable\":true},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true},\"size\":{\"type\":\"number\",\"nullable\":true},\"content\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"assetType\",\"assetId\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"unknown\"]}},\"required\":[\"type\"]}]},\"assets\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"userId\",\"tags\",\"content\",\"assets\"],\"title\":\"Bookmark\"}},\"nextCursor\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"bookmarks\",\"nextCursor\"],\"title\":\"PaginatedBookmarks\"}}}},\"404\":{\"description\":\"Tag not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/get-current-user-info.api.mdx",
    "content": "---\nid: get-current-user-info\ntitle: \"Get current user info\"\ndescription: \"Returns info about the current user\"\nsidebar_label: \"Get current user info\"\nhide_title: true\nhide_table_of_contents: true\napi: eJyNUz1v2zAQ/SvCmwnL7ajNQxukHRq0DjoYHs7SOWIsiQx5SmsI/O/FUUJiJx2iRRJ5H+/eezeh4VgH68W6ARV+soxhiIUdjq6ggxulkJaLegyBBynGyAEGQg8R1Q73kUPE3iByPQYrZ1S7CQemwGEzSotqt097g8DRuyFyRDXh83qtr+u+Pw6PXEvxx0qbmxQNCa1gULtBeBDNIO87W5NmlI9R0ybEuuWe9EvOnlHB5UIw8MF5DmLnpra5iIkS7PCAZDBQz+8vDIax6+jQMSoJIycD7sl2H4rsXE2dEnMRfXCuYxqQknLxNNrAjfJnG1wm7JM+Bj1L6xpUeOA8CSmTKJWXWPYM5Ts8K/VK9xg6VJioaQLHmErytnz+BINnClahZQKW65n5I42doEIr4mNVlhLOqxMFOjH7FXkP80aebcvFUqFwx2yJ70t8MWNBSunCCL9Ul7nzpR1eGNHOOkcOU4ZyEMzy8dWFnhTht9/bzJr6UdN16hnSp9V6tVYvWlH28YJnc3f7X/ybu9vi6MI1eB02GXgXpafsqNkSuGG5cn3eiLd1p1d7fnRz5vGF/0rpO7KDds8KTovOO2SdoTbQ1WpdFD2dpgNFvg9dSnr8NHLQddu/6py3zaBlatR9uwknPqPCpq7ZSzZEN2r3d4ukyr2Y7ubLFgZ0rdcbfXL15YqG80XtaZojtu7EQ0owCwjRfyS1+D9TYIbk\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get current user info\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/users/me\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nReturns info about the current user\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with user data.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\",\"nullable\":true},\"email\":{\"type\":\"string\",\"nullable\":true},\"localUser\":{\"type\":\"boolean\"}},\"required\":[\"id\",\"localUser\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/get-current-user-stats.api.mdx",
    "content": "---\nid: get-current-user-stats\ntitle: \"Get current user stats\"\ndescription: \"Returns stats about the current user\"\nsidebar_label: \"Get current user stats\"\nhide_title: true\nhide_table_of_contents: true\napi: eJylVktv20gM/isCz4Ll7FE3F32lu4sWGwdFYfgwkmlramlG5VBuVEH/veBItiXF3iTNKYr5+kh+Q7KBDbqUdMnaGojhP+SKjAscK3aBSmzFAWcYpBURGg4qhwQhsNo5iFdw75AcrENwmFakuYZ41UCCipAWFWcQr9btOgRCV1rj0EHcwF/zufwZB/6cfMeUg5+aMx+kQzCDEFJrGA2LiSrLXKdKTKLvTuwacGmGhZIvrkuEGKz3BCGUZEsk1l1UUxVvrN0XivZuoG2qIkGCNpSv9+pgSTNeVVhQmukDbq7Jl74ul2X/aMdXhR/1Lsv1LruikRyRv6mXXvZUtrk2+4ueGB/4okA5h5ckrXTvR6VJsl51jns3R6O1+LXlW1sobYYJKCJVQwiasXBPg954BwM9x6TNTtCltjLPQNd7OOoLsEI93Hbhb+YeJ6t8IbDv9C+8XonHlX5hMjy2fjqVHtsVWJNEvfToa2i5bgd00Wa3SFkf/MN8EnCm3VfEK7TJtPvXGnnRV6TfUNFl7tYfbUV/XshsbH12/ExOePszIzyit6r+vJ0k+1Kyqvo1qDY+1gnUtL3HXgwrP6jzqarjZPxDVLt7p3avYK5RxcuYOwHv7f/nCQ6G2Z2tKH0FVDe178HKTM1zleQIMVOFIaCpCgGnSg0h/MREEObyXdhE5wIYHxiNk3UUgtNml+O2E5BzAqkoLXUJPbMQPbrrbR7tpMkGGu+b83YZ7JLp5ni8J0Zz+dHwm4y6y3NjwKhLrZOkJK0CObMbiGGHvk1KRgVEsspdVGDk17kUFukgN4PcCRXlEEOjNhtC59pIlTo63EAIB0Vamud73Iu7k2Grqpwhhoy5dHEUMdWzvSK1RyxnqiwhnNwVywyD3kNgt/6W+bvXDzos0Lbt4IK5k3uiizy8Y069lsiSh1eDuFeS2viP95YKJQg/fV16QmiztWIuWXeQbmbz2Vzqqln4CSc8iy+3F/EvvtwGW0tj8JJsG0JpHRfKnN8tfEAenWvBsfQjx835rnr20dcVQFZ/VOayZ9uw62HT93sFvt/yqIQtXeB1CJl1LNKmSZTDe8rbVn7+USHJxbg+d9wfjCFkqDZIniR7rCGGRZpiyZ4aeeUHxfQUlB6eSPjh3VLYPe7cpFPe+3HsmHrgu2k6jaXdo2lbCHsQLP9DK5T/DVVi30E=\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get current user stats\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/users/me/stats\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nReturns stats about the current user\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with user stats.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"numBookmarks\":{\"type\":\"number\"},\"numFavorites\":{\"type\":\"number\"},\"numArchived\":{\"type\":\"number\"},\"numTags\":{\"type\":\"number\"},\"numLists\":{\"type\":\"number\"},\"numHighlights\":{\"type\":\"number\"},\"bookmarksByType\":{\"type\":\"object\",\"properties\":{\"link\":{\"type\":\"number\"},\"text\":{\"type\":\"number\"},\"asset\":{\"type\":\"number\"}},\"required\":[\"link\",\"text\",\"asset\"]},\"topDomains\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"domain\":{\"type\":\"string\"},\"count\":{\"type\":\"number\"}},\"required\":[\"domain\",\"count\"]},\"maxItems\":10},\"totalAssetSize\":{\"type\":\"number\"},\"assetsByType\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\"},\"count\":{\"type\":\"number\"},\"totalSize\":{\"type\":\"number\"}},\"required\":[\"type\",\"count\",\"totalSize\"]}},\"bookmarkingActivity\":{\"type\":\"object\",\"properties\":{\"thisWeek\":{\"type\":\"number\"},\"thisMonth\":{\"type\":\"number\"},\"thisYear\":{\"type\":\"number\"},\"byHour\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"hour\":{\"type\":\"number\"},\"count\":{\"type\":\"number\"}},\"required\":[\"hour\",\"count\"]}},\"byDayOfWeek\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"day\":{\"type\":\"number\"},\"count\":{\"type\":\"number\"}},\"required\":[\"day\",\"count\"]}}},\"required\":[\"thisWeek\",\"thisMonth\",\"thisYear\",\"byHour\",\"byDayOfWeek\"]},\"tagUsage\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"},\"count\":{\"type\":\"number\"}},\"required\":[\"name\",\"count\"]},\"maxItems\":10},\"bookmarksBySource\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"count\":{\"type\":\"number\"}},\"required\":[\"source\",\"count\"]}}},\"required\":[\"numBookmarks\",\"numFavorites\",\"numArchived\",\"numTags\",\"numLists\",\"numHighlights\",\"bookmarksByType\",\"topDomains\",\"totalAssetSize\",\"assetsByType\",\"bookmarkingActivity\",\"tagUsage\",\"bookmarksBySource\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/get-highlights-of-a-bookmark.api.mdx",
    "content": "---\nid: get-highlights-of-a-bookmark\ntitle: \"Get highlights of a bookmark\"\ndescription: \"Get highlights of a bookmark\"\nsidebar_label: \"Get highlights of a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVcGO2zYQ/RVhTi1ArJwiRQPdXKBNNzk0aLboYeEDLY0triWSGY42dgT+ezG0bEm7SjaHnCyTj8M3b94Me6gwlGQ8G2ehgLfIWW32dWP2NYfM7TKdbZ07tJoOoID1PkBxD78PSwE2CgKWHRk+QXHfwxY1Ia07rqG438SNAq9Jt8hIIQFCWWOroeiBTx6hgMBk7B4U4FG3vpElg6Zqjqd9+/nhzW/u+OXX+sjsyjfCwHCCXBjcVhAVEH7qDGEFBVOHCqxuBbQdQQqM5Oc11yCsCIN3NmAQJr+sVvIzl+KuxqwxgUWEURJQUDrLaFlOaO8bU2o5kT8EObaQoNs+YMmgwJPzSGzOl05ijlhNpE9ClrENL8eYJPhM0KggsCb+e7cLyJN927VbJNlHW31jt3SNo8VC2a4VF5ywadxnEDFF4D0hWlCwbToUX1S4013DUFyAUQHjkZdC2q5p9FYqKwWMCqxj/C6gWU69C0hfUaUk1IzVeoFInHnpfm6gqZpT7YasBs6J0fX+6W2b0b1/XWoP8emVE1tsYtp9vXr93J0X/2fWcbZzna1+nDNLVy1oHxW0GILeL+09SSJFGPEpkXSea1dBAfskW2rGAvKLyiHvR8FjPmu6gPR4GSEdNVBAr6uKMISYa2/yx1eg4FGTEXOkLIbts3QXJ9bMPhR5znS6OWjSB0R/o70HtdD9QwQZAFxj9n7AZ2cuUrrJ9Pso4g5dOZmBV6XkZskjwWQ2JZC0S/r401GrheG7/+6SnMbunByXrM+UXt2sblaTEXjls/5wu8h//eE22zmak5dkowLvArc62WIYli+M/ln4frTai0/GOX1pkdw32tjUnVLBfjDA2GZS6GLWc9NuUFC7wILv+60O+C81Mcrypw5JXp/N6IDkk8oE+a6g2Okm4Ddy+Omfwbw/Z1+jfBnQ9pSMJjOuAFBwwNP8pYmbqKBGXSElFmfAuizR8+ToswYVM10b5O0fd6BAzy30xDIp+iKtvj8j7twBbYxXliz/hWCM/wNdHrx1\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get highlights of a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/bookmarks/{bookmarkId}/highlights\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet highlights of a bookmark\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The list of highlights\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"highlights\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"bookmarkId\":{\"type\":\"string\"},\"startOffset\":{\"type\":\"number\"},\"endOffset\":{\"type\":\"number\"},\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"],\"default\":\"yellow\"},\"text\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\",\"nullable\":true},\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"}},\"required\":[\"bookmarkId\",\"startOffset\",\"endOffset\",\"text\",\"note\",\"id\",\"userId\",\"createdAt\"],\"title\":\"Highlight\"}}},\"required\":[\"highlights\"]}}}},\"404\":{\"description\":\"Bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/get-lists-of-a-bookmark.api.mdx",
    "content": "---\nid: get-lists-of-a-bookmark\ntitle: \"Get lists of a bookmark\"\ndescription: \"Get lists of a bookmark\"\nsidebar_label: \"Get lists of a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVU2P3DYM/SsGTy0grCdFiga+bYs22CaHINmih8EcODZnrB1ZUiR6dyaG/ntB2fO162x76Mm29CQ+Pj7SAzQU66A9a2ehgvfEhdGRY+E2BRZr53Ydhh0oYNxGqJbw67QUYaUgUt0HzQeolgOsCQOF255bqJartFLgMWBHTCFmQKxb6hCqAfjgCSqIHLTdggLaY+eNLGnSjdkftt3Tw7tf3P7bz+2e2dXvhIHmDDkyuGsgKQj0tdeBGqg49KTAYieg9RmkQEtqHrkFYRUoemcjRWHy02Ihj2sV7lvKKogIrd62Rm9bjqCgdpbJspxA742uUU6UD1GOzSTo1g9UMyjwwXkKrMegWeELGIaAB+HJ1MV/P66blxqmY+YzG1fJvRTf9sbgWqQVBZMCXc8BU64nWb6bCT9zywiYqbXtOzFSh7ZHAwpih4HFTQ1tsDcM1XEvKfjaUzj8p3i+XxtdX0DXzhlCK7e0GH9zgncB2YU4j+ojhc/OvMraPVkK8t1odvLyqOkpr0zxV+nKlEup1lSbSdkLHU+nZihe8Fmdzf9RR4b0PMZoqFXKG28Xb19a+tg0hXVcbFxvm//PzrVr5o3XUYy4ndt7xj/fcMbnRPJ5bl0DFWwpR5UOrqA89nYsh3Obp3IUQYZSeDyOnD4YqGDApgkUYyrR6/LxjZQNgxb75ASm7VG1owdbZh+rsuRwuNlhwB2Rv0HvQc1Mi+kGGRjcUvFhwhcjFynYxbT8IrqOkS9n5kkkiSx5ZJhYNINATS9/uNChMPzz7/uspLYbJ8cl65HSm5vFzeJiZJ743H66m+V/++mu2LhwTV6SlcZykTvMjpiG6/f/Es9Gzclgr/1YxqSZ9lx6g3psRanbMFV8eZrmUt7qarRPzlfQusgCHYY1RvormJRkeZofy9W55NkYjY7y3kC1QRPpFeY/fJ6M+mPxPbbHMW4P2Vmmly9QsKPD9a8orWQYETYUMosRcFvX5Pni6ItmFPecmuH97/egAK8988wj+fZZWsMwIu7djmxKJ5Ys30IwpX8ASlbEvw==\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get lists of a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/bookmarks/{bookmarkId}/lists\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet lists of a bookmark\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The list of highlights\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"lists\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"description\":{\"type\":\"string\",\"nullable\":true},\"icon\":{\"type\":\"string\"},\"parentId\":{\"type\":\"string\",\"nullable\":true},\"type\":{\"type\":\"string\",\"enum\":[\"manual\",\"smart\"],\"default\":\"manual\"},\"query\":{\"type\":\"string\",\"nullable\":true},\"public\":{\"type\":\"boolean\"},\"hasCollaborators\":{\"type\":\"boolean\"},\"userRole\":{\"type\":\"string\",\"enum\":[\"owner\",\"editor\",\"viewer\",\"public\"]}},\"required\":[\"id\",\"name\",\"icon\",\"parentId\",\"public\",\"hasCollaborators\",\"userRole\"],\"title\":\"List\"}}},\"required\":[\"lists\"]}}}},\"404\":{\"description\":\"Bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/karakeep-api.info.mdx",
    "content": "---\nid: karakeep-api\ntitle: \"Karakeep API\"\ndescription: \"The API for the Karakeep app\"\nsidebar_label: Introduction\nsidebar_position: 0\nhide_title: true\ncustom_edit_url: null\n---\n\nimport ApiLogo from \"@theme/ApiLogo\";\nimport Heading from \"@theme/Heading\";\nimport SchemaTabs from \"@theme/SchemaTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Export from \"@theme/ApiExplorer/Export\";\n\n<span\n  className={\"theme-doc-version-badge badge badge--secondary\"}\n  children={\"Version: 1.0.0\"}\n>\n</span>\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Karakeep API\"}\n>\n</Heading>\n\n\n\nThe API for the Karakeep app\n\n<div\n  style={{\"marginBottom\":\"2rem\"}}\n>\n  <Heading\n    id={\"authentication\"}\n    as={\"h2\"}\n    className={\"openapi-tabs__heading\"}\n    children={\"Authentication\"}\n  >\n  </Heading><SchemaTabs\n    className={\"openapi-tabs__security-schemes\"}\n  >\n    <TabItem\n      label={\"HTTP: Bearer Auth\"}\n      value={\"bearerAuth\"}\n    >\n      \n      \n      \n      \n      <div>\n        <table>\n          <tbody>\n            <tr>\n              <th>\n                Security Scheme Type:\n              </th><td>\n                http\n              </td>\n            </tr><tr>\n              <th>\n                HTTP Authorization Scheme:\n              </th><td>\n                bearer\n              </td>\n            </tr><tr>\n              <th>\n                Bearer format:\n              </th><td>\n                JWT\n              </td>\n            </tr>\n          </tbody>\n        </table>\n      </div>\n    </TabItem>\n  </SchemaTabs>\n</div>\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/remove-a-bookmark-from-a-list.api.mdx",
    "content": "---\nid: remove-a-bookmark-from-a-list\ntitle: \"Remove a bookmark from a list\"\ndescription: \"Remove the bookmarks from a list\"\nsidebar_label: \"Remove a bookmark from a list\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzNVMFu2zAM/RWBpw3Q4nTosMK3DOuAbsVQdBl2CHJgbCZWY1uqJKfJDP37QNmNkzYrdhiGnWzLj+Lj4yNbyMllVhmvdA0p3FKlNyR8QWKh9bpCu3ZiaXUlUJTKeZDgceUgncG1ct7BXIKjrLHK7yCdtbAgtGQnjS8gnc3DXIJBixV5si4CXFZQhZC24HeGIAXnrapXIIG2WJmSjxSpvNzuVtXD3cV7vf35rth6r7MLzq58hHD2qxyCBEv3jbKUQ+ptQxJqrBhQdgAJiusy6AsI8q+l/9CL8yKFxQA6ojHnCGd07cgxk7fjc34cd+KrFpmuPdVevDnqh3hAJzDPKaY+H4+fxz6yE1hawnwnau2Fqh872N/LcWhMqTLkuOTOcfAJifTijjIONFYbsl51tDOd03Mhg4SKnMPVqX9HWs26Gwb8PIQQSzohB/dbaDuowCUtdVPn/3tBMd4XOocUcirJMyIaIYWEO+KStjNrSPYzl7SDeQLwjNnN4wQ1toQUWsxzS86FBI1KNmcgYYNW4aLsiul/d0ousSk9pFB4b1yaJN7uRmu0uCYyIzQG5BO5pwWJ/gahl9F/X3q86LhACOFg+L+xxl3mwxWwF4wzcx0RxrMRQSD7l0/aVsgMP/+YRlVVvdQczlV3lM5G49H4YAT3fCY3Vyf5T26uxFLbY/JcbJBgtPMVRnf0w9pvPhwcdrz2ju5vB8v90crsNPC09YkpUdVMIbax7Z0wi/vKgYR0v7j2t/HpwS6ZSyi08xzUtgt09N2WIfDxfUOW1/B88EJ0TK4cv+eQLrF09EIxr257N78Wv+PdH2K9i5YrG/4CCWvaDWuXV+0/zHqgTpgHCQVhTjbW3gEmWUbGH4Q+2xNs5v2cfry8vpxeggQ8dvET18YEJ5m1bYeY6jXVIeyJev5mjiH8AkTPj4c=\nsidebar_class_name: \"delete api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Remove a bookmark from a list\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"delete\"}\n  path={\"/lists/{listId}/bookmarks/{bookmarkId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nRemove the bookmarks from a list\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"ListId\"},\"required\":true,\"name\":\"listId\",\"in\":\"path\"},{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"204\":{\"description\":\"No content - the bookmark was added\"},\"400\":{\"description\":\"Bookmark already not in list\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}},\"404\":{\"description\":\"List or bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/replace-asset.api.mdx",
    "content": "---\nid: replace-asset\ntitle: \"Replace asset\"\ndescription: \"Replace an existing asset with a new one\"\nsidebar_label: \"Replace asset\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy9VE2P4zYM/SsCT11AO54ttujCt2yxBaYFisE0ix6CHBibiTWxJa1EzyQ19N8Lys7XTDpAgUUvgSOR1HuPjxygplgF49k4CyU8kG+xIoVW0c5ENnajMEZi9Wy4UagsPStnCTQwbiKUC/js3LbDsI2w1BCp6oPhPZSLAVaEgcKs5wbKxTItNXgM2BFTiDkgVg11COUAvPcEJUQOxm5AA+2w860cGTJ1u9tvuufHTz+73d8/NTtmV30SBIZzyAHBXQ1JQ6BvvQlUQ8mhJw0WOwlanYI0GOHqkRtI+rvBmIlOb2LAKeICwHIMp8ifXb0XFJctmTeUVR/bwE6FqUfSEdBQOctkWRLR+9ZUKInFY5TsK+Tc6pEqBg0+OE+BDcWcO2F7pUK64LM4Bi5TGq+idzaORX68/fiawB9OTRjV+4OZMB5o1Cr2VUUxrvu23Yt4H68VOfRYWcdq7Xpbfz/qlavpCm8NHcWIm2t3LzTJFU7xWZqcz42rpdN9flX6XUJxsGIshpMrU5GlicUwCZxAxik8HYalDy2UMGBdB4oxFehN8fQBNDxhMLhqpzaO16OAa+xbhhIaZh/LouCwv9liwC2Rv0HvQV/x2lRBubXihtTvU7wasUBK6WzO/xSJx5fPp/2ol7wsPHKYTGEOAj19/OpCh4Lwt7/mWVRp3cNpGr4c5u/Mn2f9MXbt5E40Ggl8uLm9uT2bySP62f3dVbaz+zu1duGSqkiTNHgXucNspWmAj+tRwLysN5z8+J/26KgU044L36Kx8nRu9jA5ZnFcXhE0lBebbDSNHB/nUkPjIkvaMKww0tfQpiTH33oKspmXJ89kZ9UmyncN5RrbSG/Q+uFhMv079W/Ip0O0+2zNtpd/oGFL+8stLJv3f3z5IE9aJg0NYU0hkx9vfxkfej+XGqfsV1tFQI8Zs6oiz2/GLs82wP3Xubh+2vBd3jgQ8Bl0/s1AXeadhymfDdCi3fR5A8FYUmYEL0fsxUhlUlelGIYxYu62ZFM6KsPyX3RJ6R8BPcUh\nsidebar_class_name: \"put api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Replace asset\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"put\"}\n  path={\"/bookmarks/{bookmarkId}/assets/{assetId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nReplace an existing asset with a new one\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"},{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"AssetId\"},\"required\":true,\"name\":\"assetId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The new asset to replace with\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"assetId\":{\"type\":\"string\"}},\"required\":[\"assetId\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"204\":{\"description\":\"No content - asset was replaced successfully\"},\"404\":{\"description\":\"Bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/search-bookmarks.api.mdx",
    "content": "---\nid: search-bookmarks\ntitle: \"Search bookmarks\"\ndescription: \"Search bookmarks\"\nsidebar_label: \"Search bookmarks\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzNWEtv4zYQ/ivCXNoCQpzt0Tdv0EdadBM0WfQQ6DCWRhbXFKmQlBOvof9eDClLsiMn8m6A9hRHnMc3T85wBxnZ1IjKCa1gDneEJi2ipdbrEs3aQgwOVxbmD/Cx+5bEYCmtjXBbmD/sYEloyCxqV8D8IWmSGCo0WJIjYz2BTQsqEeY7cNuKYA7WGaFW0MRg6LEWhjKYO1NTDApLJniEGAQDeqzJbKGJTwuJgVRdMkK0KcTeIGDBkjaoUmK4GeVYSwfzwecD5TlK22u32rgbk5GZgELV5ZLMa9KkKIU7xx4nnOQPV7Wx+lXRaaB4W/ZSa0moYOCK4O/D8F/nkSUXOR350y4PfrBRqpUj5aInIWW0pEioVNYZZZFQkSsoMmQrrSxdRJ+0ozhyheiZUlTMI9GsKMq1iawuqc+yi1Eb/yfQOme3Yq8C45HTEzYgqLHs958vL/nPoQ03yy+UMk5XeGA2VJshW0vnVbWomBWrSooUmXX2xTL/SFy1lwgxVEZXZJwI2vv67UnRGNwyakelfVuEyEbrNTWEjrKFGz0tdSZyMX4cg6qlxCWnNoew6RJ9AiX7SWwoG0npJoYcN5rb0alzh6uVUKs7h662b+vrO4qt05Qsd8EchawNsZtIZcyVNDHYuizRiK8+Su8vXmk3zT0Bx3Yara5NOkHsoK9WAmJ4oiUnqOTfpV4KyWjp2ZGynN0xWKFWkvJwYLxZoqy0cd6W2pK5Hs+pcMO8c6KGoh05QOcwLSj7OOquzmY2s6hLVJA0B93pgTW28g+kebpBAWtFN7m//143IJyehiKFWgcXGjnuv8lldNCOJtCLElf0eUztKeKFteTG4jyWiakhUrbQ7hyuvJbyljWFlnAOa2UoNfgkKfsG5o3ISJ8FFDcinejowpXyqk+dN+nbNDsHzt7yaQqwdoU207xaL6WwBU2jztDRbcsxDThz/NVeLBMYjorVU4faSXg0+r5adPQc2pn/MVaMob1Oq5lxqHsd34sVOTk8WP/r/g1yX76sJMt7phMdm5v8p9EGe/LCmdpFrPg6FNuO16dL4Jvd3Dult/UdvF6rtdJPI7eGZ0iazrXvf+VNCjPfJr8Pus2wDe89cTf8skSlyFy32XHUfaHti0zXzpy+J8FIs4UwBHyupMaM+A7tfXVWSo1dx73tSTNK0A+uB2PqYLQ8mCOPh8bxWa8ba9ohps/KLshJv8/tN2hggIqeXbvgnW3xcD0fyBmousWVUGxvv7U3jXdMSa7QGcxhFaKEvLXDrBM5C0sJG0xms9/g/egBO8wyQ9Y2M6zEbPOBo49GMFCfl+1xWHz2G3fhXGXns5kz24s1GlwTVRdYVXC84d0XFLUSIp37BenPlj4KWNiGwePDHW9D7cIzeILovMmafX4zGW8EnohT1f/4VZsSGeEf/9z7kAiVa2ZnqwOkDxeXF5eDjbzDs7i9HsW/uL32S+QBeDaW70ltHc+T891+oxx5bTma0LomN/oyE8zk+2JWSRR+12mHxBDXw1RpI5vEUGjr+HS3W6Ll1tw0/DnsshzvTFiO6ollfIjrx7/bxPwpOgVpTdv2SWeDsuZz/0jxQkegGz6+TKHfP69Moe3eS14nPmnsf/gI8YpnXzxM9NYlfYX6t7kYCkL2LQc5cC/SlKoh14uHB5bStY3ffrmHMB4Od+3DyhouPai2A9m7XaC412tSTQN7Exz/Dw337n8BoeUaoA==\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Search bookmarks\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/bookmarks/search\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nSearch bookmarks\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\"},\"required\":true,\"name\":\"q\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"enum\":[\"asc\",\"desc\",\"relevance\"],\"default\":\"relevance\"},\"required\":false,\"name\":\"sortOrder\",\"in\":\"query\"},{\"schema\":{\"type\":\"number\"},\"required\":false,\"name\":\"limit\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"title\":\"Cursor\"},\"required\":false,\"name\":\"cursor\",\"in\":\"query\"},{\"schema\":{\"type\":\"boolean\",\"default\":true,\"description\":\"If set to true, bookmark's content will be included in the response. Note, this content can be large for some bookmarks.\"},\"required\":false,\"description\":\"If set to true, bookmark's content will be included in the response. Note, this content can be large for some bookmarks.\",\"name\":\"includeContent\",\"in\":\"query\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with the search results.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarks\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"userId\":{\"type\":\"string\"},\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"attachedBy\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\"]}},\"required\":[\"id\",\"name\",\"attachedBy\"]}},\"content\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true},\"description\":{\"type\":\"string\",\"nullable\":true},\"imageUrl\":{\"type\":\"string\",\"nullable\":true},\"imageAssetId\":{\"type\":\"string\",\"nullable\":true},\"screenshotAssetId\":{\"type\":\"string\",\"nullable\":true},\"fullPageArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"precrawledArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"videoAssetId\":{\"type\":\"string\",\"nullable\":true},\"favicon\":{\"type\":\"string\",\"nullable\":true},\"htmlContent\":{\"type\":\"string\",\"nullable\":true},\"contentAssetId\":{\"type\":\"string\",\"nullable\":true},\"crawledAt\":{\"type\":\"string\",\"nullable\":true},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\",\"nullable\":true},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true},\"size\":{\"type\":\"number\",\"nullable\":true},\"content\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"assetType\",\"assetId\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"unknown\"]}},\"required\":[\"type\"]}]},\"assets\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"userId\",\"tags\",\"content\",\"assets\"],\"title\":\"Bookmark\"}},\"nextCursor\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"bookmarks\",\"nextCursor\"],\"title\":\"PaginatedBookmarks\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/sidebar.ts",
    "content": "import type { SidebarsConfig } from \"@docusaurus/plugin-content-docs\";\n\nconst sidebar: SidebarsConfig = {\n  apisidebar: [\n    {\n      type: \"doc\",\n      id: \"api/karakeep-api\",\n    },\n    {\n      type: \"category\",\n      label: \"Bookmarks\",\n      items: [\n        {\n          type: \"doc\",\n          id: \"api/get-all-bookmarks\",\n          label: \"Get all bookmarks\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/create-a-new-bookmark\",\n          label: \"Create a new bookmark\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/search-bookmarks\",\n          label: \"Search bookmarks\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-a-single-bookmark\",\n          label: \"Get a single bookmark\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/delete-a-bookmark\",\n          label: \"Delete a bookmark\",\n          className: \"api-method delete\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/update-a-bookmark\",\n          label: \"Update a bookmark\",\n          className: \"api-method patch\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/summarize-a-bookmark\",\n          label: \"Summarize a bookmark\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/attach-tags-to-a-bookmark\",\n          label: \"Attach tags to a bookmark\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/detach-tags-from-a-bookmark\",\n          label: \"Detach tags from a bookmark\",\n          className: \"api-method delete\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-lists-of-a-bookmark\",\n          label: \"Get lists of a bookmark\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-highlights-of-a-bookmark\",\n          label: \"Get highlights of a bookmark\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/attach-asset\",\n          label: \"Attach asset\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/replace-asset\",\n          label: \"Replace asset\",\n          className: \"api-method put\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/detach-asset\",\n          label: \"Detach asset\",\n          className: \"api-method delete\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"Lists\",\n      items: [\n        {\n          type: \"doc\",\n          id: \"api/get-all-lists\",\n          label: \"Get all lists\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/create-a-new-list\",\n          label: \"Create a new list\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-a-single-list\",\n          label: \"Get a single list\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/delete-a-list\",\n          label: \"Delete a list\",\n          className: \"api-method delete\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/update-a-list\",\n          label: \"Update a list\",\n          className: \"api-method patch\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-bookmarks-in-the-list\",\n          label: \"Get bookmarks in the list\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/add-a-bookmark-to-a-list\",\n          label: \"Add a bookmark to a list\",\n          className: \"api-method put\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/remove-a-bookmark-from-a-list\",\n          label: \"Remove a bookmark from a list\",\n          className: \"api-method delete\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"Tags\",\n      items: [\n        {\n          type: \"doc\",\n          id: \"api/get-all-tags\",\n          label: \"Get all tags\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/create-a-new-tag\",\n          label: \"Create a new tag\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-a-single-tag\",\n          label: \"Get a single tag\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/delete-a-tag\",\n          label: \"Delete a tag\",\n          className: \"api-method delete\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/update-a-tag\",\n          label: \"Update a tag\",\n          className: \"api-method patch\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-bookmarks-with-the-tag\",\n          label: \"Get bookmarks with the tag\",\n          className: \"api-method get\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"Highlights\",\n      items: [\n        {\n          type: \"doc\",\n          id: \"api/get-all-highlights\",\n          label: \"Get all highlights\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/create-a-new-highlight\",\n          label: \"Create a new highlight\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-a-single-highlight\",\n          label: \"Get a single highlight\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/delete-a-highlight\",\n          label: \"Delete a highlight\",\n          className: \"api-method delete\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/update-a-highlight\",\n          label: \"Update a highlight\",\n          className: \"api-method patch\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"Users\",\n      items: [\n        {\n          type: \"doc\",\n          id: \"api/get-current-user-info\",\n          label: \"Get current user info\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-current-user-stats\",\n          label: \"Get current user stats\",\n          className: \"api-method get\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"Assets\",\n      items: [\n        {\n          type: \"doc\",\n          id: \"api/upload-a-new-asset\",\n          label: \"Upload a new asset\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-a-single-asset\",\n          label: \"Get a single asset\",\n          className: \"api-method get\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"Admin\",\n      items: [\n        {\n          type: \"doc\",\n          id: \"api/update-user\",\n          label: \"Update user\",\n          className: \"api-method put\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"Backups\",\n      items: [\n        {\n          type: \"doc\",\n          id: \"api/get-all-backups\",\n          label: \"Get all backups\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/trigger-a-new-backup\",\n          label: \"Trigger a new backup\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-a-single-backup\",\n          label: \"Get a single backup\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/delete-a-backup\",\n          label: \"Delete a backup\",\n          className: \"api-method delete\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/download-a-backup\",\n          label: \"Download a backup\",\n          className: \"api-method get\",\n        },\n      ],\n    },\n  ],\n};\n\nexport default sidebar.apisidebar;\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/summarize-a-bookmark.api.mdx",
    "content": "---\nid: summarize-a-bookmark\ntitle: \"Summarize a bookmark\"\ndescription: \"Attaches a summary to the bookmark and returns the updated record.\"\nsidebar_label: \"Summarize a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVU2P2zYQ/SvCnFKAsJ0iRQLd3EOBbQ9ddLfIwfBhLI0triWSIYdeOwL/ezCUP3eVdg97skwOZ948vnnsoaZQee1YWwMlzJmxaigUWITYdegPBduCGypW1m479NsCTV144uhNyBvR1cgka5X19QQUMG4ClAv4/XgkwFJBoCp6zQcoFz2sCD35eeQGysUyLRU49NgRkw85IFQNdQhlD3xwBCUE9tpsQAHtsXOtLGnSdbs/bLrnpy+f7f77b82e2VZfBIHmHHJCcFdDUuDpW9SeaijZR1JgsJOg1SVIgRYWHHIDgspTcNYECoLk19lMfm4Je7xi4EzRs+bmxB8oqKxhMiyH0blWVyiHp09BMoz0aldPVDEocN468qyH+rp+zUdSUHmS6nMe3e1srdd6fFuBiW2LK6FKGEln4t4Qib5q9I6uMa2sbQmNlF3jzsp1/2yfcbPRZvPAyDH8fz0FZGInmgqxqigEkBK6jZ6EJjK1nFomBQPr+num+P3TG8tvo+d0+2+KtdFXb0h7gYlOg4JnWom6Wvnu7Eq3lOeDyQQRp4Kgzaal9bDhc1u6c9Zz7iUG8ndjmroZlYXo7lplN5q60sHNpb+84fGLOWNYpiRVP80+vZ6w0wwXxnKxttHU7zdSla1HeJexoRBwM7b3gpyc4RKfG8nnubG1WIkNuaw4SgnTk0WEaX+xnTQ9kSOpAvndyQajb6GEHuvaUwhpik5Pdx9BwQ69FmHkLo7bA3VrjC1DCQ2zC+V0yv4w2aLHLZGboHOgRhzsmKGw62zpfx3jiwELpJSuHPxByB0qX/v4mSmpLH3kMBn7HATq+PGH9R0Kwj+/PmY6tVlbOS5dD5A+TmaT2ZWNn/HM7+9G8c/v74q19bfgpdmk8g10mGVxNPyHE9sFnj37Zdr+IrF3ehQHcpj2PHUt6myD+X77ozoW55dIJqO8eZYuAlkqaERT5QL6foWB/vVtSrL8LZIYzmJ5kUcWUa2DfNdQrrEN9B+NfvjnqOxfip8hPi6iOWQVtlH+gYItHW6f0iQW0xDW5DOKIWBeVeT46uir6RWlnafn/u+HR/GYW4G9EFROP4qr74eIR7slk9IZJst/QZjSD3WCHTs=\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Summarize a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/bookmarks/{bookmarkId}/summarize\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nAttaches a summary to the bookmark and returns the updated record.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The updated bookmark with summary\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"userId\":{\"type\":\"string\"}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"userId\"]}}}},\"404\":{\"description\":\"Bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/trigger-a-new-backup.api.mdx",
    "content": "---\nid: trigger-a-new-backup\ntitle: \"Trigger a new backup\"\ndescription: \"Trigger a new backup\"\nsidebar_label: \"Trigger a new backup\"\nhide_title: true\nhide_table_of_contents: true\napi: eJyNVE1v2zAM/SsGz0Lc7qhbNmBANwwr0Aw7BDnQNhOrtiWVkrplhv77QNvIR5cC88W2SJGPfI8coaFQs/HROAsaNmwOB+ICC0u/igrrLnlQEPEQQG/h43QQYKcgUJ3YxCPo7QgVIROvU2xBb3d5p4ApeGcDBdAjfLi7l9d1qjlWUTNhpKYIqa4phH3q+yMoqJ2NZKNcQ+97U6NcK5+D3B0h1C0NKF/x6Ak0uOqZ6ggKPDtPHM2c2TQXPiGysQfIClIgfrhtwhAo3rIpsKnvseoJdOREWcGCfR1vRgrmD10YbBoqYjFUznUDcvfJJRtveoSIMYVbGMimQZjwZJv5ZGkcKNij6RMT7LICYnb8jULAA/1HLVkYe0mGqZHopoFTk84tuSx4Ke9tMSfouyyPgoFi6xrQ4F2Y6EHRCJTVoiQREr8Sh0lHiXvQMGLTMIWQS/SmfL0HBa/IRuBOPVnMs6T2mPoIGtoYfdBlGfm46pCxI/Ir9KLeNxJvqVgiFG5fxJaKr4t/MWOBnPOFwp9Ea3PmS52feiqZpY7JDfTiJK2ZPj47HlAQfvm5AWmJsXsn16XqGdL96m51J2NmojACJzzrx4eb+NePD8Xe8TV4KTarqdEDTlNicQL0zlBfhR3PE/fuEpjLjfQ7lr5HY6dJEsbGhdYtVOcF0QrhegvjWGGgH9znLMcviViWxu5M6rQzFLSEDfGkg46OoGFd1+TjxH6fJPU/m0BoOins8fvTRtR6zc4bNqbwiwnt8SL4OM4eG9eRzRnUgiLKP2QR9F+uE8Zd\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Trigger a new backup\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/backups\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nTrigger a new backup\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"201\":{\"description\":\"Backup created successfully\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"assetId\":{\"type\":\"string\",\"nullable\":true},\"createdAt\":{\"type\":\"string\"},\"size\":{\"type\":\"number\"},\"bookmarkCount\":{\"type\":\"number\"},\"status\":{\"type\":\"string\",\"enum\":[\"pending\",\"success\",\"failure\"]},\"errorMessage\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"userId\",\"assetId\",\"createdAt\",\"size\",\"bookmarkCount\",\"status\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/update-a-bookmark.api.mdx",
    "content": "---\nid: update-a-bookmark\ntitle: \"Update a bookmark\"\ndescription: \"Update bookmark by its id\"\nsidebar_label: \"Update a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVtuO2zYQ/RVhnlpAtb1FigZ6c4IW3V7QRbJBHww/jMSRxbVEKrx47Rj692IoWZddZeOiqyeRHJLDM+fMzBkE2czI2kmtIIFPtUBHUar1vkKzj9JTJJ2NpIAYHO4sJBt41y1a2MZgKfNGuhMkmzOkhIbM2rsCks222cZQo8GKHBkbDGxWUIWQnMGdaoIErDNS7SAGOmJVlzwlSYryeNpVjw9vf9bHLz8VR+d09pY9kC6YXDy4FdDEYOizl4YEJM54ikFhxUbpYBSD5MfV6Apgr3gHWfdOixP7MoXgvqBIoMPI6cgHOBbR36o8Ra6gKJdUChudtI8eUbnBJlJEgocpRbXRBylILCCGTCtHyvE1WNelzJCvWT5YvmsGEJ0+UOYghtromoyTZMNekxXywG/sLVOtS0LFCOR40ByFr61bX1VoTnO4K1+WmDKqDF4Tg9KOnhs2PfjfOiKGCo9/ktoxCW5Wq1UTQ2YIHYm1u8oDb8o5u1ybCh0k4I1kfyZRu+JY9K7Q5irT2qeltAVdZ83hv+t2iKt3/KWFzOWVGxwdr8MOrSX3fuDcNzaEj/Vga61sS7UfV6t5UbREF31yeD12yxkUXqZNE0PVAXglq66kL2P4v7TmcLeTavfRofP2GrmQ8hWnVeuzjKxlpqMsvSGGiZTgXdtexPJLgPj1j5/X/Qw8/yWZWO1NdlXSuLiJtYQYHilldpX8X+lUlhRKhCNlmY8xWKl2JeXtggnPklWtjQtv8ZbM7RynJtViA6GsDSybcGrEg0nQn0Z4PjC9D9tWYW9Wb56L6lLGIqVdlGuvxOtJKtNiPo1XZC3u5taegBNOGOy3Xa6oyBVatNU0K/herqoJLC9pwS7PQ+ltGB8yh0sDEHI7nFEIQ9Y2S6zl8nADMRzQSOZDW+3a5RaxHH3Jab9wrrbJcunMabFHg3uieoF1DfFMrupOiHQeyvYfnX3U+gJN04x6l4+MaXvzuIPpAeKb+R3BjNUejCDufn69FKbf/7kPKHKsPgwtxi+X1mZcxlvWj5NJO9OraySVoMxhfOmC+olRnhwmW6T74RSifvpSFIeZUe0b7Z6WuOnCUMlGPoaCNbpnUpdGdJQq14wMM6T17WaxWqxGz+xjt767nY31+u42yrWZBpqJwZVcW1dhUE7XFnYdLo7L2JNWopfgy+1wSw5+6bIuUYbs3/UurSY2fQ/KCSEZNaTbGAptHZuczyla+mTKpuHpz544+pvtIIkgHCEt/wtIciwtveDzdx86EX8ffc3LbhLVKSiv9DyCGPZ0mjbODWfTglAwHTbnzqCL4w/3fMxwwLN01cSXHesso9q9aLsdpZa79f3731hfXYNehWQGBh852+Nj660Ojw+yDXNnKFHtfEhu0B7adH3fuFRPxRueNYvH+dxa3Os9qabp4XE8ZmSa5l9CRZNh\nsidebar_class_name: \"patch api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Update a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"patch\"}\n  path={\"/bookmarks/{bookmarkId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nUpdate bookmark by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The data to update. Only the fields you want to update need to be provided.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"summary\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true,\"maxLength\":1000},\"createdAt\":{\"type\":\"string\",\"nullable\":true},\"url\":{\"type\":\"string\",\"format\":\"uri\"},\"description\":{\"type\":\"string\",\"nullable\":true},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true},\"text\":{\"type\":\"string\",\"nullable\":true},\"assetContent\":{\"type\":\"string\",\"nullable\":true}}}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The updated bookmark\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"userId\":{\"type\":\"string\"}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"userId\"]}}}},\"404\":{\"description\":\"Bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/update-a-highlight.api.mdx",
    "content": "---\nid: update-a-highlight\ntitle: \"Update a highlight\"\ndescription: \"Update highlight by its id\"\nsidebar_label: \"Update a highlight\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVcGO2zYQ/RViTg3A2E6QooFubtAimx6ySBzkYPhAi2OLa4lkyNGuVUH/Xgwl2/Kusg2Q+CSLT5yZN+/NtKAx5sF4Ms5CBl+8VoSiMPuiNPuCxLYRhqIwGiSQ2kfI1vD+dBphIyFiXgdDDWTrFraoAoZlTQVk6023keBVUBUShpgAMS+wUpC1QI1HyCBSMHYPEvCoKl/yK4NGl8dmXz3cvf3DHf/9vTgSufwtp2AoQc4p3GjoJAT8VpuAGjIKNUqwqmJUMUJJMFygV1QA58WfYKQ/nW44m2saVgUKrUgJcqJOlMzER1s2ggoUO4OljqJxtXhQli4YYRE1/92i8MHdG416BhJyZwktcRjlfWlyxWHmd5FjTVDitneYE0jwwXkMZDDyae5KFyaZs3XFfWmwLN0DcG1c7z4gWpCwLWuETSfBOsKp721dlmrLvDJ7XfrxJdE7G/vYrxeLaZb6yvVFMb+u3q1zh0qFw41+mnQnIZIK9HG3i0ijc1tXWwx8jlY/c/pzZErQuFN1SZCdgJ0EwiP9AL8/3AgJZrr0OmL4Dit5QO7HciKR7soo6zG/12yOuRuqGnJOGZ3jj6NtJrwJvZDeLN481c4ZJKwjsXO11b/SKXqC305ChTGq/dTZI27SDRf8ZvBEhVQ43Y+RvOC4PE4ymJ/lH+ftaOp0zCyG+9P0q0MJGbRK64AxdnPlzfz+FUi4V8Fw51P6w3FP2klmBZGP2XxOoZkdVFAHRD9T3oOccOVwg3C7NLH+GfCiz4X7Mhrcn5nVwXKj8X2miCNzHQkG2QBiL6SHv12oFGf44esq8cjd+nSZrn+d5vp5gl281Rth1CBjd46RzFhfzqvZYrYYTf5zLcvbm8nal7c3YufCdeFMVCfBu0iVSloaVsSw8NTVALu6tL2o8n/WY08X+2XuS2Vssip3vB10sr5spAgSsvF+2kgoXCQGte1WRfwSyq7j199qDLxdNxeZJDFpE/lZQ7ZTZcRn0v7t0yDtF+J7eQ4vlW2SGnnKZQASDtg8WqQdr5IClcaQ0ugR7/pgL1d8z+WGJy7u5OmLZZ6jp2exm5Hjbperd+9ZdMPCrpLHIag0o9VDn65L1Sctp3ctlMru6+R56C9liaprhT9SdCprkpC27RErd0DbdWd+iP8zM133H3AUMso=\nsidebar_class_name: \"patch api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Update a highlight\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"patch\"}\n  path={\"/highlights/{highlightId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nUpdate highlight by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"HighlightId\"},\"required\":true,\"name\":\"highlightId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The data to update. Only the fields you want to update need to be provided.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"]},\"note\":{\"type\":\"string\",\"nullable\":true}}}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The updated highlight\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarkId\":{\"type\":\"string\"},\"startOffset\":{\"type\":\"number\"},\"endOffset\":{\"type\":\"number\"},\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"],\"default\":\"yellow\"},\"text\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\",\"nullable\":true},\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"}},\"required\":[\"bookmarkId\",\"startOffset\",\"endOffset\",\"text\",\"note\",\"id\",\"userId\",\"createdAt\"],\"title\":\"Highlight\"}}}},\"404\":{\"description\":\"Highlight not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/update-a-list.api.mdx",
    "content": "---\nid: update-a-list\ntitle: \"Update a list\"\ndescription: \"Update list by its id\"\nsidebar_label: \"Update a list\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVk1v4zYQ/SvEnFqAtZ1iF13o5gYtmnaBBqkXPRg6jMWxxUQitSQVWxX03xdDyZYdK1kUiA+GSI7m4817Q7WgyGdOV0FbAwl8qRQGEoX2QWwaoYMXWoGEgDsPyRo+ax88pBI8ZbXToYFk3cKG0JFb1iGHZJ12qYQKHZYUyPlo4LOcSoSkhdBUBAn44LTZgQQ6YFkVvKVJq+LQ7Mr946df7OG/j/khBJt94ug6RBOOfqegk+Doa60dKUiCq0mCwZINit5AguZiKgw5cDZsTT78alXDOVyWvMpJKAwoghV1LH8m/jZFI0JOYqupUF40thZ7NGG0EYZI8XJDonL2WStSM5CQWRPIBA6DVVXoDDnM/NFzrAkg7OaRsgASKmcrckGT59O+nmu4Sm0+k9kx0DcSSjycVotFJy8ru37b1EWBG4ayR+3M2+LC28foTWdTbrrYXTIM9HdjdBK+1uSa79XCTutNobMzw421BaGBLv64i76yxvcA/bxYTLeyb4+KFH6/fuiJWjv5Spv+byPeDereYEJkpi5ZvSWaGguQ4Et0gWWsaIt1ESA5nr3VsKt4r7dMQo7+1rK9dRis89NWtSf3YIs3s7Z7Q47XSgfLD8+a9nFniJ92FxNhDXFkxd4MyJ7heHprIsWzfNLLqQM9BT8sPlyzjs+FsUFsbW3U+3Eus2qaXCV5j7upsxc4RA+jfToIqaSQW9UPyCyP4LAIYc6a8fO2n6Id84Tc83GG166ABFpUypH33RwrPX++4Xag00yLmPRw3KN05FYeQuWT+Ty4ZvaEDp+IqhlWFcgJAQ8ehN3GCfzXYC/6XLgRZ9fPP4xlH/n8EjoBw5G5jmjG1ItGIIeH360rkTP8899VRI979DDeFr8db6fjRB7ZeZn4absX8rge9TvuDQI7Mxp0dBwGZms5JEPfu7+ZLWaLs4vwBMry/m4SxOX9ndhad4kgI86qtT6UaM6KGm59PA7NF9PrxOfXPw96tAMdwrwqUPfaZsK0A7nW8W5miSXDJZ1KyK0PfNS2G/T0xRVdx9sDPut05FZkoNKenxUkWyw8vZHnDw+DCn4Ur2U3bKJpIoWLmlcg4Yma8UOiS3mSESpyMYP+8LaP89OKXYwvX2m9k8c3lllGVXjTNj3T5f1ydfsHk3T4YCnjJACHe5DxP2ZqY+GR+3GvhQLNro6TAXqnTGm8VMQLBcSyJrFo295iZZ/IdN0JmsBrRqbrvgEFSoAx\nsidebar_class_name: \"patch api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Update a list\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"patch\"}\n  path={\"/lists/{listId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nUpdate list by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"ListId\"},\"required\":true,\"name\":\"listId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The data to update. Only the fields you want to update need to be provided.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\",\"minLength\":1,\"maxLength\":100},\"description\":{\"type\":\"string\",\"nullable\":true,\"minLength\":0,\"maxLength\":500},\"icon\":{\"type\":\"string\"},\"parentId\":{\"type\":\"string\",\"nullable\":true},\"query\":{\"type\":\"string\",\"minLength\":1},\"public\":{\"type\":\"boolean\"}}}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The updated list\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"description\":{\"type\":\"string\",\"nullable\":true},\"icon\":{\"type\":\"string\"},\"parentId\":{\"type\":\"string\",\"nullable\":true},\"type\":{\"type\":\"string\",\"enum\":[\"manual\",\"smart\"],\"default\":\"manual\"},\"query\":{\"type\":\"string\",\"nullable\":true},\"public\":{\"type\":\"boolean\"},\"hasCollaborators\":{\"type\":\"boolean\"},\"userRole\":{\"type\":\"string\",\"enum\":[\"owner\",\"editor\",\"viewer\",\"public\"]}},\"required\":[\"id\",\"name\",\"icon\",\"parentId\",\"public\",\"hasCollaborators\",\"userRole\"],\"title\":\"List\"}}}},\"404\":{\"description\":\"List not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/update-a-tag.api.mdx",
    "content": "---\nid: update-a-tag\ntitle: \"Update a tag\"\ndescription: \"Update tag by its id\"\nsidebar_label: \"Update a tag\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVMGO2zYQ/RViTgnA2k6RooFubpCg2x66SB3kYOgwFscWdyWRIUe7VgX9ezCkdu3deoMc4oNtcZ44b2bemxEMxSpYz9Z1UMBnb5BJMR7UblCWo7IGNDAeIhRb2MhvqSFS1QfLAxTbEXaEgcK65xqKbTmVGjwGbIkpxASIVU0tQjECD56ggMjBdgfQQEdsfSNHlqxpjsOhvb9597s7/vdbfWR21TtJbjlBNni4MjBpCPS1t4EMFBx60tBhK3FOcQ1WCvHINQgXAVPkP5wZhMHTcjc1KYOMip3qU+kL9U/XDIprUntLjYlqcL26x45PGNURGXnckfLB3VlDZgEaKtcxdSxp0PvGVihpljdRcl1og9vdUMWgwQfnKbClKNFczvNmTekj9UTvupihv65Wl4vKRI0M8ucRs+YCLf0i3/NBbSHpKEHLXMjb1dsL3PGgOsdq7/rO/DzmlTOXKGpoKUY8/AD9dMMJX87TaIlrZ7Leqlryiu4KWIpjlmOS5ARimHD3YIc+NFDAiMYEinFaorfLuzeg4Q6DxV2TKc/h3KI99g1DATWzj8VyyWFY3GLAWyK/QO9BX9DAfINy+yTnv2e8ylxEUGdO/lc6mTOf+/mxLZJZ6kgwKGYQ6PnPRxdaFIZ/fdmk3smEPp2s9+HB6A/yPpuB7fZOAtKgzP7NYrVYnTn/kfr6+upiqevrK7V34Wmd0pdJg3eRW+zOUs9LDmd3PLluPEnuxWWYO8J05KVv0HaSJQ11nMe/zQtTQ5F3UqmhdpElMI47jPQ5NNMkx197CrJFy9P0k0aMjfLfQLHHJtJ3SL76NKv0tXqJ23yI3ZBE1vTyBBpuaXhcm1M5aagJDYVEIMfe5zS/bOSG07v/s+KkH95YVxV5/i62PLPN9Xrz/k9R0bye22RUCHgPOn0noi7VncSZzkZosDv0ybiQLxXN4VPJPpNoKutiK8YxIzbulrppOnVGnqUz0/QN/t1xKA==\nsidebar_class_name: \"patch api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Update a tag\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"patch\"}\n  path={\"/tags/{tagId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nUpdate tag by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"TagId\"},\"required\":true,\"name\":\"tagId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The data to update. Only the fields you want to update need to be provided.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"}}}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The updated tag\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"}},\"required\":[\"id\",\"name\"]}}}},\"404\":{\"description\":\"Tag not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/update-user.api.mdx",
    "content": "---\nid: update-user\ntitle: \"Update user\"\ndescription: \"Update a user's role, bookmark quota, or storage quota. Admin access required.\"\nsidebar_label: \"Update user\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzFVk1v4zYQ/SvEXNoCjD92txfdvF+A20u6TdCDYRRjcWxxLZEMSSXrCvzvi6Fkx7GdPRRZRAdbIofDN28eZ9iBolB67aK2Bgq4dQojCRRtIP9LEN7WJMXK2m2DfivuWhtRCutFiNbjhvqRkZipRhuBZUkhCE93rfakRiAh4iZAsYBsAEsJgcrW67iDYtHBitCTn7WxgmKxTEsJDj02FMmHbBDKihqEooO4cwQFhOi12YA8wX1TkZh/FHYtYkUZvIhWtDkakEDfsHE1r+epf6dv3kKSsMcJRfQtSTDY7E3mCiRo9uwwVsDI2JpCfG/VjvGU1kQykV/RuVqXyFDGXwPjuQDcrr5SGUGC89aRj5oCzzLBl8Ij0zbMG4MBCdjTlyTsc/EXE3+0UptIm2xr2rrGFbvtw2q00Q17myQJQ97+5+qVtw+B/AePD7U2m0+GLdWRn5W1NaE585PSacZuOUd9goTCiMdZ2tMyhH0W9HQymZyG8vtk/yR+OF/BWRN6mt9MJvz3LAQlQpvFu27regfypdI7eL3AUHoiwMXBctmjf3cJ8HtUYpChuBJzc4+1VkIb18bMIR/MEo2xcc+sfTBikNALRUTeW3+u2NNwerNDMNML7BtsY2W9/o+UuBJcBMjEAdahhrw28LfnwD9bv9JKkWHUl+rea2N+94zUWRdr25pXBZgkNBQrq7i2ttkjV9gCxvmwj1mtYdz1RTgBNwx/v28Hra+hgA6V8hRCGqPT4/spSLhHr7naZFDDdM/CGts6QgFVjC4U43H0u9EWPW6J3Aidu9hKBg/7fvLnYC96LJBSOupkfzNX/c7H/ezACe/McWQzrgDZiKtafvlsfYOM8I9/bjJxnIMvj83m0wtVRe5na8tumM4+1uloMppwl9Yxez4EOrueXyRmdj0Xa+ufssIsJgnOhthgls/QSYfbxFCAnng7aqA/4dLREx/pWxy7GrVheFk73aC2xYHErDeQUAxtfymhsiGySdetMNCtr1Pi4buWPF9blo9yy6JUOgxtcI11oB8E+uuXAeRv4jmUwyCaXVZ13fIXSNjS7vFqkvgiUBEq8hlBP/mh3+fqhl08Lj473knuV8zKklz8oe3y6Lhe395k1fUXoMYqXuLxAWT+zThtDru/2vBYBzWaTYsbtu1dssbx6RE5ORI5qItMdF1vcWO3ZFI6EBP5m3lJ6Tty27rt\nsidebar_class_name: \"put api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Update user\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"put\"}\n  path={\"/admin/users/{userId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nUpdate a user's role, bookmark quota, or storage quota. Admin access required.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The ID of the user to update\",\"example\":\"user_123\"},\"required\":true,\"name\":\"userId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"role\":{\"type\":\"string\",\"enum\":[\"user\",\"admin\"]},\"bookmarkQuota\":{\"type\":\"integer\",\"nullable\":true,\"minimum\":0},\"storageQuota\":{\"type\":\"integer\",\"nullable\":true,\"minimum\":0},\"browserCrawlingEnabled\":{\"type\":\"boolean\",\"nullable\":true}},\"description\":\"User update data\",\"example\":{\"role\":\"admin\",\"bookmarkQuota\":1000,\"storageQuota\":5000000000}}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"User updated successfully\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"success\":{\"type\":\"boolean\"}},\"required\":[\"success\"]}}}},\"400\":{\"description\":\"Bad request - Invalid input data or cannot update own user\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"error\":{\"type\":\"string\"}},\"required\":[\"error\"]}}}},\"401\":{\"description\":\"Unauthorized - Authentication required\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"error\":{\"type\":\"string\"}},\"required\":[\"error\"]}}}},\"403\":{\"description\":\"Forbidden - Admin access required\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"error\":{\"type\":\"string\"}},\"required\":[\"error\"]}}}},\"404\":{\"description\":\"User not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"error\":{\"type\":\"string\"}},\"required\":[\"error\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.29.0/api/upload-a-new-asset.api.mdx",
    "content": "---\nid: upload-a-new-asset\ntitle: \"Upload a new asset\"\ndescription: \"Upload a new asset\"\nsidebar_label: \"Upload a new asset\"\nhide_title: true\nhide_table_of_contents: true\napi: eJyNVE1v2zAM/SsGz26c7uhbtqFANmAL0BQ7BDnQNlOrsS1VotNlhv77QMlNnDQDdpMlfrzH98wBKnKlVYaV7iCHJ9NorBJMOnpL0DliSIHx2UG+gYV8O9im4KjsreIj5JsBCkJLdtFzDflm67cpWHrtyfFnXR0hH65arGtKKmRMWCelJWRKuKbYLHlTXM8ghVJ3TB1Ldts3rAxaznbatneSKteurKkNJz4aghx08UKlwDVWG7KsyMnrTjUUohTLAR5UQ9K6oKQPZKkC7yNmZakSoiFn6328d0Z3Lhb7NJ9/JPSVGFXjEix0z4FLpFWdBjhhg8Y0qkRJzV6c5P8/lVBuWU0CHVvVPYM/tViH+xvvTv2ZPnR9W5CVB+H6A9tbWVdTeW9/2WwsPSm0TU/DDo4BHyfZEte6ghyMdoEcimMgw2grcZU9kHXBVL1tIIcBq8qScz5Do7LDPaRwQKuwaMaJxOeoyQ77hiGHmtm4PMvYHmd7tLgnMjM0BtIbThwrJHoXlPs+xicRi0Cf2P1RhIqdp6Y/DU46C48QBvkYBOl4eNC2RUH47dc6TFd1Oy3pwjpCup/NZ3M4D/CEZ7Fa3sS/WC2TnbaX4IWsT8OcWwwW64LCt3/vi6LD2az/WAaRKtNvzkyDqpNOQa1hVHR0SlgUtUidb2AYCnT0ZBvv5fq1JyvLY3vWM+yOFGrCimywwJ6OkMOXCOduNNsBm17639oKPn1PWpQlGZ6Ef/jtRNaTIVc/H9ei0rivWl1JjhQOddPzMYLES9mvZA7gxyfsjhMUwxAj1npPnfeQjnBZvsHLxvkLeuzpPg==\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Upload a new asset\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/assets\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nUpload a new asset\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The data to create the asset with.\",\"content\":{\"multipart/form-data\":{\"schema\":{\"type\":\"object\",\"properties\":{\"file\":{\"title\":\"File to be uploaded\"}},\"required\":[\"file\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Details about the created asset\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"assetId\":{\"type\":\"string\"},\"contentType\":{\"type\":\"string\"},\"size\":{\"type\":\"number\"},\"fileName\":{\"type\":\"string\"}},\"required\":[\"assetId\",\"contentType\",\"size\",\"fileName\"],\"title\":\"Asset\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/01-getting-started/01-intro.md",
    "content": "---\nslug: /\n---\n\n# Introduction\n\nKarakeep (previously Hoarder) is an open source \"Bookmark Everything\" app that uses AI for automatically tagging the content you throw at it. The app is built with self-hosting as a first class citizen.\n\n![Screenshot](https://raw.githubusercontent.com/karakeep-app/karakeep/main/screenshots/homepage.png)\n\n\n## Features\n\n- 🔗 Bookmark links, take simple notes and store images and pdfs.\n- ⬇️ Automatic fetching for link titles, descriptions and images.\n- 📋 Sort your bookmarks into lists.\n- 👥 Collaborate with others on the same list.\n- 🔎 Full text search of all the content stored.\n- ✨ AI-based (aka chatgpt) automatic tagging and summarization. With supports for local models using ollama!\n- 🤖 Rule-based engine for customized management.\n- 🎆 OCR for extracting text from images.\n- 🔖 [Chrome plugin](https://chromewebstore.google.com/detail/karakeep/kgcjekpmcjjogibpjebkhaanilehneje) and [Firefox addon](https://addons.mozilla.org/en-US/firefox/addon/karakeep/) for quick bookmarking.\n- 📱 An [iOS app](https://apps.apple.com/us/app/karakeep-app/id6479258022), and an [Android app](https://play.google.com/store/apps/details?id=app.hoarder.hoardermobile&pcampaignid=web_share).\n- 📰 Auto hoarding from RSS feeds.\n- 🔌 REST API and multiple clients.\n- 🌐 Multi-language support.\n- 🖍️ Mark and store highlights from your hoarded content.\n- 🗄️ Full page archival (using [monolith](https://github.com/Y2Z/monolith)) to protect against link rot.\n- ▶️ Auto video archiving using [yt-dlp](https://github.com/yt-dlp/yt-dlp).\n- ☑️ Bulk actions support.\n- 🔐 SSO support.\n- 🌙 Dark mode support.\n- 💾 Self-hosting first.\n- ⬇️ Bookmark importers from Chrome, Pocket, Linkwarden, Omnivore, Tab Session Manager.\n- 🔄 Automatic sync with browser bookmarks via [floccus](https://floccus.org/).\n- [Planned] Offline reading on mobile, semantic search across bookmarks, ...\n\n**⚠️ This app is under heavy development.**\n\n\n## Demo\n\nYou can access the demo at [https://try.karakeep.app](https://try.karakeep.app). Login with the following creds:\n\n```\nemail: demo@karakeep.app\npassword: demodemo\n```\n\nThe demo is seeded with some content, but it's in read-only mode to prevent abuse.\n\n## About the name\n\nThe name Karakeep is inspired by the Arabic word \"كراكيب\" (karakeeb), a colloquial term commonly used to refer to miscellaneous clutter, odds and ends, or items that may seem disorganized but often hold personal value or hidden usefulness. It evokes the image of a messy drawer or forgotten box, full of stuff you can't quite throw away—because somehow, it matters (or more likely, because you're a hoarder!).\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/01-getting-started/02-screenshots.md",
    "content": "# Screenshots\n\n## Homepage\n\n![Homepage](/img/screenshots/homepage.png)\n\n## Homepage (Dark Mode)\n\n![Homepage](/img/screenshots/homepage-dark.png)\n\n## Tags\n\n![All Tags](/img/screenshots/all-tags.png)\n\n## Lists\n\n![All Lists](/img/screenshots/all-lists.png)\n\n## Bookmark Preview\n\n![Bookmark Preview](/img/screenshots/bookmark-preview.png)\n\n## Settings\n\n![Settings](/img/screenshots/settings.png)\n\n\n## Sharing\n\n<img src=\"/img/screenshots/share-sheet.png\" width=\"400px\"  />\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/01-getting-started/_category_.json",
    "content": "{\n  \"label\": \"Getting Started\",\n  \"position\": 1\n}\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/02-installation/01-docker.md",
    "content": "# Docker\n\n### Requirements\n\n- Docker\n- Docker Compose\n\n### 1. Create a new directory\n\nCreate a new directory to host the compose file and env variables.\n\nThis is where you’ll place the `docker-compose.yml` file from the next step and the environment variables.\n\nFor example you could make a new directory called \"karakeep-app\" with the following command:\n```\nmkdir karakeep-app\n```\n\n\n### 2. Download the compose file\n\nDownload the docker compose file provided [here](https://github.com/karakeep-app/karakeep/blob/main/docker/docker-compose.yml) directly into your new directory.\n\n```\nwget https://raw.githubusercontent.com/karakeep-app/karakeep/main/docker/docker-compose.yml\n```\n\n### 3. Populate the environment variables\n\nTo configure the app, create a `.env` file in the directory and add this minimal env file:\n\n```\nKARAKEEP_VERSION=release\nNEXTAUTH_SECRET=super_random_string\nMEILI_MASTER_KEY=another_random_string\nNEXTAUTH_URL=http://localhost:3000\n```\n\nYou **should** change the random strings. You can use `openssl rand -base64 36` in a seperate terminal window to generate the random strings. You should also change the `NEXTAUTH_URL` variable to point to your server address.\n\nUsing `KARAKEEP_VERSION=release` will pull the latest stable version. You might want to pin the version instead to control the upgrades (e.g. `KARAKEEP_VERSION=0.10.0`). Check the latest versions [here](https://github.com/karakeep-app/karakeep/pkgs/container/karakeep).\n\nPersistent storage and the wiring between the different services is already taken care of in the docker compose file.\n\nKeep in mind that every time you change the `.env` file, you'll need to re-run `docker compose up`.\n\nIf you want more config params, check the config documentation [here](../03-configuration/01-environment-variables.md).\n\n### 4. Setup OpenAI\n\nTo enable automatic tagging, you'll need to configure OpenAI. This is optional though but highly recommended.\n\n- Follow [OpenAI's help](https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key) to get an API key.\n- Add the OpenAI API key to the env file:\n\n```\nOPENAI_API_KEY=<key>\n```\n\nLearn more about the costs of using openai [here](../06-administration/03-openai.md).\n\n<details>\n    <summary>If you want to use Ollama (https://ollama.com/) instead for local inference.</summary>\n\n    **Note:** The quality of the tags you'll get will depend on the quality of the model you choose.\n\n    - Make sure ollama is running.\n    - Set the `OLLAMA_BASE_URL` env variable to the address of the ollama API.\n    - Set `INFERENCE_TEXT_MODEL` to the model you want to use for text inference in ollama (for example: `llama3.1`)\n    - Set `INFERENCE_IMAGE_MODEL` to the model you want to use for image inference in ollama (for example: `llava`)\n    - Make sure that you `ollama pull`-ed the models that you want to use.\n    - You might want to tune the `INFERENCE_CONTEXT_LENGTH` as the default is quite small. The larger the value, the better the quality of the tags, but the more expensive the inference will be.\n\n</details>\n\n### 5. Start the service\n\nStart the service by running:\n\n```\ndocker compose up -d\n```\n\nThen visit `http://localhost:3000` and you should be greeted with the Sign In page.\n\n### [Optional] 6. Enable optional features\n\nCheck the [configuration docs](../03-configuration/01-environment-variables.md) for extra features to enable such as full page archival, full page screenshots, inference languages, etc.\n\n### [Optional] 7. Setup quick sharing extensions\n\nGo to the [quick sharing page](../04-using-karakeep/quick-sharing.md) to install the mobile apps and the browser extensions. Those will help you hoard things faster!\n\n## Updating\n\nUpdating Karakeep will depend on what you used for the `KARAKEEP_VERSION` env variable.\n\n- If you pinned the app to a specific version, bump the version and re-run `docker compose up -d`. This should pull the new version for you.\n- If you used `KARAKEEP_VERSION=release`, you'll need to force docker to pull the latest version by running `docker compose up --pull always -d`.\n\nNote that if you want to upgrade/migrate `Meilisearch` versions, refer to the [troubleshooting](../06-administration/05-troubleshooting.md) page.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/02-installation/02-unraid.md",
    "content": "# Unraid\n\n## Docker Compose Manager Plugin (Recommended)\n\nYou can use [Docker Compose Manager](https://forums.unraid.net/topic/114415-plugin-docker-compose-manager/) plugin to deploy Karakeep using the official docker compose file provided [here](https://github.com/karakeep-app/karakeep/blob/main/docker/docker-compose.yml). After creating the stack, you'll need to setup some env variables similar to that from the docker compose installation docs [here](/installation/docker#3-populate-the-environment-variables).\n\n## Community Apps\n\n:::info\nThe community application template is maintained by the community.\n:::\n\nKarakeep can be installed on Unraid using the community application plugins. Karakeep is a multi-container service, and because unraid doesn't natively support that, you'll have to install the different pieces as separate applications and wire them manually together.\n\nHere's a high level overview of the services you'll need:\n\n- **Karakeep** ([Support post](https://forums.unraid.net/topic/165108-support-collectathon-karakeep/)): Karakeep's main web app.\n- **Browserless** ([Support post](https://forums.unraid.net/topic/130163-support-template-masterwishxbrowserless/)): The chrome headless service used for fetching the content. Karakeep's official docker compose doesn't use browserless, but it's currently the only headless chrome service available on unraid, so you'll have to use it.\n- **MeiliSearch** ([Support post](https://forums.unraid.net/topic/164847-support-collectathon-meilisearch/)): The search engine used by Karakeep. It's optional but highly recommended. If you don't have it set up, search will be disabled.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/02-installation/03-archlinux.md",
    "content": "# Arch Linux\n\n## Installation\n\n> [Karakeep on AUR](https://aur.archlinux.org/packages/karakeep) is not maintained by the karakeep official.\n\n1. Install karakeep\n\n    ```shell\n    paru -S karakeep\n    ```\n\n2. (**Optional**) Install optional dependencies\n\n    ```shell\n    # karakeep-cli: karakeep cli tool\n    paru -S karakeep-cli\n\n    # ollama: for automatic tagging\n    sudo pacman -S ollama\n\n    # yt-dlp: for download video\n    sudo pacman -S yt-dlp\n    ```\n\n    You can use Open-AI instead of `ollama`. If you use `ollama`, you need to download the ollama model. Please refer to: [https://ollama.com/library](https://ollama.com/library).\n\n3. Set up\n\n    Environment variables can be set in `/etc/karakeep/karakeep.env` according to [configuration page](../03-configuration/01-environment-variables.md). **The environment variables that are not specified in `/etc/karakeep/karakeep.env` need to be added by yourself.**\n\n4. Enable service\n\n    ```shell\n    sudo systemctl enable --now karakeep.target\n    ```\n\n    Then visit `http://localhost:3000` and you should be greated with the sign in page.\n\n## Services and Ports\n\n`karakeep.target` include 3 services: `karakeep-web.service`, `karakeep-works.service`, `karakeep-browser.service`.\n\n- `karakeep-web.service`: Provide karakeep webui service, uses `3000` port by default.\n\n- `karakeep-workers.service`: Provide karakeep workers service, no port.\n\n- `karakeep-browser.service`: Provide browser headless service, uses `9222` port by default.\n\nNow `karakeep` depends on `meilisearch`, and `karakeep-workers.service` wants `meilisearch.service`, starting `karakeep.target` will start `meilisearch.service` at the same time.\n\n## How to Migrate from Hoarder to Karakeep\n\nThe PKGBUILD has been fully updated to replace all references to `hoarder` with `karakeep`. If you want to preserve your existing `hoarder` data during the upgrade, please follow the steps below:\n\n**1. Stop the old services**\n\n```shell\nsudo systemctl stop hoarder-web.service hoarder-worker.service hoarder-browser.service\nsudo systemctl disable --now hoarder.target\n```\n\n**2. Uninstall Hoarder**  \nAfter uninstalling, you can manually remove the old `hoarder` user and group if needed.\n```shell\nparu -R hoarder\n```\n\n**3. Rename the old data directory**\n```shell\nsudo mv /var/lib/hoarder /var/lib/karakeep\n```\n\n**4. Install Karakeep**\n```shell\nparu -S karakeep\n```\n\n**5. Fix ownership of the data directory**\n```shell\nsudo chown -R karakeep:karakeep /var/lib/karakeep\n```\n\n**6. Set Karakeep**  \nEdit `/etc/karakeep/karakeep.env` according to [configuration page](../03-configuration/01-environment-variables.md). **The environment variables that are not specified in `/etc/karakeep/karakeep.env` need to be added by yourself.**\n\nOr you can copy old hoarder env file to karakeep:\n```shell\nsudo cp -f /etc/hoarder/hoarder.env /etc/karakeep/karakeep.env\n```\n\n**7. Start Karakeep**\n```shell\nsudo systemctl enable --now karakeep.target\n```\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/02-installation/04-kubernetes.md",
    "content": "# Kubernetes\n\n### Requirements\n\n- A kubernetes cluster\n- kubectl\n- kustomize\n\n### 1. Get the deployment manifests\n\nYou can clone the repository and copy the `/kubernetes` directory into another directory of your choice.\n\n### 2. Populate the environment variables and secrets\n\nTo configure the app, copy the `.env_sample` to `.env` and change to your specific needs.\n\nYou should also change the `NEXTAUTH_URL` variable to point to your server address.\n\nUsing `KARAKEEP_VERSION=release` will pull the latest stable version. You might want to pin the version instead to control the upgrades (e.g. `KARAKEEP_VERSION=0.10.0`). Check the latest versions [here](https://github.com/karakeep-app/karakeep/pkgs/container/karakeep).\n\nTo see all available configuration options check the [documentation](../03-configuration/01-environment-variables.md).\n\nTo configure the neccessary secrets for the application copy the `.secrets_sample` file to `.secrets` and change the sample secrets to your generated secrets.\n\n> Note: You **should** change the random strings. You can use `openssl rand -base64 36` to generate the random strings. \n\n### 3. Setup OpenAI\n\nTo enable automatic tagging, you'll need to configure OpenAI. This is optional though but highly recommended.\n\n- Follow [OpenAI's help](https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key) to get an API key.\n- Add the OpenAI API key to the `.env` file:\n\n```\nOPENAI_API_KEY=<key>\n```\n\nLearn more about the costs of using openai [here](../06-administration/03-openai.md).\n\n<details>\n    <summary>[EXPERIMENTAL] If you want to use Ollama (https://ollama.com/) instead for local inference.</summary>\n\n    **Note:** The quality of the tags you'll get will depend on the quality of the model you choose. Running local models is a recent addition and not as battle tested as using openai, so proceed with care (and potentially expect a bunch of inference failures).\n\n    - Make sure ollama is running.\n    - Set the `OLLAMA_BASE_URL` env variable to the address of the ollama API.\n    - Set `INFERENCE_TEXT_MODEL` to the model you want to use for text inference in ollama (for example: `mistral`)\n    - Set `INFERENCE_IMAGE_MODEL` to the model you want to use for image inference in ollama (for example: `llava`)\n    - Make sure that you `ollama pull`-ed the models that you want to use.\n\n\n</details>\n\n### 4. Deploy the service\n\nDeploy the service by running:\n\n```\nmake deploy\n```\n\n### 5. Access the service\n\n#### via LoadBalancer IP\n\nBy default, these manifests expose the application as a LoadBalancer Service. You can run `kubectl get services` to identify the IP of the loadbalancer for your service.\n\nThen visit `http://<loadbalancer-ip>:3000` and you should be greated with the Sign In page.\n\n> Note: Depending on your setup you might want to expose the service via an Ingress, or have a different means to access it.\n\n#### Via Ingress\n\nIf you want to use an ingress, you can customize the sample ingress in the kubernetes folder and change the host to the DNS name of your choice.\n\nAfter that you have to configure the web service to the type ClusterIP so it is only reachable via the ingress.\n\nIf you have already deployed the service you can patch the web service to the type ClusterIP with the following command:\n\n` kubectl -n karakeep patch service web -p '{\"spec\":{\"type\":\"ClusterIP\"}}' `\n\nAfterwards you can apply the ingress and access the service via your chosen URL.\n\n#### Setting up HTTPS access to the Service\n\nTo access karakeep securely you can configure the ingress to use a preconfigured TLS certificate. This requires that you already have the needed files, namely your .crt and .key file, on hand.\n\nAfter you have deployed the karakeep manifests you can deploy your certificate for karakeep in the `karakeep` namespace with this example command. You can name the secret however you want. But be aware that the secret name in the ingress definition has to match the secret name.\n\n` $ kubectl --namespace karakeep create secret tls karakeep-web-tls --cert=/path/to/crt --key=/path/to/key `\n\nIf the secret is successfully created you can now configure the Ingress to use TLS via this changes to the spec:\n\n```` yaml\n spec:\n  tls:\n  - hosts:\n      - karakeep.example.com\n    secretName: karakeep-web-tls\n````\n\n> Note: Be aware that the hosts have to match between the tls spec and the HTTP spec.\n\n### [Optional] 6. Setup quick sharing extensions\n\nGo to the [quick sharing page](../04-using-karakeep/quick-sharing.md) to install the mobile apps and the browser extensions. Those will help you hoard things faster!\n\n## Updating\n\nEdit the `KARAKEEP_VERSION` variable in the `kustomization.yaml` file and run `make clean deploy`.\n\nIf you have chosen `release` as the image tag you can also destroy the web pod, since the deployment has an ImagePullPolicy set to always the pod always pulls the image from the registry, this way we can ensure that the newest release image is pulled.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/02-installation/06-debuntu.md",
    "content": "# Debian 12/Ubuntu 24.04\n\n:::warning\nThis script is a stripped-down version of those found in the [Proxmox Community Scripts](https://github.com/community-scripts/ProxmoxVE) repo. It has been adapted to work on baremetal Debian 12 or Ubuntu 24.04 installs **only**. Any other use is not supported and you use this script at your own risk.\n:::\n\n### Requirements\n\n- **Debian 12** (Buster) or\n- **Ubuntu 24.04** (Noble Numbat)\n\nThe script will download and install all dependencies (except for Ollama), install Karakeep, do a basic configuration of Karakeep and Meilisearch (the search app used by Karakeep), and create and enable the systemd service files needed to run Karakeep on startup. Karakeep and Meilisearch are run in the context of their low-privilege user environments for more security.\n\nThe script functions as an update script in addition to an installer. See **[Updating](#updating)**.\n\n### 1. Download the script from the [Karakeep repository](https://github.com/karakeep-app/karakeep/blob/main/karakeep-linux.sh)\n\n```\nwget https://raw.githubusercontent.com/karakeep-app/karakeep/main/karakeep-linux.sh\n```\n\n### 2. Run the script\n\n> This script must be run as `root`, or as a user with `sudo` privileges.\n\n    If this is a fresh install, then run the installer by using the following command:\n\n    ```shell\n    bash karakeep-linux.sh install\n    ```\n\n### 3. Create an account/sign in\n\n    Then visit `http://localhost:3000` and you should be greated with the Sign In page.\n\n## Updating\n\n> This script must be run as `root`, or as a user with `sudo` privileges.\n\n    If Karakeep has previously been installed using this script, then run the updater like so:\n\n    ```shell\n     bash karakeep-linux.sh update\n    ```\n\n## Services and Ports\n\n`karakeep.target` includes 4 services: `meilisearch.service`, `karakeep-web.service`, `karakeep-workers.service`, `karakeep-browser.service`.\n\n- `meilisearch.service`: Provides full-text search, Karakeep Workers service connects to it, uses port `7700` by default.\n\n- `karakeep-web.service`: Provides the karakeep web service, uses `3000` port by default.\n\n- `karakeep-workers.service`: Provides the karakeep workers service, no port.\n\n- `karakeep-browser.service`: Provides the headless browser service, uses `9222` port by default.\n\n## Configuration, ENV file, database locations\n\nDuring installation, the script created a configuration file for `meilisearch`, an `ENV` file for Karakeep, and located config paths and database paths separate from the installation path of Karakeep, so as to allow for easier updating. Their names/locations are as follows:\n\n- `/etc/meilisearch.toml` - a basic configuration for meilisearch, that contains configs for the database location, disabling analytics, and using a master key, which prevents unauthorized connections.\n- `/var/lib/meilisearch` - Meilisearch DB location.\n- `/etc/karakeep/karakeep.env` - The Karakeep `ENV` file. Edit this file to configure Karakeep beyond the default. The web service and the workers service need to be restarted after editing this file:\n\n    ```shell\n    sudo systemctl restart karakeep-workers karakeep-web\n    ```\n\n- `/var/lib/karakeep` - The Karakeep database location. If you delete the contents of this folder you will lose all your data.\n\n## Still Running Hoarder?\n\nThere is a way to upgrade. Please see [Hoarder to Karakeep Migration](../06-administration/08-hoarder-to-karakeep-migration.md)\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/02-installation/07-minimal-install.md",
    "content": "# Minimal Installation\n\n:::warning\nUnless necessary, prefer the [full installation](/installation/docker) to leverage all the features of Karakeep. You'll be sacrificing a lot of functionality if you go with the minimal installation route.\n:::\n\nKarakeep's default installation has a dependency on Meilisearch for the full text search, Chrome for crawling and OpenAI/Ollama for AI tagging. You can however run Karakeep without those dependencies if you're willing to sacrifice those features.\n\n- If you run without meilisearch, the search functionality will be completely disabled.\n- If you run without chrome, crawling will still work, but you'll lose ability to take screenshots of websites and websites with javascript content won't get crawled correctly.\n- If you don't setup OpenAI/Ollama, AI tagging will be disabled.\n\nThose features are important for leveraging Karakeep's full potential, but if you're running in constrained environments, you can use the following minimal docker compose to skip all those dependencies:\n\n```yaml\nservices:\n  web:\n    image: ghcr.io/karakeep-app/karakeep:release\n    restart: unless-stopped\n    volumes:\n      - data:/data\n    ports:\n      - 3000:3000\n    environment:\n      DATA_DIR: /data\n      NEXTAUTH_SECRET: super_random_string\nvolumes:\n  data:\n```\n\nOr just with the following docker command:\n\n```base\ndocker run -d \\\n  --restart unless-stopped \\\n  -v data:/data \\\n  -p 3000:3000 \\\n  -e DATA_DIR=/data \\\n  -e NEXTAUTH_SECRET=super_random_string \\\n  ghcr.io/karakeep-app/karakeep:release\n```\n\n:::warning\nYou **MUST** change the `super_random_string` to a true random string which you can generate with `openssl rand -hex 32`.\n:::\n\nCheck the [configuration docs](../03-configuration/01-environment-variables.md) for extra features to enable such as full page archival, full page screenshots, inference languages, etc.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/02-installation/08-truenas.md",
    "content": "# TrueNAS\n\nKarakeep is available directly from TrueNAS's app catalog ([link](https://apps.truenas.com/catalog/karakeep/)).\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/02-installation/09-cloud-hosting.md",
    "content": "# Karakeep Cloud\n\n:::tip\nIf you want to use Karakeep without running your own servers, the hosted cloud option is the fastest way to start.\n:::\n\n[Karakeep Cloud](https://cloud.karakeep.app) is the fully managed version of Karakeep operated by the core team. It handles hosting, updates, monitoring, and backups for you, so you can focus on saving content instead of maintaining infrastructure.\n\n### Get started\n\n1. Visit [cloud.karakeep.app](https://cloud.karakeep.app) and create an account.\n2. Follow the onboarding flow to create your workspace.\n3. Install the browser extension or mobile apps from the [quick sharing page](../04-using-karakeep/quick-sharing.md) and start saving links immediately.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/02-installation/10-pikapods.md",
    "content": "# PikaPods\n\n:::info\nNote: PikaPods shares some of its revenue from hosting Karakeep with the maintainer of this project.\n:::\n\n[PikaPods](https://www.pikapods.com/) offers managed paid hosting for many open source apps, including Karakeep.\nServer administration, updates, migrations and backups are all taken care of, which makes it well suited\nfor less technical users. As of Nov 2024, running Karakeep there will cost you ~$3 per month.\n\n### Requirements\n\n- A _PikaPods_ account. Can be created for free [here](https://www.pikapods.com/register). You get an initial welcome credit of $5.\n\n### 1. Choose app\n\nChoose _Karakeep_ from their [list of apps](https://www.pikapods.com/apps) or use this [direct link](https://www.pikapods.com/pods?run=hoarder). This will either\nopen a new dialog to add a new _Karakeep_ pod or ask you to log in.\n\n### 2. Add settings\n\nThere are a few settings to configure in the dialog:\n\n- **Basics**: Give the pod a name and choose a region that's near you.\n- **Env Vars**: Here you can disable signups or set an OpenAI API key. All settings are optional.\n- **Resources**: The resources your _Karakeep_ pod can use. The defaults are fine, unless you have a very large collection.\n\n### 3. Start pod and add user\n\nAfter hitting _Add pod_ it will take about a minute for the app to fully start. After this you can visit\nthe pod's URL and add an initial user under _Sign Up_. After this you may want to disable further sign-ups\nby setting the pod's `DISABLE_SIGNUPS` _Env Var_ to `true`.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/02-installation/_category_.json",
    "content": "{\n  \"label\": \"Installation\",\n  \"position\": 3\n}\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/03-configuration/01-environment-variables.md",
    "content": "# Configuration\n\nThe app is mainly configured by environment variables. All the used environment variables are listed in [packages/shared/config.ts](https://github.com/karakeep-app/karakeep/blob/main/packages/shared/config.ts). The most important ones are:\n\n| Name                                   | Required                              | Default         | Description                                                                                                                                                                                                                                                            |\n| -------------------------------------- | ------------------------------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| PORT                                   | No                                    | 3000            | The port on which the web server will listen. DON'T CHANGE THIS IF YOU'RE USING DOCKER, instead changed the docker bound external port.                                                                                                                                |\n| WORKERS_PORT                           | No                                    | 0 (Random Port) | The port on which the worker will export its prometheus metrics on `/metrics`. By default it's a random unused port. If you want to utilize those metrics, fix the port to a value (and export it in docker if you're using docker).                                   |\n| WORKERS_HOST                           | No                                    | 127.0.0.1       | Host to listen to for requests to WORKERS_PORT. You will need to set this if running in a container, since localhost will not be reachable from outside                                                                                                                |\n| WORKERS_ENABLED_WORKERS                | No                                    | Not set         | Comma separated list of worker names to enable. If set, only these workers will run. Valid values: crawler,inference,search,adminMaintenance,video,feed,assetPreprocessing,webhook,ruleEngine.                                                                         |\n| WORKERS_DISABLED_WORKERS               | No                                    | Not set         | Comma separated list of worker names to disable. Takes precedence over `WORKERS_ENABLED_WORKERS`.                                                                                                                                                                      |\n| LOG_LEVEL                              | No                                    | debug           | The application log level as defined in the [winston documentation](https://github.com/winstonjs/winston?tab=readme-ov-file#logging-levels). You may want to set this to `notice` or `warning` when running Karakeep in a production environment.                      |\n| DATA_DIR                               | Yes                                   | Not set         | The path for the persistent data directory. This is where the db lives. Assets are stored here by default unless `ASSETS_DIR` is set.                                                                                                                                  |\n| ASSETS_DIR                             | No                                    | Not set         | The path where crawled assets will be stored. If not set, defaults to `${DATA_DIR}/assets`.                                                                                                                                                                            |\n| NEXTAUTH_URL                           | Yes                                   | Not set         | Should point to the address of your server. The app will function without it, but will redirect you to wrong addresses on signout for example.                                                                                                                         |\n| NEXTAUTH_SECRET                        | Yes                                   | Not set         | Random string used to sign the JWT tokens. Generate one with `openssl rand -base64 36`.                                                                                                                                                                                |\n| MEILI_ADDR                             | No                                    | Not set         | The address of meilisearch. If not set, Search will be disabled. E.g. (`http://meilisearch:7700`)                                                                                                                                                                      |\n| MEILI_MASTER_KEY                       | Only in Prod and if search is enabled | Not set         | The master key configured for meilisearch. Not needed in development environment. Generate one with `openssl rand -base64 36 \\| tr -dc 'A-Za-z0-9'`                                                                                                                    |\n| MAX_ASSET_SIZE_MB                      | No                                    | 50              | Sets the maximum allowed asset size (in MB) to be uploaded                                                                                                                                                                                                             |\n| DISABLE_NEW_RELEASE_CHECK              | No                                    | false           | If set to true, latest release check will be disabled in the admin panel.                                                                                                                                                                                              |\n| RATE_LIMITING_ENABLED                  | No                                    | false           | If set to true, API rate limiting will be enabled.                                                                                                                                                                                                                     |\n| CRAWLER_DOMAIN_RATE_LIMIT_WINDOW_MS    | No                                    | Not set         | Time window in milliseconds for per-domain crawler rate limiting.                                                                                                                                                                                                      |\n| CRAWLER_DOMAIN_RATE_LIMIT_MAX_REQUESTS | No                                    | Not set         | Maximum crawler requests allowed per domain inside the configured window.                                                                                                                                                                                              |\n| DB_WAL_MODE                            | No                                    | false           | Enables WAL mode for the sqlite database. This should improve the performance of the database. There's no reason why you shouldn't set this to true unless you're running the db on a network attached drive. This will become the default at some time in the future. |\n| SEARCH_NUM_WORKERS                     | No                                    | 1               | Number of concurrent workers for search indexing tasks. Increase this if you have a high volume of content being indexed for search.                                                                                                                                   |\n| SEARCH_JOB_TIMEOUT_SEC                 | No                                    | 30              | How long to wait for a search indexing job to finish before timing out. Increase this if you have large bookmarks with extensive content that takes longer to index.                                                                                                   |\n| WEBHOOK_NUM_WORKERS                    | No                                    | 1               | Number of concurrent workers for webhook delivery. Increase this if you have multiple webhook endpoints or high webhook traffic.                                                                                                                                       |\n| ASSET_PREPROCESSING_NUM_WORKERS        | No                                    | 1               | Number of concurrent workers for asset preprocessing tasks (image processing, OCR, etc.). Increase this if you have many images or documents that need processing.                                                                                                     |\n| ASSET_PREPROCESSING_JOB_TIMEOUT_SEC    | No                                    | 60              | How long to wait for an asset preprocessing job to finish before timing out. Increase this if you have large images or PDFs that take longer to process.                                                                                                               |\n| RULE_ENGINE_NUM_WORKERS                | No                                    | 1               | Number of concurrent workers for rule engine processing. Increase this if you have complex automation rules that need to be processed quickly.                                                                                                                         |\n| MAX_RSS_FEEDS_PER_USER                 | No                                    | 1000            | The maximum number of RSS feeds a user can create.                                                                                                                                                                                                                     |\n| MAX_WEBHOOKS_PER_USER                  | No                                    | 100             | The maximum number of webhooks a user can create.                                                                                                                                                                                                                      |\n\n## Asset Storage\n\nKarakeep supports two storage backends for assets: local filesystem (default) and S3-compatible object storage. S3 storage is automatically detected when an S3 endpoint is passed.\n\n| Name                             | Required          | Default | Description                                                                                               |\n| -------------------------------- | ----------------- | ------- | --------------------------------------------------------------------------------------------------------- |\n| ASSET_STORE_S3_ENDPOINT          | No                | Not set | The S3 endpoint URL. Required for S3-compatible services like MinIO. **Setting this enables S3 storage**. |\n| ASSET_STORE_S3_REGION            | No                | Not set | The S3 region to use.                                                                                     |\n| ASSET_STORE_S3_BUCKET            | Yes when using S3 | Not set | The S3 bucket name where assets will be stored.                                                           |\n| ASSET_STORE_S3_ACCESS_KEY_ID     | Yes when using S3 | Not set | The S3 access key ID for authentication.                                                                  |\n| ASSET_STORE_S3_SECRET_ACCESS_KEY | Yes when using S3 | Not set | The S3 secret access key for authentication.                                                              |\n| ASSET_STORE_S3_FORCE_PATH_STYLE  | No                | false   | Whether to force path-style URLs for S3 requests. Set to true for MinIO and other S3-compatible services. |\n\n:::info\nWhen using S3 storage, make sure the bucket exists and the provided credentials have the necessary permissions to read, write, and delete objects in the bucket.\n:::\n\n:::warning\nSwitching between storage backends after data has been stored will require manual migration of existing assets. Plan your storage backend choice carefully before deploying.\n:::\n\n## Authentication / Signup\n\nBy default, Karakeep uses the database to store users, but it is possible to also use OAuth.\nThe flags need to be provided to the `web` container.\n\n:::info\nOnly OIDC compliant OAuth providers are supported! For information on how to set it up, consult the documentation of your provider.\n:::\n\n:::info\nWhen setting up OAuth, the allowed redirect URLs configured at the provider should be set to `<KARAKEEP_ADDRESS>/api/auth/callback/custom` where `<KARAKEEP_ADDRESS>` is the address you configured in `NEXTAUTH_URL` (for example: `https://try.karakeep.app/api/auth/callback/custom`).\n:::\n\n| Name                                        | Required | Default                | Description                                                                                                                                                                                           |\n| ------------------------------------------- | -------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| DISABLE_SIGNUPS                             | No       | false                  | If enabled, no new signups will be allowed and the signup button will be disabled in the UI                                                                                                           |\n| DISABLE_PASSWORD_AUTH                       | No       | false                  | If enabled, only signups and logins using OAuth are allowed and the signup button and login form for local accounts will be disabled in the UI                                                        |\n| EMAIL_VERIFICATION_REQUIRED                 | No       | false                  | Whether email verification is required during user signup. If enabled, users must verify their email address before they can use their account. If you enable this, you must configure SMTP settings. |\n| OAUTH_WELLKNOWN_URL                         | No       | Not set                | The \"wellknown Url\" for openid-configuration as provided by the OAuth provider                                                                                                                        |\n| OAUTH_CLIENT_SECRET                         | No       | Not set                | The \"Client Secret\" as provided by the OAuth provider                                                                                                                                                 |\n| OAUTH_CLIENT_ID                             | No       | Not set                | The \"Client ID\" as provided by the OAuth provider                                                                                                                                                     |\n| OAUTH_SCOPE                                 | No       | \"openid email profile\" | \"Full list of scopes to request (space delimited)\"                                                                                                                                                    |\n| OAUTH_PROVIDER_NAME                         | No       | \"Custom Provider\"      | The name of your provider. Will be shown on the signup page as \"Sign in with `<name>`\"                                                                                                                |\n| OAUTH_ALLOW_DANGEROUS_EMAIL_ACCOUNT_LINKING | No       | false                  | Whether existing accounts in karakeep stored in the database should automatically be linked with your OAuth account. Only enable it if you trust the OAuth provider!                                  |\n| OAUTH_TIMEOUT                               | No       | 3500                   | The wait time in milliseconds for the OAuth provider response. Increase this if you are having `outgoing request timed out` errors                                                                    |\n\nFor more information on `OAUTH_ALLOW_DANGEROUS_EMAIL_ACCOUNT_LINKING`, check the [next-auth.js documentation](https://next-auth.js.org/configuration/providers/oauth#allowdangerousemailaccountlinking-option).\n\n## Inference Configs (For automatic tagging)\n\nEither `OPENAI_API_KEY` or `OLLAMA_BASE_URL` need to be set for automatic tagging to be enabled. Otherwise, automatic tagging will be skipped.\n\n:::warning\n\n- The quality of the tags you'll get will depend on the quality of the model you choose.\n- You might want to tune the `INFERENCE_CONTEXT_LENGTH` as the default is quite small. The larger the value, the better the quality of the tags, but the more expensive the inference will be (money-wise on OpenAI and resource-wise on ollama).\n  :::\n\n| Name                                 | Required | Default                | Description                                                                                                                                                                                                                                                                                                                                                                           |\n| ------------------------------------ | -------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| OPENAI_API_KEY                       | No       | Not set                | The OpenAI key used for automatic tagging. More on that in [here](../06-administration/03-openai.md).                                                                                                                                                                                                                                                                                            |\n| OPENAI_BASE_URL                      | No       | Not set                | If you just want to use OpenAI you don't need to pass this variable. If, however, you want to use some other openai compatible API (e.g. azure openai service), set this to the url of the API.                                                                                                                                                                                       |\n| OPENAI_PROXY_URL                     | No       | Not set                | HTTP proxy server URL for OpenAI API requests (e.g., `http://proxy.example.com:8080`).                                                                                                                                                                                                                                                                                                |\n| OLLAMA_BASE_URL                      | No       | Not set                | If you want to use ollama for local inference, set the address of ollama API here.                                                                                                                                                                                                                                                                                                    |\n| OLLAMA_KEEP_ALIVE                    | No       | Not set                | Controls how long the model will stay loaded into memory following the request (examples: \"5m\" for 5 minutes, \"-1m\" to keep the model loaded indefinitely, \"0\" to unload the model instantly after processing is complete).                                                                                                                                                                                                                                                                                 |\n| INFERENCE_TEXT_MODEL                 | No       | gpt-4.1-mini           | The model to use for text inference. You'll need to change this to some other model if you're using ollama.                                                                                                                                                                                                                                                                           |\n| INFERENCE_IMAGE_MODEL                | No       | gpt-4o-mini            | The model to use for image inference. You'll need to change this to some other model if you're using ollama and that model needs to support vision APIs (e.g. llava).                                                                                                                                                                                                                 |\n| EMBEDDING_TEXT_MODEL                 | No       | text-embedding-3-small | The model to be used for generating embeddings for the text.                                                                                                                                                                                                                                                                                                                          |\n| INFERENCE_CONTEXT_LENGTH             | No       | 2048                   | The max number of tokens that we'll pass to the inference model. If your content is larger than this size, it'll be truncated to fit. The larger this value, the more of the content will be used in tag inference, but the more expensive the inference will be (money-wise on openAI and resource-wise on ollama). Check the model you're using for its max supported content size. |\n| INFERENCE_MAX_OUTPUT_TOKENS          | No       | 2048                   | The maximum number of tokens that the inference model is allowed to generate in its response. This controls the length of AI-generated content like tags and summaries. Increase this if you need longer responses, but be aware that higher values will increase costs (for OpenAI) and processing time.                                                                             |\n| INFERENCE_USE_MAX_COMPLETION_TOKENS  | No       | false                  | \\[OpenAI Only\\] Whether to use the newer `max_completion_tokens` parameter instead of the deprecated `max_tokens` parameter. Set to `true` if using GPT-5 or o-series models which require this. Will become the default in a future release.                                                                                                                                         |\n| INFERENCE_LANG                       | No       | english                | The language in which the tags will be generated.                                                                                                                                                                                                                                                                                                                                     |\n| INFERENCE_NUM_WORKERS                | No       | 1                      | Number of concurrent workers for AI inference tasks (tagging and summarization). Increase this if you have multiple AI inference requests and want to process them in parallel.                                                                                                                                                                                                       |\n| INFERENCE_ENABLE_AUTO_TAGGING        | No       | true                   | Whether automatic AI tagging is enabled or disabled.                                                                                                                                                                                                                                                                                                                                  |\n| INFERENCE_ENABLE_AUTO_SUMMARIZATION  | No       | false                  | Whether automatic AI summarization is enabled or disabled.                                                                                                                                                                                                                                                                                                                            |\n| INFERENCE_JOB_TIMEOUT_SEC            | No       | 30                     | How long to wait for the inference job to finish before timing out. If you're running ollama without powerful GPUs, you might want to increase the timeout a bit.                                                                                                                                                                                                                     |\n| INFERENCE_FETCH_TIMEOUT_SEC          | No       | 300                    | \\[Ollama Only\\] The timeout of the fetch request to the ollama server. If your inference requests take longer than the default 5mins, you might want to increase this timeout.                                                                                                                                                                                                        |\n| INFERENCE_SUPPORTS_STRUCTURED_OUTPUT | No       | Not set                | \\[DEPRECATED\\] Whether the inference model supports structured output or not. Use INFERENCE_OUTPUT_SCHEMA instead. Setting this to true translates to INFERENCE_OUTPUT_SCHEMA=structured, and to false translates to INFERENCE_OUTPUT_SCHEMA=plain.                                                                                                                                   |\n| INFERENCE_OUTPUT_SCHEMA              | No       | structured             | Possible values are \"structured\", \"json\", \"plain\". Structured is the preferred option, but if your model doesn't support it, you can use \"json\" if your model supports JSON mode, otherwise \"plain\" which should be supported by all the models but the model might not output the data in the correct format.                                                                        |\n\n:::info\n\n- You can append additional instructions to the prompt used for automatic tagging, in the `AI Settings` (in the `User Settings` screen)\n- You can use the placeholders `$tags`, `$aiTags`, `$userTags` in the prompt. These placeholders will be replaced with all tags, ai generated tags or human created tags when automatic tagging is performed (e.g. `[karakeep, computer, ai]`)\n  :::\n\n## Crawler Configs\n\n| Name                                     | Required | Default   | Description                                                                                                                                                                                                                                                                                                                                                                   |\n| ---------------------------------------- | -------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| CRAWLER_NUM_WORKERS                      | No       | 1         | Number of allowed concurrent crawling jobs. By default, we're only doing one crawling request at a time to avoid consuming a lot of resources.                                                                                                                                                                                                                                |\n| BROWSER_WEB_URL                          | No       | Not set   | The browser's http debugging address. The worker will talk to this endpoint to resolve the debugging console's websocket address. If you already have the websocket address, use `BROWSER_WEBSOCKET_URL` instead. If neither `BROWSER_WEB_URL` nor `BROWSER_WEBSOCKET_URL` are set, the worker will use plain http requests skipping screenshotting and javascript execution. |\n| BROWSER_WEBSOCKET_URL                    | No       | Not set   | The websocket address of browser's debugging console. If you want to use [browserless](https://browserless.io), use their websocket address here. If neither `BROWSER_WEB_URL` nor `BROWSER_WEBSOCKET_URL` are set, the worker will use plain http requests skipping screenshotting and javascript execution.                                                                 |\n| BROWSER_CONNECT_ONDEMAND                 | No       | false     | If set to false, the crawler will proactively connect to the browser instance and always maintain an active connection. If set to true, the browser will be launched on demand only whenever a crawling is requested. Set to true if you're using a service that provides you with browser instances on demand.                                                               |\n| CRAWLER_DOWNLOAD_BANNER_IMAGE            | No       | true      | Whether to cache the banner image used in the cards locally or fetch it each time directly from the website. Caching it consumes more storage space, but is more resilient against link rot and rate limits from websites.                                                                                                                                                    |\n| CRAWLER_STORE_SCREENSHOT                 | No       | true      | Whether to store a screenshot from the crawled website or not. Screenshots act as a fallback for when we fail to extract an image from a website. You can also view the stored screenshots for any link.                                                                                                                                                                      |\n| CRAWLER_FULL_PAGE_SCREENSHOT             | No       | false     | Whether to store a screenshot of the full page or not. Disabled by default, as it can lead to much higher disk usage. If disabled, the screenshot will only include the visible part of the page                                                                                                                                                                              |\n| CRAWLER_SCREENSHOT_TIMEOUT_SEC           | No       | 5         | How long to wait for the screenshot finish before timing out. If you are capturing full-page screenshots of long webpages, consider increasing this value.                                                                                                                                                                                                                    |\n| CRAWLER_STORE_PDF                        | No       | false     | Whether to store a PDF snapshot of the crawled page. Disabled by default, as it can lead to much higher disk usage. When enabled, a PDF version of each crawled page will be captured and stored as an asset, which can be viewed in the bookmark preview.                                                                                                                    |\n| CRAWLER_FULL_PAGE_ARCHIVE                | No       | false     | Whether to store a full local copy of the page or not. Disabled by default, as it can lead to much higher disk usage. If disabled, only the readable text of the page is archived.                                                                                                                                                                                            |\n| CRAWLER_JOB_TIMEOUT_SEC                  | No       | 60        | How long to wait for the crawler job to finish before timing out. If you have a slow internet connection or a low powered device, you might want to bump this up a bit                                                                                                                                                                                                        |\n| CRAWLER_NAVIGATE_TIMEOUT_SEC             | No       | 30        | How long to spend navigating to the page (along with its redirects). Increase this if you have a slow internet connection                                                                                                                                                                                                                                                     |\n| CRAWLER_VIDEO_DOWNLOAD                   | No       | false     | Whether to download videos from the page or not (using yt-dlp)                                                                                                                                                                                                                                                                                                                |\n| CRAWLER_VIDEO_DOWNLOAD_MAX_SIZE          | No       | 50        | The maximum file size for the downloaded video. The quality will be chosen accordingly. Use -1 to disable the limit.                                                                                                                                                                                                                                                          |\n| CRAWLER_VIDEO_DOWNLOAD_TIMEOUT_SEC       | No       | 600       | How long to wait for the video download to finish                                                                                                                                                                                                                                                                                                                             |\n| CRAWLER_ENABLE_ADBLOCKER                 | No       | true      | Whether to enable an adblocker in the crawler or not. If you're facing troubles downloading the adblocking lists on worker startup, you can disable this.                                                                                                                                                                                                                     |\n| CRAWLER_YTDLP_ARGS                       | No       | []        | Include additional yt-dlp arguments to be passed at crawl time separated by %%: https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#general-options                                                                                                                                                                                                                           |\n| BROWSER_COOKIE_PATH                      | No       | Not set   | Path to a JSON file containing cookies to be loaded into the browser context. The file should be an array of cookie objects, each with name and value (required), and optional fields like domain, path, expires, httpOnly, secure, and sameSite (e.g., `[{\"name\": \"session\", \"value\": \"xxx\", \"domain\": \".example.com\"}`]).                                                   |\n| HTML_CONTENT_SIZE_INLINE_THRESHOLD_BYTES | No       | 5 \\* 1024 | The thresholds in bytes after which larger assets will be stored in the assetdb (folder/s3) instead of inline in the database.                                                                                                                                                                                                                                                |\n\n<details>\n\n  <summary>More info on BROWSER_COOKIE_PATH</summary>\n\nBROWSER_COOKIE_PATH specifies the path to a JSON file containing cookies to be loaded into the browser context for crawling.\n\nThe JSON file must be an array of cookie objects, each with:\n\n- name: The cookie name (required).\n- value: The cookie value (required).\n- Optional fields: domain, path, expires, httpOnly, secure, sameSite (values: \"Strict\", \"Lax\", or \"None\").\n\nExample JSON file:\n\n```json\n[\n  {\n    \"name\": \"session\",\n    \"value\": \"xxx\",\n    \"domain\": \".example.com\",\n    \"path\": \"/\",\n    \"expires\": 1735689600,\n    \"httpOnly\": true,\n    \"secure\": true,\n    \"sameSite\": \"Lax\"\n  }\n]\n```\n\n</details>\n\n## OCR Configs\n\nKarakeep uses [tesseract.js](https://github.com/naptha/tesseract.js) to extract text from images.\n\n| Name                     | Required | Default   | Description                                                                                                                                                                                                                               |\n| ------------------------ | -------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| OCR_CACHE_DIR            | No       | $TEMP_DIR | The dir where tesseract will download its models. By default, those models are not persisted and stored in the OS' temp dir.                                                                                                              |\n| OCR_LANGS                | No       | eng       | Comma separated list of the language codes that you want tesseract to support. You can find the language codes [here](https://tesseract-ocr.github.io/tessdoc/Data-Files-in-different-versions.html). Set to empty string to disable OCR. |\n| OCR_CONFIDENCE_THRESHOLD | No       | 50        | A number between 0 and 100 indicating the minimum acceptable confidence from tessaract. If tessaract's confidence is lower than this value, extracted text won't be stored.                                                               |\n\n## Webhook Configs\n\nYou can use webhooks to trigger actions when bookmarks are created, changed or crawled.\n\n| Name                | Required | Default | Description                                       |\n| ------------------- | -------- | ------- | ------------------------------------------------- |\n| WEBHOOK_TIMEOUT_SEC | No       | 5       | The timeout for the webhook request in seconds.   |\n| WEBHOOK_RETRY_TIMES | No       | 3       | The number of times to retry the webhook request. |\n\n:::info\n\n- The WEBHOOK_TOKEN is used for authentication. It will appear in the Authorization header as Bearer token.\n  ```\n  Authorization: Bearer <WEBHOOK_TOKEN>\n  ```\n- The webhook will be triggered with the job id (used for idempotence), bookmark id, bookmark type, the user id, the url and the operation in JSON format in the body.\n\n  ```json\n  {\n    \"jobId\": \"123\",\n    \"type\": \"link\",\n    \"bookmarkId\": \"exampleBookmarkId\",\n    \"userId\": \"exampleUserId\",\n    \"url\": \"https://example.com\",\n    \"operation\": \"crawled\"\n  }\n  ```\n\n  :::\n\n## SMTP Configuration\n\nKarakeep can send emails for various purposes such as email verification during signup. Configure these settings to enable email functionality.\n\n| Name          | Required | Default | Description                                                                                     |\n| ------------- | -------- | ------- | ----------------------------------------------------------------------------------------------- |\n| SMTP_HOST     | No       | Not set | The SMTP server hostname or IP address. Required if you want to enable email functionality.     |\n| SMTP_PORT     | No       | 587     | The SMTP server port. Common values are 587 (STARTTLS), 465 (SSL/TLS), or 25 (unencrypted).     |\n| SMTP_SECURE   | No       | false   | Whether to use SSL/TLS encryption. Set to true for port 465, false for port 587 with STARTTLS.  |\n| SMTP_USER     | No       | Not set | The username for SMTP authentication. Usually your email address.                               |\n| SMTP_PASSWORD | No       | Not set | The password for SMTP authentication. For services like Gmail, use an app-specific password.    |\n| SMTP_FROM     | No       | Not set | The \"from\" email address that will appear in sent emails. This should be a valid email address. |\n\n## Proxy Configuration\n\nIf your Karakeep instance needs to connect through a proxy server, you can configure the following settings:\n\n| Name                               | Required | Default | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |\n| ---------------------------------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| CRAWLER_HTTP_PROXY                 | No       | Not set | HTTP proxy server URL for outgoing HTTP requests (e.g., `http://proxy.example.com:8080`). You can pass multiple comma separated proxies and the used one will be chosen at random. The proxy is used for crawling, RSS feed fetches and webhooks.                                                                                                                                                                                                                                                                           |\n| CRAWLER_HTTPS_PROXY                | No       | Not set | HTTPS proxy server URL for outgoing HTTPS requests (e.g., `http://proxy.example.com:8080`). You can pass multiple comma separated proxies and the used one will be chosen at random. The proxy is used for crawling, RSS feed fetches and webhooks.                                                                                                                                                                                                                                                                         |\n| CRAWLER_NO_PROXY                   | No       | Not set | Comma-separated list of hostnames/IPs that should bypass the proxy (e.g., `localhost,127.0.0.1,.local`)                                                                                                                                                                                                                                                                                                                                                                                                                     |\n| CRAWLER_ALLOWED_INTERNAL_HOSTNAMES | No       | Not set | By default, Karakeep blocks worker-initiated requests whose DNS resolves to private, loopback, link-local, or Tailscale CGNAT IP addresses. Use this to allowlist specific hostnames for internal access (e.g., `internal.company.com`, `app-name.local`). Supports domain wildcards by prefixing with a dot (e.g., `.internal.company.com`, `.<tailnet-name>.ts.net`). Passing `.` allowlists all domains. Note: Internal IP validation is bypassed when a proxy is configured for the URL as the local DNS resolver won't necessarily be the same as the one used by the proxy. |\n\n:::info\nThese proxy settings will be used by the crawler and other components that make outgoing HTTP requests.\n:::\n\n## Monitoring\n\nKarakeep supports distributed tracing via OpenTelemetry. When enabled, traces are collected for tRPC API calls, background worker operations, and other key workflows. Karakeep also exports prometheus-based metrics.\n\n| Name                        | Required | Default  | Description                                                                                                                                                                                                                                                                                                             |\n| --------------------------- | -------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| OTEL_TRACING_ENABLED        | No       | false    | Set to `true` to enable OpenTelemetry tracing. When disabled, all tracing operations are no-ops.                                                                                                                                                                                                                        |\n| OTEL_EXPORTER_OTLP_ENDPOINT | No       | Not set  | The OTLP HTTP endpoint to send traces to (e.g., `http://jaeger:4318/v1/traces` or `http://otel-collector:4318/v1/traces`). If not set, traces are logged to the console.                                                                                                                                                |\n| OTEL_SERVICE_NAME           | No       | karakeep | The service name that will appear in your tracing backend. The actual service name will include a suffix (e.g., `karakeep-api`, `karakeep-workers`).                                                                                                                                                                    |\n| OTEL_SAMPLE_RATE            | No       | 1.0      | The sampling rate for traces, between 0.0 and 1.0. A value of 1.0 means all traces are sampled, while 0.1 means only 10% of traces are sampled. Lower values reduce overhead and storage costs in production.                                                                                                           |\n| PROMETHEUS_AUTH_TOKEN       | No       | Random   | Enable a prometheus metrics endpoint at `/api/metrics`. This endpoint will require this token being passed in the Authorization header as a Bearer token. If not set, a new random token is generated everytime at startup. This cannot contain any special characters or you may encounter a 400 Bad Request response. |\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/03-configuration/02-different-ai-providers.md",
    "content": "# Configuring different AI Providers\n\nKarakeep uses LLM providers for AI tagging and summarization. We support OpenAI-compatible providers and ollama. This guide will show you how to configure different providers.\n\n## OpenAI\n\nIf you want to use OpenAI itself, you just need to pass in the OPENAI_API_KEY environment variable.\n\n```\nOPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\n# You can change the default models by uncommenting the following lines, and choosing your model.\n# INFERENCE_TEXT_MODEL=gpt-4.1-mini\n# INFERENCE_IMAGE_MODEL=gpt-4o-mini\n```\n\n## Ollama\n\nOllama is a local LLM provider that you can use to run your own LLM server. You'll need to pass ollama's address to karakeep and you need to ensure that it's accessible from within the karakeep container (e.g. no localhost addresses).\n\n```\n# MAKE SURE YOU DON'T HAVE OPENAI_API_KEY set, otherwise it takes precedence.\n\nOLLAMA_BASE_URL=http://ollama.mylab.com:11434\n\n# Make sure to pull the models in ollama first. Example models:\nINFERENCE_TEXT_MODEL=gemma3\nINFERENCE_IMAGE_MODEL=llava\n\n# If the model you're using doesn't support structured output, you also need:\n# INFERENCE_OUTPUT_SCHEMA=plain\n```\n\n## Gemini\n\nGemini has an OpenAI-compatible API. You need to get an api key from Google AI Studio.\n\n```\n\nOPENAI_BASE_URL=https://generativelanguage.googleapis.com/v1beta\nOPENAI_API_KEY=YOUR_API_KEY\n\n# Example models:\nINFERENCE_TEXT_MODEL=gemini-2.0-flash\nINFERENCE_IMAGE_MODEL=gemini-2.0-flash\n```\n\n## OpenRouter\n\n```\nOPENAI_BASE_URL=https://openrouter.ai/api/v1\nOPENAI_API_KEY=YOUR_API_KEY\n\n# Example models:\nINFERENCE_TEXT_MODEL=meta-llama/llama-4-scout\nINFERENCE_IMAGE_MODEL=meta-llama/llama-4-scout\n```\n\n## Perplexity\n\n```\nOPENAI_BASE_URL: https://api.perplexity.ai\nOPENAI_API_KEY: Your Perplexity API Key\nINFERENCE_TEXT_MODEL: sonar-pro\nINFERENCE_IMAGE_MODEL: sonar-pro\n```\n\n## Azure\n\nAzure has an OpenAI-compatible API.\n\nYou can get your API key from the Overview page of the Azure AI Foundry Portal or via \"Keys + Endpoints\" on the resource in the Azure Portal.\n\n:::warning\nThe [model name is the deployment name](https://learn.microsoft.com/en-us/azure/ai-foundry/openai/how-to/switching-endpoints#keyword-argument-for-model) you specified when deploying the model, which may differ from the base model name.\n:::\n\n```\n# Deployed via Azure AI Foundry:\nOPENAI_BASE_URL=https://{your-azure-ai-foundry-resource-name}.cognitiveservices.azure.com/openai/v1/\n\n# Deployed via Azure OpenAI Service:\nOPENAI_BASE_URL=https://{your-azure-openai-resource-name}.openai.azure.com/openai/v1/\n\nOPENAI_API_KEY=YOUR_API_KEY\nINFERENCE_TEXT_MODEL=YOUR_DEPLOYMENT_NAME\nINFERENCE_IMAGE_MODEL=YOUR_DEPLOYMENT_NAME\n```\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/03-configuration/_category_.json",
    "content": "{\n  \"label\": \"Configuration\",\n  \"position\": 4\n}\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/04-using-karakeep/_category_.json",
    "content": "{\n  \"label\": \"Using Karakeep\",\n  \"position\": 2\n}\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/04-using-karakeep/advanced-workflows.md",
    "content": "---\nsidebar_position: 10\nslug: advanced-workflows\n---\n\n# Advanced workflows\n\nPush Karakeep further with automation and integrations.\n\n## Rule engine\n\n- Create if-this-then-that style rules to auto-tag, favourite, or route bookmarks into lists based on metadata or content.\n- Useful for keeping inboxes tidy (e.g. auto-archive newsletters, auto-tag domains, or flag videos).\n\n## API\n\n- Use the API to script imports, syncs, or custom tools. Same surface area the apps use.\n- Great for integrating with personal scripts, cron jobs, or other services.\n\n## Webhooks\n\n- Subscribe to bookmark events and trigger your own systems when something is added, updated, or archived.\n- Pair with the API to build end-to-end automations (e.g. push new saves into a writing queue or a team chat).\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/04-using-karakeep/bookmarking.md",
    "content": "---\nsidebar_position: 1\nslug: bookmarking\n---\n\n# Bookmarking\n\nEverything in Karakeep starts as a bookmark. Here’s how the different types work and how to keep your home view tidy with favourites and archive.\n\n## Favourites\n\n- Star bookmarks you like so they sit in their own dedicated favourites view for quick return visits.\n- Handy for saved gems you want to re-open often like articles you enjoyed, references you come back to, or things worth sharing.\n\n## Archiving\n\n- Archive hides a bookmark from the homepage without deleting it.\n- Archived items stay searchable and keep all tags, highlights, and attachments.\n- Ideal for achieving inbox-zero style for your homepage.\n\n## Bookmark types\n\n- **Links**: URLs saved from the web or extension. Karakeep grabs metadata, previews, screenshots, and archives when configured.\n- **Text**: Quick notes or snippets you paste in. Great for ideas, quotes, or saving context alongside links.\n- **Media**: Images or PDFs you want to save for later. Karakeep automatically extracts content out of those files and makes them searchable.\n\n## Notes\n\n- Attach personal notes to any bookmark to capture context, reminders, or next steps.\n- Notes live with the bookmark and are searchable, so you can recall why something mattered.\n\n## Highlights\n\n- Save quotes, summaries, or TODOs while reading.\n- Highlights show up in the bookmark detail view/reader and are searchable, so you can jump straight to the key ideas.\n\n## Attachments\n\n- Store extra context alongside a bookmark: screenshots, page captures, videos, and files you upload.\n- **Screenshots & archives**: fallback when the original page changes or disappear.\n- **Uploaded files**: keep PDFs, notes, or supporting assets right with the link.\n- Manage attachments from the bookmark detail view: upload, download, or detach as needed.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/04-using-karakeep/import.md",
    "content": "---\nsidebar_position: 8\nslug: import\n---\n\n# Import your library\n\n\nKarakeep supports importing bookmarks using the Netscape HTML Format, Pocket's new CSV format & Omnivore's JSONs. Titles, tags and addition date will be preserved during the import. An automatically created list will contain all the imported bookmarks.\n\n:::info\nAll the URLs in the bookmarks file will be added automatically, you will not be able to pick and choose which bookmarks to import!\n:::\n\n## Import from Chrome\n\n- Open Chrome and go to `chrome://bookmarks`\n- Click on the three dots on the top right corner and choose `Export bookmarks`\n- This will download an html file with all of your bookmarks.\n- To import the bookmark file, go to the settings and click \"Import Bookmarks from HTML file\".\n\n## Import from Firefox\n- Open Firefox and click on the menu button (☰) in the top right corner.\n- Navigate to Bookmarks > Manage bookmarks (or press Ctrl + Shift + O / Cmd + Shift + O to open the Bookmarks Library).\n- In the Bookmarks Library, click the Import and Backup button at the top. Select Export Bookmarks to HTML... to save your bookmarks as an HTML file.\n- To import a bookmark file, go back to the Import and Backup menu, then select Import Bookmarks from HTML... and choose your saved HTML file.\n\n## Import from Pocket\n\n- Go to the [Pocket export page](https://getpocket.com/export) and follow the instructions to export your bookmarks.\n- Pocket after a couple of minutes will mail you a zip file with all the bookmarks.\n- Unzip the file and you'll get a CSV file.\n- To import the bookmark file, go to the settings and click \"Import Bookmarks from Pocket export\".\n\n## Import from Omnivore\n\n- Follow Omnivore's [documentation](https://docs.omnivore.app/using/exporting.html) to export your bookmarks.\n- This will give you a zip file with all your data.\n- The zip file contains a lot of JSONs in the format `metadata_*.json`. You can either import every JSON file manually, or merge the JSONs into a single JSON file and import that.\n- To  merge the JSONs into a single JSON file, you can use the following command in the unzipped directory: `jq -r '.[]' metadata_*.json | jq -s > omnivore.json` and then import the `omnivore.json` file. You'll need to have the [jq](https://github.com/jqlang/jq) tool installed.\n\n## Import using the CLI\n\n:::warning\nImporting bookmarks using the CLI requires some technical knowledge and might not be very straightforward for non-technical users. Don't hesitate to ask questions in github discussions or discord though.\n:::\n\nIf you can get your bookmarks in a text file with one link per line, you can use the following command to import them using the [karakeep cli](../05-integrations/02-command-line.md):\n\n```\nwhile IFS= read -r url; do\n    karakeep --api-key \"<KEY>\" --server-addr \"<SERVER_ADDR>\" bookmarks add --link \"$url\"\ndone < all_links.txt\n```\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/04-using-karakeep/lists.md",
    "content": "---\nsidebar_position: 2\n---\n\n# Lists\n\nLists are the core organizational layer in Karakeep. Every saved item can sit in multiple lists so you can group links by project, topic, or audience without duplicating them.\n\n## Manual lists\n\n- Curated sets you add bookmarks to by hand. Great for projects, reading queues, or hand-picked collections.\n- Can be **private** (visible only to you) or **public** (share a read-only link).\n- Can be **collaborative**: invite people by email as viewers or editors. Editors can add their own bookmarks; viewers can browse. Your personal states (favourite/archive) stay yours even inside a shared list.\n\n## Smart lists\n\n- Auto-updating lists powered by a saved search query (e.g. `#ai -archived`).\n- Best for dynamic views like `Youtube links added last week` or `All reddit links from r/selfhosted`.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/04-using-karakeep/quick-sharing.md",
    "content": "---\nsidebar_position: 7\nslug: quick-sharing\n---\n\n# Quick capture and sharing\n\nThe whole point of Karakeep is making it easy to hoard the content. That's why there are a couple of \n\n## Mobile Apps\n\n<img src=\"/img/quick-sharing/mobile.png\" alt=\"mobile screenshot\" width=\"300\"/>\n\n\n- **iOS app**: [App Store Link](https://apps.apple.com/us/app/karakeep-app/id6479258022).\n- **Android App**: [Play Store link](https://play.google.com/store/apps/details?id=app.hoarder.hoardermobile&pcampaignid=web_share).\n\n## Browser Extensions\n\n<img src=\"/img/quick-sharing/extension.png\" alt=\"mobile screenshot\" width=\"300\"/>\n\n- **Chrome extension**: [here](https://chromewebstore.google.com/detail/karakeep/kgcjekpmcjjogibpjebkhaanilehneje).\n- **Firefox addon**: [here](https://addons.mozilla.org/en-US/firefox/addon/karakeep/).\n\n- ## Community Extensions\n- **Safari extension**: [App Store Link](https://apps.apple.com/us/app/karakeeper-bookmarker/id6746722790).  For macOS and iOS to allow a simple way to add your bookmarks to your self hosted karakeep instance.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/04-using-karakeep/search-query-language.md",
    "content": "---\nsidebar_position: 9\nslug: search-query-language\n---\n\n# Search Query Language\n\nKarakeep provides a search query language to filter and find bookmarks. Here are all the supported qualifiers and how to use them:\n\n## Basic Syntax\n\n- Use spaces to separate multiple conditions (implicit AND)\n- Use `and`/`or` keywords for explicit boolean logic\n- Prefix qualifiers with `-` to negate them\n- Use parentheses `()` for grouping conditions (note that groups can't be negated)\n\n## Qualifiers\n\nHere's a comprehensive table of all supported qualifiers:\n\n| Qualifier                        | Description                                                                                                                                                                                               | Example Usage                                |\n| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |\n| `is:fav`                         | Favorited bookmarks                                                                                                                                                                                       | `is:fav`                                     |\n| `is:archived`                    | Archived bookmarks                                                                                                                                                                                        | `-is:archived`                               |\n| `is:tagged`                      | Bookmarks that has one or more tags                                                                                                                                                                       | `is:tagged`                                  |\n| `is:inlist`                      | Bookmarks that are in one or more lists                                                                                                                                                                   | `is:inlist`                                  |\n| `is:link`, `is:text`, `is:media` | Bookmarks that are of type link, text or media                                                                                                                                                            | `is:link`                                    |\n| `is:broken`                      | Bookmarks with broken/failed links (crawl failures or non-2xx status codes)                                                                                                                               | `is:broken`                                  |\n| `url:<value>`                    | Match bookmarks with URL substring                                                                                                                                                                        | `url:example.com`                            |\n| `title:<value>`                  | Match bookmarks with title substring                                                                                                               | `title:example`                              |\n|                                  | Supports quoted strings for titles with spaces                                                                                                   | `title:\"my title\"`                           |\n| `#<tag>`                         | Match bookmarks with specific tag                                                                                                                                                                         | `#important`                                 |\n|                                  | Supports quoted strings for tags with spaces                                                                                                                                                              | `#\"work in progress\"`                        |\n| `list:<name>`                    | Match bookmarks in specific list                                                                                                                                                                          | `list:reading`                               |\n|                                  | Supports quoted strings for list names with spaces                                                                                                                                                        | `list:\"to review\"`                           |\n| `after:<date>`                   | Bookmarks created on or after date (YYYY-MM-DD)                                                                                                                                                           | `after:2023-01-01`                           |\n| `before:<date>`                  | Bookmarks created on or before date (YYYY-MM-DD)                                                                                                                                                          | `before:2023-12-31`                          |\n| `feed:<name>`                    | Bookmarks imported from a particular rss feed                                                                                                                                                             | `feed:Hackernews`                            |\n| `age:<time-range>`               | Match bookmarks based on how long ago they were created. Use `<` or `>` to indicate the maximum / minimum age of the bookmarks. Supported units: `d` (days), `w` (weeks), `m` (months), `y` (years). | `age:<1d` `age:>2w` `age:<6m` `age:>3y` |\n\n### Examples\n\n```plaintext\n# Find favorited bookmarks from 2023 that are tagged \"important\"\nis:fav after:2023-01-01 before:2023-12-31 #important\n\n# Find archived bookmarks that are either in \"reading\" list or tagged \"work\"\nis:archived and (list:reading or #work)\n\n# Find bookmarks that are not tagged or not in any list\n-is:tagged or -is:inlist\n# Find bookmarks with \"React\" in the title\ntitle:React\n```\n\n## Combining Conditions\n\nYou can combine multiple conditions using boolean logic:\n\n```plaintext\n# Find favorited bookmarks from 2023 that are tagged \"important\"\nis:fav after:2023-01-01 before:2023-12-31 #important\n\n# Find archived bookmarks that are either in \"reading\" list or tagged \"work\"\nis:archived and (list:reading or #work)\n\n# Find bookmarks that are not favorited and not archived\n-is:fav -is:archived\n```\n\n## Text Search\n\nAny text not part of a qualifier will be treated as a full-text search:\n\n```plaintext\n# Search for \"machine learning\" in bookmark content\nmachine learning\n\n# Combine text search with qualifiers\nmachine learning is:fav\n```\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/04-using-karakeep/tags.md",
    "content": "---\nsidebar_position: 3\n---\n\n# Tags\n\nTags are lightweight labels you can attach to any bookmark to add meaning without rigid folders.\n\n- Use tags to capture topics, sources, people, or workflow states (e.g. `ai`, `design`, `to-read`).\n- Combine multiple tags to filter or build smart lists; tags travel with a bookmark wherever it appears.\n- AI tags might look a little messy, but the extra labels make finding things easier. Use tags for broad discovery and lists when you want a clean, hand-picked setup.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/05-integrations/02-command-line.md",
    "content": "# Command Line Tool (CLI)\n\nKarakeep comes with a simple CLI for those users who want to do more advanced manipulation.\n\n## Features\n\n- Manipulate bookmarks, lists and tags\n- Mass import/export of bookmarks\n\n## Installation (NPM)\n\n```\nnpm install -g @karakeep/cli\n```\n\n\n## Installation (Docker)\n\n```\ndocker run --rm ghcr.io/karakeep-app/karakeep-cli:release --help\n```\n\n## Usage\n\n```\nkarakeep\n```\n\n```\nUsage: karakeep [options] [command]\n\nA CLI interface to interact with the karakeep api\n\nOptions:\n  --api-key <key>       the API key to interact with the API (env: KARAKEEP_API_KEY)\n  --server-addr <addr>  the address of the server to connect to (env: KARAKEEP_SERVER_ADDR)\n  -V, --version         output the version number\n  -h, --help            display help for command\n\nCommands:\n  bookmarks             manipulating bookmarks\n  lists                 manipulating lists\n  tags                  manipulating tags\n  whoami                returns info about the owner of this API key\n  help [command]        display help for command\n```\n\nAnd some of the subcommands:\n\n```\nkarakeep bookmarks\n```\n\n```\nUsage: karakeep bookmarks [options] [command]\n\nManipulating bookmarks\n\nOptions:\n  -h, --help             display help for command\n\nCommands:\n  add [options]          creates a new bookmark\n  get <id>               fetch information about a bookmark\n  update [options] <id>  updates bookmark\n  list [options]         list all bookmarks\n  delete <id>            delete a bookmark\n  help [command]         display help for command\n\n```\n\n```\nkarakeep lists\n```\n\n```\nUsage: karakeep lists [options] [command]\n\nManipulating lists\n\nOptions:\n  -h, --help                 display help for command\n\nCommands:\n  list                       lists all lists\n  delete <id>                deletes a list\n  add-bookmark [options]     add a bookmark to list\n  remove-bookmark [options]  remove a bookmark from list\n  help [command]             display help for command\n```\n\n## Obtaining an API Key\n\nTo use the CLI, you'll need to get an API key from your karakeep settings. You can validate that it's working by running:\n\n```\nkarakeep --api-key <key> --server-addr <addr> whoami\n```\n\nFor example:\n\n```\nkarakeep --api-key mysupersecretkey --server-addr https://try.karakeep.app whoami\n{\n  id: 'j29gnbzxxd01q74j2lu88tnb',\n  name: 'Test User',\n  email: 'test@gmail.com'\n}\n```\n\n\n## Other clients\n\nThere also exists a **non-official**, community-maintained, python package called [karakeep-python-api](https://github.com/thiswillbeyourgithub/karakeep_python_api) that can be accessed from the CLI, but is **not** official.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/05-integrations/03-mcp.md",
    "content": "# Model Context Protocol Server (MCP)\n\nKarakeep comes with a Model Context Protocol server that can be used to interact with it through LLMs.\n\n## Supported Tools\n\n- Searching bookmarks\n- Adding and removing bookmarks from lists\n- Attaching and detaching tags to bookmarks\n- Creating new lists\n- Creating text and URL bookmarks\n\n\n## Usage with Claude Desktop\n\nFrom NPM:\n\n```json\n{\n  \"mcpServers\": {\n    \"karakeep\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"@karakeep/mcp\"\n      ],\n      \"env\": {\n        \"KARAKEEP_API_ADDR\": \"https://<YOUR_SERVER_ADDR>\",\n        \"KARAKEEP_API_KEY\": \"<YOUR_TOKEN>\"\n      }\n    }\n  }\n}\n```\n\nFrom Docker:\n\n```json\n{\n  \"mcpServers\": {\n    \"karakeep\": {\n      \"command\": \"docker\",\n      \"args\": [\n        \"run\",\n        \"-e\",\n        \"KARAKEEP_API_ADDR=https://<YOUR_SERVER_ADDR>\",\n        \"-e\",\n        \"KARAKEEP_API_KEY=<YOUR_TOKEN>\",\n        \"ghcr.io/karakeep-app/karakeep-mcp:latest\"\n      ]\n    }\n  }\n}\n```\n\n\n### Demo\n\n#### Search\n![mcp-1](/img/mcp-1.gif)\n\n#### Adding Text Bookmarks\n![mcp-2](/img/mcp-2.gif)\n\n#### Adding URL Bookmarks\n![mcp-2](/img/mcp-3.gif)\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/05-integrations/05-singlefile.md",
    "content": "# Using Karakeep with SingleFile Extension\n\nKarakeep supports being a destination for the [SingleFile extension](https://github.com/gildas-lormeau/SingleFile). This has the benefit of allowing you to use the singlefile extension to hoard links as you're seeing them in the browser. This is perfect for websites that don't like to get crawled, has annoying cookie banner or require authentication.\n\n## Setup\n\n1. Install the [SingleFile extension](https://github.com/gildas-lormeau/SingleFile).\n2. In the extension settings, select `Destinations`.\n3. Select `upload to a REST Form API`.\n4. In the URL, insert the address: `https://YOUR_SERVER_ADDRESS/api/v1/bookmarks/singlefile`.\n5. In the `authorization token` field, paste an API key that you can get from your karakeep settings.\n6. Set `data field name` to `file`.\n7. Set `URL field name` to `url`.\n8. (Optional) Add `&ifexists=MODE` to the URL where MODE is one of `skip`, `overwrite`, `overwrite-recrawl`, `append`, or `append-recrawl`. See \"Handling Existing Bookmarks\" section below for details.\n\nNow, go to any page and click the singlefile extension icon. Once it's done with the upload, the bookmark should show up in your karakeep instance. Note that the singlefile extension doesn't show any progress on the upload. Given that archives are typically large, it might take 30+ seconds until the upload is done and starts showing up in Karakeep.\n\n## Handling Existing Bookmarks\n\nWhen uploading a page that already exists in your archive (same URL), you can control the behavior by setting the `ifexists` query parameter in the upload URL. The available modes are:\n\n- `skip` (default): If the bookmark already exists, skip creating a new one\n- `overwrite`: Replace existing precrawled archive (only the most recent archive is kept)\n- `overwrite-recrawl`: Replace existing archive and queue a recrawl to update content\n- `append`: Add new archive version alongside existing ones\n- `append-recrawl`: Add new archive and queue a recrawl\n\nTo use these modes, append `?ifexists=MODE` to your upload URL, replacing `MODE` with your desired behavior.\n\nFor example:  \n`https://YOUR_SERVER_ADDRESS/api/v1/bookmarks/singlefile?ifexists=overwrite`\n\n\n## Recommended settings\n\nIn the singlefile extension, you probably will want to change the following settings for better experience:\n* Stylesheets > compress CSS content: on\n* Stylesheets > group duplicate stylesheets together: on\n* HTML content > remove frames: on\n\nAlso, you most likely will want to change the default `MAX_ASSET_SIZE_MB` in karakeep to something higher, for example `100`.\n\n:::info\nCurrently, we don't support screenshots for singlefile uploads, but this will change in the future.\n:::\n\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/05-integrations/06-rss-feeds.md",
    "content": "# RSS Feeds\n\nKarakeep offers RSS feed integration, allowing you to both consume RSS feeds from external sources and publish your lists as RSS feeds for others to subscribe to.\n\n## Publishing RSS Feeds\n\nYou can publish any of your lists as an RSS feed, making it easy to share your bookmarks with others or integrate them into RSS readers.\n\n### Enabling RSS for a List\n\n1. Navigate to one of your lists\n2. Click on the list settings (three dots menu)\n3. Toggle the \"RSS Feed\" switch to enable it\n4. Copy the generated RSS feed URL\n\n### What Gets Published\n\nRSS feeds include:\n- **Links**: Bookmarks of type \"link\" with their URL, title, description, and author\n- **Assets**: Uploaded files (PDFs, images) are included with a link to view them\n- **Tags**: Bookmark tags are exported as RSS categories\n- **Dates**: The bookmark creation date is used as the publication date\n\nNote: Text notes are not included in RSS feeds as they don't have an associated URL.\n\n### Security Considerations\n\n- Each RSS feed requires a unique token for access\n- Tokens can be regenerated at any time, which will invalidate the old URL\n- Disabling RSS for a list immediately revokes access\n\n## Consuming RSS Feeds\n\nKarakeep can automatically monitor RSS feeds and create bookmarks from new entries, making it perfect for staying up to date with blogs, news sites, and other content sources.\n\n### Adding an RSS Feed\n\n1. Go to **Settings** → **RSS Feeds**\n2. Click **Add Feed**\n3. Enter the feed details:\n   - **Name**: A friendly name for the feed\n   - **URL**: The RSS/Atom feed URL\n   - **Enabled**: Toggle to enable/disable the feed\n   - **Import Tags**: Enable to import RSS categories as bookmark tags\n\n### How It Works\n\n- Karakeep checks enabled RSS feeds **every hour**\n- New entries are automatically created as bookmarks\n- Duplicate entries are automatically detected and skipped\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/05-integrations/_category_.json",
    "content": "{\n  \"label\": \"Integrations\",\n  \"position\": 5\n}\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/06-administration/01-security-considerations.md",
    "content": "# Security Considerations\n\nIf you're going to give app access to untrusted users, there's some security considerations that you'll need to be aware of given how the crawler works. The crawler is basically running a browser to fetch the content of the bookmarks. Any untrusted user can submit bookmarks to be crawled from your server and they'll be able to see the crawling result. This can be abused in multiple ways:\n\n1. Untrusted users can submit crawl requests to websites that you don't want to be coming out of your IPs.\n2. Crawling user controlled websites can expose your origin IP (and location) even if your service is hosted behind cloudflare for example.\n3. The crawling requests will be coming out from your own network, which untrusted users can leverage to crawl internal non-internet exposed endpoints.\n\nTo mitigate those risks, you can do one of the following:\n\n1. Limit access to trusted users\n2. Let the browser traffic go through some VPN with restricted network policies.\n3. Host the browser container outside of your network.\n4. Use a hosted browser as a service (e.g. [browserless](https://browserless.io)). Note: I've never used them before.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/06-administration/02-FAQ.md",
    "content": "# Frequently Asked Questions (FAQ)\r\n\r\n## User Management\r\n\r\n### Lost password\r\n\r\n#### If you are not an administrator\r\n\r\nAdministrators can reset the password of any user. Contact an administrator to reset the password for you.\r\n\r\n- Navigate to the `Admin Settings` page\r\n- Find the user in the `Users List`\r\n- In the `Actions` column, there is a button to reset the password\r\n- Enter a new password and press `Reset`\r\n- The new password is now set\r\n- If required, you can change your password again (so the admin does not know your password) in the `User Settings`\r\n\r\n#### If you are an administrator\r\n\r\nIf you are an administrator and lost your password, you have to reset the password in the database.\r\n\r\nTo reset the password:\r\n\r\n- Acquire some kind of tools that helps you to connect to the database:\r\n  - `sqlite3` on Linux: run `apt-get install sqlite3` (depending on your package manager)\r\n  - e.g. `dbeaver` on Windows\r\n- Shut down Karakeep\r\n- Connect to the `db.db` database, which is located in the `data` directory you have mounted to your docker container:\r\n  - by e.g. running `sqlite3 db.db` (in your `data` directory)\r\n  - or going through e.g. the `dbeaver` UI to locate the file in the data directory and connecting to it\r\n- Update the password in the database by running:\r\n  - `update user set password='$2a$10$5u40XUq/cD/TmLdCOyZ82ePENE6hpkbodJhsp7.e/BgZssUO5DDTa', salt='' where email='<YOUR_EMAIL_HERE>';`\r\n  - (don't forget to put your email address into the command)\r\n- The new password for your user is now `adminadmin`.\r\n- Start Karakeep again\r\n- Log in with your email address and the password `adminadmin` and change the password to whatever you want in the `User Settings`\r\n\r\n### Adding another administrator\r\n\r\nBy default, the first user to sign up gets promoted to administrator automatically.\r\n\r\nIn case you want to grant those permissions to another user:\r\n\r\n- Navigate to the `Admin Settings` page\r\n- Find the user in the `Users List`\r\n- In the `Actions` column, there is a button to change the Role\r\n- Change the Role to `Admin`\r\n- Press `Change`\r\n- The new administrator has to log out and log in again to get the new role assigned\r\n\r\n### Adding new users, when signups are disabled\r\n\r\nAdministrators can create new accounts any time:\r\n\r\n- Navigate to the `Admin Settings` page\r\n- Go to the `Users List`\r\n- Press the `Create User` Button.\r\n- Enter the information for the user\r\n- Press `create`\r\n- The new user can now log in\r\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/06-administration/03-openai.md",
    "content": "# Tagging Costs\n\nThis service uses OpenAI for automatic tagging. This means that you'll incur some costs if automatic tagging is enabled. There are two type of inferences that we do:\n\n## Text Tagging\n\nFor text tagging, we use the `gpt-4.1-mini` model. This model is [extremely cheap](https://openai.com/api/pricing). Cost per inference varies depending on the content size per article. Though, roughly, You'll be able to generate tags for almost 3000+ bookmarks for less than $1.\n\n## Image Tagging\n\nFor image uploads, we use the `gpt-4o-mini` model for extracting tags from the image. You can learn more about the costs of using this model [here](https://platform.openai.com/docs/guides/images?api-mode=chat#calculating-costs). To lower the costs, we're using the low resolution mode (fixed number of tokens regardless of image size). You'll be able to run inference for 1000+ images for less than a $1.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/06-administration/05-troubleshooting.md",
    "content": "# Troubleshooting\n\n## SqliteError: no such table: user\n\nThis usually means that there's something wrong with the database setup (more concretely, it means that the database is not initialized). This can be caused by multiple problems:\n1. **Wiped DATA_DIR:** Your `DATA_DIR` got wiped (or the backing storage dir changed). If you did this intentionally, restart the container so that it can re-initalize the database.\n2. **Missing DATA_DIR**: You're not using the default docker compose file, and you forgot to configure the `DATA_DIR` env var. This will result into the database getting set up in a different directory than the one used by the service.\n\n## Chrome Failed to Read DnsConfig\n\nIf you see this error in the logs of the chrome container, it's a benign error and you can safely ignore it. Whatever problems you're having, is unrelated to this error.\n\n## AI Tagging not working (when using OpenAI)\n\nCheck the logs of the container and this will usually tell you what's wrong. Common problems are:\n1. Typo in the env variable `OPENAI_API_KEY` name resulting into logs saying something like \"skipping inference as it's not configured\".\n2. You forgot to call `docker compose up` after configuring open ai.\n3. OpenAI requires pre-charging the account with credits before using it, otherwise you'll get an error like \"insufficient funds\".\n\n## AI Tagging not working (when using Ollama)\n\nCheck the logs of the container and this will usually tell you what's wrong. Common problems are:\n1. Typo in the env variable `OLLAMA_BASE_URL` name resulting into logs saying something like \"skipping inference as it's not configured\".\n2. You forgot to call `docker compose up` after configuring ollama.\n3. You didn't change the `INFERENCE_TEXT_MODEL` env variable, resulting into karakeep attempting to use gpt models with ollama which won't work.\n4. Ollama server is not reachable by the karakeep container. This can be caused by:\n    1. Ollama server being in a different docker network than the karakeep container.\n    2. You're using `localhost` as the `OLLAMA_BASE_URL` instead of the actual address of the ollama server. `localhost` points to the container itself, not the docker host. Check this [stackoverflow answer](https://stackoverflow.com/questions/24319662/from-inside-of-a-docker-container-how-do-i-connect-to-the-localhost-of-the-mach) to find how to correctly point to the docker host address instead.\n\n## Crawling not working\n\nCheck the logs of the container and this will usually tell you what's wrong. Common problems are:\n1. You changed the name of the chrome container but didn't change the `BROWSER_WEB_URL` env variable.\n\n## Upgrading Meilisearch - Migrating the Meilisearch db version\n\n[Meilisearch](https://www.meilisearch.com/) is the database used by karakeep for searching in your bookmarks. The version used by karakeep is `1.13.3` and it is advised not to upgrade it without good reasons. If you do, you might see errors like `Your database version (1.11.1) is incompatible with your current engine version (1.13.3). To migrate data between Meilisearch versions, please follow our guide on https://www.meilisearch.com/docs/learn/update_and_migration/updating.`.\n\nLuckily we can easily workaround this:\n1. Stop the Meilisearch container.\n2. Inside the Meilisearch volume bound to `/meili_data`, erase/rename the folder called `data.ms`.\n3. Launch Meilisearch again.\n4. Login to karakeep as administrator and go to (as of v0.24.1) `Admin Settings > Background Jobs` then click on `Reindex All Bookmarks`.\n5. When the reindexing has finished, Meilisearch should be working as usual.\n\nIf you run into issues, the official documentation can be found [there](https://www.meilisearch.com/docs/learn/update_and_migration/updating).\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/06-administration/06-server-migration.md",
    "content": "# Migrating Between Servers\n\nThis guide explains how to migrate all of your data from one Karakeep server to another using the official CLI.\n\n## What the command does\n\nThe migration copies user-owned data from a source server to a destination server in this order:\n\n- User settings\n- Lists (preserving hierarchy and settings)\n- RSS feeds\n- AI prompts (custom prompts and their enabled state)\n- Webhooks (URL and events)\n- Tags (ensures tags by name exist)\n- Rule engine rules (IDs remapped to destination equivalents)\n- Bookmarks (links, text, and assets)\n  - After creation, attaches the correct tags and adds to the correct lists\n\nNotes:\n- Webhook tokens cannot be read via the API, so tokens are not migrated. Re‑add them on the destination if needed.\n- Asset bookmarks are migrated by downloading the original asset and re‑uploading it to the destination. Only images and PDFs are supported for asset bookmarks.\n- Link bookmarks on the destination may be de‑duplicated if the same URL already exists.\n\n## Prerequisites\n\n- Install the CLI:\n  - NPM: `npm install -g @karakeep/cli`\n  - Docker: `docker run --rm ghcr.io/karakeep-app/karakeep-cli:release --help`\n- Collect API keys and base URLs for both servers:\n  - Source: `--server-addr`, `--api-key`\n  - Destination: `--dest-server`, `--dest-api-key`\n\n## Quick start\n\n```\nkarakeep --server-addr https://src.example.com --api-key <SOURCE_API_KEY> migrate \\\n  --dest-server https://dest.example.com \\\n  --dest-api-key <DEST_API_KEY>\n```\n\nThe command is long‑running and shows live progress for each phase. You will be prompted for confirmation; pass `--yes` to skip the prompt.\n\n### Options\n\n- `--server-addr <url>`: Source server base URL\n- `--api-key <key>`: API key for the source server\n- `--dest-server <url>`: Destination server base URL\n- `--dest-api-key <key>`: API key for the destination server\n- `--batch-size <n>`: Page size for bookmark migration (default 50, max 100)\n- `-y`, `--yes`: Skip the confirmation prompt\n\n## What to expect\n\n- Lists are recreated parent‑first and retain their hierarchy.\n- Feeds, prompts, webhooks, and tags are recreated by value.\n- Rules are recreated after IDs (tags, lists, feeds) are remapped to their corresponding destination IDs.\n- After each bookmark is created, the command attaches the correct tags and adds it to the correct lists.\n\n## Caveats and tips\n\n- Webhook auth tokens must be re‑entered on the destination after migration.\n- If your destination already contains data, duplicate links may be de‑duplicated; tags and list membership are still applied to the existing bookmark.\n\n## Troubleshooting\n\n- If the command exits early, you can re‑run it, but note:\n  - Tags and lists that already exist are reused.\n  - Link de‑duplication avoids duplicate link bookmarks. Notes and assets will get re-created.\n  - Rules, webhooks, rss feeds will get re-created and you'll have to manually clean them up afterwards.\n  - The progress log indicates how far it got.\n- Use a smaller `--batch-size` if your source or destination is under heavy load.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/06-administration/07-legacy-container-upgrade.md",
    "content": "# Legacy Container Upgrade\n\nKarakeep's 0.16 release consolidated the web and worker containers into a single container and also dropped the need for the redis container. The legacy containers will stop being supported soon, to upgrade to the new container do the following:\n\n1. Remove the redis container and its volume if it had one.\n2. Move the environment variables that you've set exclusively to the `workers` container to the `web` container.\n3. Delete the `workers` container.\n4. Rename the web container image from `hoarder-app/hoarder-web` to `hoarder-app/hoarder`.\n\n```diff\ndiff --git a/docker/docker-compose.yml b/docker/docker-compose.yml\nindex cdfc908..6297563 100644\n--- a/docker/docker-compose.yml\n+++ b/docker/docker-compose.yml\n@@ -1,7 +1,7 @@\n version: \"3.8\"\n services:\n   web:\n-    image: ghcr.io/hoarder-app/hoarder-web:${KARAKEEP_VERSION:-release}\n+    image: ghcr.io/karakeep-app/karakeep:${KARAKEEP_VERSION:-release}\n     restart: unless-stopped\n     volumes:\n       - data:/data\n@@ -10,14 +10,10 @@ services:\n     env_file:\n       - .env\n     environment:\n-      REDIS_HOST: redis\n       MEILI_ADDR: http://meilisearch:7700\n+      BROWSER_WEB_URL: http://chrome:9222\n+      # OPENAI_API_KEY: ...\n       DATA_DIR: /data\n-  redis:\n-    image: redis:7.2-alpine\n-    restart: unless-stopped\n-    volumes:\n-      - redis:/data\n   chrome:\n     image: gcr.io/zenika-hub/alpine-chrome:123\n     restart: unless-stopped\n@@ -37,24 +33,7 @@ services:\n       MEILI_NO_ANALYTICS: \"true\"\n     volumes:\n       - meilisearch:/meili_data\n-  workers:\n-    image: ghcr.io/hoarder-app/hoarder-workers:${KARAKEEP_VERSION:-release}\n-    restart: unless-stopped\n-    volumes:\n-      - data:/data\n-    env_file:\n-      - .env\n-    environment:\n-      REDIS_HOST: redis\n-      MEILI_ADDR: http://meilisearch:7700\n-      BROWSER_WEB_URL: http://chrome:9222\n-      DATA_DIR: /data\n-      # OPENAI_API_KEY: ...\n-    depends_on:\n-      web:\n-        condition: service_started\n\n volumes:\n-  redis:\n   meilisearch:\n   data:\n```\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/06-administration/08-hoarder-to-karakeep-migration.md",
    "content": "# Hoarder to Karakeep Migration\n\nHoarder is rebranding to Karakeep. Due to github limitations, the old docker image might not be getting new updates after the rebranding. You might need to update your docker image to point to the new karakeep image instead by applying the following change in the docker compose file.\n\n```diff\ndiff --git a/docker/docker-compose.yml b/docker/docker-compose.yml\nindex cdfc908..6297563 100644\n--- a/docker/docker-compose.yml\n+++ b/docker/docker-compose.yml\n@@ -1,7 +1,7 @@\n version: \"3.8\"\n services:\n   web:\n-    image: ghcr.io/hoarder-app/hoarder:${HOARDER_VERSION:-release}\n+    image: ghcr.io/karakeep-app/karakeep:${HOARDER_VERSION:-release}\n```\n\nYou can also change the `HOARDER_VERSION` environment variable but if you do so remember to change it in the `.env` file as well.\n\n## Migrating a Baremetal Installation\n\nIf you previously used the [Debian/Ubuntu install script](../02-installation/06-debuntu.md) to install Hoarder, there is an option to migrate your installation to Karakeep.\n\n```bash\nbash karakeep-linux.sh migrate\n```\n\nThis will migrate your installation with no user input required. After the migration, the script will also check for an update.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/06-administration/_category_.json",
    "content": "{\n  \"label\": \"Administration\",\n  \"position\": 6\n}\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/07-community/01-community-projects.md",
    "content": "# Community Projects\n\nThis page lists community projects that are built around Karakeep, but not officially supported by the development team.\n\n:::warning\nThis list comes with no guarantees about security, performance, reliability, or accuracy. Use at your own risk.\n:::\n\n### Raycast Extension\n\n_By [@luolei](https://github.com/foru17)._\n\nA user-friendly Raycast extension that seamlessly integrates with Karakeep, bringing powerful bookmark management to your fingertips. Quickly save, search, and organize your bookmarks, texts, and images—all through Raycast's intuitive interface.\n\nGet it [here](https://www.raycast.com/luolei/karakeep).\n\n### Alfred Workflow\n\n_By [@yinan-c](https://github.com/yinan-c)_\n\nAn Alfred workflow to quickly hoard stuff or access your hoarded bookmarks!\n\nGet it [here](https://www.alfredforum.com/topic/22528-hoarder-workflow-for-self-hosted-bookmark-management/).\n\n### Obsidian Plugin\n\n_By [@jhofker](https://github.com/jhofker)_\n\nAn Obsidian plugin that syncs your Karakeep bookmarks with Obsidian, creating markdown notes for each bookmark in a designated folder.\n\nGet it [here](https://github.com/jhofker/obsidian-hoarder/), or install it directly from Obsidian's community plugin store ([link](https://obsidian.md/plugins?id=hoarder-sync)).\n\n### Telegram Bot\n\n_By [@Madh93](https://github.com/Madh93)_\n\nA Telegram Bot for saving bookmarks to Karakeep directly through Telegram.\n\nGet it [here](https://github.com/Madh93/karakeepbot).\n\n### Hoarder's Pipette\n\n_By [@DanSnow](https://github.com/DanSnow)_\n\nA chrome extension that injects karakeep's bookmarks into your search results.\n\nGet it [here](https://dansnow.github.io/hoarder-pipette/guides/installation/).\n\n### Karakeep-Python-API\n\n_By [@thiswillbeyourgithub](https://github.com/thiswillbeyourgithub/)_\n\nA python package to simplify access to the karakeep API. Can be used as a library or from the CLI. Aims for feature completeness and high test coverage but do check its feature matrix before relying too much on it.\n\nIts repository also hosts the [Community Script](https://github.com/thiswillbeyourgithub/karakeep_python_api/tree/main/community_scripts), for example:\n\n| Community Script | Description | Documentation |\n|----------------|-------------|---------------|\n| **Karakeep-Time-Tagger** | Automatically adds time-to-read tags (`0-5m`, `5-10m`, etc.) to bookmarks based on content length analysis. Includes systemd service and timer files for automated periodic execution. | [`Link`](https://github.com/thiswillbeyourgithub/karakeep_python_api/tree/main/community_scripts/karakeep-time-tagger) |\n| **Karakeep-List-To-Tag** | Converts a Karakeep list into tags by adding a specified tag to all bookmarks within that list. | [`Link`](https://github.com/thiswillbeyourgithub/karakeep_python_api/tree/main/community_scripts/karakeep-list-to-tag) |\n| **Omnivore2Karakeep-Highlights** | Imports highlights from Omnivore export data to Karakeep, with intelligent position detection and bookmark matching. Supports dry-run mode for testing. | [`Link`](https://github.com/thiswillbeyourgithub/karakeep_python_api/tree/main/community_scripts/omnivore2karakeep-highlights) |\n\n\nGet it [here](https://github.com/thiswillbeyourgithub/karakeep_python_api).\n\n### FreshRSS_to_Karakeep\n\n_By [@thiswillbeyourgithub](https://github.com/thiswillbeyourgithub/)_\n\nA python script to automatically create Karakeep bookmarks from your [FreshRSS](https://github.com/FreshRSS/FreshRSS) *favourites/saved* RSS item. Made to be called periodically. Based on the community project `Karakeep-Python-API` above, by the same author.\n\nGet it [here](https://github.com/thiswillbeyourgithub/freshrss_to_karakeep).\n\n### karakeep-sync\n_By [@sidoshi](https://github.com/sidoshi/)_\n\nSync links from Hacker News upvotes, Reddit Saves to Karakeep for centralized bookmark management.\n\nGet it [here](https://github.com/sidoshi/karakeep-sync)\n\n### Home Assistant Integration\n\n_By [@sli-cka](https://github.com/sli-cka)_\n\nA custom integration that brings Karakeep data into Home Assistant. It exposes your Karakeep statistics data (like lists, bookmarks, tag, etc.) as Home Assistant entities, enabling dashboards, automations, and notifications based on your Karakeep data.\n\nGet it [here](https://github.com/sli-cka/karakeep-homeassistant)\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/07-community/02-community-channels.md",
    "content": "# Community Channels\n\nStay connected with the Karakeep team and community for updates, support, and feature discussions.\n\n## Discord\n\n- Join the official server: [discord.gg/NrgeYywsFh](https://discord.gg/NrgeYywsFh)\n- Great for getting help, sharing setups, and chatting with the team and other users.\n\n## Twitter / X\n\n- Follow [@karakeep_app](https://twitter.com/karakeep_app) for release announcements, tips, and product news.\n- DM or tag us with feedback or things you'd like to see next.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/07-community/_category_.json",
    "content": "{\n  \"label\": \"Community\",\n  \"position\": 7\n}\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/08-development/01-setup.md",
    "content": "# Setup\n\n## Quick Start\n\nFor the fastest way to get started with development, use the one-command setup script:\n\n```bash\n./start-dev.sh\n```\n\nThis script will automatically:\n- Start Meilisearch in Docker (on port 7700)\n- Start headless Chrome in Docker (on port 9222) \n- Install dependencies with `pnpm install` if needed\n- Start both the web app and workers in parallel\n- Provide cleanup when you stop with Ctrl+C\n\n**Prerequisites:**\n- Docker installed and running\n- pnpm installed (see manual setup below for installation instructions)\n\nThe script will output the running services:\n- Web app: http://localhost:3000\n- Meilisearch: http://localhost:7700  \n- Chrome debugger: http://localhost:9222\n\nPress Ctrl+C to stop all services and clean up Docker containers.\n\n## Manual Setup\n\nKarakeep uses `node` version 24. To install it, you can use `nvm` [^1]\n\n```\n$ nvm install  24\n```\n\nVerify node version using this command:\n```\n$ node --version\nv24.14.0\n```\n\nKarakeep also makes use of `corepack`[^2]. If you have `node` installed, then `corepack` should already be\ninstalled on your machine, and you don't need to do anything. To verify the `corepack` is installed run:\n\n```\n$ command -v corepack\n/home/<user>/.nvm/versions/node/v24.14.0/bin/corepack\n```\n\nTo enable `corepack` run the following command:\n\n```\n$ corepack enable\n```\n\nThen, from the root of the repository, install the packages and dependencies using:\n\n```\n$ pnpm install\n```\n\nOutput of a successful `pnpm install` run should look something like:\n\n```\nScope: all 20 workspace projects\nLockfile is up to date, resolution step is skipped\nPackages: +3129\n+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\nProgress: resolved 0, reused 2699, downloaded 0, added 3129, done\n\ndevDependencies:\n+ @karakeep/prettier-config 0.1.0 <- tooling/prettier\n\n. prepare$ husky\n└─ Done in 45ms\nDone in 5.5s\n```\n\nYou can now continue with the rest of this documentation.\n\n### First Setup\n\n- You'll need to prepare the environment variables for the dev env.\n- Easiest would be to set it up once in the root of the repo and then symlink it in each app directory (e.g. `/apps/web`, `/apps/workers`) and also `/packages/db`.\n- Start by copying the template by `cp .env.sample .env`.\n- The most important env variables to set are:\n  - `DATA_DIR`: Where the database and assets will be stored. This is the only required env variable. You can use an absolute path so that all apps point to the same dir.\n  - `NEXTAUTH_SECRET`: Random string used to sign the JWT tokens. Generate one with `openssl rand -base64 36`. Logging in will not work if this is missing!\n  - `MEILI_ADDR`: If not set, search will be disabled. You can set it to `http://127.0.0.1:7700` if you run meilisearch using the command below.\n  - `OPENAI_API_KEY`: If you want to enable auto tag inference in the dev env.\n- run `pnpm run db:migrate` in the root of the repo to set up the database.\n\n### Dependencies\n\n#### Meilisearch\n\nMeilisearch is the provider for the full text search (and at some point embeddings search too). You can get it running with `docker run -p 7700:7700 getmeili/meilisearch:v1.13.3`.\n\nMount persistent volume if you want to keep index data across restarts. You can trigger a re-index for the entire items collection in the admin panel in the web app.\n\n#### Chrome\n\nThe worker app will automatically start headless chrome on startup for crawling pages. You don't need to do anything there.\n\n### Web App\n\n- Run `pnpm web` in the root of the repo.\n- Go to `http://localhost:3000`.\n\n> NOTE: The web app kinda works without any dependencies. However, search won't work unless meilisearch is running. Also, new items added won't get crawled/indexed unless workers are running.\n\n### Workers\n\n- Run `pnpm workers` in the root of the repo.\n\n### Mobile App (iOS & Android)\n\n#### Prerequisites\n\nTo build and run the mobile app locally, you'll need:\n\n- **For iOS development**: \n  - macOS computer\n  - Xcode installed from the App Store\n  - iOS Simulator (comes with Xcode)\n\n- **For Android development**:\n  - Android Studio installed\n  - Android SDK configured\n  - Android Emulator or physical device\n\nFor detailed setup instructions, refer to the [Expo documentation](https://docs.expo.dev/guides/local-app-development/).\n\n#### Running the app\n\n- `cd apps/mobile`\n- `pnpm exec expo prebuild --no-install` to build the app.\n\n**For iOS:**\n- `pnpm exec expo run:ios`\n- The app will be installed and started in the simulator.\n\n**Troubleshooting iOS Setup:**\nIf you encounter an error like `xcrun: error: SDK \"iphoneos\" cannot be located`, you may need to set the correct Xcode developer directory:\n```bash\nsudo xcode-select -s /Applications/Xcode.app/Contents/Developer\n```\n\n**For Android:**\n- Start the Android emulator or connect a physical device.\n- `pnpm exec expo run:android`\n- The app will be installed and started on the emulator/device.\n\nChanging the code will hot reload the app. However, installing new packages requires restarting the expo server.\n\n### Browser Extension\n\n- `cd apps/browser-extension`\n- `pnpm dev`\n- This will generate a `dist` package\n- Go to extension settings in chrome and enable developer mode.\n- Press `Load unpacked` and point it to the `dist` directory.\n- The plugin will pop up in the plugin list.\n\nIn dev mode, opening and closing the plugin menu should reload the code.\n\n\n## Docker Dev Env\n\nIf the manual setup is too much hassle for you. You can use a docker based dev environment by running `docker compose -f docker/docker-compose.dev.yml up` in the root of the repo. This setup wasn't super reliable for me though.\n\n\n[^1]: [nvm](https://github.com/nvm-sh/nvm) is a node version manager. You can install it following [these\ninstructions](https://github.com/nvm-sh/nvm?tab=readme-ov-file#installing-and-updating).\n\n[^2]: [corepack](https://nodejs.org/api/corepack.html) is an experimental tool to help with managing versions of your\npackage managers.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/08-development/02-directories.md",
    "content": "# Directory Structure\n\n## Apps\n\n| Directory                | Description                                            |\n| ------------------------ | ------------------------------------------------------ |\n| `apps/web`               | The main web app                                       |\n| `apps/workers`           | The background workers logic                           |\n| `apps/mobile`            | The react native based mobile app                      |\n| `apps/browser-extension` | The browser extension                                  |\n| `apps/landing`           | The landing page of [karakeep.app](https://karakeep.app) |\n\n## Shared Packages\n\n| Directory         | Description                                                                  |\n| ----------------- | ---------------------------------------------------------------------------- |\n| `packages/db`     | The database schema and migrations                                           |\n| `packages/trpc`   | Where most of the business logic lies built as TRPC routes                   |\n| `packages/shared` | Some shared code between the different apps (e.g. loggers, configs, assetdb) |\n\n## Toolings\n\n| Directory            | Description             |\n| -------------------- | ----------------------- |\n| `tooling/typescript` | The shared tsconfigs    |\n| `tooling/eslint`     | ESlint configs          |\n| `tooling/prettier`   | Prettier configs        |\n| `tooling/tailwind`   | Shared tailwind configs |\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/08-development/03-database.md",
    "content": "# Database Migrations\n\n- The database schema lives in `packages/db/schema.ts`.\n- Changing the schema, requires a migration.\n- You can generate the migration by running `pnpm run db:generate --name description_of_schema_change` in the root dir.\n- You can then apply the migration by running `pnpm run db:migrate`.\n\n## Drizzle Studio\n\nYou can start the drizzle studio by running `pnpm run db:studio` in the root of the repo.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/08-development/04-architecture.md",
    "content": "# Architecture\n\n![Architecture Diagram](/img/architecture/arch.png)\n\n- Webapp: NextJS based using sqlite for data storage.\n- Workers: Consume the jobs from sqlite based job queue and executes them, there are three job types:\n  1. Crawling: Fetches the content of links using a headless chrome browser running in the workers container.\n  2. OpenAI: Uses OpenAI APIs to infer the tags of the content.\n  3. Indexing: Indexes the content in meilisearch for faster retrieval during search.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/08-development/_category_.json",
    "content": "{\n  \"label\": \"Development\",\n  \"position\": 8\n}\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/_category_.json",
    "content": "{ \"label\": \"API\", \"position\": 9 }\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/add-a-bookmark-to-a-list.api.mdx",
    "content": "---\nid: add-a-bookmark-to-a-list\ntitle: \"Add a bookmark to a list\"\ndescription: \"Add the bookmarks to a list\"\nsidebar_label: \"Add a bookmark to a list\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy9VE1v00AQ/SurOYG01C0qovItHJAKCFWQikPkw8SexNvY3u3uuE2w9r+jsU2ctKFcEL7Yu56PN2/eTAcFhdwbx8Y2kMKsKBSXpJbWbmr0m6DYKlSVCQwaGNcB0gV8MYEDZBoC5a03vIN00cGS0JOftVxCushipsGhx5qYfOgNQl5SjZB2wDtHkEJgb5o1aKAt1q6SK0OmqLa7df14d/Xebn++K7fMNr+S7IZ7E8l+XUDU4Om+NZ4KSNm3pKHBWgyqwUCDkZIccglR/7P0H0ZqXoSwnIyOYGTiEZxtAgVB8vb8Ul7HTfhqVW4bpobVm6NuqEcMCouC+tSXp3yFHGX95NJYVivbNgJkjCpe6FxlchSv5C6I6wmC7PKOcmm889aRZzOAzm1Bz2mMGmoKAden/h0xtRgiTPZZlEfOXNpCyGr7rEJZCon0MyTd0NaY7LWZdBPNEUSN/uG31lpfQQodFoWnEGKCziQPF6DhAb3BZTVUMv4eaFxhWzGkUDK7kCYJ+93ZBj1uiNwZOgf6CdfzktQYQdlV36nPo70asECM8WBMvgvBQ+bDYdmzJZmljt5MVNQbgR4/PlpfoyD89GPeU2qalRV3qXqAdHF2fnZ+INY9ntnN9Un8s5trtbL+GLwUGzU4G7jGXhqjrGU94KStw91wFLqbpPa3lTJUzrTlxFVoGkncN68b+7/o5zmAhnQ/2PtYcnswa5mG0gYWp65bYqBbX8Uo1/cteVlT2aSAXieFCfJdQLrCKtALdbz6Ngr4tfoT7vESm10vtKqVE2jY0G5aS7KK/mPWA3ZiFjWUhAX5vvbBYJbn5PjA9dlqEAnvR/Pmdg4a8Fi4T4TaRz8Jq+sGi7ndUBPjHiXLWQDG+As6ID3T\nsidebar_class_name: \"put api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Add a bookmark to a list\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"put\"}\n  path={\"/lists/{listId}/bookmarks/{bookmarkId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nAdd the bookmarks to a list\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"ListId\"},\"required\":true,\"name\":\"listId\",\"in\":\"path\"},{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"204\":{\"description\":\"No content - the bookmark was added\"},\"404\":{\"description\":\"List or bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/attach-asset.api.mdx",
    "content": "---\nid: attach-asset\ntitle: \"Attach asset\"\ndescription: \"Attach a new asset to a bookmark\"\nsidebar_label: \"Attach asset\"\nhide_title: true\nhide_table_of_contents: true\napi: eJztVU2P2zYQ/SvCnBqAXe0GKRro5hQtsgnQLrIOejB8GIsji2uKVEjKHxX434Oh5JUdO8EeesxFkEaP5Lw3b4Y9SPKlU21Q1kABsxCwrDPMDO0y9J5CFmyG2craTYNuAwICrj0UC3g3hjwsBXgqO6fCAYpFDytCR27WhRqKxTIuBbTosKFAzieAL2tqEIoewqElKMAHp8waBNAem1ZzSJGSen9YN7unt7/b/X+/1fsQbPmWM1AhQY4Z3EuIAhx96ZQjCUVwHQkw2DBoNYEEKObYYqiBs+IV5MM7Kw+cy7kS85pOBEiygIDSmkAmMBzbVqsSGZ4/eV5zhZhdPVEZQEDrbEsuKPL8V8lL8lFAOm+eolekMV3DumtlNu9Do/8YUxHgS0dkfG3TQbKCcafH0/gKjSF33+CaQEDVaf2Aa5q5slZbjmyVJMu4Ua8Z75ASp9LhTpOcsJ0n97nVFiWxqrjFgI7jZmPszsAyntVjwXxP2S1jHBC+tcYPkry+vftOEZL2JIdq/KzBi2sgoFKa/k5tcEnFdFrjivuI2+VlBXtz++ayRscuzIwNWWU7I/+/GpVWXkk+CmjIe1bx8t83RNIOEz4RSetDbSUPA+vTsTwTCsiPyvu8nwZHzJMSnstMbnucYp3TUECPUjryPubYqnx7x2VEp1jaRGH8PehWYacDFFCH0Poiz4M73GzQ4YaovcG2BXGtAYYdMltloabs44jPhlwgxngygB9Z2eHk0zH8LBOfnOzKMB6PCcSeSy9/WdcgZ/jh33nSkiv2aZqTfx7n89A/k51O2uaiOyIP3sryIhZvYHZ3c3tzezLMn2nNHu6vyjB7uM8q6841YM2iSFVsMFlrHPvHi2zsoLPt+smeL7rwBlaB9iFvNSrDJ6bi96NvFs8dyx4pzq6c0TpLATVbrVhA36/Q02enY+Twl44c35vLyTjJXlJ5fpdQVKg9/YDDL59Gw7/KvpfuGERzSP7UHX+BgA0dzu/IyHOjJpTkUhYDYKzkr6nA0wYXrR3FccWsLKkNP8QuT9rw4Z/HeRp8w1XcpL4HhzsQ6ZmStYl78naK9aDRrLs0B2DYky2L547/xuGJ1VU5+n5AzO2GTIzP6gT+ZmFi/ApH4yvL\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Attach asset\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/bookmarks/{bookmarkId}/assets\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nAttach a new asset to a bookmark\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The asset to attach\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"pdf\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"avatar\",\"unknown\"]}},\"required\":[\"id\",\"assetType\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"201\":{\"description\":\"The attached asset\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"pdf\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"avatar\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}}},\"404\":{\"description\":\"Bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/attach-tags-to-a-bookmark.api.mdx",
    "content": "---\nid: attach-tags-to-a-bookmark\ntitle: \"Attach tags to a bookmark\"\ndescription: \"Attach tags to a bookmark\"\nsidebar_label: \"Attach tags to a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVU2P0zAQ/SvRnEAyTUEgUG4FgbQgwQqKOFQ9TJNp420SG3uytET572icr5aWFUL0UCXO83jmzZvnBjLyqdOWtakggQUzpnnEuPMRmwijjTH7Et0eFMgiJCt43S95WCvwlNZO8xGSVQMbQkduUXMOyWrdrhVYdFgSk/MB4NOcSoSkAT5aggQ8O13tQAEdsLSFLGnSWXE47sofd69emsPPF/mB2aSvJAPNATJkcJNBq8DR91o7yiBhV5OCCksBbSaQAi3FWeQcJCvZQZ5fm+wouZxTsMxpqj/QMQMFqamYKhY4WlvoFAUe33nZc6Uws7mjlEGBdcaSY00+fA0cjih0Do+SHlPp/2r3TXbJXht68zGUffFNfqccrboc1sO6t6byXfhn8/l1OgrtOTLbng7KhJ9IZ/7/8TJE/gtu/k0yy0DdBRfjuT0fz+fPLykY5BZVhqOtqavs/xWemuxa1xSU5D3urnb0vIQQYcKv+46XxLnJRPXGh2NF/AnEw1j4uJkmpI2DKGSc3f0wrLUrIIEGs8yR922MVsf3T0HBPTqNm6LvXPe5Y22LdcGQQM5sfRLH7I6zPTrcE9kZWgvqirr6CCIwzin60OOjLhfp2YnPfBFeu5NP3WYkSU6WOgJMXCCAQPUP74wrUTJ8/20ZmJR+fZ7s4O2gqWFUV+PUTcIbh21syboVi9ka2Sf8dcU9nc1n8xMNjpUtbm+uMrG4vYm2xp3TILS1KrSxxKCt3uAe8uqz2M0k1ocNvqOQ6cCxLVBXcm5QQdPLZzW6qoglObPYzlYU5KK3ZAVNs0FPX13RtrL8vSYnt8R60k9gN9NenjNItlh4eiD1R5971T+O/pTsYB3VMci0qOUNFOzpeH4jhI7lhBm5kEUHeNOd9WQpYaYAF/PdqmHHIk3J8oPY9cks3n76shQt9hdPGYYfHP4AFf5DsibUHiQe1hoosNrVwQygiynKxXPh/yb0TrnX6GiaDrE0e6radmSH5V2IadtfmYTHdA==\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Attach tags to a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/bookmarks/{bookmarkId}/tags\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nAttach tags to a bookmark\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The tags to attach.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"tagId\":{\"type\":\"string\"},\"tagName\":{\"type\":\"string\"}}}}},\"required\":[\"tags\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The list of attached tag ids\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"attached\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"TagId\"}}},\"required\":[\"attached\"]}}}},\"404\":{\"description\":\"Bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/create-a-new-bookmark.api.mdx",
    "content": "---\nid: create-a-new-bookmark\ntitle: \"Create a new bookmark\"\ndescription: \"Create a new bookmark\"\nsidebar_label: \"Create a new bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJztWktv4zYQ/ivGnNXYWfTkm3fRoulrgyaLHgwfxtLI4loitSTlxCvovxdDMpKclWJ7ExQtYAQIFHGGHM77y6iGhEysRWmFkjCHD5rQ0gQnkh4ma6W2BeotRGBxY2C+hPfhlYFVBIbiSgu7h/myhjWhJr2obAbz5apZRaDpS0XGvlfJHub1s4PuM2r3n1g1id3BEEGspCVpmQPLMhcxMsf0s2G2GkycUYFuNc8/pu5ouy8J5qDWnym2EEGpVUnaCjJMZ4XNyT14MmO1kBuIQFZ5jmtes7qiCAp8/J3khi9wPZvNmghQx5nYUdLjXiuVE0poIkhxp/j+Y+tS2YFzmwhMVRSo94NrXg/Jwh6X2FHjQ36rhfJm+JaDZFWw3XL1wPxKF5jDqolAFKXS9o6MEUreJMNyqkrHg6p72hZLARE80JoNl/NzodYiZzvSoyXJm0MERshNTqlf0MbA0/mwapomqkFJOs2WbvWFawq5dderdD5El7ICLMyh0oJvWGpyKqRk4U09qInG+7LQbOmlX/VHrFj414ls6dE6kd3DuBk+Dd1oRLSnPV8rGxpDXjj3dH+EXBS44ePLJO2YRnyLneFPLEYC5Owbd/J1x64a/mk8hymVNP7W72azI/kIc02Y7Cf0KIw15ySll/UthnXxQsw3HFGJSMXJKeHEdPfq7GZxsxFyc2fRVub4eZ2XmCqOyeWAFEVeaecxJBPmWrXZUXx1Kn777Yez8oB6RrP0EO1ophwT8w0yZwSVIT0SX75gtwuoNe6Z2VJhvttR5VjAorUYZ5S8f7EIIV8zqwqUnPcP41gkEPY/2M3R9aLv3ykUZ4XRQR45gd5lycHcNka8GMujQ54YayJpMmXP4SqT9BzytMrzWxbMZ5CzTnpecc9h3omE1FmC4k7EJ9ols0X+ofO0472Xpz1HnKebn9HcvX0CxMpmSp9mrGqdC5PRadQJWroNHKfpgzn+COXtBIb/bh92oqj/675stOydmsuM+NrfVlbFmvQLkfXdah5uBl+t9UpupXoYqF2OgXvNcNzbF96TzMw17ZdeEusXg2D9sNNd//0apSR9E3zkWWqHkHSZLnTHLuHBQCYH35B8KnOFCXE9xx1aZBN3qjvLw4Z6hE4Vq2aQoOumD3rnXr970Nw+72SHG9C21wqdVeekrc1XbdPQ/pcEPPp4N7s+Ajg2yob/gCQXtHFBGxe0cUEbF7RxQRsXtHFBGxe0cUEbF7RxQRsno40fh8Yb7zGZhHns2wGMWCXDDVpBxrCBj05v3A4dvZvWOH6bqQTmUConb4k8lIXpup0+8/BZ70gb1525tgpqTBJNxjRTLMV0d83ehFqwhZ24YdkrJ8Uq50FgZm1p5tOp1furLWrcEpVXWJYQDeC1sMNEpROb0eS3QD/xsrD+e1PxO9aiP7k/G29Vwie7qGEyRjuOiF3fPfz8NKr89e97pze2zl/dSP2nRyzK0DQGR2gdu8Navlj30ZV/43FJx9HCj+5VDyf2Xx4MnMNc+Zthcm/jgFUCBAl3dw1xaIc72sFpbM+thEwV35ft7o1yfTW7mkEXCq1FFrc3gxZc3N5MUqUPzcfm5v5DGctoYV4H6DH6NcSzJrwNp/HPJ/y1uSJPyxyFw7QBDHjnXsK6/2lFxo4/X0Jdr9FwzWsafv2lIjbSctW5tvvaIoKMMOHOaVnDltgyIS//ECrTDvPKm+FZvHOl8hyLOKbSvki76sXm7ce7e5eo/ecdhUsGoJEdgn/PASJQTkkuCNy7GnKUm8olB/B7NqFL7AP/w1DoIzCU+56Ede0p7tWWZNNAFK5i+W9w099/AD6dDb4=\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Create a new bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/bookmarks\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nCreate a new bookmark\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The bookmark to create\",\"content\":{\"application/json\":{\"schema\":{\"allOf\":[{\"type\":\"object\",\"properties\":{\"title\":{\"type\":\"string\",\"nullable\":true,\"maxLength\":1000},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"note\":{\"type\":\"string\"},\"summary\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\",\"nullable\":true},\"crawlPriority\":{\"type\":\"string\",\"enum\":[\"low\",\"normal\"]},\"importSessionId\":{\"type\":\"string\"},\"source\":{\"type\":\"string\",\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]}}},{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\",\"format\":\"uri\"},\"precrawledArchiveId\":{\"type\":\"string\"}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\"}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\"}},\"required\":[\"type\",\"assetType\",\"assetId\"]}]}]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The bookmark already exists\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"userId\":{\"type\":\"string\"},\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"attachedBy\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\"]}},\"required\":[\"id\",\"name\",\"attachedBy\"]}},\"content\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true},\"description\":{\"type\":\"string\",\"nullable\":true},\"imageUrl\":{\"type\":\"string\",\"nullable\":true},\"imageAssetId\":{\"type\":\"string\",\"nullable\":true},\"screenshotAssetId\":{\"type\":\"string\",\"nullable\":true},\"pdfAssetId\":{\"type\":\"string\",\"nullable\":true},\"fullPageArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"precrawledArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"videoAssetId\":{\"type\":\"string\",\"nullable\":true},\"favicon\":{\"type\":\"string\",\"nullable\":true},\"htmlContent\":{\"type\":\"string\",\"nullable\":true},\"contentAssetId\":{\"type\":\"string\",\"nullable\":true},\"crawledAt\":{\"type\":\"string\",\"nullable\":true},\"crawlStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\",\"nullable\":true},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true},\"size\":{\"type\":\"number\",\"nullable\":true},\"content\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"assetType\",\"assetId\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"unknown\"]}},\"required\":[\"type\"]}]},\"assets\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"pdf\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"avatar\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"userId\",\"tags\",\"content\",\"assets\"],\"title\":\"Bookmark\"}}}},\"201\":{\"description\":\"The bookmark got created\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"userId\":{\"type\":\"string\"},\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"attachedBy\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\"]}},\"required\":[\"id\",\"name\",\"attachedBy\"]}},\"content\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true},\"description\":{\"type\":\"string\",\"nullable\":true},\"imageUrl\":{\"type\":\"string\",\"nullable\":true},\"imageAssetId\":{\"type\":\"string\",\"nullable\":true},\"screenshotAssetId\":{\"type\":\"string\",\"nullable\":true},\"pdfAssetId\":{\"type\":\"string\",\"nullable\":true},\"fullPageArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"precrawledArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"videoAssetId\":{\"type\":\"string\",\"nullable\":true},\"favicon\":{\"type\":\"string\",\"nullable\":true},\"htmlContent\":{\"type\":\"string\",\"nullable\":true},\"contentAssetId\":{\"type\":\"string\",\"nullable\":true},\"crawledAt\":{\"type\":\"string\",\"nullable\":true},\"crawlStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\",\"nullable\":true},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true},\"size\":{\"type\":\"number\",\"nullable\":true},\"content\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"assetType\",\"assetId\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"unknown\"]}},\"required\":[\"type\"]}]},\"assets\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"pdf\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"avatar\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"userId\",\"tags\",\"content\",\"assets\"],\"title\":\"Bookmark\"}}}},\"400\":{\"description\":\"Bad request\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/create-a-new-highlight.api.mdx",
    "content": "---\nid: create-a-new-highlight\ntitle: \"Create a new highlight\"\ndescription: \"Create a new highlight\"\nsidebar_label: \"Create a new highlight\"\nhide_title: true\nhide_table_of_contents: true\napi: eJztVj1v2zAQ/SvCzaztFJm4uUWLph0SNC46GB7O0tlSLJEMSSUxBP734ig5khwl6JChQzdbd7yPd+8e2UBGLrWF8YVWIOGzJfSUYKLoMcmLfV4W+9yDAI97B3IN307fHGwEOEprW/gjyHUDW0JLdln7HOR6EzYCLN3X5PwnnR1BNmepVjn1GRKvkzTmBgGpVp6U5yNoTFmkyEfmd47PNeDSnCrkX/5oCCTo7R2lXKSx2pD1BTm2brU+VGgPV9nA13lbqD0EAc6j9de7nSM/sKu62pJlO6nsDWuqS21fxuVjdcVAHaks9SMwCBkI2FsiBQK2ZU2MXEY7rEsP8uQYBHh68lMhVV2WuC0JpLc1BQFKe/oLx9BOoOAK5HqIx7j7Ya9dFV2OTQhtFGe0ci2sHxcX08Ns55eNaPN/lO8zSgHFdOu1I/sKKt08lhOFvAM1YkXP+YfZNgJ84bn0Xi2gJdLlYvGSO59wwJmk04z3406qswmEg4CKnMP9lO0MnRih99+cermc6KUDMlHaJztdq+xfbySe97nOQILREXiDLOEwz3utZ6m3D2RdVPraliChwSyz5FyYoynmDxcg4AFtwayN9XbmFqTTiuTeGyfnc2+PswNaPBCZGRoDYkJRugiJ3iU+p+RH55+0tTCnBnfQLcPYycXgJnrGhDNzH9ENZOfEexx/fNW2Qq7w++9VBI7H87O/wL48YWVKOlejfmNHIrQYic7iWWR6LWkVYrDwUQgGUy3UTnM2Rr2F5GK2mC2g365nPJY3V5P4LW+ukp22Y/AY7CDirCuMBFQYAXn17h8Fbno6v/FaaCHnHuemxEJFqWLWNB271pCPXhI5U0+uoWm26OiXLUPgz/c1WX5dbHpuxceFgJwwIxvpeKAjF9OW9WHFudmd5Vm+3LggTieWaUrGv+m7GWzHzfXtiunSvWaquI5gMd4M+AgSQICOMEUWxm8NlKj2dVxPaGMyuXDMzTMuxq46E6rjoMKmaT1W+kAqBBBdK57/Q+CN/gPpI19E\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Create a new highlight\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/highlights\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nCreate a new highlight\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The highlight to create\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarkId\":{\"type\":\"string\"},\"startOffset\":{\"type\":\"number\"},\"endOffset\":{\"type\":\"number\"},\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"],\"default\":\"yellow\"},\"text\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"bookmarkId\",\"startOffset\",\"endOffset\",\"text\",\"note\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"201\":{\"description\":\"The created highlight\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarkId\":{\"type\":\"string\"},\"startOffset\":{\"type\":\"number\"},\"endOffset\":{\"type\":\"number\"},\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"],\"default\":\"yellow\"},\"text\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\",\"nullable\":true},\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"}},\"required\":[\"bookmarkId\",\"startOffset\",\"endOffset\",\"text\",\"note\",\"id\",\"userId\",\"createdAt\"],\"title\":\"Highlight\"}}}},\"400\":{\"description\":\"Bad highlight request\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}},\"404\":{\"description\":\"Bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/create-a-new-list.api.mdx",
    "content": "---\nid: create-a-new-list\ntitle: \"Create a new list\"\ndescription: \"Create a new list\"\nsidebar_label: \"Create a new list\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy1VsFu2zAM/RWDZ61Jh+3iW1dsQLcCK7YMOwQ5MDYba5UlVZLbBob+faDkJE7qtRiwXQJLIsXHx0cqPdTkKydtkEZDCZeOMFCBhabHQkkfQEDAjYdyCdfSBw8rAZ6qzsmwhXLZw5rQkbvoQgPlchVXAhzdd+TDB1NvoexPAiwaSvcWwRRVCgYCKqMD6cDWaK2SFbL17Jdnlx581VCL/BW2lqAEs/5FFUOzzlhyQZLnU40tjax8cFJvQEAr9TXpDSM8F9Di0341n0dxDPBF7/mR9/vkLasptyiGjef3ke5aprNF3aECAb5FF5jXmm6xUwHK3VkUcN+R276WVBRg0ZEOV/WUqe6UwrUiKIPrKMZcIumoZhyJtSGPVYz52FujfWb17fx8uoy5fPVOJv+oiHIihSj+UNvXq3eS+wv1+gsG/3txn8Wz3VrJamS6NkYRar6lQX9p2N44DMb5aavOk/tm1IuozaMmx+taBsMfD5Ie084Qf3UqHlmDOFLQiMe91wTEEZ6VgCADA0sTBrIE383nz1X3AetimC7/TnCVqaeV1ZL3uJk6OyEh3XCwT02U/ENjaijBmoTXIjcrzFSaozxG3QNxuZY9dE5BCT3WtSPv4wytnD2ccwXQSVZCgjocZ2J2cmpCsL6czYLbnt2hwzsie4bWgpjo2eGGwtwWoaHiy2BfZCzM/Wi+f2cGc+TxlN/TwZE5j2TGaktGIIaPT8a1yAg//1wkzrgy3w6Pw8cnbG0WZO7ugyCPge+3c+8e1gOOfbMN/XQwOPT0qK5S3xoOyuTnAOdn87M5HHS4p+Xi5mqSxoubq+LWuGMOmXNuVeNDi3qU1tSjejK29lqefoFznoGewswqlLmhWTL9oKolqN3r3LDayiX0/Ro9/XAqRt4euFmuDppKD7aAhrAml2R4R0zfZQbzZsFh2Vx1HP5Zk0Wx87ioKrLhRdvVqCFuvn5fsEyGfwht6kBw+Agi/ZYAAkwiJ6kv7fWgUG+61JGQ72RR4bEmTzSYshqOUG9HCPs+WyzMHekYQQypBF5D5Cb+DVD4G/I=\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Create a new list\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/lists\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nCreate a new list\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The list to create\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\",\"minLength\":1,\"maxLength\":100},\"description\":{\"type\":\"string\",\"minLength\":0,\"maxLength\":500},\"icon\":{\"type\":\"string\"},\"type\":{\"type\":\"string\",\"enum\":[\"manual\",\"smart\"],\"default\":\"manual\"},\"query\":{\"type\":\"string\",\"minLength\":1},\"parentId\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"name\",\"icon\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"201\":{\"description\":\"The created list\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"description\":{\"type\":\"string\",\"nullable\":true},\"icon\":{\"type\":\"string\"},\"parentId\":{\"type\":\"string\",\"nullable\":true},\"type\":{\"type\":\"string\",\"enum\":[\"manual\",\"smart\"],\"default\":\"manual\"},\"query\":{\"type\":\"string\",\"nullable\":true},\"public\":{\"type\":\"boolean\"},\"hasCollaborators\":{\"type\":\"boolean\"},\"userRole\":{\"type\":\"string\",\"enum\":[\"owner\",\"editor\",\"viewer\",\"public\"]}},\"required\":[\"id\",\"name\",\"icon\",\"parentId\",\"public\",\"hasCollaborators\",\"userRole\"],\"title\":\"List\"}}}},\"400\":{\"description\":\"Bad request\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/create-a-new-tag.api.mdx",
    "content": "---\nid: create-a-new-tag\ntitle: \"Create a new tag\"\ndescription: \"Create a new tag\"\nsidebar_label: \"Create a new tag\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVD1v2zAQ/SvCzayVdNTmFi2QdmjQuOhgaDhLZ4uxRDLkKYkh8L8XR6qxkhpFhy6STN7Hu/feeYKWQuO1Y20NVPDREzIVWBh6KhgPoIDxEKDawkbetYJAzeg1n6DaTrAj9OTXI3dQbetYK/D0MFLgD7Y9QTW9Kb/pqGiRsWBbNLkVdySNiifN3QoUNNYwGZZcdK7XDUpueR+kwASh6WhA+eKTI6jA7u6pYVDgvHXkWVOQW4MDLaICe20OEGMGqD21MlOKqmPM58FZE3L6+6vry+gz6nbm5j+B1e0FqOofZ9AtqMUgUcFA3NkWKnA2pGYo8kCZlBQB/SP5kPQbfQ8VTNi2nkKIJTpdPl6Dgkf0Gnd9xjdfZ0b2OPYMFXTMLlRlyf60OqLHI5FboXOgLtA2VyjsPgn+dY4vMhaIMS6cdSe05c5Lf70QIZ1ljhQG1RwEav74bP2AgvDLz01iS+T4frblp2ccXE9nlywo12Zv5UIIyuivV1erK1kDzZIDL9DXtzcXR13f3hR761/PKbxElQQZ0CxaX1i4VyWns8cuLmdmhOmZS9ejNtIliTrNsm/zAtcKOnFDtYVp2mGgH76PUY4fRvKyzPVZ9LTLCjrClnzyyZFOAiBDebeRrhLej9L9D+tH9Ttj3TTk+K+x9cKwt9/uNqLj/Ocx2FZyPD6BSs8KQIFN1CR7pLMJejSHEQ8Sm2uK6vjaNG9Mkqaar9CcFginKUds7JFMjKDmUVh+Q5Ql+wUehMct\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Create a new tag\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/tags\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nCreate a new tag\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The data to create the tag with.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"}},\"required\":[\"name\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"201\":{\"description\":\"The created tag\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"}},\"required\":[\"id\",\"name\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/delete-a-backup.api.mdx",
    "content": "---\nid: delete-a-backup\ntitle: \"Delete a backup\"\ndescription: \"Delete backup by its id\"\nsidebar_label: \"Delete a backup\"\nhide_title: true\nhide_table_of_contents: true\napi: eJx9U8Fu2zAM/RWBpw3Q4nbosMG3DO2AbsNQbBl2CHJgbCZWY1uqRLfJDP37QNlNkzarL5bEJ/Hx8bGHkkLhjWNjW8jhkmpiUkssNp1Ty50yHJQpQQPjOkA+h88pFGChIVDRecM7yOc9LAk9+WnHFeTzRVxocOixISYfEiAUFTUIeQ+8cwQ5BPamXYMG2mLjajkyZMp6u1s3D7efPtrt3w/VltkWnyS/4QQZ8l+XEDV4uuuMpxJy9h1paLERyPIRosFIUQ65AmHkKTjbBgrC4v3ZhfyO6/9hVWFbppbVO8XVXokHDKpM2qTEF6fuDsxUa1mtbNdK+vEtwaJztSlQsNltkAsnJLHLWyoYNDhvHXk2A9XClvRSuKihoRBwfSp2pM58eOEJv4jyyZ4rW0IOQ22SWLTKIRvqDln/KGYEabi/f2xn52vIocey9BRCzNCZ7P4cNNyjN7isB+ZjeBBrhV3NkEPF7EKeZex3kw163BC5CToH+pmis4rU+IKyq9SPbyNeDVwgxnjgxF8i6JD50I97dSSz1JFg4pQEAj0uvljfoDD8+meWJDTtysp1qXqgdD45m5wd2HHPZ3pzfZL/9OZaraw/Ji/FRg3OBm4wWWG07jh/OPru+Yv9k6NeG9WhWKYtZ65G00qu1K9+7O98nJEAGvL9uCw0VDawhPt+iYF++zpGOb7ryMuYL57am0xQmiDrEvIV1oFeYfvm5+jGt+p/DMdDbHfJRXUnO9Cwod3hUMdF1FARluQThyE8LQpyfHDxxbyJT/Z+v7z6fjW7Ag14bJBnhkgJTvLq+wExsxtqY9zTZNkLxxj/AVwN0vk=\nsidebar_class_name: \"delete api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Delete a backup\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"delete\"}\n  path={\"/backups/{backupId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nDelete backup by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BackupId\"},\"required\":true,\"name\":\"backupId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"204\":{\"description\":\"No content - the backup was deleted\"},\"404\":{\"description\":\"Backup not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/delete-a-bookmark.api.mdx",
    "content": "---\nid: delete-a-bookmark\ntitle: \"Delete a bookmark\"\ndescription: \"Delete bookmark by its id\"\nsidebar_label: \"Delete a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJx9U01v2zAM/SsCTxugxe3QYYNvGdoB3Yah2DLsEORA20ysxrZUiW6TGfrvA23nq83qi/XxKD4+PnZQUMi9cWxsAylcU0VMKrN2XaNfq2yrDAdlCtDAuAqQzuHzeBlgoSFQ3nrDW0jnHWSEnvy05RLS+SIuNDj0WBOTDz0g5CXVCGkHvHUEKQT2plmBBtpg7So5MmSKarNd1U/3nz7azd8P5YbZ5p+EgeEesmNwW0DU4OmhNZ4KSNm3pKHBWkDZAaTBSHEOuQRh5Sk42wQKwuT9xZX8TnX4YVVuG6aG1TvF5ZEiTxhU0avUJ786F73jpxrLamnbRiiM7wkanatMjoJO7oOEnJHGZveUM2hw3jrybAa6uS3opYBRQ00h4Orc3YlG8+GFA34R5ZM9l7aAFIbqJLHolUKyqz0k3UHUCNJ8/7hrbesrSKHDovAUQkzQmeTxEjQ8ojeYVQP78XqQbIltxZBCyexCmiTst5M1elwTuQk6B/qZrrOS1PiCssu+L99GvBq4QIzxyJW/RNQh87E39wpJZqmjh4lnehDocfHF+hqF4dc/s15G0yythEvVA6XLycXk4siaez7Tu9uz/Kd3t2pp/Sl5KTZqcDZwjb0dRhOP84h7/z1/szv46vXhHQpm2nDiKjSN5Ot71o19nu8nJoCG9Gh8FhpKG1ggXZdhoN++ilGOH1ryMvqLQ5t7MxQmyLqAdIlVoFc4v/k5OvOt+h/L8RCbbe+mqpUdaFjT9nTM4yJqKAkL8j2LATDNc3J8FPpi+sQxe/df33y/md2ABjy1yjNr9AnOMuu6ATGza2pi3BNl2QvHGP8BgxHeWQ==\nsidebar_class_name: \"delete api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Delete a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"delete\"}\n  path={\"/bookmarks/{bookmarkId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nDelete bookmark by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"204\":{\"description\":\"No content - the bookmark was deleted\"},\"404\":{\"description\":\"Bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/delete-a-highlight.api.mdx",
    "content": "---\nid: delete-a-highlight\ntitle: \"Delete a highlight\"\ndescription: \"Delete highlight by its id\"\nsidebar_label: \"Delete a highlight\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVcFu2zAM/RWDpw0QknTosMK3AO2wbANWbBl2CHJQLCZWY0uqRLfJDP37QNtJnNbtdtgpjvkkPj4+0jUoDJnXjrQ1kMI1FkiY5HqTF3qTU7LaJ5pCohUIILkJkC7g0yEaYCkgYFZ5TXtIFzWsUHr004pySBfLuBTgpJclEvrQAEKWYykhrYH2DiGFQF6bDQjAnSxdwa80alXs9pvy8e7qg939fp/viGx2xRQ0NZAjhZmCKMDjfaU9KkjJVyjAyJJReQ8lQHOBTlIOzMtjcNYEDMzl3WTCP+dazHNMVKOHOgkCAjJrCA3xAelcoTPJB8Z3gU8NVGhXd5jxQeetQ0+6zbmydltKv52p52pEAYGkp2/rdUDqxU1VrtBzHI16JZrZwvpBlU1Vcg/3WBT2EVgH1mbjEQ0IWBUVclMVrmVVEKQHYBRAuKOhK01VFHLFbWHxowBjCf8JqIdLrwL6F1TJPEpCNR0gEs98sOjre65mX7uuqo5zw+iYv59tOWA9iJFzXk4un3vnCEqMpWRtK6P+n3Myqwb0jQJKDEFuhmJPtGluOOGXsS2lRMqtghRa23NiHpcUxkf/h3Hdm6rI0qJ/OEx35QtIoZZKeQwhjqXT44cLEPAgvebWN/y7cKvawWc5kQvpeEx+P9pKL7eIbiSdAzEwlt0NiV0nlGPypcMnLRduTG8x/WBZu5nrraejRpyZ62hgkHYgHobm4aP1pWSGn3/NGyG1WVs+zlW3lC5Gk9Gkt52OfKa3s0H+09tZsrb+nDwXGwU4G6iUjSG6NdYtZXm2hc4urU/W+ssKb0tm049dIbVp5o27Vne9Xpy2ZgABaX+HLgXkNhCD6nolA/70RYz8+r5Cz1+A5anVjSGUDvysIF3LIuArtN987/z5NnmJZ/dSmn3jKF5VKYCALe6fLPu4jAJylAp9Q6NFTLMMHfXOPhtCts1xCK5vvt7Mb0CAPPfLE380CQap1XWLmNstmhiPTIn/M8cY/wCwMJE0\nsidebar_class_name: \"delete api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Delete a highlight\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"delete\"}\n  path={\"/highlights/{highlightId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nDelete highlight by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"HighlightId\"},\"required\":true,\"name\":\"highlightId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The deleted highlight\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarkId\":{\"type\":\"string\"},\"startOffset\":{\"type\":\"number\"},\"endOffset\":{\"type\":\"number\"},\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"],\"default\":\"yellow\"},\"text\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\",\"nullable\":true},\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"}},\"required\":[\"bookmarkId\",\"startOffset\",\"endOffset\",\"text\",\"note\",\"id\",\"userId\",\"createdAt\"],\"title\":\"Highlight\"}}}},\"404\":{\"description\":\"Highlight not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/delete-a-list.api.mdx",
    "content": "---\nid: delete-a-list\ntitle: \"Delete a list\"\ndescription: \"Delete list by its id\"\nsidebar_label: \"Delete a list\"\nhide_title: true\nhide_table_of_contents: true\napi: eJx9U8tu2zAQ/BViTy3ARkmRooFuBpICaYMiaF30YPiwltYWY0lkyFViV+C/F0upfiROfbFIDjmzs7M9lBQKbxwb20IO11QTk6pNYLXYKsNBmRI0MK4C5DO4M4EDzDUEKjpveAv5rIcFoSc/6biCfDaPcw0OPTbE5EMChKKiBiHvgbeOIIfA3rQr0EAbbFwtW4ZMWW+2q+b54eqz3fz5VG2YbXEl7IYTRNhvS4gaPD12xlMJOfuONLTYCKAeABqMFOOQKxA1noKzbaAgCj6eX8rfcd3frSpsy9Sy+qC4IrWwdt2gX6tnDKpMriTiy1O3RZdqLaul7VqhH98SJDpXmwIFmT0EgZ+wwy4eqGDQ4Lx15NkMUgtb0mvTooaGQsDVqbMjb2bDC3v8PMpP1lzZEnIYKhNi8SqHTBwMWT8YGUEa7Z/+tbHzNeTQY1l6CiFm6Ez2dAEantAbXNSD6vF4sGmJXc2QQ8XsQp5l7Ldna/S4JnJn6BzoF15OK1LjC8ouUy++jXg1aIEY40ECf4qZA/NhDnfOCLPUkWCQjyDQ48cX6xsUhV9/T5N9pl1auS5VD5Iuzs7Pzg9iuNMzub89qX9yf6uW1h+Ll2KjBmcDN5hiMIZ2nDlMU/fyvX6fpbeHcyiUacOZq9G0wpN61Y99naXJCKAhH0dkrqGygeWo7xcY6JevY5Ttx468jPV839bU/NIE+S4hX2Id6D863/0YE/hevaVu3MR2m9JTd7ICDWva7sc4zqOGirAknxQMh5OiIMcH115NmKRjl/Drm7ub6Q1owONYvIhBIjipqu8HxNSuqY1xJ5JlLRpj/AuPmMsh\nsidebar_class_name: \"delete api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Delete a list\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"delete\"}\n  path={\"/lists/{listId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nDelete list by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"ListId\"},\"required\":true,\"name\":\"listId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"204\":{\"description\":\"No content - the bookmark was deleted\"},\"404\":{\"description\":\"List not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/delete-a-tag.api.mdx",
    "content": "---\nid: delete-a-tag\ntitle: \"Delete a tag\"\ndescription: \"Delete tag by its id\"\nsidebar_label: \"Delete a tag\"\nhide_title: true\nhide_table_of_contents: true\napi: eJx9U01v2zAM/SsCTxugxe3QYYNvAdoB3Yah2DLsEOTA2EysxrZUiW6TGfrvA2UvH206X2SJT+Lj42MPJYXCG8fGtpDDNdXEpBjXarlThoMyJWhgXAfI5zCTdaEhUNF5wzvI5z0sCT35accV5PNFXGhw6LEhJh8SIBQVNQh5D7xzBDkE9qZdgwbaYuNqOTJkynq7WzdP958+2u2fD9WW2RafJLnhBJnh+raEqMHTQ2c8lZCz70hDi43EOcU1GCnEIVcgXDwFZ9tAQfK/v7iS5bTm71YVtmVqWb1TXJFaWrtp0G/UEwZVJkVS3qtzt2e4Vq1ltbJdK9nHpwSIztWmQAFm90HQZ7Swy3sqGDQ4bx15NgPTwpb0UrGooaEQcH0udqLMfHjhgF9E+WTPlS0hh6EwSSxS5ZBJk7M+qRhBeuwf/3Ww8zXk0GNZegohZuhM9ngJGh7RG1zWA+cxPGi0wq5myKFidiHPMva7yQY9bojcBJ0D/VzIitT4grKr1IivI14NXCDGeGS+nyLlkPnYgntdJLPUkWCQjyDQ489n6xsUhl9+z5J4pl1ZuS5VD5QuJxeTiyMH7vlM727P8p/e3aqV9afkpdiowdnADSYTjIYdhw1l3J4/1x+M9OpQDmUybTlzNZpWsqRO9WNP58PgasiH2VhoqGxgCfT9EgP98nWMcvzQkZdpXhxamhpfmiD/JeQrrAP9h+SbH6P33qrXuI2H2O6Sc+pOdqBhQ7v9+MZF1FARluQTgSE2LQpyfHTrxWiJMfbWvr75djO7AQ146ohnDkgJzpLq+wExsxtqYzxwlL1wjPEvTTHFIQ==\nsidebar_class_name: \"delete api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Delete a tag\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"delete\"}\n  path={\"/tags/{tagId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nDelete tag by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"TagId\"},\"required\":true,\"name\":\"tagId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"204\":{\"description\":\"No content - the bookmark was deleted\"},\"404\":{\"description\":\"Tag not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/detach-asset.api.mdx",
    "content": "---\nid: detach-asset\ntitle: \"Detach asset\"\ndescription: \"Detach an asset from a bookmark\"\nsidebar_label: \"Detach asset\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy9VE1v2zAM/SsCTxugxe3QYYVvGdoB3Yah2DLsEPjA2HTsxrZUiW6TGfrvA23nq82KHYblEkt6oh4fH9lBRj51peXSNBDDFTGmhcJGoffEKnemVqgWxqxqdCvQwLj0EM/hw7jlIdHgKW1dyRuI5x0sCB25acsFxPMkJBosOqyJyfke4NOCaoS4A95Yghg8u7JZggZaY20r2SqpzKr1Zlk/3l2+N+tf74o1s0kvhUHJPWTL4CaDoMHRfVs6yiBm15KGBmsBLfYgDaWkaJELCPqf0ZiKUC9ywBFxRCARuLem8eSFw9uzC/k7LsdXo1LTMDWs3owVeUSvsr5KlCnfpil5n7dVtRECF6eCbHVSjWGVm7YRJmNYQaO1VZmioKM7L1dOaGMWd5QyaLDOWHJcDqxTk9FzBYOGmrzH5amzI53mQ4Q9PgnykzUXJoMYMqqIBdHLFkO0raiPun1xQ9Sr46NuFDuAuNI9bD3Xugpi6DDLHHkfIrRl9HAOGh7QlbiohnTG40HDHNuKIYaC2fo4ithtJit0uCKyE7QW9BOhZwWpMYIyueKC1OcRrwYuEEI4aJfvovLw8mHT7CSTlyWPHiZm7kGgx4+PxtUoDD/9nPW6lk1u5LpkPVA6n5xNzg7MuuMzvb05yX96e6Ny447JS7JBgzWea+z9MTp7Oy5E86fhur3H/masDBkzrTmyFZaNPNgXrRsrP9/1sgcN8VFjD8WX7W2vJRoK41mudd0CPf1wVQiyfd+Sk0GV7GvfOyQrvXxnEOdYeXohm1ffRv++Vn9iPm5is+ktVrWyAg0r2hwPJRlE//HlrTwhCRoKwoxcn/xwOk1Tsnxw79loEPfuWvPq+sv17FrkP7btE5v2D5yk1XUDYmZW1ISwY8myFo4h/Abov0RX\nsidebar_class_name: \"delete api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Detach asset\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"delete\"}\n  path={\"/bookmarks/{bookmarkId}/assets/{assetId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nDetach an asset from a bookmark\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"},{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"AssetId\"},\"required\":true,\"name\":\"assetId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"204\":{\"description\":\"No content - asset was detached successfully\"},\"404\":{\"description\":\"Bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/detach-tags-from-a-bookmark.api.mdx",
    "content": "---\nid: detach-tags-from-a-bookmark\ntitle: \"Detach tags from a bookmark\"\ndescription: \"Detach tags from a bookmark\"\nsidebar_label: \"Detach tags from a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVV2v0zgQ/SvRPLGStykrVovydoG70gWEEBTxUPVhGk8b3yaxsSeXdqP8dzTOR1taLmhFH6rEOR7PnDlz3IKmkHvj2NgaMnhFjHmRMG5DsvG2SjBZW7ur0O9AgSxDtoQXw1KAlYJAeeMNHyBbtrAm9ORvGi4gW666lQKHHiti8iECQl5QhZC1wAdHkEFgb+otKKA9Vq6UJUNGl/vDtvp6//wfu//v72LPbPPnkoHhCBkzuNPQKfD0pTGeNGTsG1JQYyWg9RGkwEh5DrkAyUp2UOAXVh8kl3MSFgX1DLBNdCRkBgpyWzPVLHB0rjQ5Cjy9D7LnSmF2fU85gwLnrSPPhkL8GjmcUOg9HiQ9pir80u47fcleF3vzLpZ98U1+pxwt+xxW43pwtg59+L/m8+t0lCZwYjcDHaSFn8To8Pt4GSP/Ajf/TzKLSN0FF9O5Ax/P5s8uKRjlltSWk41tav37Cs+tvtY1BRWFgNurHT0vIUY44ldDxyviwmrIQFNJLIgo/wzScTBC2h5npEujLGSg/cM4ro0vIYMWtfYUQpeiM+nDU1DwgN7guuxLGD73vG2wKRkyKJhdyNKU/WG2Q487IjdD50Bd0dcQQSTGBSVvBnzS5yJdO3Gaj8Jsf/Kp30w0yclSR4SJD0QQqOHhX+srlAxff15ELqVjH46GcDuqahzW5TR3R+lN4zY1ZdWJyWys7BP++uKezuaz+YkKp8pu3t9dZeLm/V2ysf6cBqGtU+Bs4AqjugaLe9yvz6K3R8H+zOZ7Gpn2nLoSTS1nRyW0g4SWk7eKYLIzo+3NRUFhAwuybdcY6JMvu06WvzTk5a5YHTUUGdYmyLOGbINloEeSf/Jh0P4fyY+SHQ2kPkSplo28gYIdHc7vhdi1glCTj1n0gJf9WX8uJMwxwMWUd2rccZPn5PhR7OpkIl/dvr1d3IoihwuoiiYAHr+Civ8xXRurj0KPay2UWG+baArQRxX94rn8v5N7r99rhLRtj1jYHdVdN/HD8i7UdN03RJTKrQ==\nsidebar_class_name: \"delete api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Detach tags from a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"delete\"}\n  path={\"/bookmarks/{bookmarkId}/tags\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nDetach tags from a bookmark\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The tags to detach.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"tagId\":{\"type\":\"string\"},\"tagName\":{\"type\":\"string\"}}}}},\"required\":[\"tags\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The list of detached tag ids\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"detached\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"TagId\"}}},\"required\":[\"detached\"]}}}},\"404\":{\"description\":\"Bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/download-a-backup.api.mdx",
    "content": "---\nid: download-a-backup\ntitle: \"Download a backup\"\ndescription: \"Download backup file\"\nsidebar_label: \"Download a backup\"\nhide_title: true\nhide_table_of_contents: true\napi: eJx9VE1v2zAM/SsGTy0gxOnQYYVvKfaBbpdiy7BD4ANjM7Ea21IlOk1q6L8PtJ3PJTvZpijyvcdHt5CTz5y2rE0NCXw2b3VpMI/mmK0aGy10SaCAcekhmcFjF/WQKvCUNU7zFpJZC3NCR27ScAHJLA2pAosOK2JyvkvwWUEVQtICby1BAp6drpeggDZY2VJCmnRebrbL6u3l4ZPZvH8sNswme5D+mruUvv9TDkGBo9dGO8ohYdeQghorSZnvUhRoYWSRCxBEjrw1tScvKD6Mx/I4Jf944BzdvGsbocsKvaZbUJCZmqlmuYTWljpDuRS/ayuhPb0QQlBwP76/Wr02HC1MU+fXi754uXBBNDN/oYxBgXXGkmPdk8lMTv9KGxRU5D0uL52d6DfrKxzy09DzqIgLk0MCS+q6ipQJxL3EPm53Woc4H3wDYgy33o29cSUk0GKeO/I+xGh1vL4DBWt0Gudlj3847iVbYFMyJFAwW5/EMbvtaIUOV0R2hNaCOtN1WlA0VIjMIuKCoh9DftRjgRDCkWN/iax952Pf7jWSzsKjSxNHdUmghpevxlUoCL//mXZC6nph5Lqw7iHdjcaj8ZFt93gmz08X8U+en6KFcafghWxQYI3nCjtDDBbfLykOa3pesz046+pG91yZNhzbEnUtrbpxtcOcZ8MqeVCQHG3VftSpgsJ4lsy2naOn364MQcKvDTn5MaSHQXd2yLWX9xySBZae/oP65ufgztvoGtghiPW281PZyBcoWNH2+DcQ0qCgIMzJdRj640mWkeWji+dLLYbZ2//blykowFObnNmiK34RU9v2GVOzojqEPUSWb8EXwl+BY+PE\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Download a backup\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/backups/{backupId}/download\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nDownload backup file\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BackupId\"},\"required\":true,\"name\":\"backupId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Backup file (zip archive)\",\"content\":{\"application/zip\":{\"schema\":{}}}},\"404\":{\"description\":\"Backup not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/get-a-single-asset.api.mdx",
    "content": "---\nid: get-a-single-asset\ntitle: \"Get a single asset\"\ndescription: \"Get asset by its id\"\nsidebar_label: \"Get a single asset\"\nhide_title: true\nhide_table_of_contents: true\napi: eJx9U02P0zAQ/SvWnECymi4SYuVbhWC1cFlBEYeqB7eZNqaJ7bUnpSHyf0fjeEu72uUUf7zMezPveYQa4zYYT8ZZUHCHJHSMSGIzCENRmBokkN5HUCtY8E2EtYSI2z4YGkCtRtigDhgWPTWgVuu0luB10B0ShpgBcdtgp0GNQINHUBApGLsHCXjSnW/5yKCp29Ow737/uv3gTn/eNycit71lekMZkunva0gSAj72JmANikKPEqzuGKELQoLhbrymBlhPwOidjRhZw7v5nD/XjefaYussoaWZ+DgtBOsVJoqam+mMxZoHQw2WKfH9DFKS0CE1rgYFeySQE7WCKsNiNRZlCXh24fg0mT60oGDUdR0wxlRpb6rjDUg46mD0pp0Ul+tJ9U73LYGChshHVVUUhtlBB31A9DPtPchnrS1Z7VRBuF0W/7XgxaQFUkoXpn5nuybmS2vP7jEz95FhoAoIZFl8dqHTrPDLz2UejbE7x79z15Okm9l8Nr9w9qxn8XD/ov7Fw73YuXAtnptNEryL1GnLDCUGOcUiGrtvi1HPi45QrH4981OvhCeqfKuNZaps11jMXU1xiyBBPQVvLaFxkfhyHDc64o/QpsTHjz0Gfi7rf97mBNQm8roGtdNtxP/ofPOthP6teE1fOdR2yBFqe96BhAMOF68jrS/zevdpCRL0tcXPLM1KXyw+jhNi6Q5oUzpzEe+ZKaW/hvt+aA==\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get a single asset\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/assets/{assetId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet asset by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"AssetId\"},\"required\":true,\"name\":\"assetId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Asset content. Content type is determined by the asset type.\"}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/get-a-single-backup.api.mdx",
    "content": "---\nid: get-a-single-backup\ntitle: \"Get a single backup\"\ndescription: \"Get backup by its id\"\nsidebar_label: \"Get a single backup\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVMGO2zYQ/RVhTi1A2E6RooFubpEG2yBo0G6Rg+HDSBpbXEskQw43dgT+ezGU1uv1aoMccrLMGc689+ZxBmgo1F471tZACe+IiwrrQ3RFdSo0h0I3oIBxH6DcwO85FGCrIFAdveYTlJsBKkJPfh25hXKzTVsFDj32xORDTgh1Sz1COQCfHEEJgb02e1BAR+xdJ0eadNMdT/v+y92b3+zx66/tkdnWb6S/5pwy9r9pICnw9DlqTw2U7CMpMNhLSvWQokALI4fcgiDyFJw1gYKg+GW1kp+n5P+u7qjm4ovm9kGEBhkXoKC2hsmw3EHnOl2j3FneBbk4Q8/mUqDAeevIsx7b6ua5BElBDORv5kMYAvFcTIGJXYeV6CICJAW1J2Rq1jxbKeivdBEwsa/IS6Cy9tCjP/xho+HZjMDIMcxOz8RejOHINONJiHVNIYCCHeoueoJtUkDeW/+BQsA9fQeXJ+PdQPbgJNKjJJeEJ3rXZM7QtylJ0der18/HPpqqMJaLnY2m+XHjrm0zwzYp6F9S4pp5rvCYn2nk+9zaBkrYU+4qHi9hOZo2LIeHR5BEAfL3D88w+g5KGLBpPIWQluj08v4VKLhHr0X/DHsKj0rtMHYMJbTMLpTLJfvT4oAeD0Rugc6BupLztqViqlDYXcEtFe+n/GLEAimliw3yr6g5dr7cI2dppLPwyGnywnOSzDp//Gl9j4Lwr0+3WT9tdlauC+sR0qvFarG6WCNnPOuPN7P41x9vip31T8EL2aTA2cA9Zh9MK0eWJhZBm31H0+K4rjo8WurFHTuyZTry0nWoTd4MMrBhGvBmWm7yuMrzntsqaG1gCQ9DhYH+811Kcvw5kpf9vH2cb3ZBo4N8N1DusAv0Dag//TN58efiJYTTIZpTtlEX5R8oONDpchsn2QItYUM+YxjD67omxxcXn702McrZ7e/e3sr7f2qPKzvk6rOghmHMuLUHMimdMbL8F4Ap/Q8pJ2jm\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get a single backup\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/backups/{backupId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet backup by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BackupId\"},\"required\":true,\"name\":\"backupId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with backup data.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"assetId\":{\"type\":\"string\",\"nullable\":true},\"createdAt\":{\"type\":\"string\"},\"size\":{\"type\":\"number\"},\"bookmarkCount\":{\"type\":\"number\"},\"status\":{\"type\":\"string\",\"enum\":[\"pending\",\"success\",\"failure\"]},\"errorMessage\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"userId\",\"assetId\",\"createdAt\",\"size\",\"bookmarkCount\",\"status\"]}}}},\"404\":{\"description\":\"Backup not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/get-a-single-bookmark.api.mdx",
    "content": "---\nid: get-a-single-bookmark\ntitle: \"Get a single bookmark\"\ndescription: \"Get bookmark by its id\"\nsidebar_label: \"Get a single bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzNWF9v2zYQ/yrCvWwDhDgdOqzwW1psXTasDZYUewj8cBJPFmOKVEnKiWPouxdHyZKcyImcBtieoph3x9/9v+MWBLnUytJLo2EOH8lHiTGrAu0qSjaR9C6SAmLwuHQwv4b37aGDRQyO0spKv4H59RYSQkv2rPI5zK8X9SKGEi0W5Mm6QODSnAqE+Rb8piSYg/NW6iXEQHdYlIp/kiSFutssi9ubd7+au/tf8jvvTfqOEUgfSHYIzgXUMVj6WklLAubeVhSDxoKJkp4oBsmalehzqOMRGIkxilBDDIIyrJTfydo3zXkWOfKRN1E47cz0g4tSoz1pH91KpaKEIqlTVQkSkdSRzymy5EqjHZ1En4ynOPK57JlS1Myj0C4pyoyNnCmok+5O9rXMULn/D7TO3q3YDw3jzuZfK7Ib4FDYXePY7j+fnvKffR0+JzeUMk6f9xEo0CPf0gJiLixLJVNkrtmNY9YRl5ogDGIorSnJetlcLMXj6KtjSC2hJ3HmR08LI2Qmx49j0JVSmHBgsunrLkwnUKJNc7kmMRKKdQwZrg0n16Fzj8ul1MtLj75yz98XA+mq4Ax2VZqSc8BXSFVZYjORFsy1qGNwVVGglffBxK8vXhs/zTwNjs00WlPZdILYHiaWEmK4pYSjS/F3YRKpKFQjT9pxVMbgpF4qypoDG9SSRWmsD7pUjuz5eEw19bI7QGtxw8yeCvfiQG2SbeQAvcc0J/F+1FydzqxmXhWoYVHvVZVrCEU+yN+TFugG2Wc0fc5CNX9ageb0MBQl9aoxoVXj9pucRntlZAK9LHBJX8auPUR85hz5MT+PRWJqibTLjT+GqxTZMeRZpdQFA2sqyFE3WUot3ioSL2BeS0HmKKC4lulEv+S+UB/6SHuWvo3KY+DsNJ94AVO/fgHEyufGTnNWlSjpcppGLdDTRcsxzR7M8Xfb3iYwPCgZgbrJ4AUPVt9XETzdNUU1fIyVhKbIT8vccai7O74XK3LMNc7kr6tnyEMRgZDlPdOBvsGt5tNomT/Y9qbWMifvh2J1VSRkn8isF5u5N0qv6ytYvdIrbW5HeldgWNSdaV+/8U5yM/e0PwZFbNgMWu+3ki6HvyeoNdnzNkYelHZoiy7TtUNxKHgwUsmhGUi+lMqgIO7nuEaP7OLedEdF2NiM0JtiUY8S9NP03uw8mHf3htuHk+z4ANrNWu1k1Qdp5/PF4xUR6gDx7enbx/vGjibSxkeZqbR4vT0jNWJ8TivIOfbz47MHlgwSeno2dR34fW4EzGHZhADvtHOYdRvZbNsvvjWbkux6t36HSQu2KIQl5+oZlnK2fsMBhlay0wP09rixV7sNQ+596eazmbebkxVaXBGVJ1iW8HARvcopaiVEJgsL5l8tfdRgYZcMXg4u2aLNzcP3g848fHNIJCbjBSgQcTaEj9+NLZAR/vnvVbCh1Jlhdta6gfTm5PTkdPB80OE5uzgfxX92cR523T3wrCw3ZOM8j8/z7W7x5QcTjJo1odtbH8rd9oH1xAtLozH3qFmpUIYtrx2PG0dfdzWAE2A+eONYxJAb55lku03QcU+oa/65WcHZ/0I69vKBN4QhxP/wOWHUCCvajD0xrFFVTBweGHZBfKSqP/7TJt1P0aHbd41Eb4Z37lANnBAaUE4oeF673rYEZ2lK5RDuo8LC8LvE/vjbFTQz4nDt34/64f61D2u7bSiuzIp0XXcoPf/PAOv6G9QU5oo=\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get a single bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/bookmarks/{bookmarkId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet bookmark by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"},{\"schema\":{\"type\":\"boolean\",\"default\":true,\"description\":\"If set to true, bookmark's content will be included in the response. Note, this content can be large for some bookmarks.\"},\"required\":false,\"description\":\"If set to true, bookmark's content will be included in the response. Note, this content can be large for some bookmarks.\",\"name\":\"includeContent\",\"in\":\"query\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with bookmark data.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"userId\":{\"type\":\"string\"},\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"attachedBy\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\"]}},\"required\":[\"id\",\"name\",\"attachedBy\"]}},\"content\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true},\"description\":{\"type\":\"string\",\"nullable\":true},\"imageUrl\":{\"type\":\"string\",\"nullable\":true},\"imageAssetId\":{\"type\":\"string\",\"nullable\":true},\"screenshotAssetId\":{\"type\":\"string\",\"nullable\":true},\"pdfAssetId\":{\"type\":\"string\",\"nullable\":true},\"fullPageArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"precrawledArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"videoAssetId\":{\"type\":\"string\",\"nullable\":true},\"favicon\":{\"type\":\"string\",\"nullable\":true},\"htmlContent\":{\"type\":\"string\",\"nullable\":true},\"contentAssetId\":{\"type\":\"string\",\"nullable\":true},\"crawledAt\":{\"type\":\"string\",\"nullable\":true},\"crawlStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\",\"nullable\":true},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true},\"size\":{\"type\":\"number\",\"nullable\":true},\"content\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"assetType\",\"assetId\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"unknown\"]}},\"required\":[\"type\"]}]},\"assets\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"pdf\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"avatar\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"userId\",\"tags\",\"content\",\"assets\"],\"title\":\"Bookmark\"}}}},\"404\":{\"description\":\"Bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/get-a-single-highlight.api.mdx",
    "content": "---\nid: get-a-single-highlight\ntitle: \"Get a single highlight\"\ndescription: \"Get highlight by its id\"\nsidebar_label: \"Get a single highlight\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVcFu2zAM/RWDpw0QknTosMK3HLau22HD1mGHIAfFYmw1tqRKdJvM0L8PVNzESd1ih53imE/U4+Mj3YHCUHjtSFsDOVwjZZUuq1qXFWWrXaYpZFqBAJJlgHwBn5+iAZYCAhat17SDfNHBCqVHP2+pgnyxjEsBTnrZIKEPCRCKChsJeQe0cwg5BPLalCAAt7JxNb/SqFW93ZXN493VB7v9877aEtniiiloSpADhRsFUYDH+1Z7VJCTb1GAkQ2jqgFKgObqnKQKmJfH4KwJGJjLu9mMf06F+La6w4KyR03VQBAlSU5AQGENoSE+Jp2rdSH52PQu8NmROm3KBgKctw496f3NK2s3jfSbG/VckyggkPT0bb0OSIO4aZsVeo6jUa9EC1tbP6q1aRvu5A7r2j4Cq8EKlR7RgIBV3SK3VuFatjVB/gSMAgi3NJbStHUtV9wcbkEUYCzhPwH1eOltQP+CKoVHSajmI0TiiRsWQ31P1Rxq11fVc06MDvcPb1uOGBBi5DsvZ5fPHXQAZcZStratUf/POYVVI/pGAQ2GIMux2Jk2KcMRv4z7UhqkyirIoUzSpInJYXoYgTDtBoMVWVf0D08D3voacuikUh5DiFPp9PThAgQ8SK+574l8H95L9mSyisiFfDolv5tspJcbRDeRzoE40/W2wqzPkNl1RhVmX3t8tufCXRnspp+saT9wgw11EIhv5joSDPIexJOQHj5Z30hm+OX3bVJRm7Xl41z1ntLFZDaZDRbUgc/8+80o//n3m2xt/Sl5LjYKcDZQI5Mb+k3GS1lmQZuyxuMyOk/cHb312hrf18yWn7paapOmjdvW9c1eHDdnAAH5cI8uBVQ2EIO6biUD/vJ1jPz6vkXPX4HlsdfJEUoHflaQr2Ud8BXOb3707nybvcSzfynNLlmKF1UOIGCDu7OFH5dRQIVSoU809oh5UaCjwdlnI8i+OYzA9cdbECBP3XLmjpR9lFfX7RG3doMmxgNN4v9MMMa/V8aQOg==\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get a single highlight\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/highlights/{highlightId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet highlight by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"HighlightId\"},\"required\":true,\"name\":\"highlightId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with highlight data.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarkId\":{\"type\":\"string\"},\"startOffset\":{\"type\":\"number\"},\"endOffset\":{\"type\":\"number\"},\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"],\"default\":\"yellow\"},\"text\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\",\"nullable\":true},\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"}},\"required\":[\"bookmarkId\",\"startOffset\",\"endOffset\",\"text\",\"note\",\"id\",\"userId\",\"createdAt\"],\"title\":\"Highlight\"}}}},\"404\":{\"description\":\"Highlight not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/get-a-single-list.api.mdx",
    "content": "---\nid: get-a-single-list\ntitle: \"Get a single list\"\ndescription: \"Get list by its id\"\nsidebar_label: \"Get a single list\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVUFv2zoM/isGTxtgJNmwhzf4Vjy8Dd0GbNg67BD4wFhMrFaWNIlukxn67wNlN01ab9hhp8jiJ/Ij+ZEZQFFsgvasnYUK3hIXRkcuNodCcyy0ghIYdxGqNXzQkSPUJURq+qD5ANV6gA1hoHDRcwvVuk51CR4DdsQUYgbEpqUOoRqAD56ggshB2x2UQHvsvJErTVqZ/WHX3V2//tftf/zT7pld81qia84QiX6pIJUQ6HuvAymoOPRUgsVOAGYElKAlE4/cgrAJFL2zkaIweLlayc950h8319Rwcae5HZNXyLiAEhpnmSzLC/Te6AblxfI6yrOZxFx2BCX44DwF1mNQrZ4mn+5pzxjO2D2tmu2NwY3URNJPJehmDphyI8hKTf7EywiYaZLtO+l+h7ZHAyXEDgOLDBRtsTcM1b0tlfC9p3D4o3i+3xjdnEA3zhlCK15ajP85wbuA7EKcR/WRwmdnfsva3VkK8q00OzncarrLN1P8Op0pag1Z8rk3U2VP6nh8NUPxhE99rlpISYK8Wr16qj2xF9ZxsXW9VX9Pc41T8+LqKEbczdke1SF7eMDXacyiI26dggp2lKPKmFWwlLmJy2GcwSQqoXB7vwH6YKCCAZUKFGNaotfL2xfSDAxaRJEpT+axRvfKapl9rJZLDofFDQa8IfIL9B4ejQlctVRMHgq3Lbil4v2EL0Yu0oaT5fVFKjlGPl1hx7JIZMkjw0R4GQTldHjjQofC8N23q1w7bbdOnkvWI6UXi9VidbLBjnwuPl3O8r/4dFlsXTgnL8nKuLjIHWYNTPtOdjUWUdudoby3HvscHsT0i8U+Zsq056U3qMepkmYNU2PXeauKuKtpvdYltC6ymIZhg5G+BpOSXE+jv64f+pq7r3SUs4JqiybSb0g++zzp73nxK3bTJdpDlo/p5QtKuKHDw19AqmWHECoKmcFovGga8nzy7Ml8iTyO+n77/xWUgOeieCSC7H2W0jCMiCt3QzalI0OWbyGY0k9EOoyc\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get a single list\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/lists/{listId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet list by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"ListId\"},\"required\":true,\"name\":\"listId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with list data.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"description\":{\"type\":\"string\",\"nullable\":true},\"icon\":{\"type\":\"string\"},\"parentId\":{\"type\":\"string\",\"nullable\":true},\"type\":{\"type\":\"string\",\"enum\":[\"manual\",\"smart\"],\"default\":\"manual\"},\"query\":{\"type\":\"string\",\"nullable\":true},\"public\":{\"type\":\"boolean\"},\"hasCollaborators\":{\"type\":\"boolean\"},\"userRole\":{\"type\":\"string\",\"enum\":[\"owner\",\"editor\",\"viewer\",\"public\"]}},\"required\":[\"id\",\"name\",\"icon\",\"parentId\",\"public\",\"hasCollaborators\",\"userRole\"],\"title\":\"List\"}}}},\"404\":{\"description\":\"List not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/get-a-single-tag.api.mdx",
    "content": "---\nid: get-a-single-tag\ntitle: \"Get a single tag\"\ndescription: \"Get tag by its id\"\nsidebar_label: \"Get a single tag\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVMFu2zAM/RWBpw0QkmzosMK3FNiKbocVW4YdghwYm7HV2JYq0W0yw/8+UHbTJPW6y06WRYp85HtkCxmF1BvHxtaQwDWxYszVeq8MB2Uy0MCYB0iWsJDvSkOgtPGG95AsW1gTevLzhgtIlqtupcGhx4qYfIgOIS2oQkha4L0jSCCwN3UOGmiHlSvlypDJyt0+rx7vLj/a3e8PxY7ZppeS3HB0WWB+k0GnwdN9YzxlkLBvSEONldg52jUYqcIhFyBYPAVn60BB8r+fzeRzWvC39R2lrB4NF6o0gVWGjBPQkNqaqWZ5gc6VJkV5Mb0L8mykLBsDgQbnrSPPpk9qspeld0+oxwxNdWXttkK/DUcOdVOtyZ87XO3nzJgWlC2i27/AoBkNWTQV1iOWrjvp9xKiHCL0M6CvwlqdsDiEvZhdvCRjgbmqLauNbers/3GQ2my82RWFgPmY7azwGOHZf9X1RVTEhc0ggZxiVpFdAlMZmGkbFdmBzIt/eJqGxpeQQItZ5imEborOTB/egYYH9AbX5cBTb+4btMGmZEigYHYhmU7Z7ydb9LglchN0DvR5FwtSQwRlN4oLUl8Hf9VjEQ6OBvmH9LHPfDzOh6ZIZqkjukEyOIEeDp+tr1AQfvm1iJ0z9cbKc6m6h/RuMpvMjqb5gGd+ezOKf357ozbWn4KXYjsNzgYeBDsMv2wtVMHUeUmyvs5Dts9KGt9wfZ1MO566Ek0taSJV7UDqst+CGpJ+0aw0FDawGNp2jYF++rLr5Pq+IS+rcfXMaWQ+M0HOGSQbLAO9gvDN90F5b9XfsA2XWO+jdMpG/kDDlvaHXditZLQJM/IRQG+bpyk5Pnr1YrBEGQdhX39agAY81cMZ/zH6KKK27T0Wdkt11z0DlH8B2HV/ANg2NwA=\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get a single tag\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/tags/{tagId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet tag by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"TagId\"},\"required\":true,\"name\":\"tagId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with list data.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"numBookmarks\":{\"type\":\"number\"},\"numBookmarksByAttachedType\":{\"type\":\"object\",\"properties\":{\"ai\":{\"type\":\"number\"},\"human\":{\"type\":\"number\"}}}},\"required\":[\"id\",\"name\",\"numBookmarks\",\"numBookmarksByAttachedType\"],\"title\":\"Tag\"}}}},\"404\":{\"description\":\"Tag not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/get-all-backups.api.mdx",
    "content": "---\nid: get-all-backups\ntitle: \"Get all backups\"\ndescription: \"Get all backups\"\nsidebar_label: \"Get all backups\"\nhide_title: true\nhide_table_of_contents: true\napi: eJyNVMGO00AM/ZXI51Hb5ZhbQYAWhFiJIg6rHtzEbWabzMx6PAslmn9HTrNtWirBqenY43nP79k91BQrtkGsd1DCR5IC27bYYLVPIYIBwV2E8hHejidrA5GqxFYOUD72sCFk4mWSBsrHdV4bYIrBu0gRyh7eLBb6c/nK180TVVL8tNJMXytqFJyBgco7ISd6EUNobYV6cf4U9XYPsWqoQ/2SQyAowQ/1wEBgH4jFHt9+JXFORGY8gAEr1MV/F7D1JCcKW7eDbCBF4vvbIYyR5FbMgEtti5uWoBROlA1UTChUL+VmpWh/0yTgUrch1sDG+32HvH/nk5ObGVFQUryFgVzqVMxArj6exFRVFFXoLdo2McE6GyBmz18oRtzRf3DJKvpzsky1Vrc1nJp0bsmU8EjvmswJ+jpf13zVUiMa60gaX0MJOxp0Q/UfzM++jcQvxHHwaOIWSuixrplizHMMdv5yBwZekK3yGJo1ho923WJqBUpoREIs53Phw2yPjHuiMMMQwFx5etVQMVYo/LaQhorPY35xxAI558n0fFMXj0adzNCp2fqy8hjSoByTtGfDxwfPHSrCTz9WoB2xbuv1urI+QrqbLWYLHWErKhWc8Cwf7m/iXz7cF1vPl+CVbDYQfJQOh/lzOAD6e1dcVOzPY3xrrRxJCv2SeWjRumGwVKd+FHOiuYHGR9Gjvt9gpO/c5qzHz4lY19D6LOWwhQw0hDXxoP6eDlDCsqooyKB5m4ZdcL1ZVJyTrT6+X6l3LyW5kmCo/rpZ3GFSu++PGSu/J5czmBGE6H/IauI/FkLfgQ==\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get all backups\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/backups\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet all backups\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with all backups data.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"backups\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"assetId\":{\"type\":\"string\",\"nullable\":true},\"createdAt\":{\"type\":\"string\"},\"size\":{\"type\":\"number\"},\"bookmarkCount\":{\"type\":\"number\"},\"status\":{\"type\":\"string\",\"enum\":[\"pending\",\"success\",\"failure\"]},\"errorMessage\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"userId\",\"assetId\",\"createdAt\",\"size\",\"bookmarkCount\",\"status\"]}}},\"required\":[\"backups\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/get-all-bookmarks.api.mdx",
    "content": "---\nid: get-all-bookmarks\ntitle: \"Get all bookmarks\"\ndescription: \"Get all bookmarks\"\nsidebar_label: \"Get all bookmarks\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzNWM1u4zYQfhVhLr0IcbZH37KLdpsW3QRIFj0EPoylkcU1RWpJyonX0LsXQ8qS7NCJnA3QnuKI8z/DjzOzg5xsZkTthFYwh8/kEpQyWWq9rtCsLaTgcGVh/gAf+2+LFCxljRFuC/OHHSwJDZmrxpUwf1i0ixRqNFiRI2M9gc1KqhDmO3DbmmAOS60loYI2BUPfG2Eoh3mB0lIKCismQZOVYkM5pCDYtO8NmS206dukFbjRbPAkedYZoVaQAqmmYtfRZpD6ULHvORXYSAfz8OUFrVYbd2NyMhOUqqZaknlJmhSVcOeY74ST/OFTY6x+UXQWKKaHehQFZxpKj+roukgsucTpxJ/29fSLTTKtHCmXPAquM0qEymSTU54IlbiSEkO21srSRfJFO0oTV4qBKUPFPBLNipJCm8TqioZqvYj6+D8xrQ92J/ZTYDwK+oIdCGosx/3Xy0v+c+jDzfIbZWynKw+va5KjQ1bVWcWsWNdSZMiss2+W+SN51V4ipFAbXZNxImgfcGAgRWNwy1Y7quzrIkT+vDTbFDJD6Ci/ctHTSueiEPHjFFQjJS65tDmFbV/oEyh7VImixwgloucOVyuhVncOXWNf1zcAiG2yjCyjaYFCNoY4TKRy5lq0KdimqtCIHz5L7y9eaTctPMGO7TRa3ZhsgtgRjNYCUnikJReo5N+VXgrJ1tKTI2W5ulOwQq0kFeHAeLdEVWvjvC+NJXMdr6nwUr1zoYZLGzlA5zArKf8YDVfvM7tZNhUqWLQH6PTAGjv5B9I83egCa0U3hX9HX3YgnJ42RQq1DiE0Mh6/ydfoAI4m0IsKV/Q1pvYU8ZW15GJ5jlViZoiULbU7h6vOi3PIi0bKWzYsIMhZmgxlBh8l5W9g3oic9FmG4kZkE/NSukp+GirtVfquKs8xZ+/5RAVM/f4AiI0rtZmWrGYphS1pGnWOjm47jmnxYI6/u+dtAsMRZHjqcIMX3KD9HCI4egqg6n/EICGA/LSbGzd1r+NnbUWuuZBM/nX/CrkHEfC3fGA68W7wU/MlCvMnn72pWGbFj7HYrsk/fbPeHOYhKIOv7xD1Rq2Vfoy8XZ5h0fahff+Hd1Ka+U37YwRi48egy34n6W78fYlKkbnuauQI2qEDXabr+l8PeBBBcggNyddaasz9cIkbdMgpHkJ3VoXFeoQhFIs2SjB00we9czqeog9G4MNONt6A9r1W11kNRdrnfDEMmfv1ALCBip5cN3We7fF49zCSM1J1iyuh2N9hJdG2PjAVuVLnMIdVSBfySgJmY5GWzGa/l/CNEOwwzw1Z286wFrPNB84/GsEW+vrsjsMYth/9S+dqO5/NnNlerNHgmqi+wLqG43nzvqSkk5Dows+Rf3X0SbCFjR+tVO54NuvGr9FipQ8ja/Z1zmQ8n3giLlb/43dtKmQL//zn3udCqEIzO3sdTPpwcXlxOdoP9PZc3V5H7b+6vfYj7YHx7Cy/l9o67m7nu/18G1siHTWMPdrFN07BUX45ZrVE4WevrmkNKR1XySKFUlvHH3e7JVrG5rblz2Gk5kTnwnI6h53AmraHO6YNyoaV+rXHCfKDGzSFYbz/mUK/3/BMoe1XNi8Tnwz8f7gHieY3ePVsNzJ4txiupV8zplAScmw5wYH7KsuoHnM9232wlB4kPv92D6E3HI/7h9dpPHeh2o5k73aB4l6vSbUt7F1w/D+0jNT/AhF9X5k=\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get all bookmarks\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/bookmarks\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet all bookmarks\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"boolean\"},\"required\":false,\"name\":\"archived\",\"in\":\"query\"},{\"schema\":{\"type\":\"boolean\"},\"required\":false,\"name\":\"favourited\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"enum\":[\"asc\",\"desc\"],\"default\":\"desc\"},\"required\":false,\"name\":\"sortOrder\",\"in\":\"query\"},{\"schema\":{\"type\":\"number\"},\"required\":false,\"name\":\"limit\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"title\":\"Cursor\"},\"required\":false,\"name\":\"cursor\",\"in\":\"query\"},{\"schema\":{\"type\":\"boolean\",\"default\":true,\"description\":\"If set to true, bookmark's content will be included in the response. Note, this content can be large for some bookmarks.\"},\"required\":false,\"description\":\"If set to true, bookmark's content will be included in the response. Note, this content can be large for some bookmarks.\",\"name\":\"includeContent\",\"in\":\"query\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with all bookmarks data.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarks\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"userId\":{\"type\":\"string\"},\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"attachedBy\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\"]}},\"required\":[\"id\",\"name\",\"attachedBy\"]}},\"content\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true},\"description\":{\"type\":\"string\",\"nullable\":true},\"imageUrl\":{\"type\":\"string\",\"nullable\":true},\"imageAssetId\":{\"type\":\"string\",\"nullable\":true},\"screenshotAssetId\":{\"type\":\"string\",\"nullable\":true},\"pdfAssetId\":{\"type\":\"string\",\"nullable\":true},\"fullPageArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"precrawledArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"videoAssetId\":{\"type\":\"string\",\"nullable\":true},\"favicon\":{\"type\":\"string\",\"nullable\":true},\"htmlContent\":{\"type\":\"string\",\"nullable\":true},\"contentAssetId\":{\"type\":\"string\",\"nullable\":true},\"crawledAt\":{\"type\":\"string\",\"nullable\":true},\"crawlStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\",\"nullable\":true},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true},\"size\":{\"type\":\"number\",\"nullable\":true},\"content\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"assetType\",\"assetId\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"unknown\"]}},\"required\":[\"type\"]}]},\"assets\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"pdf\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"avatar\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"userId\",\"tags\",\"content\",\"assets\"],\"title\":\"Bookmark\"}},\"nextCursor\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"bookmarks\",\"nextCursor\"],\"title\":\"PaginatedBookmarks\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/get-all-highlights.api.mdx",
    "content": "---\nid: get-all-highlights\ntitle: \"Get all highlights\"\ndescription: \"Get all highlights\"\nsidebar_label: \"Get all highlights\"\nhide_title: true\nhide_table_of_contents: true\napi: eJyNVU2P2zYQ/SvCnAl706NuRtGm2x6yQLfoYeHDWBpbjCmSGY42MQT+92Ik2ZY22qS+2KYe37z5euqhplSxjWKDhxI+khToXNHYU+PsqZEEBgRPCcoX+ON+uDeQqOrYygXKlx4OhEy866SB8mWf9wYiMrYkxGkApKqhFqHsQS6RoATftQdiyAaYvnSWqYbyiC6RAY+tIpxtrYABq8K+dMQXyGaFKQlbf1KdVpwe/NpxCj+krkbEknuv+BSDT5SU/peHB/1aFujT4TNVUny10rwpVFGj4AYMVMELedG7GKOzFerd7eekBCv6w0AJBiKHSCx2DD9rwR2LzHhR3UJt+jnHIYRzi3x+rL+vVzaQBFk+HY+JZLUz5OsfPK2CC7zWB/Jdq/NyIefCV9Cy1mDgxEQeDBxcRzpANR2xcwLlFZgNCH2TNUrfOYcH7a5wR9mAD0L/C2jXU+8S8TtVqZhQqN6tCMmLmXqZ13dZzXntpqwmzYOiW/x5tP19gm+bNkT09E2mmf55xm8ULhZ5RjQL9oQn61XDbL+zfgy0JE2ooYTTkEdE3W/YLkgT8et1yzt2UEKPdc2UUt5itNvXD2DgFdmqyGEqp8fjcl1noBGJqdxuhS+bMzKeieIGYwTzZgOfGyomhiIcC2mo+GvCF6MWVT8zqL914aZ9mNnUrZIaWfMYYFBOIB3U4cfvgVtUhX/++zy0w/pj0Oua9Sjpw+Zh8zAzoJue3dPjqv7d02NxDLwUr8lmAzEkaXGwismtVk15QdrfTecdCx9T1UHcRofWDzug3eqnri5GZW+gCUn0tO8PmOgfdjnr8eiV2uvaJu3o3VvPdJm59ivqmpcwePY72JsN38H7+6wMbxIDDWFNPIQcb+2qiuI8xHcuqyy30f342zMYwGXP3/R4YL9arL/MuPt+RDyHM/mc4Spd9D/kfc75P+nqe/Y=\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get all highlights\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/highlights\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet all highlights\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"number\"},\"required\":false,\"name\":\"limit\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"title\":\"Cursor\"},\"required\":false,\"name\":\"cursor\",\"in\":\"query\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with all highlights data.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"highlights\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"bookmarkId\":{\"type\":\"string\"},\"startOffset\":{\"type\":\"number\"},\"endOffset\":{\"type\":\"number\"},\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"],\"default\":\"yellow\"},\"text\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\",\"nullable\":true},\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"}},\"required\":[\"bookmarkId\",\"startOffset\",\"endOffset\",\"text\",\"note\",\"id\",\"userId\",\"createdAt\"],\"title\":\"Highlight\"}},\"nextCursor\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"highlights\",\"nextCursor\"],\"title\":\"PaginatedHighlights\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/get-all-lists.api.mdx",
    "content": "---\nid: get-all-lists\ntitle: \"Get all lists\"\ndescription: \"Get all lists\"\nsidebar_label: \"Get all lists\"\nhide_title: true\nhide_table_of_contents: true\napi: eJyNVMFu2zAM/RWDZyHJdvQtGLai24AVW4YdghwYm6nVyJIq0e0CQ/8+UHYSJw22nSxLj+QjH8keaopV0J61s1DCHXGBxhRGR46ggPExQrmGr/l/oyBS1QXNByjXPWwJA4Vlxw2U603aKAgUvbORIpQ9vF8s5HMZ4dv2iSouXjU350hFjYwzUFA5y2RZzNB7oysUs/lTFNseYtVQi3LigycowWVvoMAH5ymwHiIP9M8wDAEPoEAztfHf5rqeYCIHbR8hKbDY0s2Hiwyv3xXYzhjcGoKSQ0dJga5uAZMCj4Es398If8PLAHgLJNu1olmLtkMDCmKLgUW8mnbYGYby+JYUPHcUDv8Vz3dbo6sJdOucIbTipcH4wQneBWQX4m1UFyl8d+avrN2rpSD/tWYnhxdNr/lmjL9JSfrsudOBarHQNYzajJWd1PFkdYPihM9GAWsWYrnTIV3HGBpqk1J+aYkbV0MJj5SbB2UAYH4cmkjhhaQG6x66YKCEHus6UIxpjl7PX95JWhi0lDeXanwepuWoUcPsYzmfczjM9hhwT+Rn6D1cNRysGipGD4XbFdxQ8WXEFwMXSWgyvD9kjIbI0xE+iSKRJY8MEwkzCNR4+ORCi8Lw868VSD203Tkxl6wHSu9mi9kCzlU98Vk+3N/kv3y4L3YuXJKXZKXxXOQW88QMI/hmTV0N4GmLvN1nQ4JMv3nuDeqhLUWjfpTxpLWCxkWWi77fYqSfwaQk1+PErDdnEfP6U9AQ1hSy7ns6QAnLqiLPWW3T5UV0vdREllM73X1cgQK8FOOq+Nn7ca3Zw8R33w+IlduTTQnUSILlH5I07x+0owYu\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get all lists\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/lists\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet all lists\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with all lists data.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"lists\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"description\":{\"type\":\"string\",\"nullable\":true},\"icon\":{\"type\":\"string\"},\"parentId\":{\"type\":\"string\",\"nullable\":true},\"type\":{\"type\":\"string\",\"enum\":[\"manual\",\"smart\"],\"default\":\"manual\"},\"query\":{\"type\":\"string\",\"nullable\":true},\"public\":{\"type\":\"boolean\"},\"hasCollaborators\":{\"type\":\"boolean\"},\"userRole\":{\"type\":\"string\",\"enum\":[\"owner\",\"editor\",\"viewer\",\"public\"]}},\"required\":[\"id\",\"name\",\"icon\",\"parentId\",\"public\",\"hasCollaborators\",\"userRole\"],\"title\":\"List\"}}},\"required\":[\"lists\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/get-all-tags.api.mdx",
    "content": "---\nid: get-all-tags\ntitle: \"Get all tags\"\ndescription: \"Get all tags\"\nsidebar_label: \"Get all tags\"\nhide_title: true\nhide_table_of_contents: true\napi: eJydVU1v2zAM/SsGz0LS7uhbWmxFt8MKLMMOgQ+MzcRqbEmV6KyB4f8+0HK+0zTYSXFMPj6ST88tFBRyrx1rayCFJ+IEqyphXAZQ0B/pDKZyZgoC5Y3XvIF01sKc0JOfNFxCOsu6TIFDjzUx+dAHhLykGiFtgTeOIIXAXpsldAo8vTXaUwHpAqtACgzWEiHHozWM2kh9LZzeGvIb6NTHgArINLUQ7WEUNAGXcnqqaI0mJyFf0AKbiiEdXl+hEazn/ymPGhSUTY0GFBhrCLIrVZAZ85KKh83tta7A5Y0P1t8AZZp6ThJomqrCeUWQsm/oCnSla306kEzCg7MmUBDwL3d3chzr6ef8lXJO/moud7pKCmQcgYLcGibDkoXOVTpHyRq/Bkm9wNv2YKDAeevIs46Fo0h3Ueg99gNlqsPn2bq4OOTY+KUXTf1g7apGvwrnMz0JeNhMhh1P+7DPyKC+CBkVdf6m6452NpNm1PYOHBG9SitTwJpFBnLRQUANvfNj1NMFuZ/o5oTE4B0HEFkXqdbEpS0ghSX1vaNYB4yHhEB+vbWOxleQQotF4SmEboxOj9f3oGCNXkvpOK74Ospue7lLZhfS8Zj9ZrRCjysiN0LnQJ1oc1pSMiAkdpFwScmPIT6JXGTCB673SwQZKx96324+Uln66MMgHYJADT++WV+jMPz+Z9oPWZuFlXTpOlK6H92N7mC/jh2fycvzRf6Tl+dkYf0xeWm2U+Bs4EE3wzU+cfcjuHZ/Hc++ArE9pnceuwq1EfR+Q+2ww2HpmYLSBpbntp1joN++6jr5O3qGbLbQQfa3t5gVbc6Nf41VIzV7A/sgZTDpW0KPnPaWhJ2X3hK8dcd9bLYXav9tVFASFuT7CcSkSZ6TO8w6s0BB2d2Yp69TUIDHgjsRWI++dUFz2GrbxoipXZHpOtgyZ3mGTu7nPyWsuhQ=\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get all tags\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/tags\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet all tags\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\"},\"required\":false,\"name\":\"nameContains\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"enum\":[\"name\",\"usage\",\"relevance\"],\"default\":\"usage\"},\"required\":false,\"name\":\"sort\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\",\"none\"]},\"required\":false,\"name\":\"attachedBy\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\"},\"required\":false,\"name\":\"cursor\",\"in\":\"query\"},{\"schema\":{\"type\":\"number\",\"nullable\":true},\"required\":false,\"name\":\"limit\",\"in\":\"query\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with all tags data.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"numBookmarks\":{\"type\":\"number\"},\"numBookmarksByAttachedType\":{\"type\":\"object\",\"properties\":{\"ai\":{\"type\":\"number\"},\"human\":{\"type\":\"number\"}}}},\"required\":[\"id\",\"name\",\"numBookmarks\",\"numBookmarksByAttachedType\"],\"title\":\"Tag\"}},\"nextCursor\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"tags\",\"nextCursor\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/get-bookmarks-in-the-list.api.mdx",
    "content": "---\nid: get-bookmarks-in-the-list\ntitle: \"Get bookmarks in the list\"\ndescription: \"Get bookmarks in the list\"\nsidebar_label: \"Get bookmarks in the list\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzNWN1v2zYQ/1cEvmwDhDgdOqzQW1psXfbRBmuKPQR+OEsnizVFquTJiWvofx+OlCXZkR25DbA9xRHv83fHu+NtRYYutbIiabRIxFukaGHMqgS7cpHUERUYKelIxIJg6URyJ/6UjpyYx8JhWltJG5HcbcUCwaK9qqkQyd28mceiAgslElrnCVxaYAki2QraVCgS4chKvRSxwAcoK8WfJMpMPWyW5f2nVz+bhy8/FQ9EJn3F2iV5EtZ+nYkmFhY/19JiJhKyNcZCQ8kEKhDEQrJDFVAhmvikel2X7Be4VMQeD3YuwxxqRSIJX/b05aBcr9AZS+9thnan83ONdjOuVNflAu0paUqWkiZI6szfAfOmts6cFJ0GiqdlL4xRCFoMUAgQ7yfLdR45pIhM5E+7xPnORanRhJqie6lUtMBI6lTVGWa7lLLoKqMdXkTvDGEcUSF7phQ08yiwS4xyYyNnSuzT8mLUx/+JaR3Yrdg3gfEA9Dk7ENQ4xv3Hy0v+s+/D+8UnTNlOKvwVjDIgYA2tMcwBVaVkCswx++SYbSScxgsSsaisqdCSDEo7qwekYC1s2FjC0j0tQmaPM7KJRWoRCLMrGj0tTSZzOX4cC10rBQvOaI5c0+X3BEqwaSHXmI1kchOLHNaGy9Wxc4LlUurlBwKq3dP6+rrh6jRF5wSrkKq2yDChzphr3sTC1WUJVn7xUXp+8drQNHiCHZtptKa26QSxg+pZSRGLe1xwgir+XZqFVOjrO6F2nNSxcFIvFebhwHq3ZFkZS96X2qG9Hs+p0H2eOVHDXR05ACJIC8xej8LV+cxuFnUJWsybvaJ0xxpb+XvSPN3gAhuN73PfH087EE6Pm6KkXgUIrRrHb/I12qtCE+hlCUv8OKb2GPGVc0hjcR7LxNQialcYOoeryvJzyPNaqRs2LFSQszRZTC3cK8y+gnktMzRnGQprmU6MS0GletNn2pP0bVaeY87O84kKmPr5CyDUVBg7LVj1QklX4DTqDAhvWo5peDDHX217m8BwUDI8dbjBc57Lvq0iED6Eoup/jJWEUOSn3dxxU3c6vtVW4JwLweRft0+Q+yIi/C3vmY70DW4170bL/NG2N7WWOfllKLad7Y/frK+GuQel9/UZUK/1Spv7kd7lGeZNB+3zN95JYeae9tugiA2bQRv9VtKH4fcFaI32us2Rg9Iu2qLLdO386wueGKnkIgwkHytlIEPu57AGAg5xD91ZGTY2I/RQzJtRgn6a3pudB/Pu3nB7OMmOD6DdrNVOVn2SdjGf92/L1y1Ugg3U+EDtY/Nsj/s3x56cgaobWErN/r7uSBuPy8vLl4/fSLwNiLShKDe1zp7vbZSabHwwLNE5TqzHZweeegk9Pce28fxUmEwkYhlyjtcTiZjx+87NtmF30cyGMDm0690OxQ93YgtZZtG5ZgaVnK1fcE6DlYy6N749DmjtthgFUeWS2Yzs5mIFFlaI1QVUlTh8Ot8WGLUSIpP7J/EfLX0UbOGADNY/HxjT9kk5WAJ1ALFmf3eZjN9cnogvoP/xq7ElsIW//3PrUZQ6N8zOXgeTXlxcXlwOVh2dPVc316P2X91c+9f5nvHsLM8AxhFP7Ml291Q/tfQ6GIa79Dq9KQuOc3ecVQqkf1+2g3mI+J1fU3F4k25f1Qd9HovCOGKy7XYBjjtS0/DnsD/gVMik44D3C5AVbg6WUWtQNdvhlzxH6Hfrpim03f7oNPFRyP7DpcxoRIJXjxY1vXfz/mKNY37U1e//bkvBD9Ex7bt+qjdDnX1cwpqTG0yBwAFlC8LhVZpiNTT1Ualj07tS8/aXWxHG5OHmY/8WDp+g+yZtt4Hi1qxQN01nIfH/bGDT/Au5qa9K\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get bookmarks in the list\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/lists/{listId}/bookmarks\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet bookmarks in the list\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"ListId\"},\"required\":true,\"name\":\"listId\",\"in\":\"path\"},{\"schema\":{\"type\":\"string\",\"enum\":[\"asc\",\"desc\"],\"default\":\"desc\"},\"required\":false,\"name\":\"sortOrder\",\"in\":\"query\"},{\"schema\":{\"type\":\"number\"},\"required\":false,\"name\":\"limit\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"title\":\"Cursor\"},\"required\":false,\"name\":\"cursor\",\"in\":\"query\"},{\"schema\":{\"type\":\"boolean\",\"default\":true,\"description\":\"If set to true, bookmark's content will be included in the response. Note, this content can be large for some bookmarks.\"},\"required\":false,\"description\":\"If set to true, bookmark's content will be included in the response. Note, this content can be large for some bookmarks.\",\"name\":\"includeContent\",\"in\":\"query\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with list data.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarks\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"userId\":{\"type\":\"string\"},\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"attachedBy\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\"]}},\"required\":[\"id\",\"name\",\"attachedBy\"]}},\"content\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true},\"description\":{\"type\":\"string\",\"nullable\":true},\"imageUrl\":{\"type\":\"string\",\"nullable\":true},\"imageAssetId\":{\"type\":\"string\",\"nullable\":true},\"screenshotAssetId\":{\"type\":\"string\",\"nullable\":true},\"pdfAssetId\":{\"type\":\"string\",\"nullable\":true},\"fullPageArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"precrawledArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"videoAssetId\":{\"type\":\"string\",\"nullable\":true},\"favicon\":{\"type\":\"string\",\"nullable\":true},\"htmlContent\":{\"type\":\"string\",\"nullable\":true},\"contentAssetId\":{\"type\":\"string\",\"nullable\":true},\"crawledAt\":{\"type\":\"string\",\"nullable\":true},\"crawlStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\",\"nullable\":true},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true},\"size\":{\"type\":\"number\",\"nullable\":true},\"content\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"assetType\",\"assetId\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"unknown\"]}},\"required\":[\"type\"]}]},\"assets\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"pdf\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"avatar\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"userId\",\"tags\",\"content\",\"assets\"],\"title\":\"Bookmark\"}},\"nextCursor\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"bookmarks\",\"nextCursor\"],\"title\":\"PaginatedBookmarks\"}}}},\"404\":{\"description\":\"List not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/get-bookmarks-with-the-tag.api.mdx",
    "content": "---\nid: get-bookmarks-with-the-tag\ntitle: \"Get bookmarks with the tag\"\ndescription: \"Get bookmarks with the tag\"\nsidebar_label: \"Get bookmarks with the tag\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzNWF9v2zYQ/yoCX7YBQpwOHVboLS22LhvWBquLPQR+OEtniTVFquTJiWvouxdHypLsyIncBtie4oj393fHu+PtRIYutbIiabRIxFukaGnMugS7dtGdpCKiAiOCXMSCIHciuRVz/ruIhcO0tpK2IrndiSWCRXtVUyGS20WziEUFFkoktM4TuLTAEkSyE7StUCTCkZWa5eI9lJXiTxJlpu63eXn36dWv5v7LL8U9kUlfsXJJnmQO+XUmmlhY/FxLi5lIyNYYCw0ln5M/j4VkdyqgQjTxo8p1XbJT4FIRezTYtQxXUCsSSfhyoG4FyvX6nLH03mZo9zo/12i340p1XS7RPiZNyVLSBEmd+XtY3tTWmUdFp4HiadlLYxSCFgMUAsKHqXK9ihxSRCbyp13a/OCi1GhCTdGdVCpaYiR1quoMs0hqn04WXWW0w4vonSGMIypkz5SCZh4FNsdoZWzkTIl9Ul6M+vg/Ma0DuxX7JjAegb5gB4Iax7j/fHnJfw59eL/8hCmFK6ikoygDAtbQGsMcUFVKpsAcs0+O2UbCabwgEYvKmgotyaC0s3pACtbClo0lLN3TImT2MCObWKQWgTC7otHT0mRyJcePY6FrpWDJGc2Ra7r8nkAJNi3kBrORTG5isYKN4WJ16pwgz6XOPxBQ7Z7W19cNV6cpOidYhVS1RYYJdcZciyYWri5LsPKLj9Lzi9eGpsET7NhOozW1TSeIHVTPSopY3OGSE1Tx79IspUJf3Qm146SOhZM6V7gKB9a7JcvKWPK+1A7t9XhOhdbzzIka7urIARBBWmD2ehSuzmd2s6hL0GLRHBSlW9bYyj+Q5ukGF9hofL/y3fFxB8LpaVOU1OsAoVXj+E2+RgdVaAK9LCHHj2NqTxFfOYc0FuexTEwtonaFoXO4qmx1DvmqVuqGDQsV5CxNFlMLdwqzb2DeyAzNWYbCRqYT41JQqd70mfYkfZuV55iz93yiAqZ+/gIINRXGTgtWvVTSFTiNOgPCm5ZjGh7M8Xfb3iYwHJUMTx1u8ILnsu+rCIT3oaj6H2MlIRT5aTd33NS9ju+1FTjnQjD51/wJcl9EhL/lPdOJvsGt5t1omT/Z9qbWMie/DMW2s/3pm/XNMPeg9L4+A+q1XmtzN9K7PMOi6aB9/sY7Kczc0/4YFLFhM2ij30r6MPy+BK3RXrc5clTaRVt0ma6df33BEyOVXISB5GOlDGTI/Rw2QMAh7qE7K8PGZoQeikUzStBP0wez82DePRhujyfZ8QG0m7XayapP0i7mi/5t+bqFSrCBGu+pfWye7XH/5jiQM1B1A7nU7O/rjrTxuLy8fPnwjTSHPNKGopWpdfZ8T6PUZONzYYnOcV49PDty1Evo6Tm0jeenwmQiEXlIOd5OJGLGIZjt/OKimQ0xcmg3+/WJn+zEDrLMonPNDCo527zghAYrGXJvenscoNqvMAqiyiWzGdntxRosrBGrC6gqcfxunhcYtRIis/Lv4b9a+ijYwtEYbH4+MKLte3Kw/+ngYc3+4jIZP7g8Ed8+/+N3Y0tgC//8d+4xlHplmJ29Dia9uLi8uBzsOTp7rm6uR+2/urn2T/MD49lZHgCMIx7Xk93+nf7ovutoFO6y64ktWXCdm+OsUiD987Kdy0PEb/eXLtkvq/qgL2JRGEdMtNstwXE7ahr+HJYHnAqZdBzwfvuxxu3RJmoDqmYr/IbnBP1+1zSFtlsePU58ErH/cCMzGo/g1YMtTe/dor9Y45ifdPXHf9pC8FN0Svu+mertUOfeqpAUvv8WCBxPNiCcXaUpVkNLH9Q5tryrM29/m4swIg+3HoeXcPj8PLRotwsUc7NG3TS9gfw/G9g0XwGixqv5\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get bookmarks with the tag\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/tags/{tagId}/bookmarks\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet bookmarks with the tag\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"TagId\"},\"required\":true,\"name\":\"tagId\",\"in\":\"path\"},{\"schema\":{\"type\":\"string\",\"enum\":[\"asc\",\"desc\"],\"default\":\"desc\"},\"required\":false,\"name\":\"sortOrder\",\"in\":\"query\"},{\"schema\":{\"type\":\"number\"},\"required\":false,\"name\":\"limit\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"title\":\"Cursor\"},\"required\":false,\"name\":\"cursor\",\"in\":\"query\"},{\"schema\":{\"type\":\"boolean\",\"default\":true,\"description\":\"If set to true, bookmark's content will be included in the response. Note, this content can be large for some bookmarks.\"},\"required\":false,\"description\":\"If set to true, bookmark's content will be included in the response. Note, this content can be large for some bookmarks.\",\"name\":\"includeContent\",\"in\":\"query\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with list data.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarks\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"userId\":{\"type\":\"string\"},\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"attachedBy\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\"]}},\"required\":[\"id\",\"name\",\"attachedBy\"]}},\"content\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true},\"description\":{\"type\":\"string\",\"nullable\":true},\"imageUrl\":{\"type\":\"string\",\"nullable\":true},\"imageAssetId\":{\"type\":\"string\",\"nullable\":true},\"screenshotAssetId\":{\"type\":\"string\",\"nullable\":true},\"pdfAssetId\":{\"type\":\"string\",\"nullable\":true},\"fullPageArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"precrawledArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"videoAssetId\":{\"type\":\"string\",\"nullable\":true},\"favicon\":{\"type\":\"string\",\"nullable\":true},\"htmlContent\":{\"type\":\"string\",\"nullable\":true},\"contentAssetId\":{\"type\":\"string\",\"nullable\":true},\"crawledAt\":{\"type\":\"string\",\"nullable\":true},\"crawlStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\",\"nullable\":true},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true},\"size\":{\"type\":\"number\",\"nullable\":true},\"content\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"assetType\",\"assetId\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"unknown\"]}},\"required\":[\"type\"]}]},\"assets\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"pdf\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"avatar\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"userId\",\"tags\",\"content\",\"assets\"],\"title\":\"Bookmark\"}},\"nextCursor\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"bookmarks\",\"nextCursor\"],\"title\":\"PaginatedBookmarks\"}}}},\"404\":{\"description\":\"Tag not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/get-current-user-info.api.mdx",
    "content": "---\nid: get-current-user-info\ntitle: \"Get current user info\"\ndescription: \"Returns info about the current user\"\nsidebar_label: \"Get current user info\"\nhide_title: true\nhide_table_of_contents: true\napi: eJyNUz1v2zAQ/SvCmwnL6ajNQxukHRq0DjoYHs7S2WIsiQx5SmsI/O/FUUZiJxmiRRJ5H+/eezeh4VgH68W6ARV+sYxhiIUd9q6gnRulkJaLegyBBynGyAEGQoeIaoOHyCFiaxC5HoOVE6rNhB1T4LAapUW12aatQeDo3RA5oprwZbnU13Xfn7tHrqX4a6XNTYqGhBYwqN0gPIhmkPedrUkzyseoaRNi3XJP+iUnz6jgciEY+OA8B7FzU9tcxEQJdjggGQzU8/sLg2HsOtp1jErCyMmAe7LdpyJtT4fP1excTZ1SeBG9c65jGpCSsvY02sCNMm0bXCZskz4GPUvrGlQ4cJ6ZlHOUymAse4YqE55VJBVmDB0qTNQ0gWNMJXlbPt/A4JmCVWiZqvP1rNGexk5QoRXxsSpLCafFkQIdmf2CvId5I+S65eJcoXD7bJ4f5/hixoKU0oVlfquCc+dL47wwop11jhymDOUgmPPHNxd6UoTf/6wza+pcTdepZ0g3i+Viqa61ouzjBc/q/u5D/Kv7u2LvwjV4HTYZeBelp+y92Ty4Zbnaj7w7b+tOr0b+7I7N4wv/k9J3ZAftnhWczjpvkHWG2kCXsHVR9HSadhT5IXQp6fHTyEEXc/uqc95Lg5apUfdtJhz5hAqrumYv2RDdqN3frZwq92K6269rGNC1Xm/0ydXPVzScLmpP0xyxdkceUoI5gxD9R1KL/wcmvpVd\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get current user info\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/users/me\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nReturns info about the current user\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with user data.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\",\"nullable\":true},\"email\":{\"type\":\"string\",\"nullable\":true},\"image\":{\"type\":\"string\",\"nullable\":true},\"localUser\":{\"type\":\"boolean\"}},\"required\":[\"id\",\"localUser\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/get-current-user-stats.api.mdx",
    "content": "---\nid: get-current-user-stats\ntitle: \"Get current user stats\"\ndescription: \"Returns stats about the current user\"\nsidebar_label: \"Get current user stats\"\nhide_title: true\nhide_table_of_contents: true\napi: eJylVktv20gM/isCz4Ll7FE3F32lu4sWGwdFYfgwkmlramlG5VBuVEH/veBItiXF3iTNKYr5+kh+Q7KBDbqUdMnaGojhP+SKjAscK3aBSmzFAWcYpBURGg4qhwQhsNo5iFdw75AcrENwmFakuYZ41UCCipAWFWcQr9btOgRCV1rj0EHcwF/zufwZB/6cfMeUg5+aMx+kQzCDEFJrGA2LiSrLXKdKTKLvTuwacGmGhZIvrkuEGKz3BCGUZEsk1l1UUxVvrN0XivZuoG2qIkGCNpSv9+pgSTNeVVhQmukDbq7Jl74ul2X/aMdXhR/1Lsv1LruikRyRv6mXXvZUtrk2+4ueGB/4okA5h5ckrXTvR6VJsl51jns3R6O1+LXlW1sobYYJKCJVQwiasXBPg954BwM9x6TNTtCltjLPQNd7OOoLsEI93Hbhb+YeJ6t8IbDv9C+8XonHlX5hMjy2fjqVHtsVWJNEvfToa2i5bgd00Wa3SFkf/MN8EnCm3VfEK7TJtPvXGnnRV6TfUNFl7tYfbUV/XshsbH12/ExOePszIzyit6r+vJ0k+1Kyqvo1qDY+1gnUtL3HXgwrP6jzqarjZPxDVLt7p3avYK5RxcuYOwHv7f/nCQ6G2Z2tKH0FVDe178HKTM1zleQIMVOFIaCpCgGnSg0h/MREEObyXdhE5wIYHxiNk3UUgtNml+O2E5BzAqkoLXUJPbMQPbrrbR7tpMkGGu+b83YZ7JLp5ni8J0Zz+dHwm4y6y3NjwKhLrZOkJK0CObMbiGGHvk1KRgVEsspdVGDk17kUFukgN4PcCRXlEEOjNhtC59pIlTo63EAIB0Vamud73Iu7k2Grqpwhhoy5dHEUMdWzvSK1RyxnqiwhnNwVywyD3kNgt/6W+bvXDzos0Lbt4IK5k3uiizy8Y069lsiSh1eDuFeS2viP95YKJQg/fV16QmiztWIuWXeQbmbz2Vzqqln4CSc8iy+3F/EvvtwGW0tj8JJsG0JpHRfKnN8tfEAenWvBsfQjx835rnr20dcVQFZ/VOayZ9uw62HT93sFvt/yqIQtXeB1CJl1LNKmSZTDe8rbVn7+USHJxbg+d9wfjCFkqDZIniR7rCGGRZpiyZ4aeeUHxfQUlB6eSPjh3VLYPe7cpFPe+3HsmHrgu2k6jaXdo2lbCHsQLP9DK5T/DVVi30E=\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get current user stats\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/users/me/stats\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nReturns stats about the current user\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with user stats.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"numBookmarks\":{\"type\":\"number\"},\"numFavorites\":{\"type\":\"number\"},\"numArchived\":{\"type\":\"number\"},\"numTags\":{\"type\":\"number\"},\"numLists\":{\"type\":\"number\"},\"numHighlights\":{\"type\":\"number\"},\"bookmarksByType\":{\"type\":\"object\",\"properties\":{\"link\":{\"type\":\"number\"},\"text\":{\"type\":\"number\"},\"asset\":{\"type\":\"number\"}},\"required\":[\"link\",\"text\",\"asset\"]},\"topDomains\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"domain\":{\"type\":\"string\"},\"count\":{\"type\":\"number\"}},\"required\":[\"domain\",\"count\"]},\"maxItems\":10},\"totalAssetSize\":{\"type\":\"number\"},\"assetsByType\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\"},\"count\":{\"type\":\"number\"},\"totalSize\":{\"type\":\"number\"}},\"required\":[\"type\",\"count\",\"totalSize\"]}},\"bookmarkingActivity\":{\"type\":\"object\",\"properties\":{\"thisWeek\":{\"type\":\"number\"},\"thisMonth\":{\"type\":\"number\"},\"thisYear\":{\"type\":\"number\"},\"byHour\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"hour\":{\"type\":\"number\"},\"count\":{\"type\":\"number\"}},\"required\":[\"hour\",\"count\"]}},\"byDayOfWeek\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"day\":{\"type\":\"number\"},\"count\":{\"type\":\"number\"}},\"required\":[\"day\",\"count\"]}}},\"required\":[\"thisWeek\",\"thisMonth\",\"thisYear\",\"byHour\",\"byDayOfWeek\"]},\"tagUsage\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"},\"count\":{\"type\":\"number\"}},\"required\":[\"name\",\"count\"]},\"maxItems\":10},\"bookmarksBySource\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"count\":{\"type\":\"number\"}},\"required\":[\"source\",\"count\"]}}},\"required\":[\"numBookmarks\",\"numFavorites\",\"numArchived\",\"numTags\",\"numLists\",\"numHighlights\",\"bookmarksByType\",\"topDomains\",\"totalAssetSize\",\"assetsByType\",\"bookmarkingActivity\",\"tagUsage\",\"bookmarksBySource\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/get-highlights-of-a-bookmark.api.mdx",
    "content": "---\nid: get-highlights-of-a-bookmark\ntitle: \"Get highlights of a bookmark\"\ndescription: \"Get highlights of a bookmark\"\nsidebar_label: \"Get highlights of a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVcGO2zYQ/RVhTi1ArJwiRQPdXKBNNzk0aLboYeEDLY0triWSGY42dgT+ezG0bEm7SjaHnCyTj8M3b94Me6gwlGQ8G2ehgLfIWW32dWP2NYfM7TKdbZ07tJoOoID1PkBxD78PSwE2CgKWHRk+QXHfwxY1Ia07rqG438SNAq9Jt8hIIQFCWWOroeiBTx6hgMBk7B4U4FG3vpElg6Zqjqd9+/nhzW/u+OXX+sjsyjfCwHCCXBjcVhAVEH7qDGEFBVOHCqxuBbQdQQqM5Oc11yCsCIN3NmAQJr+sVvIzl+KuxqwxgUWEURJQUDrLaFlOaO8bU2o5kT8EObaQoNs+YMmgwJPzSGzOl05ijlhNpE9ClrENL8eYJPhM0KggsCb+e7cLyJN927VbJNlHW31jt3SNo8VC2a4VF5ywadxnEDFF4D0hWlCwbToUX1S4013DUFyAUQHjkZdC2q5p9FYqKwWMCqxj/C6gWU69C0hfUaUk1IzVeoFInHnpfm6gqZpT7YasBs6J0fX+6W2b0b1/XWoP8emVE1tsYtp9vXr93J0X/2fWcbZzna1+nDNLVy1oHxW0GILeL+09SSJFGPEpkXSea1dBAfskW2rGAvKLyiHvR8FjPmu6gPR4GSEdNVBAr6uKMISYa2/yx1eg4FGTEXOkLIbts3QXJ9bMPhR5znS6OWjSB0R/o70HtdD9QwQZAFxj9n7AZ2cuUrrJ9Pso4g5dOZmBV6XkZskjwWQ2JZC0S/r401GrheG7/+6SnMbunByXrM+UXt2sblaTEXjls/5wu8h//eE22zmak5dkowLvArc62WIYli+M/ln4frTai0/GOX1pkdw32tjUnVLBfjDA2GZS6GLWc9NuUFC7wILv+60O+C81Mcrypw5JXp/N6IDkk8oE+a6g2Okm4Ddy+Omfwbw/Z1+jfBnQ9pSMJjOuAFBwwNP8pYmbqKBGXSElFmfAuizR8+ToswYVM10b5O0fd6BAzy30xDIp+iKtvj8j7twBbYxXliz/hWCM/wNdHrx1\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get highlights of a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/bookmarks/{bookmarkId}/highlights\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet highlights of a bookmark\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The list of highlights\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"highlights\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"bookmarkId\":{\"type\":\"string\"},\"startOffset\":{\"type\":\"number\"},\"endOffset\":{\"type\":\"number\"},\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"],\"default\":\"yellow\"},\"text\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\",\"nullable\":true},\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"}},\"required\":[\"bookmarkId\",\"startOffset\",\"endOffset\",\"text\",\"note\",\"id\",\"userId\",\"createdAt\"],\"title\":\"Highlight\"}}},\"required\":[\"highlights\"]}}}},\"404\":{\"description\":\"Bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/get-lists-of-a-bookmark.api.mdx",
    "content": "---\nid: get-lists-of-a-bookmark\ntitle: \"Get lists of a bookmark\"\ndescription: \"Get lists of a bookmark\"\nsidebar_label: \"Get lists of a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVU2P3DYM/SsGTy0grCdFiga+bYs22CaHINmih8EcODZnrB1ZUiR6dyaG/ntB2fO162x76Mm29CQ+Pj7SAzQU66A9a2ehgvfEhdGRY+E2BRZr53Ydhh0oYNxGqJbw67QUYaUgUt0HzQeolgOsCQOF255bqJartFLgMWBHTCFmQKxb6hCqAfjgCSqIHLTdggLaY+eNLGnSjdkftt3Tw7tf3P7bz+2e2dXvhIHmDDkyuGsgKQj0tdeBGqg49KTAYieg9RmkQEtqHrkFYRUoemcjRWHy02Ihj2sV7lvKKogIrd62Rm9bjqCgdpbJspxA742uUU6UD1GOzSTo1g9UMyjwwXkKrMegWeELGIaAB+HJ1MV/P66blxqmY+YzG1fJvRTf9sbgWqQVBZMCXc8BU64nWb6bCT9zywiYqbXtOzFSh7ZHAwpih4HFTQ1tsDcM1XEvKfjaUzj8p3i+XxtdX0DXzhlCK7e0GH9zgncB2YU4j+ojhc/OvMraPVkK8t1odvLyqOkpr0zxV+nKlEup1lSbSdkLHU+nZihe8Fmdzf9RR4b0PMZoqFXKG28Xb19a+tg0hXVcbFxvm//PzrVr5o3XUYy4ndt7xj/fcMbnRPJ5bl0DFWwpR5UOrqA89nYsh3Obp3IUQYZSeDyOnD4YqGDApgkUYyrR6/LxjZQNgxb75ASm7VG1owdbZh+rsuRwuNlhwB2Rv0HvQc1Mi+kGGRjcUvFhwhcjFynYxbT8IrqOkS9n5kkkiSx5ZJhYNINATS9/uNChMPzz7/uspLYbJ8cl65HSm5vFzeJiZJ743H66m+V/++mu2LhwTV6SlcZykTvMjpiG6/f/Es9Gzclgr/1YxqSZ9lx6g3psRanbMFV8eZrmUt7qarRPzlfQusgCHYY1RvormJRkeZofy9W55NkYjY7y3kC1QRPpFeY/fJ6M+mPxPbbHMW4P2Vmmly9QsKPD9a8orWQYETYUMosRcFvX5Pni6ItmFPecmuH97/egAK8988wj+fZZWsMwIu7djmxKJ5Ys30IwpX8ASlbEvw==\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get lists of a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/bookmarks/{bookmarkId}/lists\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nGet lists of a bookmark\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The list of highlights\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"lists\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"description\":{\"type\":\"string\",\"nullable\":true},\"icon\":{\"type\":\"string\"},\"parentId\":{\"type\":\"string\",\"nullable\":true},\"type\":{\"type\":\"string\",\"enum\":[\"manual\",\"smart\"],\"default\":\"manual\"},\"query\":{\"type\":\"string\",\"nullable\":true},\"public\":{\"type\":\"boolean\"},\"hasCollaborators\":{\"type\":\"boolean\"},\"userRole\":{\"type\":\"string\",\"enum\":[\"owner\",\"editor\",\"viewer\",\"public\"]}},\"required\":[\"id\",\"name\",\"icon\",\"parentId\",\"public\",\"hasCollaborators\",\"userRole\"],\"title\":\"List\"}}},\"required\":[\"lists\"]}}}},\"404\":{\"description\":\"Bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/karakeep-api.info.mdx",
    "content": "---\nid: karakeep-api\ntitle: \"Karakeep API\"\ndescription: \"The API for the Karakeep app\"\nsidebar_label: Introduction\nsidebar_position: 0\nhide_title: true\ncustom_edit_url: null\n---\n\nimport ApiLogo from \"@theme/ApiLogo\";\nimport Heading from \"@theme/Heading\";\nimport SchemaTabs from \"@theme/SchemaTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Export from \"@theme/ApiExplorer/Export\";\n\n<span\n  className={\"theme-doc-version-badge badge badge--secondary\"}\n  children={\"Version: 1.0.0\"}\n>\n</span>\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Karakeep API\"}\n>\n</Heading>\n\n\n\nThe API for the Karakeep app\n\n<div\n  style={{\"marginBottom\":\"2rem\"}}\n>\n  <Heading\n    id={\"authentication\"}\n    as={\"h2\"}\n    className={\"openapi-tabs__heading\"}\n    children={\"Authentication\"}\n  >\n  </Heading><SchemaTabs\n    className={\"openapi-tabs__security-schemes\"}\n  >\n    <TabItem\n      label={\"HTTP: Bearer Auth\"}\n      value={\"bearerAuth\"}\n    >\n      \n      \n      \n      \n      <div>\n        <table>\n          <tbody>\n            <tr>\n              <th>\n                Security Scheme Type:\n              </th><td>\n                http\n              </td>\n            </tr><tr>\n              <th>\n                HTTP Authorization Scheme:\n              </th><td>\n                bearer\n              </td>\n            </tr><tr>\n              <th>\n                Bearer format:\n              </th><td>\n                JWT\n              </td>\n            </tr>\n          </tbody>\n        </table>\n      </div>\n    </TabItem>\n  </SchemaTabs>\n</div>\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/remove-a-bookmark-from-a-list.api.mdx",
    "content": "---\nid: remove-a-bookmark-from-a-list\ntitle: \"Remove a bookmark from a list\"\ndescription: \"Remove the bookmarks from a list\"\nsidebar_label: \"Remove a bookmark from a list\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzNVMFu2zAM/RWBpw3Q4nTosMK3DOuAbsVQdBl2CHJgbCZWY1uqJKfJDP37QNmNkzYrdhiGnWzLj+Lj4yNbyMllVhmvdA0p3FKlNyR8QWKh9bpCu3ZiaXUlUJTKeZDgceUgncG1ct7BXIKjrLHK7yCdtbAgtGQnjS8gnc3DXIJBixV5si4CXFZQhZC24HeGIAXnrapXIIG2WJmSjxSpvNzuVtXD3cV7vf35rth6r7MLzq58hHD2qxyCBEv3jbKUQ+ptQxJqrBhQdgAJiusy6AsI8q+l/9CL8yKFxQA6ojHnCGd07cgxk7fjc34cd+KrFpmuPdVevDnqh3hAJzDPKaY+H4+fxz6yE1hawnwnau2Fqh872N/LcWhMqTLkuOTOcfAJifTijjIONFYbsl51tDOd03Mhg4SKnMPVqX9HWs26Gwb8PIQQSzohB/dbaDuowCUtdVPn/3tBMd4XOocUcirJMyIaIYWEO+KStjNrSPYzl7SDeQLwjNnN4wQ1toQUWsxzS86FBI1KNmcgYYNW4aLsiul/d0ousSk9pFB4b1yaJN7uRmu0uCYyIzQG5BO5pwWJ/gahl9F/X3q86LhACOFg+L+xxl3mwxWwF4wzcx0RxrMRQSD7l0/aVsgMP/+YRlVVvdQczlV3lM5G49H4YAT3fCY3Vyf5T26uxFLbY/JcbJBgtPMVRnf0w9pvPhwcdrz2ju5vB8v90crsNPC09YkpUdVMIbax7Z0wi/vKgYR0v7j2t/HpwS6ZSyi08xzUtgt09N2WIfDxfUOW1/B88EJ0TK4cv+eQLrF09EIxr257N78Wv+PdH2K9i5YrG/4CCWvaDWuXV+0/zHqgTpgHCQVhTjbW3gEmWUbGH4Q+2xNs5v2cfry8vpxeggQ8dvET18YEJ5m1bYeY6jXVIeyJev5mjiH8AkTPj4c=\nsidebar_class_name: \"delete api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Remove a bookmark from a list\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"delete\"}\n  path={\"/lists/{listId}/bookmarks/{bookmarkId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nRemove the bookmarks from a list\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"ListId\"},\"required\":true,\"name\":\"listId\",\"in\":\"path\"},{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"204\":{\"description\":\"No content - the bookmark was added\"},\"400\":{\"description\":\"Bookmark already not in list\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}},\"404\":{\"description\":\"List or bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/replace-asset.api.mdx",
    "content": "---\nid: replace-asset\ntitle: \"Replace asset\"\ndescription: \"Replace an existing asset with a new one\"\nsidebar_label: \"Replace asset\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy9VE2P4zYM/SsCT11AO54ttujCt2yxBaYFisE0ix6CHBibiTWxJa1EzyQ19N8Lys7XTDpAgUUvgSOR1HuPjxygplgF49k4CyU8kG+xIoVW0c5ENnajMEZi9Wy4UagsPStnCTQwbiKUC/js3LbDsI2w1BCp6oPhPZSLAVaEgcKs5wbKxTItNXgM2BFTiDkgVg11COUAvPcEJUQOxm5AA+2w860cGTJ1u9tvuufHTz+73d8/NTtmV30SBIZzyAHBXQ1JQ6BvvQlUQ8mhJw0WOwlanYI0GOHqkRtI+rvBmIlOb2LAKeICwHIMp8ifXb0XFJctmTeUVR/bwE6FqUfSEdBQOctkWRLR+9ZUKInFY5TsK+Tc6pEqBg0+OE+BDcWcO2F7pUK64LM4Bi5TGq+idzaORX68/fiawB9OTRjV+4OZMB5o1Cr2VUUxrvu23Yt4H68VOfRYWcdq7Xpbfz/qlavpCm8NHcWIm2t3LzTJFU7xWZqcz42rpdN9flX6XUJxsGIshpMrU5GlicUwCZxAxik8HYalDy2UMGBdB4oxFehN8fQBNDxhMLhqpzaO16OAa+xbhhIaZh/LouCwv9liwC2Rv0HvQV/x2lRBubXihtTvU7wasUBK6WzO/xSJx5fPp/2ol7wsPHKYTGEOAj19/OpCh4Lwt7/mWVRp3cNpGr4c5u/Mn2f9MXbt5E40Ggl8uLm9uT2bySP62f3dVbaz+zu1duGSqkiTNHgXucNspWmAj+tRwLysN5z8+J/26KgU044L36Kx8nRu9jA5ZnFcXhE0lBebbDSNHB/nUkPjIkvaMKww0tfQpiTH33oKspmXJ89kZ9UmyncN5RrbSG/Q+uFhMv079W/Ip0O0+2zNtpd/oGFL+8stLJv3f3z5IE9aJg0NYU0hkx9vfxkfej+XGqfsV1tFQI8Zs6oiz2/GLs82wP3Xubh+2vBd3jgQ8Bl0/s1AXeadhymfDdCi3fR5A8FYUmYEL0fsxUhlUlelGIYxYu62ZFM6KsPyX3RJ6R8BPcUh\nsidebar_class_name: \"put api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Replace asset\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"put\"}\n  path={\"/bookmarks/{bookmarkId}/assets/{assetId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nReplace an existing asset with a new one\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"},{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"AssetId\"},\"required\":true,\"name\":\"assetId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The new asset to replace with\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"assetId\":{\"type\":\"string\"}},\"required\":[\"assetId\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"204\":{\"description\":\"No content - asset was replaced successfully\"},\"404\":{\"description\":\"Bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/search-bookmarks.api.mdx",
    "content": "---\nid: search-bookmarks\ntitle: \"Search bookmarks\"\ndescription: \"Search bookmarks\"\nsidebar_label: \"Search bookmarks\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzNWEtv4zYQ/ivCXNoCQpzt0Tdv0EdadBM0WfQQ6DCWRhbXFKmQlBOvof9eDClLsiMn8m6A9hRHnMc3Dw5nZgcZ2dSIygmtYA53hCYtoqXW6xLN2kIMDlcW5g/wsfuWxGAprY1wW5g/7GBJaMgsalfA/CFpkhgqNFiSI2M9gU0LKhHmO3DbimAO1hmhVtDEYOixFoYymDtTUwwKSyZ4hBgEA3qsyWyhiU8LiYFUXTJCtCnE3iBgwZI2qFJiuBnlWEsH88HnA+U5Sttrt9q4G5ORmYBC1eWSzGvSpCiFO8ceJ5zkD1e1sfpV0WmgeFv2UmtJqGDgiuDvw/Bf55ElFzkd+dMuD36wUaqVI+WiJyFltKRIqFTWGWWRUJErKDJkK60sXUSftKM4coXomVJUzCPRrCjKtYmsLqnPsotRG/8n0Dpnt2KvAuOR0xM2IKix7PefLy/5z6ENN8svlDJOV3hgNtw2Q7aWzqtqUTErVpUUKTLr7Itl/pG4ai8RYqiMrsg4EbT397cnRWNwy6gdlfZtESIbva+pIXSULdzoaakzkYvx4xhULSUuObU5hE2X6BMo2U9iQ9lISjcx5LjRXI5OnTtcrYRa3Tl0tX1bX19RbJ2mZLkK5ihkbYjdRCpjrqSJwdZliUZ89VF6f/FKu2nuCTi202h1bdIJYgd1tRIQwxMtOUEl/y71UkhGS8+OlOXsjsEKtZKUhwPjzRJlpY3zttSWzPV4ToUX5p0TNVzakQN0DtOCso+j7upsZjOLukQFSXNQnR5YYyv/QJqnG1xgregm9+/f6waE09NQpFDr4EIjx/03+RodlKMJ9KLEFX0eU3uKeGEtubE4j2ViaoiULbQ7h6vK8nPI81rKWwYWKshZmgylBp8kZd/AvBEZ6bOA4kakE+NSuFJe9Zn2Jn2blefA2Vs+UQFTv38BxNoV2kwLVr2UwhY0jTpDR7ctxzR/MMdf7fM2geGoZHjqcIMTbtC+ryI4eg5F1f8YKwmhyE+7ueNQ9zq+FytyzoVg8q/7N8h9EQF/y3umE+8GPzWfRsv8yWdvai2z4utQbNvkn75Z3+zm3im9re/g9VqtlX4aebs8Q9J0rn3/h3dSmPlN+31QxIaPQRv9VtLd8PsSlSJz3ebIUWmHtugyXdv/+oIHI5UcQkPyuZIaM+L3HDfokEPcu+6sDBvrEXpXJM0oQd9NH/TOg373oLk97mTHG9Cu12o7qz5Ju5gn/ZC5H+uBASp6du3UebbFw53BQM5A1S2uhGJ7+1VC03jHlOQKncEcViFcyKsEmHUiZ2FSYoPJbPZrBd8PwQ6zzJC1zQwrMdt84DRAIxioT9P2OExj+zVA4Vxl57OZM9uLNRpcE1UXWFVwPHbeFxS1EiKd+6ntz5Y+CljYhsFG5I5HtHYKG+xFOm+yZp/uTMZjiifinPU/ftWmREb4xz/3PiRC5ZrZ2eoA6cPF5cXlYE3Q4VncXo/iX9xe+8n2ADwby8+mto6b3PluP+aOrICO2sau5o2ui4KZ/HzMKonCD2Bt5xriepgqbWSTGAptHZ/udku0XKmbhj+HAZvjnQnLUT2xIRji+vHvNjF/ik5BWtO23TNtUNZ87jcnL3QEuuFGaAr9fuczhbZb4rxOfNLY/3Az8opnX2xLeuuS/ob6hWEMBSH7loMcuBdpStWQ68U2hKV0ZeO3X+4hdIvDBcDhzRpOYqi2A9m7XaC412tSTQN7Exz/Dw3X7n8BUIFNqg==\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Search bookmarks\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/bookmarks/search\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nSearch bookmarks\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\"},\"required\":true,\"name\":\"q\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"enum\":[\"asc\",\"desc\",\"relevance\"],\"default\":\"relevance\"},\"required\":false,\"name\":\"sortOrder\",\"in\":\"query\"},{\"schema\":{\"type\":\"number\"},\"required\":false,\"name\":\"limit\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"title\":\"Cursor\"},\"required\":false,\"name\":\"cursor\",\"in\":\"query\"},{\"schema\":{\"type\":\"boolean\",\"default\":true,\"description\":\"If set to true, bookmark's content will be included in the response. Note, this content can be large for some bookmarks.\"},\"required\":false,\"description\":\"If set to true, bookmark's content will be included in the response. Note, this content can be large for some bookmarks.\",\"name\":\"includeContent\",\"in\":\"query\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object with the search results.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarks\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"userId\":{\"type\":\"string\"},\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"attachedBy\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\"]}},\"required\":[\"id\",\"name\",\"attachedBy\"]}},\"content\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true},\"description\":{\"type\":\"string\",\"nullable\":true},\"imageUrl\":{\"type\":\"string\",\"nullable\":true},\"imageAssetId\":{\"type\":\"string\",\"nullable\":true},\"screenshotAssetId\":{\"type\":\"string\",\"nullable\":true},\"pdfAssetId\":{\"type\":\"string\",\"nullable\":true},\"fullPageArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"precrawledArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"videoAssetId\":{\"type\":\"string\",\"nullable\":true},\"favicon\":{\"type\":\"string\",\"nullable\":true},\"htmlContent\":{\"type\":\"string\",\"nullable\":true},\"contentAssetId\":{\"type\":\"string\",\"nullable\":true},\"crawledAt\":{\"type\":\"string\",\"nullable\":true},\"crawlStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\",\"nullable\":true},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true},\"size\":{\"type\":\"number\",\"nullable\":true},\"content\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"assetType\",\"assetId\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"unknown\"]}},\"required\":[\"type\"]}]},\"assets\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"pdf\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"avatar\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"userId\",\"tags\",\"content\",\"assets\"],\"title\":\"Bookmark\"}},\"nextCursor\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"bookmarks\",\"nextCursor\"],\"title\":\"PaginatedBookmarks\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/sidebar.ts",
    "content": "import type { SidebarsConfig } from \"@docusaurus/plugin-content-docs\";\n\nconst sidebar: SidebarsConfig = {\n  apisidebar: [\n    {\n      type: \"doc\",\n      id: \"api/karakeep-api\",\n    },\n    {\n      type: \"category\",\n      label: \"Bookmarks\",\n      items: [\n        {\n          type: \"doc\",\n          id: \"api/get-all-bookmarks\",\n          label: \"Get all bookmarks\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/create-a-new-bookmark\",\n          label: \"Create a new bookmark\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/search-bookmarks\",\n          label: \"Search bookmarks\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-a-single-bookmark\",\n          label: \"Get a single bookmark\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/delete-a-bookmark\",\n          label: \"Delete a bookmark\",\n          className: \"api-method delete\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/update-a-bookmark\",\n          label: \"Update a bookmark\",\n          className: \"api-method patch\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/summarize-a-bookmark\",\n          label: \"Summarize a bookmark\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/attach-tags-to-a-bookmark\",\n          label: \"Attach tags to a bookmark\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/detach-tags-from-a-bookmark\",\n          label: \"Detach tags from a bookmark\",\n          className: \"api-method delete\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-lists-of-a-bookmark\",\n          label: \"Get lists of a bookmark\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-highlights-of-a-bookmark\",\n          label: \"Get highlights of a bookmark\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/attach-asset\",\n          label: \"Attach asset\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/replace-asset\",\n          label: \"Replace asset\",\n          className: \"api-method put\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/detach-asset\",\n          label: \"Detach asset\",\n          className: \"api-method delete\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"Lists\",\n      items: [\n        {\n          type: \"doc\",\n          id: \"api/get-all-lists\",\n          label: \"Get all lists\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/create-a-new-list\",\n          label: \"Create a new list\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-a-single-list\",\n          label: \"Get a single list\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/delete-a-list\",\n          label: \"Delete a list\",\n          className: \"api-method delete\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/update-a-list\",\n          label: \"Update a list\",\n          className: \"api-method patch\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-bookmarks-in-the-list\",\n          label: \"Get bookmarks in the list\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/add-a-bookmark-to-a-list\",\n          label: \"Add a bookmark to a list\",\n          className: \"api-method put\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/remove-a-bookmark-from-a-list\",\n          label: \"Remove a bookmark from a list\",\n          className: \"api-method delete\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"Tags\",\n      items: [\n        {\n          type: \"doc\",\n          id: \"api/get-all-tags\",\n          label: \"Get all tags\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/create-a-new-tag\",\n          label: \"Create a new tag\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-a-single-tag\",\n          label: \"Get a single tag\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/delete-a-tag\",\n          label: \"Delete a tag\",\n          className: \"api-method delete\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/update-a-tag\",\n          label: \"Update a tag\",\n          className: \"api-method patch\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-bookmarks-with-the-tag\",\n          label: \"Get bookmarks with the tag\",\n          className: \"api-method get\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"Highlights\",\n      items: [\n        {\n          type: \"doc\",\n          id: \"api/get-all-highlights\",\n          label: \"Get all highlights\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/create-a-new-highlight\",\n          label: \"Create a new highlight\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-a-single-highlight\",\n          label: \"Get a single highlight\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/delete-a-highlight\",\n          label: \"Delete a highlight\",\n          className: \"api-method delete\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/update-a-highlight\",\n          label: \"Update a highlight\",\n          className: \"api-method patch\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"Users\",\n      items: [\n        {\n          type: \"doc\",\n          id: \"api/get-current-user-info\",\n          label: \"Get current user info\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-current-user-stats\",\n          label: \"Get current user stats\",\n          className: \"api-method get\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"Assets\",\n      items: [\n        {\n          type: \"doc\",\n          id: \"api/upload-a-new-asset\",\n          label: \"Upload a new asset\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-a-single-asset\",\n          label: \"Get a single asset\",\n          className: \"api-method get\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"Admin\",\n      items: [\n        {\n          type: \"doc\",\n          id: \"api/update-user\",\n          label: \"Update user\",\n          className: \"api-method put\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"Backups\",\n      items: [\n        {\n          type: \"doc\",\n          id: \"api/get-all-backups\",\n          label: \"Get all backups\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/trigger-a-new-backup\",\n          label: \"Trigger a new backup\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-a-single-backup\",\n          label: \"Get a single backup\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/delete-a-backup\",\n          label: \"Delete a backup\",\n          className: \"api-method delete\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/download-a-backup\",\n          label: \"Download a backup\",\n          className: \"api-method get\",\n        },\n      ],\n    },\n  ],\n};\n\nexport default sidebar.apisidebar;\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/summarize-a-bookmark.api.mdx",
    "content": "---\nid: summarize-a-bookmark\ntitle: \"Summarize a bookmark\"\ndescription: \"Attaches a summary to the bookmark and returns the updated record.\"\nsidebar_label: \"Summarize a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVU2P2zYQ/SvCnFKAsJ0iRQLd3EOBbQ9ddLfIwfBhLI0triWSIYdeOwL/ezCUP3eVdg97skwOZ948vnnsoaZQee1YWwMlzJmxaigUWITYdegPBduCGypW1m479NsCTV144uhNyBvR1cgka5X19QQUMG4ClAv4/XgkwFJBoCp6zQcoFz2sCD35eeQGysUyLRU49NgRkw85IFQNdQhlD3xwBCUE9tpsQAHtsXOtLGnSdbs/bLrnpy+f7f77b82e2VZfBIHmHHJCcFdDUuDpW9SeaijZR1JgsJOg1SVIgRYWHHIDgspTcNYECoLk19lMfm4Je7xi4EzRs+bmxB8oqKxhMiyH0blWVyiHp09BMoz0aldPVDEocN468qyH+rp+zUdSUHmS6nMe3e1srdd6fFuBiW2LK6FKGEln4t4Qib5q9I6uMa2sbQmNlF3jzsp1/2yfcbPRZvPAyDH8fz0FZGInmgqxqigEkBK6jZ6EJjK1nFomBQPr+num+P3TG8tvo+d0+2+KtdFXb0h7gYlOg4JnWom6Wvnu7Eq3lOeDyQQRp4Kgzaal9bDhc1u6c9Zz7iUG8ndjmroZlYXo7lplN5q60sHNpb+84fGLOWNYpiRVP80+vZ6w0wwXxnKxttHU7zdSla1HeJexoRBwM7b3gpyc4RKfG8nnubG1WIkNuaw4SgnTk0WEaX+xnTQ9kSOpAvndyQajb6GEHuvaUwhpik5Pdx9BwQ69FmHkLo7bA3VrjC1DCQ2zC+V0yv4w2aLHLZGboHOgRhzsmKGw62zpfx3jiwELpJSuHPxByB0qX/v4mSmpLH3kMBn7HATq+PGH9R0Kwj+/PmY6tVlbOS5dD5A+TmaT2ZWNn/HM7+9G8c/v74q19bfgpdmk8g10mGVxNPyHE9sFnj37Zdr+IrF3ehQHcpj2PHUt6myD+X77ozoW55dIJqO8eZYuAlkqaERT5QL6foWB/vVtSrL8LZIYzmJ5kUcWUa2DfNdQrrEN9B+NfvjnqOxfip8hPi6iOWQVtlH+gYItHW6f0iQW0xDW5DOKIWBeVeT46uir6RWlnafn/u+HR/GYW4G9EFROP4qr74eIR7slk9IZJst/QZjSD3WCHTs=\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Summarize a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/bookmarks/{bookmarkId}/summarize\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nAttaches a summary to the bookmark and returns the updated record.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The updated bookmark with summary\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"userId\":{\"type\":\"string\"}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"userId\"]}}}},\"404\":{\"description\":\"Bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/trigger-a-new-backup.api.mdx",
    "content": "---\nid: trigger-a-new-backup\ntitle: \"Trigger a new backup\"\ndescription: \"Trigger a new backup\"\nsidebar_label: \"Trigger a new backup\"\nhide_title: true\nhide_table_of_contents: true\napi: eJyNVE1v2zAM/SsGz0Lc7qhbNmBANwwr0Aw7BDnQNhOrtiWVkrplhv77QNvIR5cC88W2SJGPfI8coaFQs/HROAsaNmwOB+ICC0u/igrrLnlQEPEQQG/h43QQYKcgUJ3YxCPo7QgVIROvU2xBb3d5p4ApeGcDBdAjfLi7l9d1qjlWUTNhpKYIqa4phH3q+yMoqJ2NZKNcQ+97U6NcK5+D3B0h1C0NKF/x6Ak0uOqZ6ggKPDtPHM2c2TQXPiGysQfIClIgfrhtwhAo3rIpsKnvseoJdOREWcGCfR1vRgrmD10YbBoqYjFUznUDcvfJJRtveoSIMYVbGMimQZjwZJv5ZGkcKNij6RMT7LICYnb8jULAA/1HLVkYe0mGqZHopoFTk84tuSx4Ke9tMSfouyyPgoFi6xrQ4F2Y6EHRCJTVoiQREr8Sh0lHiXvQMGLTMIWQS/SmfL0HBa/IRuBOPVnMs6T2mPoIGtoYfdBlGfm46pCxI/Ir9KLeNxJvqVgiFG5fxJaKr4t/MWOBnPOFwp9Ea3PmS52feiqZpY7JDfTiJK2ZPj47HlAQfvm5AWmJsXsn16XqGdL96m51J2NmojACJzzrx4eb+NePD8Xe8TV4KTarqdEDTlNicQL0zlBfhR3PE/fuEpjLjfQ7lr5HY6dJEsbGhdYtVOcF0QrhegvjWGGgH9znLMcviViWxu5M6rQzFLSEDfGkg46OoGFd1+TjxH6fJPU/m0BoOins8fvTRtR6zc4bNqbwiwnt8SL4OM4eG9eRzRnUgiLKP2QR9F+uE8Zd\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Trigger a new backup\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/backups\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nTrigger a new backup\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"201\":{\"description\":\"Backup created successfully\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"assetId\":{\"type\":\"string\",\"nullable\":true},\"createdAt\":{\"type\":\"string\"},\"size\":{\"type\":\"number\"},\"bookmarkCount\":{\"type\":\"number\"},\"status\":{\"type\":\"string\",\"enum\":[\"pending\",\"success\",\"failure\"]},\"errorMessage\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"userId\",\"assetId\",\"createdAt\",\"size\",\"bookmarkCount\",\"status\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/update-a-bookmark.api.mdx",
    "content": "---\nid: update-a-bookmark\ntitle: \"Update a bookmark\"\ndescription: \"Update bookmark by its id\"\nsidebar_label: \"Update a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVtuO2zYQ/RVhnlpAtb1FigZ6c4IW3V7QRbJBHww/jMSRxbVEKrx47Rj692IoWZddZeOiqyeRHJLDM+fMzBkE2czI2kmtIIFPtUBHUar1vkKzj9JTJJ2NpIAYHO4sJBt41y1a2MZgKfNGuhMkmzOkhIbM2rsCks222cZQo8GKHBkbDGxWUIWQnMGdaoIErDNS7SAGOmJVlzwlSYryeNpVjw9vf9bHLz8VR+d09pY9kC6YXDy4FdDEYOizl4YEJM54ikFhxUbpYBSD5MfV6Apgr3gHWfdOixP7MoXgvqBIoMPI6cgHOBbR36o8Ra6gKJdUChudtI8eUbnBJlJEgocpRbXRBylILCCGTCtHyvE1WNelzJCvWT5YvmsGEJ0+UOYghtromoyTZMNekxXywG/sLVOtS0LFCOR40ByFr61bX1VoTnO4K1+WmDKqDF4Tg9KOnhs2PfjfOiKGCo9/ktoxCW5Wq1UTQ2YIHYm1u8oDb8o5u1ybCh0k4I1kfyZRu+JY9K7Q5irT2qeltAVdZ83hv+t2iKt3/KWFzOWVGxwdr8MOrSX3fuDcNzaEj/Vga61sS7UfV6t5UbREF31yeD12yxkUXqZNE0PVAXglq66kL2P4v7TmcLeTavfRofP2GrmQ8hWnVeuzjKxlpqMsvSGGiZTgXdtexPJLgPj1j5/X/Qw8/yWZWO1NdlXSuLiJtYQYHilldpX8X+lUlhRKhCNlmY8xWKl2JeXtggnPklWtjQtv8ZbM7RynJtViA6GsDSybcGrEg0nQn0Z4PjC9D9tWYW9Wb56L6lLGIqVdlGuvxOtJKtNiPo1XZC3u5taegBNOGOy3Xa6oyBVatNU0K/herqoJLC9pwS7PQ+ltGB8yh0sDEHI7nFEIQ9Y2S6zl8nADMRzQSOZDW+3a5RaxHH3Jab9wrrbJcunMabFHg3uieoF1DfFMrupOiHQeyvYfnX3U+gJN04x6l4+MaXvzuIPpAeKb+R3BjNUejCDufn69FKbf/7kPKHKsPgwtxi+X1mZcxlvWj5NJO9OraySVoMxhfOmC+olRnhwmW6T74RSifvpSFIeZUe0b7Z6WuOnCUMlGPoaCNbpnUpdGdJQq14wMM6T17WaxWqxGz+xjt767nY31+u42yrWZBpqJwZVcW1dhUE7XFnYdLo7L2JNWopfgy+1wSw5+6bIuUYbs3/UurSY2fQ/KCSEZNaTbGAptHZuczyla+mTKpuHpz544+pvtIIkgHCEt/wtIciwtveDzdx86EX8ffc3LbhLVKSiv9DyCGPZ0mjbODWfTglAwHTbnzqCL4w/3fMxwwLN01cSXHesso9q9aLsdpZa79f3731hfXYNehWQGBh852+Nj660Ojw+yDXNnKFHtfEhu0B7adH3fuFRPxRueNYvH+dxa3Os9qabp4XE8ZmSa5l9CRZNh\nsidebar_class_name: \"patch api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Update a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"patch\"}\n  path={\"/bookmarks/{bookmarkId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nUpdate bookmark by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The data to update. Only the fields you want to update need to be provided.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"summary\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true,\"maxLength\":1000},\"createdAt\":{\"type\":\"string\",\"nullable\":true},\"url\":{\"type\":\"string\",\"format\":\"uri\"},\"description\":{\"type\":\"string\",\"nullable\":true},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true},\"text\":{\"type\":\"string\",\"nullable\":true},\"assetContent\":{\"type\":\"string\",\"nullable\":true}}}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The updated bookmark\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"userId\":{\"type\":\"string\"}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"userId\"]}}}},\"404\":{\"description\":\"Bookmark not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/update-a-highlight.api.mdx",
    "content": "---\nid: update-a-highlight\ntitle: \"Update a highlight\"\ndescription: \"Update highlight by its id\"\nsidebar_label: \"Update a highlight\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVcGO2zYQ/RViTg3A2E6QooFubtAimx6ySBzkYPhAi2OLa4lkyNGuVUH/Xgwl2/Kusg2Q+CSLT5yZN+/NtKAx5sF4Ms5CBl+8VoSiMPuiNPuCxLYRhqIwGiSQ2kfI1vD+dBphIyFiXgdDDWTrFraoAoZlTQVk6023keBVUBUShpgAMS+wUpC1QI1HyCBSMHYPEvCoKl/yK4NGl8dmXz3cvf3DHf/9vTgSufwtp2AoQc4p3GjoJAT8VpuAGjIKNUqwqmJUMUJJMFygV1QA58WfYKQ/nW44m2saVgUKrUgJcqJOlMzER1s2ggoUO4OljqJxtXhQli4YYRE1/92i8MHdG416BhJyZwktcRjlfWlyxWHmd5FjTVDitneYE0jwwXkMZDDyae5KFyaZs3XFfWmwLN0DcG1c7z4gWpCwLWuETSfBOsKp721dlmrLvDJ7XfrxJdE7G/vYrxeLaZb6yvVFMb+u3q1zh0qFw41+mnQnIZIK9HG3i0ijc1tXWwx8jlY/c/pzZErQuFN1SZCdgJ0EwiP9AL8/3AgJZrr0OmL4Dit5QO7HciKR7soo6zG/12yOuRuqGnJOGZ3jj6NtJrwJvZDeLN481c4ZJKwjsXO11b/SKXqC305ChTGq/dTZI27SDRf8ZvBEhVQ43Y+RvOC4PE4ymJ/lH+ftaOp0zCyG+9P0q0MJGbRK64AxdnPlzfz+FUi4V8Fw51P6w3FP2klmBZGP2XxOoZkdVFAHRD9T3oOccOVwg3C7NLH+GfCiz4X7Mhrcn5nVwXKj8X2miCNzHQkG2QBiL6SHv12oFGf44esq8cjd+nSZrn+d5vp5gl281Rth1CBjd46RzFhfzqvZYrYYTf5zLcvbm8nal7c3YufCdeFMVCfBu0iVSloaVsSw8NTVALu6tL2o8n/WY08X+2XuS2Vssip3vB10sr5spAgSsvF+2kgoXCQGte1WRfwSyq7j199qDLxdNxeZJDFpE/lZQ7ZTZcRn0v7t0yDtF+J7eQ4vlW2SGnnKZQASDtg8WqQdr5IClcaQ0ugR7/pgL1d8z+WGJy7u5OmLZZ6jp2exm5Hjbperd+9ZdMPCrpLHIag0o9VDn65L1Sctp3ctlMru6+R56C9liaprhT9SdCprkpC27RErd0DbdWd+iP8zM133H3AUMso=\nsidebar_class_name: \"patch api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Update a highlight\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"patch\"}\n  path={\"/highlights/{highlightId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nUpdate highlight by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"HighlightId\"},\"required\":true,\"name\":\"highlightId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The data to update. Only the fields you want to update need to be provided.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"]},\"note\":{\"type\":\"string\",\"nullable\":true}}}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The updated highlight\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarkId\":{\"type\":\"string\"},\"startOffset\":{\"type\":\"number\"},\"endOffset\":{\"type\":\"number\"},\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"],\"default\":\"yellow\"},\"text\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\",\"nullable\":true},\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"}},\"required\":[\"bookmarkId\",\"startOffset\",\"endOffset\",\"text\",\"note\",\"id\",\"userId\",\"createdAt\"],\"title\":\"Highlight\"}}}},\"404\":{\"description\":\"Highlight not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/update-a-list.api.mdx",
    "content": "---\nid: update-a-list\ntitle: \"Update a list\"\ndescription: \"Update list by its id\"\nsidebar_label: \"Update a list\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVk1v4zYQ/SvEnFqAtZ1iF13o5gYtmnaBBqkXPRg6jMWxxUQitSQVWxX03xdDyZYdK1kUiA+GSI7m4817Q7WgyGdOV0FbAwl8qRQGEoX2QWwaoYMXWoGEgDsPyRo+ax88pBI8ZbXToYFk3cKG0JFb1iGHZJ12qYQKHZYUyPlo4LOcSoSkhdBUBAn44LTZgQQ6YFkVvKVJq+LQ7Mr946df7OG/j/khBJt94ug6RBOOfqegk+Doa60dKUiCq0mCwZINit5AguZiKgw5cDZsTT78alXDOVyWvMpJKAwoghV1LH8m/jZFI0JOYqupUF40thZ7NGG0EYZI8XJDonL2WStSM5CQWRPIBA6DVVXoDDnM/NFzrAkg7OaRsgASKmcrckGT59O+nmu4Sm0+k9kx0DcSSjycVotFJy8ru37b1EWBG4ayR+3M2+LC28foTWdTbrrYXTIM9HdjdBK+1uSa79XCTutNobMzw421BaGBLv64i76yxvcA/bxYTLeyb4+KFH6/fuiJWjv5Spv+byPeDereYEJkpi5ZvSWaGguQ4Et0gWWsaIt1ESA5nr3VsKt4r7dMQo7+1rK9dRis89NWtSf3YIs3s7Z7Q47XSgfLD8+a9nFniJ92FxNhDXFkxd4MyJ7heHprIsWzfNLLqQM9BT8sPlyzjs+FsUFsbW3U+3Eus2qaXCV5j7upsxc4RA+jfToIqaSQW9UPyCyP4LAIYc6a8fO2n6Id84Tc83GG166ABFpUypH33RwrPX++4Xag00yLmPRw3KN05FYeQuWT+Ty4ZvaEDp+IqhlWFcgJAQ8ehN3GCfzXYC/6XLgRZ9fPP4xlH/n8EjoBw5G5jmjG1ItGIIeH360rkTP8899VRI979DDeFr8db6fjRB7ZeZn4absX8rge9TvuDQI7Mxp0dBwGZms5JEPfu7+ZLWaLs4vwBMry/m4SxOX9ndhad4kgI86qtT6UaM6KGm59PA7NF9PrxOfXPw96tAMdwrwqUPfaZsK0A7nW8W5miSXDJZ1KyK0PfNS2G/T0xRVdx9sDPut05FZkoNKenxUkWyw8vZHnDw+DCn4Ur2U3bKJpIoWLmlcg4Yma8UOiS3mSESpyMYP+8LaP89OKXYwvX2m9k8c3lllGVXjTNj3T5f1ydfsHk3T4YCnjJACHe5DxP2ZqY+GR+3GvhQLNro6TAXqnTGm8VMQLBcSyJrFo295iZZ/IdN0JmsBrRqbrvgEFSoAx\nsidebar_class_name: \"patch api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Update a list\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"patch\"}\n  path={\"/lists/{listId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nUpdate list by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"ListId\"},\"required\":true,\"name\":\"listId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The data to update. Only the fields you want to update need to be provided.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\",\"minLength\":1,\"maxLength\":100},\"description\":{\"type\":\"string\",\"nullable\":true,\"minLength\":0,\"maxLength\":500},\"icon\":{\"type\":\"string\"},\"parentId\":{\"type\":\"string\",\"nullable\":true},\"query\":{\"type\":\"string\",\"minLength\":1},\"public\":{\"type\":\"boolean\"}}}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The updated list\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"description\":{\"type\":\"string\",\"nullable\":true},\"icon\":{\"type\":\"string\"},\"parentId\":{\"type\":\"string\",\"nullable\":true},\"type\":{\"type\":\"string\",\"enum\":[\"manual\",\"smart\"],\"default\":\"manual\"},\"query\":{\"type\":\"string\",\"nullable\":true},\"public\":{\"type\":\"boolean\"},\"hasCollaborators\":{\"type\":\"boolean\"},\"userRole\":{\"type\":\"string\",\"enum\":[\"owner\",\"editor\",\"viewer\",\"public\"]}},\"required\":[\"id\",\"name\",\"icon\",\"parentId\",\"public\",\"hasCollaborators\",\"userRole\"],\"title\":\"List\"}}}},\"404\":{\"description\":\"List not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/update-a-tag.api.mdx",
    "content": "---\nid: update-a-tag\ntitle: \"Update a tag\"\ndescription: \"Update tag by its id\"\nsidebar_label: \"Update a tag\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVMGO2zYQ/RViTgnA2k6RooFubpCg2x66SB3kYOgwFscWdyWRIUe7VgX9ezCkdu3deoMc4oNtcZ44b2bemxEMxSpYz9Z1UMBnb5BJMR7UblCWo7IGNDAeIhRb2MhvqSFS1QfLAxTbEXaEgcK65xqKbTmVGjwGbIkpxASIVU0tQjECD56ggMjBdgfQQEdsfSNHlqxpjsOhvb9597s7/vdbfWR21TtJbjlBNni4MjBpCPS1t4EMFBx60tBhK3FOcQ1WCvHINQgXAVPkP5wZhMHTcjc1KYOMip3qU+kL9U/XDIprUntLjYlqcL26x45PGNURGXnckfLB3VlDZgEaKtcxdSxp0PvGVihpljdRcl1og9vdUMWgwQfnKbClKNFczvNmTekj9UTvupihv65Wl4vKRI0M8ucRs+YCLf0i3/NBbSHpKEHLXMjb1dsL3PGgOsdq7/rO/DzmlTOXKGpoKUY8/AD9dMMJX87TaIlrZ7Leqlryiu4KWIpjlmOS5ARimHD3YIc+NFDAiMYEinFaorfLuzeg4Q6DxV2TKc/h3KI99g1DATWzj8VyyWFY3GLAWyK/QO9BX9DAfINy+yTnv2e8ylxEUGdO/lc6mTOf+/mxLZJZ6kgwKGYQ6PnPRxdaFIZ/fdmk3smEPp2s9+HB6A/yPpuB7fZOAtKgzP7NYrVYnTn/kfr6+upiqevrK7V34Wmd0pdJg3eRW+zOUs9LDmd3PLluPEnuxWWYO8J05KVv0HaSJQ11nMe/zQtTQ5F3UqmhdpElMI47jPQ5NNMkx197CrJFy9P0k0aMjfLfQLHHJtJ3SL76NKv0tXqJ23yI3ZBE1vTyBBpuaXhcm1M5aagJDYVEIMfe5zS/bOSG07v/s+KkH95YVxV5/i62PLPN9Xrz/k9R0bye22RUCHgPOn0noi7VncSZzkZosDv0ybiQLxXN4VPJPpNoKutiK8YxIzbulrppOnVGnqUz0/QN/t1xKA==\nsidebar_class_name: \"patch api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Update a tag\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"patch\"}\n  path={\"/tags/{tagId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nUpdate tag by its id\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"TagId\"},\"required\":true,\"name\":\"tagId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The data to update. Only the fields you want to update need to be provided.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"}}}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The updated tag\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"}},\"required\":[\"id\",\"name\"]}}}},\"404\":{\"description\":\"Tag not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\"},\"message\":{\"type\":\"string\"}},\"required\":[\"code\",\"message\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/update-user.api.mdx",
    "content": "---\nid: update-user\ntitle: \"Update user\"\ndescription: \"Update a user's role, bookmark quota, or storage quota. Admin access required.\"\nsidebar_label: \"Update user\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzFVk1v4zYQ/SvEXNoCjD92txfdvF+A20u6TdCDYRRjcWxxLZEMSSXrCvzvi6Fkx7GdPRRZRAdbIofDN28eZ9iBolB67aK2Bgq4dQojCRRtIP9LEN7WJMXK2m2DfivuWhtRCutFiNbjhvqRkZipRhuBZUkhCE93rfakRiAh4iZAsYBsAEsJgcrW67iDYtHBitCTn7WxgmKxTEsJDj02FMmHbBDKihqEooO4cwQFhOi12YA8wX1TkZh/FHYtYkUZvIhWtDkakEDfsHE1r+epf6dv3kKSsMcJRfQtSTDY7E3mCiRo9uwwVsDI2JpCfG/VjvGU1kQykV/RuVqXyFDGXwPjuQDcrr5SGUGC89aRj5oCzzLBl8Ij0zbMG4MBCdjTlyTsc/EXE3+0UptIm2xr2rrGFbvtw2q00Q17myQJQ97+5+qVtw+B/AePD7U2m0+GLdWRn5W1NaE585PSacZuOUd9goTCiMdZ2tMyhH0W9HQymZyG8vtk/yR+OF/BWRN6mt9MJvz3LAQlQpvFu27regfypdI7eL3AUHoiwMXBctmjf3cJ8HtUYpChuBJzc4+1VkIb18bMIR/MEo2xcc+sfTBikNALRUTeW3+u2NNwerNDMNML7BtsY2W9/o+UuBJcBMjEAdahhrw28LfnwD9bv9JKkWHUl+rea2N+94zUWRdr25pXBZgkNBQrq7i2ttkjV9gCxvmwj1mtYdz1RTgBNwx/v28Hra+hgA6V8hRCGqPT4/spSLhHr7naZFDDdM/CGts6QgFVjC4U43H0u9EWPW6J3Aidu9hKBg/7fvLnYC96LJBSOupkfzNX/c7H/ezACe/McWQzrgDZiKtafvlsfYOM8I9/bjJxnIMvj83m0wtVRe5na8tumM4+1uloMppwl9Yxez4EOrueXyRmdj0Xa+ufssIsJgnOhthgls/QSYfbxFCAnng7aqA/4dLREx/pWxy7GrVheFk73aC2xYHErDeQUAxtfymhsiGySdetMNCtr1Pi4buWPF9blo9yy6JUOgxtcI11oB8E+uuXAeRv4jmUwyCaXVZ13fIXSNjS7vFqkvgiUBEq8hlBP/mh3+fqhl08Lj473knuV8zKklz8oe3y6Lhe395k1fUXoMYqXuLxAWT+zThtDru/2vBYBzWaTYsbtu1dssbx6RE5ORI5qItMdF1vcWO3ZFI6EBP5m3lJ6Tty27rt\nsidebar_class_name: \"put api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Update user\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"put\"}\n  path={\"/admin/users/{userId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nUpdate a user's role, bookmark quota, or storage quota. Admin access required.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The ID of the user to update\",\"example\":\"user_123\"},\"required\":true,\"name\":\"userId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"role\":{\"type\":\"string\",\"enum\":[\"user\",\"admin\"]},\"bookmarkQuota\":{\"type\":\"integer\",\"nullable\":true,\"minimum\":0},\"storageQuota\":{\"type\":\"integer\",\"nullable\":true,\"minimum\":0},\"browserCrawlingEnabled\":{\"type\":\"boolean\",\"nullable\":true}},\"description\":\"User update data\",\"example\":{\"role\":\"admin\",\"bookmarkQuota\":1000,\"storageQuota\":5000000000}}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"User updated successfully\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"success\":{\"type\":\"boolean\"}},\"required\":[\"success\"]}}}},\"400\":{\"description\":\"Bad request - Invalid input data or cannot update own user\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"error\":{\"type\":\"string\"}},\"required\":[\"error\"]}}}},\"401\":{\"description\":\"Unauthorized - Authentication required\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"error\":{\"type\":\"string\"}},\"required\":[\"error\"]}}}},\"403\":{\"description\":\"Forbidden - Admin access required\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"error\":{\"type\":\"string\"}},\"required\":[\"error\"]}}}},\"404\":{\"description\":\"User not found\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"error\":{\"type\":\"string\"}},\"required\":[\"error\"]}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.30.0/api/upload-a-new-asset.api.mdx",
    "content": "---\nid: upload-a-new-asset\ntitle: \"Upload a new asset\"\ndescription: \"Upload a new asset\"\nsidebar_label: \"Upload a new asset\"\nhide_title: true\nhide_table_of_contents: true\napi: eJyNVE1v2zAM/SsGz26c7uhbtqFANmAL0BQ7BDnQNlOrsS1VotNlhv77QMlNnDQDdpMlfrzH98wBKnKlVYaV7iCHJ9NorBJMOnpL0DliSIHx2UG+gYV8O9im4KjsreIj5JsBCkJLdtFzDflm67cpWHrtyfFnXR0hH65arGtKKmRMWCelJWRKuKbYLHlTXM8ghVJ3TB1Ldts3rAxaznbatneSKteurKkNJz4aghx08UKlwDVWG7KsyMnrTjUUohTLAR5UQ9K6oKQPZKkC7yNmZakSoiFn6328d0Z3Lhb7NJ9/JPSVGFXjEix0z4FLpFWdBjhhg8Y0qkRJzV6c5P8/lVBuWU0CHVvVPYM/tViH+xvvTv2ZPnR9W5CVB+H6A9tbWVdTeW9/2WwsPSm0TU/DDo4BHyfZEte6ghyMdoEcimMgw2grcZU9kHXBVL1tIIcBq8qScz5Do7LDPaRwQKuwaMaJxOeoyQ77hiGHmtm4PMvYHmd7tLgnMjM0BtIbThwrJHoXlPs+xicRi0Cf2P1RhIqdp6Y/DU46C48QBvkYBOl4eNC2RUH47dc6TFd1Oy3pwjpCup/NZ3M4D/CEZ7Fa3sS/WC2TnbaX4IWsT8OcWwwW64LCt3/vi6LD2az/WAaRKtNvzkyDqpNOQa1hVHR0SlgUtUidb2AYCnT0ZBvv5fq1JyvLY3vWM+yOFGrCimywwJ6OkMOXCOduNNsBm17639oKPn1PWpQlGZ6Ef/jtRNaTIVc/H9ei0rivWl1JjhQOddPzMYLES9mvZA7gxyfsjhMUwxAj1npPnfeQjnBZvsHLxvkLeuzpPg==\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Upload a new asset\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/assets\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nUpload a new asset\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The data to create the asset with.\",\"content\":{\"multipart/form-data\":{\"schema\":{\"type\":\"object\",\"properties\":{\"file\":{\"title\":\"File to be uploaded\"}},\"required\":[\"file\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Details about the created asset\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"assetId\":{\"type\":\"string\"},\"contentType\":{\"type\":\"string\"},\"size\":{\"type\":\"number\"},\"fileName\":{\"type\":\"string\"}},\"required\":[\"assetId\",\"contentType\",\"size\",\"fileName\"],\"title\":\"Asset\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/01-getting-started/01-intro.md",
    "content": "---\nslug: /\n---\n\n# Introduction\n\nKarakeep (previously Hoarder) is an open source \"Bookmark Everything\" app that uses AI for automatically tagging the content you throw at it. The app is built with self-hosting as a first class citizen.\n\n![Screenshot](https://raw.githubusercontent.com/karakeep-app/karakeep/main/screenshots/homepage.png)\n\n\n## Features\n\n- 🔗 Bookmark links, take simple notes and store images and pdfs.\n- ⬇️ Automatic fetching for link titles, descriptions and images.\n- 📋 Sort your bookmarks into lists.\n- 👥 Collaborate with others on the same list.\n- 🔎 Full text search of all the content stored.\n- ✨ AI-based (aka chatgpt) automatic tagging and summarization. With supports for local models using ollama!\n- 🤖 Rule-based engine for customized management.\n- 🎆 OCR for extracting text from images.\n- 🔖 [Chrome plugin](https://chromewebstore.google.com/detail/karakeep/kgcjekpmcjjogibpjebkhaanilehneje) and [Firefox addon](https://addons.mozilla.org/en-US/firefox/addon/karakeep/) for quick bookmarking.\n- 📱 An [iOS app](https://apps.apple.com/us/app/karakeep-app/id6479258022), and an [Android app](https://play.google.com/store/apps/details?id=app.hoarder.hoardermobile&pcampaignid=web_share).\n- 📰 Auto hoarding from RSS feeds.\n- 🔌 REST API and multiple clients.\n- 🌐 Multi-language support.\n- 🖍️ Mark and store highlights from your hoarded content.\n- 🗄️ Full page archival (using [monolith](https://github.com/Y2Z/monolith)) to protect against link rot.\n- ▶️ Auto video archiving using [yt-dlp](https://github.com/yt-dlp/yt-dlp).\n- ☑️ Bulk actions support.\n- 🔐 SSO support.\n- 🌙 Dark mode support.\n- 💾 Self-hosting first.\n- ⬇️ Bookmark importers from Chrome, Pocket, Linkwarden, Omnivore, Tab Session Manager.\n- 🔄 Automatic sync with browser bookmarks via [floccus](https://floccus.org/).\n- [Planned] Offline reading on mobile, semantic search across bookmarks, ...\n\n**⚠️ This app is under heavy development.**\n\n\n## Demo\n\nYou can access the demo at [https://try.karakeep.app](https://try.karakeep.app). Login with the following creds:\n\n```\nemail: demo@karakeep.app\npassword: demodemo\n```\n\nThe demo is seeded with some content, but it's in read-only mode to prevent abuse.\n\n## About the name\n\nThe name Karakeep is inspired by the Arabic word \"كراكيب\" (karakeeb), a colloquial term commonly used to refer to miscellaneous clutter, odds and ends, or items that may seem disorganized but often hold personal value or hidden usefulness. It evokes the image of a messy drawer or forgotten box, full of stuff you can't quite throw away—because somehow, it matters (or more likely, because you're a hoarder!).\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/01-getting-started/02-screenshots.md",
    "content": "# Screenshots\n\n## Homepage\n\n![Homepage](/img/screenshots/homepage.png)\n\n## Homepage (Dark Mode)\n\n![Homepage](/img/screenshots/homepage-dark.png)\n\n## Tags\n\n![All Tags](/img/screenshots/all-tags.png)\n\n## Lists\n\n![All Lists](/img/screenshots/all-lists.png)\n\n## Bookmark Preview\n\n![Bookmark Preview](/img/screenshots/bookmark-preview.png)\n\n## Settings\n\n![Settings](/img/screenshots/settings.png)\n\n\n## Sharing\n\n<img src=\"/img/screenshots/share-sheet.png\" width=\"400px\"  />\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/01-getting-started/_category_.json",
    "content": "{\n  \"label\": \"Getting Started\",\n  \"position\": 1\n}\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/02-installation/01-docker.md",
    "content": "# Docker\n\n### Requirements\n\n- Docker\n- Docker Compose\n\n### 1. Create a new directory\n\nCreate a new directory to host the compose file and env variables.\n\nThis is where you’ll place the `docker-compose.yml` file from the next step and the environment variables.\n\nFor example you could make a new directory called \"karakeep-app\" with the following command:\n```\nmkdir karakeep-app\n```\n\n\n### 2. Download the compose file\n\nDownload the docker compose file provided [here](https://github.com/karakeep-app/karakeep/blob/main/docker/docker-compose.yml) directly into your new directory.\n\n```\nwget https://raw.githubusercontent.com/karakeep-app/karakeep/main/docker/docker-compose.yml\n```\n\n### 3. Populate the environment variables\n\nTo configure the app, create a `.env` file in the directory and add this minimal env file:\n\n```\nKARAKEEP_VERSION=release\nNEXTAUTH_SECRET=super_random_string\nMEILI_MASTER_KEY=another_random_string\nNEXTAUTH_URL=http://localhost:3000\n```\n\nYou **should** change the random strings. You can use `openssl rand -base64 36` in a seperate terminal window to generate the random strings. You should also change the `NEXTAUTH_URL` variable to point to your server address.\n\nUsing `KARAKEEP_VERSION=release` will pull the latest stable version. You might want to pin the version instead to control the upgrades (e.g. `KARAKEEP_VERSION=0.10.0`). Check the latest versions [here](https://github.com/karakeep-app/karakeep/pkgs/container/karakeep).\n\nPersistent storage and the wiring between the different services is already taken care of in the docker compose file.\n\nKeep in mind that every time you change the `.env` file, you'll need to re-run `docker compose up`.\n\nIf you want more config params, check the config documentation [here](../03-configuration/01-environment-variables.md).\n\n### 4. Setup OpenAI\n\nTo enable automatic tagging, you'll need to configure OpenAI. This is optional though but highly recommended.\n\n- Follow [OpenAI's help](https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key) to get an API key.\n- Add the OpenAI API key to the env file:\n\n```\nOPENAI_API_KEY=<key>\n```\n\nLearn more about the costs of using openai [here](../06-administration/03-openai.md).\n\nIf you want to use a different AI provider (e.g. Ollama for local inference), check out the [different AI providers](../03-configuration/02-different-ai-providers.md) guide.\n\n### 5. Start the service\n\nStart the service by running:\n\n```\ndocker compose up -d\n```\n\nThen visit `http://localhost:3000` and you should be greeted with the Sign In page.\n\n### [Optional] 6. Enable optional features\n\nCheck the [configuration docs](../03-configuration/01-environment-variables.md) for extra features to enable such as full page archival, full page screenshots, inference languages, etc.\n\n### [Optional] 7. Setup quick sharing extensions\n\nGo to the [quick sharing page](../04-using-karakeep/quick-sharing.md) to install the mobile apps and the browser extensions. Those will help you hoard things faster!\n\n## Updating\n\nUpdating Karakeep will depend on what you used for the `KARAKEEP_VERSION` env variable.\n\n- If you pinned the app to a specific version, bump the version and re-run `docker compose up -d`. This should pull the new version for you.\n- If you used `KARAKEEP_VERSION=release`, you'll need to force docker to pull the latest version by running `docker compose up --pull always -d`.\n\nNote that if you want to upgrade/migrate `Meilisearch` versions, refer to the [troubleshooting](../06-administration/05-troubleshooting.md) page.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/02-installation/02-unraid.md",
    "content": "# Unraid\n\n## Docker Compose Manager Plugin (Recommended)\n\nYou can use [Docker Compose Manager](https://forums.unraid.net/topic/114415-plugin-docker-compose-manager/) plugin to deploy Karakeep using the official docker compose file provided [here](https://github.com/karakeep-app/karakeep/blob/main/docker/docker-compose.yml). After creating the stack, you'll need to setup some env variables similar to that from the docker compose installation docs [here](/installation/docker#3-populate-the-environment-variables).\n\n## Community Apps\n\n:::info\nThe community application template is maintained by the community.\n:::\n\nKarakeep can be installed on Unraid using the community application plugins. Karakeep is a multi-container service, and because unraid doesn't natively support that, you'll have to install the different pieces as separate applications and wire them manually together.\n\nHere's a high level overview of the services you'll need:\n\n- **Karakeep** ([Support post](https://forums.unraid.net/topic/165108-support-collectathon-karakeep/)): Karakeep's main web app.\n- **Browserless** ([Support post](https://forums.unraid.net/topic/130163-support-template-masterwishxbrowserless/)): The chrome headless service used for fetching the content. Karakeep's official docker compose doesn't use browserless, but it's currently the only headless chrome service available on unraid, so you'll have to use it.\n- **MeiliSearch** ([Support post](https://forums.unraid.net/topic/164847-support-collectathon-meilisearch/)): The search engine used by Karakeep. It's optional but highly recommended. If you don't have it set up, search will be disabled.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/02-installation/03-archlinux.md",
    "content": "# Arch Linux\n\n## Installation\n\n> [Karakeep on AUR](https://aur.archlinux.org/packages/karakeep) is not maintained by the karakeep official.\n\n1. Install karakeep\n\n    ```shell\n    paru -S karakeep\n    ```\n\n2. (**Optional**) Install optional dependencies\n\n    ```shell\n    # karakeep-cli: karakeep cli tool\n    paru -S karakeep-cli\n\n    # ollama: for automatic tagging\n    sudo pacman -S ollama\n\n    # yt-dlp: for download video\n    sudo pacman -S yt-dlp\n    ```\n\n    You can use Open-AI instead of `ollama`. If you use `ollama`, you need to download the ollama model. Please refer to: [https://ollama.com/library](https://ollama.com/library).\n\n3. Set up\n\n    Environment variables can be set in `/etc/karakeep/karakeep.env` according to [configuration page](../03-configuration/01-environment-variables.md). **The environment variables that are not specified in `/etc/karakeep/karakeep.env` need to be added by yourself.**\n\n4. Enable service\n\n    ```shell\n    sudo systemctl enable --now karakeep.target\n    ```\n\n    Then visit `http://localhost:3000` and you should be greated with the sign in page.\n\n## Services and Ports\n\n`karakeep.target` include 3 services: `karakeep-web.service`, `karakeep-works.service`, `karakeep-browser.service`.\n\n- `karakeep-web.service`: Provide karakeep webui service, uses `3000` port by default.\n\n- `karakeep-workers.service`: Provide karakeep workers service, no port.\n\n- `karakeep-browser.service`: Provide browser headless service, uses `9222` port by default.\n\nNow `karakeep` depends on `meilisearch`, and `karakeep-workers.service` wants `meilisearch.service`, starting `karakeep.target` will start `meilisearch.service` at the same time.\n\n## How to Migrate from Hoarder to Karakeep\n\nThe PKGBUILD has been fully updated to replace all references to `hoarder` with `karakeep`. If you want to preserve your existing `hoarder` data during the upgrade, please follow the steps below:\n\n**1. Stop the old services**\n\n```shell\nsudo systemctl stop hoarder-web.service hoarder-worker.service hoarder-browser.service\nsudo systemctl disable --now hoarder.target\n```\n\n**2. Uninstall Hoarder**  \nAfter uninstalling, you can manually remove the old `hoarder` user and group if needed.\n```shell\nparu -R hoarder\n```\n\n**3. Rename the old data directory**\n```shell\nsudo mv /var/lib/hoarder /var/lib/karakeep\n```\n\n**4. Install Karakeep**\n```shell\nparu -S karakeep\n```\n\n**5. Fix ownership of the data directory**\n```shell\nsudo chown -R karakeep:karakeep /var/lib/karakeep\n```\n\n**6. Set Karakeep**  \nEdit `/etc/karakeep/karakeep.env` according to [configuration page](../03-configuration/01-environment-variables.md). **The environment variables that are not specified in `/etc/karakeep/karakeep.env` need to be added by yourself.**\n\nOr you can copy old hoarder env file to karakeep:\n```shell\nsudo cp -f /etc/hoarder/hoarder.env /etc/karakeep/karakeep.env\n```\n\n**7. Start Karakeep**\n```shell\nsudo systemctl enable --now karakeep.target\n```\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/02-installation/04-kubernetes.md",
    "content": "# Kubernetes\n\n### Requirements\n\n- A kubernetes cluster\n- kubectl\n- kustomize\n\n### 1. Get the deployment manifests\n\nYou can clone the repository and copy the `/kubernetes` directory into another directory of your choice.\n\n### 2. Populate the environment variables and secrets\n\nTo configure the app, copy the `.env_sample` to `.env` and change to your specific needs.\n\nYou should also change the `NEXTAUTH_URL` variable to point to your server address.\n\nUsing `KARAKEEP_VERSION=release` will pull the latest stable version. You might want to pin the version instead to control the upgrades (e.g. `KARAKEEP_VERSION=0.10.0`). Check the latest versions [here](https://github.com/karakeep-app/karakeep/pkgs/container/karakeep).\n\nTo see all available configuration options check the [documentation](../03-configuration/01-environment-variables.md).\n\nTo configure the neccessary secrets for the application copy the `.secrets_sample` file to `.secrets` and change the sample secrets to your generated secrets.\n\n> Note: You **should** change the random strings. You can use `openssl rand -base64 36` to generate the random strings.\n\n### 3. Setup OpenAI\n\nTo enable automatic tagging, you'll need to configure OpenAI. This is optional though but highly recommended.\n\n- Follow [OpenAI's help](https://help.openai.com/en/articles/4936850-where-do-i-find-my-openai-api-key) to get an API key.\n- Add the OpenAI API key to the `.env` file:\n\n```\nOPENAI_API_KEY=<key>\n```\n\nLearn more about the costs of using openai [here](../06-administration/03-openai.md).\n\nIf you want to use a different AI provider (e.g. Ollama for local inference), check out the [different AI providers](../03-configuration/02-different-ai-providers.md) guide.\n\n### 4. Deploy the service\n\nDeploy the service by running:\n\n```\nmake deploy\n```\n\n### 5. Access the service\n\n#### via LoadBalancer IP\n\nBy default, these manifests expose the application as a LoadBalancer Service. You can run `kubectl get services` to identify the IP of the loadbalancer for your service.\n\nThen visit `http://<loadbalancer-ip>:3000` and you should be greated with the Sign In page.\n\n> Note: Depending on your setup you might want to expose the service via an Ingress, or have a different means to access it.\n\n#### Via Ingress\n\nIf you want to use an ingress, you can customize the sample ingress in the kubernetes folder and change the host to the DNS name of your choice.\n\nAfter that you have to configure the web service to the type ClusterIP so it is only reachable via the ingress.\n\nIf you have already deployed the service you can patch the web service to the type ClusterIP with the following command:\n\n` kubectl -n karakeep patch service web -p '{\"spec\":{\"type\":\"ClusterIP\"}}' `\n\nAfterwards you can apply the ingress and access the service via your chosen URL.\n\n#### Setting up HTTPS access to the Service\n\nTo access karakeep securely you can configure the ingress to use a preconfigured TLS certificate. This requires that you already have the needed files, namely your .crt and .key file, on hand.\n\nAfter you have deployed the karakeep manifests you can deploy your certificate for karakeep in the `karakeep` namespace with this example command. You can name the secret however you want. But be aware that the secret name in the ingress definition has to match the secret name.\n\n` $ kubectl --namespace karakeep create secret tls karakeep-web-tls --cert=/path/to/crt --key=/path/to/key `\n\nIf the secret is successfully created you can now configure the Ingress to use TLS via this changes to the spec:\n\n```` yaml\n spec:\n  tls:\n  - hosts:\n      - karakeep.example.com\n    secretName: karakeep-web-tls\n````\n\n> Note: Be aware that the hosts have to match between the tls spec and the HTTP spec.\n\n### [Optional] 6. Setup quick sharing extensions\n\nGo to the [quick sharing page](../04-using-karakeep/quick-sharing.md) to install the mobile apps and the browser extensions. Those will help you hoard things faster!\n\n## Updating\n\nEdit the `KARAKEEP_VERSION` variable in the `kustomization.yaml` file and run `make clean deploy`.\n\nIf you have chosen `release` as the image tag you can also destroy the web pod, since the deployment has an ImagePullPolicy set to always the pod always pulls the image from the registry, this way we can ensure that the newest release image is pulled.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/02-installation/06-debuntu.md",
    "content": "# Debian 12/Ubuntu 24.04\n\n:::warning\nThis script is a stripped-down version of those found in the [Proxmox Community Scripts](https://github.com/community-scripts/ProxmoxVE) repo. It has been adapted to work on baremetal Debian 12 or Ubuntu 24.04 installs **only**. Any other use is not supported and you use this script at your own risk.\n:::\n\n### Requirements\n\n- **Debian 12** (Buster) or\n- **Ubuntu 24.04** (Noble Numbat)\n\nThe script will download and install all dependencies (except for Ollama), install Karakeep, do a basic configuration of Karakeep and Meilisearch (the search app used by Karakeep), and create and enable the systemd service files needed to run Karakeep on startup. Karakeep and Meilisearch are run in the context of their low-privilege user environments for more security.\n\nThe script functions as an update script in addition to an installer. See **[Updating](#updating)**.\n\n### 1. Download the script from the [Karakeep repository](https://github.com/karakeep-app/karakeep/blob/main/karakeep-linux.sh)\n\n```\nwget https://raw.githubusercontent.com/karakeep-app/karakeep/main/karakeep-linux.sh\n```\n\n### 2. Run the script\n\n> This script must be run as `root`, or as a user with `sudo` privileges.\n\n    If this is a fresh install, then run the installer by using the following command:\n\n    ```shell\n    bash karakeep-linux.sh install\n    ```\n\n### 3. Create an account/sign in\n\n    Then visit `http://localhost:3000` and you should be greated with the Sign In page.\n\n## Updating\n\n> This script must be run as `root`, or as a user with `sudo` privileges.\n\n    If Karakeep has previously been installed using this script, then run the updater like so:\n\n    ```shell\n     bash karakeep-linux.sh update\n    ```\n\n## Services and Ports\n\n`karakeep.target` includes 4 services: `meilisearch.service`, `karakeep-web.service`, `karakeep-workers.service`, `karakeep-browser.service`.\n\n- `meilisearch.service`: Provides full-text search, Karakeep Workers service connects to it, uses port `7700` by default.\n\n- `karakeep-web.service`: Provides the karakeep web service, uses `3000` port by default.\n\n- `karakeep-workers.service`: Provides the karakeep workers service, no port.\n\n- `karakeep-browser.service`: Provides the headless browser service, uses `9222` port by default.\n\n## Configuration, ENV file, database locations\n\nDuring installation, the script created a configuration file for `meilisearch`, an `ENV` file for Karakeep, and located config paths and database paths separate from the installation path of Karakeep, so as to allow for easier updating. Their names/locations are as follows:\n\n- `/etc/meilisearch.toml` - a basic configuration for meilisearch, that contains configs for the database location, disabling analytics, and using a master key, which prevents unauthorized connections.\n- `/var/lib/meilisearch` - Meilisearch DB location.\n- `/etc/karakeep/karakeep.env` - The Karakeep `ENV` file. Edit this file to configure Karakeep beyond the default. The web service and the workers service need to be restarted after editing this file:\n\n    ```shell\n    sudo systemctl restart karakeep-workers karakeep-web\n    ```\n\n- `/var/lib/karakeep` - The Karakeep database location. If you delete the contents of this folder you will lose all your data.\n\n## Still Running Hoarder?\n\nThere is a way to upgrade. Please see [Hoarder to Karakeep Migration](../06-administration/08-hoarder-to-karakeep-migration.md)\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/02-installation/07-minimal-install.md",
    "content": "# Minimal Installation\n\n:::warning\nUnless necessary, prefer the [full installation](/installation/docker) to leverage all the features of Karakeep. You'll be sacrificing a lot of functionality if you go with the minimal installation route.\n:::\n\nKarakeep's default installation has a dependency on Meilisearch for the full text search, Chrome for crawling and OpenAI/Ollama for AI tagging. You can however run Karakeep without those dependencies if you're willing to sacrifice those features.\n\n- If you run without meilisearch, the search functionality will be completely disabled.\n- If you run without chrome, crawling will still work, but you'll lose ability to take screenshots of websites and websites with javascript content won't get crawled correctly.\n- If you don't setup OpenAI/Ollama, AI tagging will be disabled.\n\nThose features are important for leveraging Karakeep's full potential, but if you're running in constrained environments, you can use the following minimal docker compose to skip all those dependencies:\n\n```yaml\nservices:\n  web:\n    image: ghcr.io/karakeep-app/karakeep:release\n    restart: unless-stopped\n    volumes:\n      - data:/data\n    ports:\n      - 3000:3000\n    environment:\n      DATA_DIR: /data\n      NEXTAUTH_SECRET: super_random_string\nvolumes:\n  data:\n```\n\nOr just with the following docker command:\n\n```base\ndocker run -d \\\n  --restart unless-stopped \\\n  -v data:/data \\\n  -p 3000:3000 \\\n  -e DATA_DIR=/data \\\n  -e NEXTAUTH_SECRET=super_random_string \\\n  ghcr.io/karakeep-app/karakeep:release\n```\n\n:::warning\nYou **MUST** change the `super_random_string` to a true random string which you can generate with `openssl rand -hex 32`.\n:::\n\nCheck the [configuration docs](../03-configuration/01-environment-variables.md) for extra features to enable such as full page archival, full page screenshots, inference languages, etc.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/02-installation/08-truenas.md",
    "content": "# TrueNAS\n\nKarakeep is available directly from TrueNAS's app catalog ([link](https://apps.truenas.com/catalog/karakeep/)).\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/02-installation/09-cloud-hosting.md",
    "content": "# Karakeep Cloud\n\n:::tip\nIf you want to use Karakeep without running your own servers, the hosted cloud option is the fastest way to start.\n:::\n\n[Karakeep Cloud](https://cloud.karakeep.app) is the fully managed version of Karakeep operated by the core team. It handles hosting, updates, monitoring, and backups for you, so you can focus on saving content instead of maintaining infrastructure.\n\n### Get started\n\n1. Visit [cloud.karakeep.app](https://cloud.karakeep.app) and create an account.\n2. Follow the onboarding flow to create your workspace.\n3. Install the browser extension or mobile apps from the [quick sharing page](../04-using-karakeep/quick-sharing.md) and start saving links immediately.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/02-installation/10-pikapods.md",
    "content": "# PikaPods\n\n:::info\nNote: PikaPods shares some of its revenue from hosting Karakeep with the maintainer of this project.\n:::\n\n[PikaPods](https://www.pikapods.com/) offers managed paid hosting for many open source apps, including Karakeep.\nServer administration, updates, migrations and backups are all taken care of, which makes it well suited\nfor less technical users. As of Nov 2024, running Karakeep there will cost you ~$3 per month.\n\n### Requirements\n\n- A _PikaPods_ account. Can be created for free [here](https://www.pikapods.com/register). You get an initial welcome credit of $5.\n\n### 1. Choose app\n\nChoose _Karakeep_ from their [list of apps](https://www.pikapods.com/apps) or use this [direct link](https://www.pikapods.com/pods?run=karakeep). This will either\nopen a new dialog to add a new _Karakeep_ pod or ask you to log in.\n\n### 2. Add settings\n\nThere are a few settings to configure in the dialog:\n\n- **Basics**: Give the pod a name and choose a region that's near you.\n- **Env Vars**: Here you can disable signups or set an OpenAI API key. All settings are optional.\n- **Resources**: The resources your _Karakeep_ pod can use. The defaults are fine, unless you have a very large collection.\n\n### 3. Start pod and add user\n\nAfter hitting _Add pod_ it will take about a minute for the app to fully start. After this you can visit\nthe pod's URL and add an initial user under _Sign Up_. After this you may want to disable further sign-ups\nby setting the pod's `DISABLE_SIGNUPS` _Env Var_ to `true`.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/02-installation/_category_.json",
    "content": "{\n  \"label\": \"Installation\",\n  \"position\": 3\n}\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/03-configuration/01-environment-variables.md",
    "content": "# Configuration\n\nThe app is mainly configured by environment variables. All the used environment variables are listed in [packages/shared/config.ts](https://github.com/karakeep-app/karakeep/blob/main/packages/shared/config.ts). The most important ones are:\n\n| Name                                   | Required                              | Default         | Description                                                                                                                                                                                                                                                            |\n| -------------------------------------- | ------------------------------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| PORT                                   | No                                    | 3000            | The port on which the web server will listen. DON'T CHANGE THIS IF YOU'RE USING DOCKER, instead changed the docker bound external port.                                                                                                                                |\n| WORKERS_PORT                           | No                                    | 0 (Random Port) | The port on which the worker will export its prometheus metrics on `/metrics`. By default it's a random unused port. If you want to utilize those metrics, fix the port to a value (and export it in docker if you're using docker).                                   |\n| WORKERS_HOST                           | No                                    | 127.0.0.1       | Host to listen to for requests to WORKERS_PORT. You will need to set this if running in a container, since localhost will not be reachable from outside                                                                                                                |\n| WORKERS_ENABLED_WORKERS                | No                                    | Not set         | Comma separated list of worker names to enable. If set, only these workers will run. Valid values: crawler,inference,search,adminMaintenance,video,feed,assetPreprocessing,webhook,ruleEngine.                                                                         |\n| WORKERS_DISABLED_WORKERS               | No                                    | Not set         | Comma separated list of worker names to disable. Takes precedence over `WORKERS_ENABLED_WORKERS`.                                                                                                                                                                      |\n| LOG_LEVEL                              | No                                    | debug           | The application log level as defined in the [winston documentation](https://github.com/winstonjs/winston?tab=readme-ov-file#logging-levels). You may want to set this to `notice` or `warning` when running Karakeep in a production environment.                      |\n| DATA_DIR                               | Yes                                   | Not set         | The path for the persistent data directory. This is where the db lives. Assets are stored here by default unless `ASSETS_DIR` is set.                                                                                                                                  |\n| ASSETS_DIR                             | No                                    | Not set         | The path where crawled assets will be stored. If not set, defaults to `${DATA_DIR}/assets`.                                                                                                                                                                            |\n| NEXTAUTH_URL                           | Yes                                   | Not set         | Should point to the address of your server. The app will function without it, but will redirect you to wrong addresses on signout for example.                                                                                                                         |\n| NEXTAUTH_SECRET                        | Yes                                   | Not set         | Random string used to sign the JWT tokens. Generate one with `openssl rand -base64 36`.                                                                                                                                                                                |\n| MEILI_ADDR                             | No                                    | Not set         | The address of meilisearch. If not set, Search will be disabled. E.g. (`http://meilisearch:7700`)                                                                                                                                                                      |\n| MEILI_MASTER_KEY                       | Only in Prod and if search is enabled | Not set         | The master key configured for meilisearch. Not needed in development environment. Generate one with `openssl rand -base64 36 \\| tr -dc 'A-Za-z0-9'`                                                                                                                    |\n| MAX_ASSET_SIZE_MB                      | No                                    | 50              | Sets the maximum allowed asset size (in MB) to be uploaded                                                                                                                                                                                                             |\n| DISABLE_NEW_RELEASE_CHECK              | No                                    | false           | If set to true, latest release check will be disabled in the admin panel.                                                                                                                                                                                              |\n| RATE_LIMITING_ENABLED                  | No                                    | false           | If set to true, API rate limiting will be enabled.                                                                                                                                                                                                                     |\n| CRAWLER_DOMAIN_RATE_LIMIT_WINDOW_MS    | No                                    | Not set         | Time window in milliseconds for per-domain crawler rate limiting.                                                                                                                                                                                                      |\n| CRAWLER_DOMAIN_RATE_LIMIT_MAX_REQUESTS | No                                    | Not set         | Maximum crawler requests allowed per domain inside the configured window.                                                                                                                                                                                              |\n| DB_WAL_MODE                            | No                                    | false           | Enables WAL mode for the sqlite database. This should improve the performance of the database. There's no reason why you shouldn't set this to true unless you're running the db on a network attached drive. This will become the default at some time in the future. |\n| SEARCH_NUM_WORKERS                     | No                                    | 1               | Number of concurrent workers for search indexing tasks. Increase this if you have a high volume of content being indexed for search.                                                                                                                                   |\n| SEARCH_JOB_TIMEOUT_SEC                 | No                                    | 30              | How long to wait for a search indexing job to finish before timing out. Increase this if you have large bookmarks with extensive content that takes longer to index.                                                                                                   |\n| WEBHOOK_NUM_WORKERS                    | No                                    | 1               | Number of concurrent workers for webhook delivery. Increase this if you have multiple webhook endpoints or high webhook traffic.                                                                                                                                       |\n| ASSET_PREPROCESSING_NUM_WORKERS        | No                                    | 1               | Number of concurrent workers for asset preprocessing tasks (image processing, OCR, etc.). Increase this if you have many images or documents that need processing.                                                                                                     |\n| ASSET_PREPROCESSING_JOB_TIMEOUT_SEC    | No                                    | 60              | How long to wait for an asset preprocessing job to finish before timing out. Increase this if you have large images or PDFs that take longer to process.                                                                                                               |\n| RULE_ENGINE_NUM_WORKERS                | No                                    | 1               | Number of concurrent workers for rule engine processing. Increase this if you have complex automation rules that need to be processed quickly.                                                                                                                         |\n| MAX_RSS_FEEDS_PER_USER                 | No                                    | 1000            | The maximum number of RSS feeds a user can create.                                                                                                                                                                                                                     |\n| MAX_WEBHOOKS_PER_USER                  | No                                    | 100             | The maximum number of webhooks a user can create.                                                                                                                                                                                                                      |\n\n## Asset Storage\n\nKarakeep supports two storage backends for assets: local filesystem (default) and S3-compatible object storage. S3 storage is automatically detected when an S3 endpoint is passed.\n\n| Name                             | Required          | Default | Description                                                                                               |\n| -------------------------------- | ----------------- | ------- | --------------------------------------------------------------------------------------------------------- |\n| ASSET_STORE_S3_ENDPOINT          | No                | Not set | The S3 endpoint URL. Required for S3-compatible services like MinIO. **Setting this enables S3 storage**. |\n| ASSET_STORE_S3_REGION            | No                | Not set | The S3 region to use.                                                                                     |\n| ASSET_STORE_S3_BUCKET            | Yes when using S3 | Not set | The S3 bucket name where assets will be stored.                                                           |\n| ASSET_STORE_S3_ACCESS_KEY_ID     | Yes when using S3 | Not set | The S3 access key ID for authentication.                                                                  |\n| ASSET_STORE_S3_SECRET_ACCESS_KEY | Yes when using S3 | Not set | The S3 secret access key for authentication.                                                              |\n| ASSET_STORE_S3_FORCE_PATH_STYLE  | No                | false   | Whether to force path-style URLs for S3 requests. Set to true for MinIO and other S3-compatible services. |\n\n:::info\nWhen using S3 storage, make sure the bucket exists and the provided credentials have the necessary permissions to read, write, and delete objects in the bucket.\n:::\n\n:::warning\nSwitching between storage backends after data has been stored will require manual migration of existing assets. Plan your storage backend choice carefully before deploying.\n:::\n\n## Authentication / Signup\n\nBy default, Karakeep uses the database to store users, but it is possible to also use OAuth.\nThe flags need to be provided to the `web` container.\n\n:::info\nOnly OIDC compliant OAuth providers are supported! For information on how to set it up, consult the documentation of your provider.\n:::\n\n:::info\nWhen setting up OAuth, the allowed redirect URLs configured at the provider should be set to `<KARAKEEP_ADDRESS>/api/auth/callback/custom` where `<KARAKEEP_ADDRESS>` is the address you configured in `NEXTAUTH_URL` (for example: `https://try.karakeep.app/api/auth/callback/custom`).\n:::\n\n| Name                                        | Required | Default                | Description                                                                                                                                                                                           |\n| ------------------------------------------- | -------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| DISABLE_SIGNUPS                             | No       | false                  | If enabled, no new signups will be allowed and the signup button will be disabled in the UI                                                                                                           |\n| DISABLE_PASSWORD_AUTH                       | No       | false                  | If enabled, only signups and logins using OAuth are allowed and the signup button and login form for local accounts will be disabled in the UI                                                        |\n| EMAIL_VERIFICATION_REQUIRED                 | No       | false                  | Whether email verification is required during user signup. If enabled, users must verify their email address before they can use their account. If you enable this, you must configure SMTP settings. |\n| OAUTH_AUTO_REDIRECT                         | No       | false                  | If enabled and password authentication is disabled, automatically redirect to the OAuth provider instead of showing the login page. Useful when OAuth is the only authentication method available.     |\n| OAUTH_WELLKNOWN_URL                         | No       | Not set                | The \"wellknown Url\" for openid-configuration as provided by the OAuth provider                                                                                                                        |\n| OAUTH_CLIENT_SECRET                         | No       | Not set                | The \"Client Secret\" as provided by the OAuth provider                                                                                                                                                 |\n| OAUTH_CLIENT_ID                             | No       | Not set                | The \"Client ID\" as provided by the OAuth provider                                                                                                                                                     |\n| OAUTH_SCOPE                                 | No       | \"openid email profile\" | \"Full list of scopes to request (space delimited)\"                                                                                                                                                    |\n| OAUTH_PROVIDER_NAME                         | No       | \"Custom Provider\"      | The name of your provider. Will be shown on the signup page as \"Sign in with `<name>`\"                                                                                                                |\n| OAUTH_ALLOW_DANGEROUS_EMAIL_ACCOUNT_LINKING | No       | false                  | Whether existing accounts in karakeep stored in the database should automatically be linked with your OAuth account. Only enable it if you trust the OAuth provider!                                  |\n| OAUTH_TIMEOUT                               | No       | 3500                   | The wait time in milliseconds for the OAuth provider response. Increase this if you are having `outgoing request timed out` errors                                                                    |\n\nFor more information on `OAUTH_ALLOW_DANGEROUS_EMAIL_ACCOUNT_LINKING`, check the [next-auth.js documentation](https://next-auth.js.org/configuration/providers/oauth#allowdangerousemailaccountlinking-option).\n\n## Inference Configs (For automatic tagging)\n\nEither `OPENAI_API_KEY` or `OLLAMA_BASE_URL` need to be set for automatic tagging to be enabled. Otherwise, automatic tagging will be skipped.\n\n:::warning\n\n- The quality of the tags you'll get will depend on the quality of the model you choose.\n- You might want to tune the `INFERENCE_CONTEXT_LENGTH` as the default is quite small. The larger the value, the better the quality of the tags, but the more expensive the inference will be (money-wise on OpenAI and resource-wise on ollama).\n  :::\n\n| Name                                 | Required | Default                | Description                                                                                                                                                                                                                                                                                                                                                                           |\n| ------------------------------------ | -------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| OPENAI_API_KEY                       | No       | Not set                | The OpenAI key used for automatic tagging. More on that in [here](../06-administration/03-openai.md).                                                                                                                                                                                                                                                                                            |\n| OPENAI_BASE_URL                      | No       | Not set                | If you just want to use OpenAI you don't need to pass this variable. If, however, you want to use some other openai compatible API (e.g. azure openai service), set this to the url of the API.                                                                                                                                                                                       |\n| OPENAI_PROXY_URL                     | No       | Not set                | HTTP proxy server URL for OpenAI API requests (e.g., `http://proxy.example.com:8080`).                                                                                                                                                                                                                                                                                                |\n| OPENAI_SERVICE_TIER                  | No       | Not set                | Set to `auto`, `default`, or `flex`. Flex processing provides lower costs in exchange for slower response times and occasional resource unavailability. See [OpenAI Flex Processing](https://platform.openai.com/docs/guides/flex-processing) and [Chat Service Tier](https://platform.openai.com/docs/api-reference/chat/object#chat-object-service_tier) for more details.          |\n| OLLAMA_BASE_URL                      | No       | Not set                | If you want to use ollama for local inference, set the address of ollama API here.                                                                                                                                                                                                                                                                                                    |\n| OLLAMA_KEEP_ALIVE                    | No       | Not set                | Controls how long the model will stay loaded into memory following the request (examples: \"5m\" for 5 minutes, \"-1m\" to keep the model loaded indefinitely, \"0\" to unload the model instantly after processing is complete).                                                                                                                                                                                                                                                                                 |\n| INFERENCE_TEXT_MODEL                 | No       | gpt-4.1-mini           | The model to use for text inference. You'll need to change this to some other model if you're using ollama.                                                                                                                                                                                                                                                                           |\n| INFERENCE_IMAGE_MODEL                | No       | gpt-4o-mini            | The model to use for image inference. You'll need to change this to some other model if you're using ollama and that model needs to support vision APIs (e.g. llava).                                                                                                                                                                                                                 |\n| EMBEDDING_TEXT_MODEL                 | No       | text-embedding-3-small | The model to be used for generating embeddings for the text.                                                                                                                                                                                                                                                                                                                          |\n| INFERENCE_CONTEXT_LENGTH             | No       | 2048                   | The max number of tokens that we'll pass to the inference model. If your content is larger than this size, it'll be truncated to fit. The larger this value, the more of the content will be used in tag inference, but the more expensive the inference will be (money-wise on openAI and resource-wise on ollama). Check the model you're using for its max supported content size. |\n| INFERENCE_MAX_OUTPUT_TOKENS          | No       | 2048                   | The maximum number of tokens that the inference model is allowed to generate in its response. This controls the length of AI-generated content like tags and summaries. Increase this if you need longer responses, but be aware that higher values will increase costs (for OpenAI) and processing time.                                                                             |\n| INFERENCE_USE_MAX_COMPLETION_TOKENS  | No       | false                  | \\[OpenAI Only\\] Whether to use the newer `max_completion_tokens` parameter instead of the deprecated `max_tokens` parameter. Set to `true` if using GPT-5 or o-series models which require this. Will become the default in a future release.                                                                                                                                         |\n| INFERENCE_LANG                       | No       | english                | The language in which the tags will be generated.                                                                                                                                                                                                                                                                                                                                     |\n| INFERENCE_NUM_WORKERS                | No       | 1                      | Number of concurrent workers for AI inference tasks (tagging and summarization). Increase this if you have multiple AI inference requests and want to process them in parallel.                                                                                                                                                                                                       |\n| INFERENCE_ENABLE_AUTO_TAGGING        | No       | true                   | Whether automatic AI tagging is enabled or disabled.                                                                                                                                                                                                                                                                                                                                  |\n| INFERENCE_ENABLE_AUTO_SUMMARIZATION  | No       | false                  | Whether automatic AI summarization is enabled or disabled.                                                                                                                                                                                                                                                                                                                            |\n| INFERENCE_JOB_TIMEOUT_SEC            | No       | 30                     | How long to wait for the inference job to finish before timing out. If you're running ollama without powerful GPUs, you might want to increase the timeout a bit.                                                                                                                                                                                                                     |\n| INFERENCE_FETCH_TIMEOUT_SEC          | No       | 300                    | \\[Ollama Only\\] The timeout of the fetch request to the ollama server. If your inference requests take longer than the default 5mins, you might want to increase this timeout.                                                                                                                                                                                                        |\n| INFERENCE_SUPPORTS_STRUCTURED_OUTPUT | No       | Not set                | \\[DEPRECATED\\] Whether the inference model supports structured output or not. Use INFERENCE_OUTPUT_SCHEMA instead. Setting this to true translates to INFERENCE_OUTPUT_SCHEMA=structured, and to false translates to INFERENCE_OUTPUT_SCHEMA=plain.                                                                                                                                   |\n| INFERENCE_OUTPUT_SCHEMA              | No       | structured             | Possible values are \"structured\", \"json\", \"plain\". Structured is the preferred option, but if your model doesn't support it, you can use \"json\" if your model supports JSON mode, otherwise \"plain\" which should be supported by all the models but the model might not output the data in the correct format.                                                                        |\n\n:::info\n\n- You can append additional instructions to the prompt used for automatic tagging, in the `AI Settings` (in the `User Settings` screen)\n- You can use the placeholders `$tags`, `$aiTags`, `$userTags` in the prompt. These placeholders will be replaced with all tags, ai generated tags or human created tags when automatic tagging is performed (e.g. `[karakeep, computer, ai]`)\n  :::\n\n## Crawler Configs\n\n| Name                                     | Required | Default   | Description                                                                                                                                                                                                                                                                                                                                                                   |\n| ---------------------------------------- | -------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| CRAWLER_NUM_WORKERS                      | No       | 1         | Number of allowed concurrent crawling jobs. By default, we're only doing one crawling request at a time to avoid consuming a lot of resources.                                                                                                                                                                                                                                |\n| BROWSER_WEB_URL                          | No       | Not set   | The browser's http debugging address. The worker will talk to this endpoint to resolve the debugging console's websocket address. If you already have the websocket address, use `BROWSER_WEBSOCKET_URL` instead. If neither `BROWSER_WEB_URL` nor `BROWSER_WEBSOCKET_URL` are set, the worker will use plain http requests skipping screenshotting and javascript execution. |\n| BROWSER_WEBSOCKET_URL                    | No       | Not set   | The websocket address of browser's debugging console. If you want to use [browserless](https://browserless.io), use their websocket address here. If neither `BROWSER_WEB_URL` nor `BROWSER_WEBSOCKET_URL` are set, the worker will use plain http requests skipping screenshotting and javascript execution.                                                                 |\n| BROWSER_CONNECT_ONDEMAND                 | No       | false     | If set to false, the crawler will proactively connect to the browser instance and always maintain an active connection. If set to true, the browser will be launched on demand only whenever a crawling is requested. Set to true if you're using a service that provides you with browser instances on demand.                                                               |\n| CRAWLER_DOWNLOAD_BANNER_IMAGE            | No       | true      | Whether to cache the banner image used in the cards locally or fetch it each time directly from the website. Caching it consumes more storage space, but is more resilient against link rot and rate limits from websites.                                                                                                                                                    |\n| CRAWLER_STORE_SCREENSHOT                 | No       | true      | Whether to store a screenshot from the crawled website or not. Screenshots act as a fallback for when we fail to extract an image from a website. You can also view the stored screenshots for any link.                                                                                                                                                                      |\n| CRAWLER_FULL_PAGE_SCREENSHOT             | No       | false     | Whether to store a screenshot of the full page or not. Disabled by default, as it can lead to much higher disk usage. If disabled, the screenshot will only include the visible part of the page                                                                                                                                                                              |\n| CRAWLER_SCREENSHOT_TIMEOUT_SEC           | No       | 5         | How long to wait for the screenshot finish before timing out. If you are capturing full-page screenshots of long webpages, consider increasing this value.                                                                                                                                                                                                                    |\n| CRAWLER_STORE_PDF                        | No       | false     | Whether to store a PDF snapshot of the crawled page. Disabled by default, as it can lead to much higher disk usage. When enabled, a PDF version of each crawled page will be captured and stored as an asset, which can be viewed in the bookmark preview.                                                                                                                    |\n| CRAWLER_FULL_PAGE_ARCHIVE                | No       | false     | Whether to store a full local copy of the page or not. Disabled by default, as it can lead to much higher disk usage. If disabled, only the readable text of the page is archived.                                                                                                                                                                                            |\n| CRAWLER_JOB_TIMEOUT_SEC                  | No       | 60        | How long to wait for the crawler job to finish before timing out. If you have a slow internet connection or a low powered device, you might want to bump this up a bit                                                                                                                                                                                                        |\n| CRAWLER_NAVIGATE_TIMEOUT_SEC             | No       | 30        | How long to spend navigating to the page (along with its redirects). Increase this if you have a slow internet connection                                                                                                                                                                                                                                                     |\n| CRAWLER_PARSE_TIMEOUT_SEC                | No       | 60        | How long to wait for the HTML parsing subprocess (metadata extraction + readability) to finish before timing out.                                                                                                                                                                                                                                                              |\n| CRAWLER_PARSER_MEM_LIMIT_MB              | No       | 512       | Maximum heap size in MB for the HTML parsing subprocess. If the parser OOMs on large pages, increase this value. The subprocess is isolated so an OOM won't crash the main worker.                                                                                                                                                                                             |\n| CRAWLER_VIDEO_DOWNLOAD                   | No       | false     | Whether to download videos from the page or not (using yt-dlp)                                                                                                                                                                                                                                                                                                                |\n| CRAWLER_VIDEO_DOWNLOAD_MAX_SIZE          | No       | 50        | The maximum file size for the downloaded video. The quality will be chosen accordingly. Use -1 to disable the limit.                                                                                                                                                                                                                                                          |\n| CRAWLER_VIDEO_DOWNLOAD_TIMEOUT_SEC       | No       | 600       | How long to wait for the video download to finish                                                                                                                                                                                                                                                                                                                             |\n| CRAWLER_ENABLE_ADBLOCKER                 | No       | true      | Whether to enable an adblocker in the crawler or not. If you're facing troubles downloading the adblocking lists on worker startup, you can disable this.                                                                                                                                                                                                                     |\n| CRAWLER_YTDLP_ARGS                       | No       | []        | Include additional yt-dlp arguments to be passed at crawl time separated by %%: https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#general-options                                                                                                                                                                                                                           |\n| BROWSER_COOKIE_PATH                      | No       | Not set   | Path to a JSON file containing cookies to be loaded into the browser context. The file should be an array of cookie objects, each with name and value (required), and optional fields like domain, path, expires, httpOnly, secure, and sameSite (e.g., `[{\"name\": \"session\", \"value\": \"xxx\", \"domain\": \".example.com\"}`]).                                                   |\n| HTML_CONTENT_SIZE_INLINE_THRESHOLD_BYTES | No       | 5 \\* 1024 | The thresholds in bytes after which larger assets will be stored in the assetdb (folder/s3) instead of inline in the database.                                                                                                                                                                                                                                                |\n\n<details>\n\n  <summary>More info on BROWSER_COOKIE_PATH</summary>\n\nBROWSER_COOKIE_PATH specifies the path to a JSON file containing cookies to be loaded into the browser context for crawling.\n\nThe JSON file must be an array of cookie objects, each with:\n\n- name: The cookie name (required).\n- value: The cookie value (required).\n- Optional fields: domain, path, expires, httpOnly, secure, sameSite (values: \"Strict\", \"Lax\", or \"None\").\n\nExample JSON file:\n\n```json\n[\n  {\n    \"name\": \"session\",\n    \"value\": \"xxx\",\n    \"domain\": \".example.com\",\n    \"path\": \"/\",\n    \"expires\": 1735689600,\n    \"httpOnly\": true,\n    \"secure\": true,\n    \"sameSite\": \"Lax\"\n  }\n]\n```\n\n</details>\n\n## OCR Configs\n\nKarakeep uses [tesseract.js](https://github.com/naptha/tesseract.js) to extract text from images by default. Alternatively, you can use an LLM-based OCR by enabling the `OCR_USE_LLM` flag. LLM-based OCR uses the configured inference model (OpenAI or Ollama) to extract text from images, which can provide better results for complex images but requires a configured inference provider.\n\n| Name                     | Required | Default   | Description                                                                                                                                                                                                                               |\n| ------------------------ | -------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| OCR_CACHE_DIR            | No       | $TEMP_DIR | The dir where tesseract will download its models. By default, those models are not persisted and stored in the OS' temp dir.                                                                                                              |\n| OCR_LANGS                | No       | eng       | Comma separated list of the language codes that you want tesseract to support. You can find the language codes [here](https://tesseract-ocr.github.io/tessdoc/Data-Files-in-different-versions.html). Set to empty string to disable OCR. |\n| OCR_CONFIDENCE_THRESHOLD | No       | 50        | A number between 0 and 100 indicating the minimum acceptable confidence from tessaract. If tessaract's confidence is lower than this value, extracted text won't be stored.                                                               |\n| OCR_USE_LLM              | No       | false     | If set to true, uses the configured inference model (OpenAI or Ollama) for OCR instead of Tesseract. This can provide better results for complex images but requires a configured inference provider (`OPENAI_API_KEY` or `OLLAMA_BASE_URL`). Falls back to Tesseract if no inference provider is configured. |\n\n## Webhook Configs\n\nYou can use webhooks to trigger actions when bookmarks are created, changed or crawled.\n\n| Name                | Required | Default | Description                                       |\n| ------------------- | -------- | ------- | ------------------------------------------------- |\n| WEBHOOK_TIMEOUT_SEC | No       | 5       | The timeout for the webhook request in seconds.   |\n| WEBHOOK_RETRY_TIMES | No       | 3       | The number of times to retry the webhook request. |\n\n:::info\n\n- The WEBHOOK_TOKEN is used for authentication. It will appear in the Authorization header as Bearer token.\n  ```\n  Authorization: Bearer <WEBHOOK_TOKEN>\n  ```\n- The webhook will be triggered with the job id (used for idempotence), bookmark id, bookmark type, the user id, the url and the operation in JSON format in the body.\n\n  ```json\n  {\n    \"jobId\": \"123\",\n    \"type\": \"link\",\n    \"bookmarkId\": \"exampleBookmarkId\",\n    \"userId\": \"exampleUserId\",\n    \"url\": \"https://example.com\",\n    \"operation\": \"crawled\"\n  }\n  ```\n\n  :::\n\n## SMTP Configuration\n\nKarakeep can send emails for various purposes such as email verification during signup. Configure these settings to enable email functionality.\n\n| Name          | Required | Default | Description                                                                                     |\n| ------------- | -------- | ------- | ----------------------------------------------------------------------------------------------- |\n| SMTP_HOST     | No       | Not set | The SMTP server hostname or IP address. Required if you want to enable email functionality.     |\n| SMTP_PORT     | No       | 587     | The SMTP server port. Common values are 587 (STARTTLS), 465 (SSL/TLS), or 25 (unencrypted).     |\n| SMTP_SECURE   | No       | false   | Whether to use SSL/TLS encryption. Set to true for port 465, false for port 587 with STARTTLS.  |\n| SMTP_USER     | No       | Not set | The username for SMTP authentication. Usually your email address.                               |\n| SMTP_PASSWORD | No       | Not set | The password for SMTP authentication. For services like Gmail, use an app-specific password.    |\n| SMTP_FROM     | No       | Not set | The \"from\" email address that will appear in sent emails. This should be a valid email address. |\n\n## Proxy Configuration\n\nIf your Karakeep instance needs to connect through a proxy server, you can configure the following settings:\n\n| Name                               | Required | Default | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |\n| ---------------------------------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| CRAWLER_HTTP_PROXY                 | No       | Not set | HTTP proxy server URL for outgoing HTTP requests (e.g., `http://proxy.example.com:8080`). You can pass multiple comma separated proxies and the used one will be chosen at random. The proxy is used for crawling, RSS feed fetches and webhooks.                                                                                                                                                                                                                                                                           |\n| CRAWLER_HTTPS_PROXY                | No       | Not set | HTTPS proxy server URL for outgoing HTTPS requests (e.g., `http://proxy.example.com:8080`). You can pass multiple comma separated proxies and the used one will be chosen at random. The proxy is used for crawling, RSS feed fetches and webhooks.                                                                                                                                                                                                                                                                         |\n| CRAWLER_NO_PROXY                   | No       | Not set | Comma-separated list of hostnames/IPs that should bypass the proxy (e.g., `localhost,127.0.0.1,.local`)                                                                                                                                                                                                                                                                                                                                                                                                                     |\n| CRAWLER_ALLOWED_INTERNAL_HOSTNAMES | No       | Not set | By default, Karakeep blocks worker-initiated requests whose DNS resolves to private, loopback, link-local, or Tailscale CGNAT IP addresses. Use this to allowlist specific hostnames for internal access (e.g., `internal.company.com`, `app-name.local`). Supports domain wildcards by prefixing with a dot (e.g., `.internal.company.com`, `.<tailnet-name>.ts.net`). Passing `.` allowlists all domains. Note: Internal IP validation is bypassed when a proxy is configured for the URL as the local DNS resolver won't necessarily be the same as the one used by the proxy. |\n\n:::info\nThese proxy settings will be used by the crawler and other components that make outgoing HTTP requests.\n:::\n\n## Monitoring\n\nKarakeep supports distributed tracing via OpenTelemetry. When enabled, traces are collected for tRPC API calls, background worker operations, and other key workflows. Karakeep also exports prometheus-based metrics.\n\n| Name                        | Required | Default  | Description                                                                                                                                                                                                                                                                                                             |\n| --------------------------- | -------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| OTEL_TRACING_ENABLED        | No       | false    | Set to `true` to enable OpenTelemetry tracing. When disabled, all tracing operations are no-ops.                                                                                                                                                                                                                        |\n| OTEL_EXPORTER_OTLP_ENDPOINT | No       | Not set  | The OTLP HTTP endpoint to send traces to (e.g., `http://jaeger:4318/v1/traces` or `http://otel-collector:4318/v1/traces`). If not set, traces are logged to the console.                                                                                                                                                |\n| OTEL_SERVICE_NAME           | No       | karakeep | The service name that will appear in your tracing backend. The actual service name will include a suffix (e.g., `karakeep-api`, `karakeep-workers`).                                                                                                                                                                    |\n| OTEL_SAMPLE_RATE            | No       | 1.0      | The sampling rate for traces, between 0.0 and 1.0. A value of 1.0 means all traces are sampled, while 0.1 means only 10% of traces are sampled. Lower values reduce overhead and storage costs in production.                                                                                                           |\n| PROMETHEUS_AUTH_TOKEN       | No       | Random   | Enable a prometheus metrics endpoint at `/api/metrics`. This endpoint will require this token being passed in the Authorization header as a Bearer token. If not set, a new random token is generated everytime at startup. This cannot contain any special characters or you may encounter a 400 Bad Request response. |\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/03-configuration/02-different-ai-providers.md",
    "content": "# Configuring different AI Providers\n\nKarakeep uses LLM providers for AI tagging and summarization. We support OpenAI-compatible providers and ollama. This guide will show you how to configure different providers.\n\n## OpenAI\n\nIf you want to use OpenAI itself, you just need to pass in the OPENAI_API_KEY environment variable.\n\n```\nOPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n\n# You can change the default models by uncommenting the following lines, and choosing your model.\n# INFERENCE_TEXT_MODEL=gpt-4.1-mini\n# INFERENCE_IMAGE_MODEL=gpt-4o-mini\n```\n\n## Ollama\n\nOllama is a local LLM provider that you can use to run your own LLM server. You'll need to pass ollama's address to karakeep and you need to ensure that it's accessible from within the karakeep container (e.g. no localhost addresses).\n\nOllama provides two API endpoints:\n\n1. **OpenAI-compatible API (Recommended)** - Uses the `/v1` chat endpoint which handles message formatting automatically\n2. **Native Ollama API** - Requires manual formatting for some models\n\n### Option 1: OpenAI-compatible API (Recommended)\n\nThis approach uses Ollama's OpenAI-compatible endpoint and is more reliable with various models:\n\n```\nOPENAI_API_KEY=ollama\nOPENAI_BASE_URL=http://ollama.mylab.com:11434/v1\n\n# Make sure to pull the models in ollama first. Example models:\nINFERENCE_TEXT_MODEL=gemma3\nINFERENCE_IMAGE_MODEL=llava\n```\n\n### Option 2: Native Ollama API\n\nAlternatively, you can use the native Ollama API:\n\n```\n# MAKE SURE YOU DON'T HAVE OPENAI_API_KEY set, otherwise it takes precedence.\n\nOLLAMA_BASE_URL=http://ollama.mylab.com:11434\n\n# Make sure to pull the models in ollama first. Example models:\nINFERENCE_TEXT_MODEL=gemma3\nINFERENCE_IMAGE_MODEL=llava\n\n# If the model you're using doesn't support structured output, you also need:\n# INFERENCE_OUTPUT_SCHEMA=plain\n```\n\n:::tip\nIf you experience issues with certain models (especially OpenAI's gpt-oss models or other models requiring specific chat formats), try using the OpenAI-compatible API endpoint instead.\n:::\n\n## Gemini\n\nGemini has an OpenAI-compatible API. You need to get an api key from Google AI Studio. You also need to set up a billing account, even if using the free tier.\n\n```\n\nOPENAI_BASE_URL=https://generativelanguage.googleapis.com/v1beta\nOPENAI_API_KEY=YOUR_API_KEY\n\n# Example models:\nINFERENCE_TEXT_MODEL=gemini-2.5-flash-lite\nINFERENCE_IMAGE_MODEL=gemini-2.5-flash-lite\n```\n\n## OpenRouter\n\n```\nOPENAI_BASE_URL=https://openrouter.ai/api/v1\nOPENAI_API_KEY=YOUR_API_KEY\n\n# Example models:\nINFERENCE_TEXT_MODEL=meta-llama/llama-4-scout\nINFERENCE_IMAGE_MODEL=meta-llama/llama-4-scout\n```\n\n## Perplexity\n\n```\nOPENAI_BASE_URL: https://api.perplexity.ai\nOPENAI_API_KEY: Your Perplexity API Key\nINFERENCE_TEXT_MODEL: sonar-pro\nINFERENCE_IMAGE_MODEL: sonar-pro\n```\n\n## Azure\n\nAzure has an OpenAI-compatible API.\n\nYou can get your API key from the Overview page of the Azure AI Foundry Portal or via \"Keys + Endpoints\" on the resource in the Azure Portal.\n\n:::warning\nThe [model name is the deployment name](https://learn.microsoft.com/en-us/azure/ai-foundry/openai/how-to/switching-endpoints#keyword-argument-for-model) you specified when deploying the model, which may differ from the base model name.\n:::\n\n```\n# Deployed via Azure AI Foundry:\nOPENAI_BASE_URL=https://{your-azure-ai-foundry-resource-name}.cognitiveservices.azure.com/openai/v1/\n\n# Deployed via Azure OpenAI Service:\nOPENAI_BASE_URL=https://{your-azure-openai-resource-name}.openai.azure.com/openai/v1/\n\nOPENAI_API_KEY=YOUR_API_KEY\nINFERENCE_TEXT_MODEL=YOUR_DEPLOYMENT_NAME\nINFERENCE_IMAGE_MODEL=YOUR_DEPLOYMENT_NAME\n```\n\n## Cloudflare\n\nCloudflare supports OpenAI compatible endpoints. You can generate an API Token from the Cloudflare dashboard (Workers AI).\n\n```\nOPENAI_BASE_URL=https://api.cloudflare.com/client/v4/accounts/{your-account-id}/ai/v1\nOPENAI_API_KEY=Your Cloudflare Workers AI Token\n\n# Example models:\nINFERENCE_TEXT_MODEL=@cf/meta/llama-3.1-8b-instruct-fast\nINFERENCE_IMAGE_MODEL=@cf/meta/llama-3.2-11b-vision-instruct\nINFERENCE_OUTPUT_SCHEMA=json\n```\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/03-configuration/_category_.json",
    "content": "{\n  \"label\": \"Configuration\",\n  \"position\": 4\n}\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/04-using-karakeep/_category_.json",
    "content": "{\n  \"label\": \"Using Karakeep\",\n  \"position\": 2\n}\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/04-using-karakeep/advanced-workflows.md",
    "content": "---\nsidebar_position: 10\nslug: advanced-workflows\n---\n\n# Advanced workflows\n\nPush Karakeep further with automation and integrations.\n\n## Rule engine\n\n- Create if-this-then-that style rules to auto-tag, favourite, or route bookmarks into lists based on metadata or content.\n- Useful for keeping inboxes tidy (e.g. auto-archive newsletters, auto-tag domains, or flag videos).\n\n## API\n\n- Use the API to script imports, syncs, or custom tools. Same surface area the apps use.\n- Great for integrating with personal scripts, cron jobs, or other services.\n\n## Webhooks\n\n- Subscribe to bookmark events and trigger your own systems when something is added, updated, or archived.\n- Pair with the API to build end-to-end automations (e.g. push new saves into a writing queue or a team chat).\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/04-using-karakeep/bookmarking.md",
    "content": "---\nsidebar_position: 1\nslug: bookmarking\n---\n\n# Bookmarking\n\nEverything in Karakeep starts as a bookmark. Here’s how the different types work and how to keep your home view tidy with favourites and archive.\n\n## Favourites\n\n- Star bookmarks you like so they sit in their own dedicated favourites view for quick return visits.\n- Handy for saved gems you want to re-open often like articles you enjoyed, references you come back to, or things worth sharing.\n\n## Archiving\n\n- Archive hides a bookmark from the homepage without deleting it.\n- Archived items stay searchable and keep all tags, highlights, and attachments.\n- Ideal for achieving inbox-zero style for your homepage.\n\n## Bookmark types\n\n- **Links**: URLs saved from the web or extension. Karakeep grabs metadata, previews, screenshots, and archives when configured.\n- **Text**: Quick notes or snippets you paste in. Great for ideas, quotes, or saving context alongside links.\n- **Media**: Images or PDFs you want to save for later. Karakeep automatically extracts content out of those files and makes them searchable.\n\n## Notes\n\n- Attach personal notes to any bookmark to capture context, reminders, or next steps.\n- Notes live with the bookmark and are searchable, so you can recall why something mattered.\n\n## Highlights\n\n- Save quotes, summaries, or TODOs while reading.\n- Highlights show up in the bookmark detail view/reader and are searchable, so you can jump straight to the key ideas.\n\n## Attachments\n\n- Store extra context alongside a bookmark: screenshots, page captures, videos, and files you upload.\n- **Screenshots & archives**: fallback when the original page changes or disappear.\n- **Uploaded files**: keep PDFs, notes, or supporting assets right with the link.\n- Manage attachments from the bookmark detail view: upload, download, or detach as needed.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/04-using-karakeep/import.md",
    "content": "---\nsidebar_position: 8\nslug: import\n---\n\n# Import your library\n\n\nKarakeep supports importing bookmarks using the Netscape HTML Format, Pocket's new CSV format & Omnivore's JSONs. Titles, tags and addition date will be preserved during the import. An automatically created list will contain all the imported bookmarks.\n\n:::info\nAll the URLs in the bookmarks file will be added automatically, you will not be able to pick and choose which bookmarks to import!\n:::\n\n## Import from Chrome\n\n- Open Chrome and go to `chrome://bookmarks`\n- Click on the three dots on the top right corner and choose `Export bookmarks`\n- This will download an html file with all of your bookmarks.\n- To import the bookmark file, go to the settings and click \"Import Bookmarks from HTML file\".\n\n## Import from Firefox\n- Open Firefox and click on the menu button (☰) in the top right corner.\n- Navigate to Bookmarks > Manage bookmarks (or press Ctrl + Shift + O / Cmd + Shift + O to open the Bookmarks Library).\n- In the Bookmarks Library, click the Import and Backup button at the top. Select Export Bookmarks to HTML... to save your bookmarks as an HTML file.\n- To import a bookmark file, go back to the Import and Backup menu, then select Import Bookmarks from HTML... and choose your saved HTML file.\n\n## Import from Pocket\n\n- Go to the [Pocket export page](https://getpocket.com/export) and follow the instructions to export your bookmarks.\n- Pocket after a couple of minutes will mail you a zip file with all the bookmarks.\n- Unzip the file and you'll get a CSV file.\n- To import the bookmark file, go to the settings and click \"Import Bookmarks from Pocket export\".\n\n## Import from Omnivore\n\n- Follow Omnivore's [documentation](https://docs.omnivore.app/using/exporting.html) to export your bookmarks.\n- This will give you a zip file with all your data.\n- The zip file contains a lot of JSONs in the format `metadata_*.json`. You can either import every JSON file manually, or merge the JSONs into a single JSON file and import that.\n- To  merge the JSONs into a single JSON file, you can use the following command in the unzipped directory: `jq -r '.[]' metadata_*.json | jq -s > omnivore.json` and then import the `omnivore.json` file. You'll need to have the [jq](https://github.com/jqlang/jq) tool installed.\n\n## Import using the CLI\n\n:::warning\nImporting bookmarks using the CLI requires some technical knowledge and might not be very straightforward for non-technical users. Don't hesitate to ask questions in github discussions or discord though.\n:::\n\nIf you can get your bookmarks in a text file with one link per line, you can use the following command to import them using the [karakeep cli](../05-integrations/02-command-line.md):\n\n```\nwhile IFS= read -r url; do\n    karakeep --api-key \"<KEY>\" --server-addr \"<SERVER_ADDR>\" bookmarks add --link \"$url\"\ndone < all_links.txt\n```\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/04-using-karakeep/lists.md",
    "content": "---\nsidebar_position: 2\n---\n\n# Lists\n\nLists are the core organizational layer in Karakeep. Every saved item can sit in multiple lists so you can group links by project, topic, or audience without duplicating them.\n\n## Manual lists\n\n- Curated sets you add bookmarks to by hand. Great for projects, reading queues, or hand-picked collections.\n- Can be **private** (visible only to you) or **public** (share a read-only link).\n- Can be **collaborative**: invite people by email as viewers or editors. Editors can add their own bookmarks; viewers can browse. Your personal states (favourite/archive) stay yours even inside a shared list.\n\n## Smart lists\n\n- Auto-updating lists powered by a saved search query (e.g. `#ai -archived`).\n- Best for dynamic views like `Youtube links added last week` or `All reddit links from r/selfhosted`.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/04-using-karakeep/quick-sharing.md",
    "content": "---\nsidebar_position: 7\nslug: quick-sharing\n---\n\n# Quick capture and sharing\n\nThe whole point of Karakeep is making it easy to hoard the content. That's why there are a couple of \n\n## Mobile Apps\n\n<img src=\"/img/quick-sharing/mobile.png\" alt=\"mobile screenshot\" width=\"300\"/>\n\n\n- **iOS app**: [App Store Link](https://apps.apple.com/us/app/karakeep-app/id6479258022).\n- **Android App**: [Play Store link](https://play.google.com/store/apps/details?id=app.hoarder.hoardermobile&pcampaignid=web_share).\n\n## Browser Extensions\n\n<img src=\"/img/quick-sharing/extension.png\" alt=\"mobile screenshot\" width=\"300\"/>\n\n- **Chrome extension**: [here](https://chromewebstore.google.com/detail/karakeep/kgcjekpmcjjogibpjebkhaanilehneje).\n- **Firefox addon**: [here](https://addons.mozilla.org/en-US/firefox/addon/karakeep/).\n\n- ## Community Extensions\n- **Safari extension**: [App Store Link](https://apps.apple.com/us/app/karakeeper-bookmarker/id6746722790).  For macOS and iOS to allow a simple way to add your bookmarks to your self hosted karakeep instance.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/04-using-karakeep/search-query-language.md",
    "content": "---\nsidebar_position: 9\nslug: search-query-language\n---\n\n# Search Query Language\n\nKarakeep provides a search query language to filter and find bookmarks. Here are all the supported qualifiers and how to use them:\n\n## Basic Syntax\n\n- Use spaces to separate multiple conditions (implicit AND)\n- Use `and`/`or` keywords for explicit boolean logic\n- Prefix qualifiers with `-` or `!` to negate them (e.g., `-is:archived` or `!is:archived`)\n- Use parentheses `()` for grouping conditions (note that groups can't be negated)\n\n## Qualifiers\n\nHere's a comprehensive table of all supported qualifiers:\n\n| Qualifier                        | Description                                                                                                                                                                                               | Example Usage                                |\n| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |\n| `is:fav`                         | Favorited bookmarks                                                                                                                                                                                       | `is:fav`                                     |\n| `is:archived`                    | Archived bookmarks                                                                                                                                                                                        | `-is:archived`                               |\n| `is:tagged`                      | Bookmarks that has one or more tags                                                                                                                                                                       | `is:tagged`                                  |\n| `is:inlist`                      | Bookmarks that are in one or more lists                                                                                                                                                                   | `is:inlist`                                  |\n| `is:link`, `is:text`, `is:media` | Bookmarks that are of type link, text or media                                                                                                                                                            | `is:link`                                    |\n| `is:broken`                      | Bookmarks with broken/failed links (crawl failures or non-2xx status codes)                                                                                                                               | `is:broken`                                  |\n| `url:<value>`                    | Match bookmarks with URL substring                                                                                                                                                                        | `url:example.com`                            |\n| `title:<value>`                  | Match bookmarks with title substring                                                                                                               | `title:example`                              |\n|                                  | Supports quoted strings for titles with spaces                                                                                                   | `title:\"my title\"`                           |\n| `#<tag>` or `tag:<tag>`          | Match bookmarks with specific tag                                                                                                                                                                         | `#important` or `tag:important`              |\n|                                  | Supports quoted strings for tags with spaces                                                                                                                                                              | `#\"work in progress\"` or `tag:\"work in progress\"` |\n| `list:<name>`                    | Match bookmarks in specific list                                                                                                                                                                          | `list:reading`                               |\n|                                  | Supports quoted strings for list names with spaces                                                                                                                                                        | `list:\"to review\"`                           |\n| `after:<date>`                   | Bookmarks created on or after date (YYYY-MM-DD)                                                                                                                                                           | `after:2023-01-01`                           |\n| `before:<date>`                  | Bookmarks created on or before date (YYYY-MM-DD)                                                                                                                                                          | `before:2023-12-31`                          |\n| `feed:<name>`                    | Bookmarks imported from a particular rss feed                                                                                                                                                             | `feed:Hackernews`                            |\n| `source:<value>`                 | Match bookmarks from a specific source. Valid values: `api`, `web`, `cli`, `mobile`, `extension`, `singlefile`, `rss`, `import`                                                                          | `source:rss` `-source:web`                   |\n| `age:<time-range>`               | Match bookmarks based on how long ago they were created. Use `<` or `>` to indicate the maximum / minimum age of the bookmarks. Supported units: `d` (days), `w` (weeks), `m` (months), `y` (years). | `age:<1d` `age:>2w` `age:<6m` `age:>3y` |\n\n### Examples\n\n```plaintext\n# Find favorited bookmarks from 2023 that are tagged \"important\"\nis:fav after:2023-01-01 before:2023-12-31 #important\n\n# Find archived bookmarks that are either in \"reading\" list or tagged \"work\"\nis:archived and (list:reading or #work)\n\n# Find bookmarks that are not tagged or not in any list\n-is:tagged or -is:inlist\n# Find bookmarks with \"React\" in the title\ntitle:React\n```\n\n## Combining Conditions\n\nYou can combine multiple conditions using boolean logic:\n\n```plaintext\n# Find favorited bookmarks from 2023 that are tagged \"important\"\nis:fav after:2023-01-01 before:2023-12-31 #important\n\n# Find archived bookmarks that are either in \"reading\" list or tagged \"work\"\nis:archived and (list:reading or #work)\n\n# Find bookmarks that are not favorited and not archived\n-is:fav -is:archived\n\n# Using ! as an alias for negation\n!is:fav !is:archived\n\n# Using tag: as an alias for #\ntag:important tag:\"work in progress\"\n```\n\n## Text Search\n\nAny text not part of a qualifier will be treated as a full-text search:\n\n```plaintext\n# Search for \"machine learning\" in bookmark content\nmachine learning\n\n# Combine text search with qualifiers\nmachine learning is:fav\n```\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/04-using-karakeep/tags.md",
    "content": "---\nsidebar_position: 3\n---\n\n# Tags\n\nTags are lightweight labels you can attach to any bookmark to add meaning without rigid folders.\n\n- Use tags to capture topics, sources, people, or workflow states (e.g. `ai`, `design`, `to-read`).\n- Combine multiple tags to filter or build smart lists; tags travel with a bookmark wherever it appears.\n- AI tags might look a little messy, but the extra labels make finding things easier. Use tags for broad discovery and lists when you want a clean, hand-picked setup.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/05-integrations/02-command-line.md",
    "content": "# Command Line Tool (CLI)\n\nKarakeep comes with a simple CLI for those users who want to do more advanced manipulation.\n\n## Features\n\n- Manipulate bookmarks, lists and tags\n- Mass import/export of bookmarks\n\n## Installation (NPM)\n\n```\nnpm install -g @karakeep/cli\n```\n\n\n## Installation (Docker)\n\n```\ndocker run --rm ghcr.io/karakeep-app/karakeep-cli:release --help\n```\n\n## Usage\n\n```\nkarakeep\n```\n\n```\nUsage: karakeep [options] [command]\n\nA CLI interface to interact with the karakeep api\n\nOptions:\n  --api-key <key>       the API key to interact with the API (env: KARAKEEP_API_KEY)\n  --server-addr <addr>  the address of the server to connect to (env: KARAKEEP_SERVER_ADDR)\n  -V, --version         output the version number\n  -h, --help            display help for command\n\nCommands:\n  bookmarks             manipulating bookmarks\n  lists                 manipulating lists\n  tags                  manipulating tags\n  whoami                returns info about the owner of this API key\n  help [command]        display help for command\n```\n\nAnd some of the subcommands:\n\n```\nkarakeep bookmarks\n```\n\n```\nUsage: karakeep bookmarks [options] [command]\n\nManipulating bookmarks\n\nOptions:\n  -h, --help             display help for command\n\nCommands:\n  add [options]          creates a new bookmark\n  get <id>               fetch information about a bookmark\n  update [options] <id>  updates bookmark\n  list [options]         list all bookmarks\n  delete <id>            delete a bookmark\n  help [command]         display help for command\n\n```\n\n```\nkarakeep lists\n```\n\n```\nUsage: karakeep lists [options] [command]\n\nManipulating lists\n\nOptions:\n  -h, --help                 display help for command\n\nCommands:\n  list                       lists all lists\n  delete <id>                deletes a list\n  add-bookmark [options]     add a bookmark to list\n  remove-bookmark [options]  remove a bookmark from list\n  help [command]             display help for command\n```\n\n## Obtaining an API Key\n\nTo use the CLI, you'll need to get an API key from your karakeep settings. You can validate that it's working by running:\n\n```\nkarakeep --api-key <key> --server-addr <addr> whoami\n```\n\nFor example:\n\n```\nkarakeep --api-key mysupersecretkey --server-addr https://try.karakeep.app whoami\n{\n  id: 'j29gnbzxxd01q74j2lu88tnb',\n  name: 'Test User',\n  email: 'test@gmail.com'\n}\n```\n\n\n## Other clients\n\nThere also exists a **non-official**, community-maintained, python package called [karakeep-python-api](https://github.com/thiswillbeyourgithub/karakeep_python_api) that can be accessed from the CLI, but is **not** official.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/05-integrations/03-mcp.md",
    "content": "# Model Context Protocol Server (MCP)\n\nKarakeep comes with a Model Context Protocol server that can be used to interact with it through LLMs.\n\n## Supported Tools\n\n- Searching bookmarks\n- Adding and removing bookmarks from lists\n- Attaching and detaching tags to bookmarks\n- Creating new lists\n- Creating text and URL bookmarks\n\n\n## Usage with Claude Desktop\n\nFrom NPM:\n\n```json\n{\n  \"mcpServers\": {\n    \"karakeep\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"@karakeep/mcp\"\n      ],\n      \"env\": {\n        \"KARAKEEP_API_ADDR\": \"https://<YOUR_SERVER_ADDR>\",\n        \"KARAKEEP_API_KEY\": \"<YOUR_TOKEN>\"\n      }\n    }\n  }\n}\n```\n\nFrom Docker:\n\n```json\n{\n  \"mcpServers\": {\n    \"karakeep\": {\n      \"command\": \"docker\",\n      \"args\": [\n        \"run\",\n        \"-e\",\n        \"KARAKEEP_API_ADDR=https://<YOUR_SERVER_ADDR>\",\n        \"-e\",\n        \"KARAKEEP_API_KEY=<YOUR_TOKEN>\",\n        \"ghcr.io/karakeep-app/karakeep-mcp:latest\"\n      ]\n    }\n  }\n}\n```\n\n\n### Demo\n\n#### Search\n![mcp-1](/img/mcp-1.gif)\n\n#### Adding Text Bookmarks\n![mcp-2](/img/mcp-2.gif)\n\n#### Adding URL Bookmarks\n![mcp-2](/img/mcp-3.gif)\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/05-integrations/05-singlefile.md",
    "content": "# Using Karakeep with SingleFile Extension\n\nKarakeep supports being a destination for the [SingleFile extension](https://github.com/gildas-lormeau/SingleFile). This has the benefit of allowing you to use the singlefile extension to hoard links as you're seeing them in the browser. This is perfect for websites that don't like to get crawled, has annoying cookie banner or require authentication.\n\n## Setup\n\n1. Install the [SingleFile extension](https://github.com/gildas-lormeau/SingleFile).\n2. In the extension settings, select `Destinations`.\n3. Select `upload to a REST Form API`.\n4. In the URL, insert the address: `https://YOUR_SERVER_ADDRESS/api/v1/bookmarks/singlefile`.\n5. In the `authorization token` field, paste an API key that you can get from your karakeep settings.\n6. Set `data field name` to `file`.\n7. Set `URL field name` to `url`.\n8. (Optional) Add `?ifexists=MODE` to the URL where MODE is one of `skip`, `overwrite`, `overwrite-recrawl`, `append`, or `append-recrawl`. See \"Handling Existing Bookmarks\" section below for details.\n\nNow, go to any page and click the singlefile extension icon. Once it's done with the upload, the bookmark should show up in your karakeep instance. Note that the singlefile extension doesn't show any progress on the upload. Given that archives are typically large, it might take 30+ seconds until the upload is done and starts showing up in Karakeep.\n\n## Handling Existing Bookmarks\n\nWhen uploading a page that already exists in your archive (same URL), you can control the behavior by setting the `ifexists` query parameter in the upload URL. The available modes are:\n\n- `skip` (default): If the bookmark already exists, skip creating a new one\n- `overwrite`: Replace existing precrawled archive (only the most recent archive is kept)\n- `overwrite-recrawl`: Replace existing archive and queue a recrawl to update content\n- `append`: Add new archive version alongside existing ones\n- `append-recrawl`: Add new archive and queue a recrawl\n\nTo use these modes, append `?ifexists=MODE` to your upload URL, replacing `MODE` with your desired behavior.\n\nFor example:  \n`https://YOUR_SERVER_ADDRESS/api/v1/bookmarks/singlefile?ifexists=overwrite`\n\n\n## Recommended settings\n\nIn the singlefile extension, you probably will want to change the following settings for better experience:\n* Stylesheets > compress CSS content: on\n* Stylesheets > group duplicate stylesheets together: on\n* HTML content > remove frames: on\n\nAlso, you most likely will want to change the default `MAX_ASSET_SIZE_MB` in karakeep to something higher, for example `100`.\n\n:::info\nCurrently, we don't support screenshots for singlefile uploads, but this will change in the future.\n:::\n\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/05-integrations/06-rss-feeds.md",
    "content": "# RSS Feeds\n\nKarakeep offers RSS feed integration, allowing you to both consume RSS feeds from external sources and publish your lists as RSS feeds for others to subscribe to.\n\n## Publishing RSS Feeds\n\nYou can publish any of your lists as an RSS feed, making it easy to share your bookmarks with others or integrate them into RSS readers.\n\n### Enabling RSS for a List\n\n1. Navigate to one of your lists\n2. Click on the list settings (three dots menu)\n3. Toggle the \"RSS Feed\" switch to enable it\n4. Copy the generated RSS feed URL\n\n### What Gets Published\n\nRSS feeds include:\n- **Links**: Bookmarks of type \"link\" with their URL, title, description, and author\n- **Assets**: Uploaded files (PDFs, images) are included with a link to view them\n- **Tags**: Bookmark tags are exported as RSS categories\n- **Dates**: The bookmark creation date is used as the publication date\n\nNote: Text notes are not included in RSS feeds as they don't have an associated URL.\n\n### Security Considerations\n\n- Each RSS feed requires a unique token for access\n- Tokens can be regenerated at any time, which will invalidate the old URL\n- Disabling RSS for a list immediately revokes access\n\n## Consuming RSS Feeds\n\nKarakeep can automatically monitor RSS feeds and create bookmarks from new entries, making it perfect for staying up to date with blogs, news sites, and other content sources.\n\n### Adding an RSS Feed\n\n1. Go to **Settings** → **RSS Feeds**\n2. Click **Add Feed**\n3. Enter the feed details:\n   - **Name**: A friendly name for the feed\n   - **URL**: The RSS/Atom feed URL\n   - **Enabled**: Toggle to enable/disable the feed\n   - **Import Tags**: Enable to import RSS categories as bookmark tags\n\n### How It Works\n\n- Karakeep checks enabled RSS feeds **every hour**\n- New entries are automatically created as bookmarks\n- Duplicate entries are automatically detected and skipped\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/05-integrations/_category_.json",
    "content": "{\n  \"label\": \"Integrations\",\n  \"position\": 5\n}\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/06-administration/01-security-considerations.md",
    "content": "# Security Considerations\n\nIf you're going to give app access to untrusted users, there's some security considerations that you'll need to be aware of given how the crawler works. The crawler is basically running a browser to fetch the content of the bookmarks. Any untrusted user can submit bookmarks to be crawled from your server and they'll be able to see the crawling result. This can be abused in multiple ways:\n\n1. Untrusted users can submit crawl requests to websites that you don't want to be coming out of your IPs.\n2. Crawling user controlled websites can expose your origin IP (and location) even if your service is hosted behind cloudflare for example.\n3. The crawling requests will be coming out from your own network, which untrusted users can leverage to crawl internal non-internet exposed endpoints.\n\nTo mitigate those risks, you can do one of the following:\n\n1. Limit access to trusted users\n2. Let the browser traffic go through some VPN with restricted network policies.\n3. Host the browser container outside of your network.\n4. Use a hosted browser as a service (e.g. [browserless](https://browserless.io)). Note: I've never used them before.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/06-administration/02-FAQ.md",
    "content": "# Frequently Asked Questions (FAQ)\r\n\r\n## User Management\r\n\r\n### Lost password\r\n\r\n#### If you are not an administrator\r\n\r\nAdministrators can reset the password of any user. Contact an administrator to reset the password for you.\r\n\r\n- Navigate to the `Admin Settings` page\r\n- Find the user in the `Users List`\r\n- In the `Actions` column, there is a button to reset the password\r\n- Enter a new password and press `Reset`\r\n- The new password is now set\r\n- If required, you can change your password again (so the admin does not know your password) in the `User Settings`\r\n\r\n#### If you are an administrator\r\n\r\nIf you are an administrator and lost your password, you have to reset the password in the database.\r\n\r\nTo reset the password:\r\n\r\n- Acquire some kind of tools that helps you to connect to the database:\r\n  - `sqlite3` on Linux: run `apt-get install sqlite3` (depending on your package manager)\r\n  - e.g. `dbeaver` on Windows\r\n- Shut down Karakeep\r\n- Connect to the `db.db` database, which is located in the `data` directory you have mounted to your docker container:\r\n  - by e.g. running `sqlite3 db.db` (in your `data` directory)\r\n  - or going through e.g. the `dbeaver` UI to locate the file in the data directory and connecting to it\r\n- Update the password in the database by running:\r\n  - `update user set password='$2a$10$5u40XUq/cD/TmLdCOyZ82ePENE6hpkbodJhsp7.e/BgZssUO5DDTa', salt='' where email='<YOUR_EMAIL_HERE>';`\r\n  - (don't forget to put your email address into the command)\r\n- The new password for your user is now `adminadmin`.\r\n- Start Karakeep again\r\n- Log in with your email address and the password `adminadmin` and change the password to whatever you want in the `User Settings`\r\n\r\n### Adding another administrator\r\n\r\nBy default, the first user to sign up gets promoted to administrator automatically.\r\n\r\nIn case you want to grant those permissions to another user:\r\n\r\n- Navigate to the `Admin Settings` page\r\n- Find the user in the `Users List`\r\n- In the `Actions` column, there is a button to change the Role\r\n- Change the Role to `Admin`\r\n- Press `Change`\r\n- The new administrator has to log out and log in again to get the new role assigned\r\n\r\n### Adding new users, when signups are disabled\r\n\r\nAdministrators can create new accounts any time:\r\n\r\n- Navigate to the `Admin Settings` page\r\n- Go to the `Users List`\r\n- Press the `Create User` Button.\r\n- Enter the information for the user\r\n- Press `create`\r\n- The new user can now log in\r\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/06-administration/03-openai.md",
    "content": "# Tagging Costs\n\nThis service uses OpenAI for automatic tagging. This means that you'll incur some costs if automatic tagging is enabled. There are two type of inferences that we do:\n\n## Text Tagging\n\nFor text tagging, we use the `gpt-4.1-mini` model. This model is [extremely cheap](https://openai.com/api/pricing). Cost per inference varies depending on the content size per article. Though, roughly, You'll be able to generate tags for almost 3000+ bookmarks for less than $1.\n\n## Image Tagging\n\nFor image uploads, we use the `gpt-4o-mini` model for extracting tags from the image. You can learn more about the costs of using this model [here](https://platform.openai.com/docs/guides/images?api-mode=chat#calculating-costs). To lower the costs, we're using the low resolution mode (fixed number of tokens regardless of image size). You'll be able to run inference for 1000+ images for less than a $1.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/06-administration/05-troubleshooting.md",
    "content": "# Troubleshooting\n\n## SqliteError: no such table: user\n\nThis usually means that there's something wrong with the database setup (more concretely, it means that the database is not initialized). This can be caused by multiple problems:\n1. **Wiped DATA_DIR:** Your `DATA_DIR` got wiped (or the backing storage dir changed). If you did this intentionally, restart the container so that it can re-initalize the database.\n2. **Missing DATA_DIR**: You're not using the default docker compose file, and you forgot to configure the `DATA_DIR` env var. This will result into the database getting set up in a different directory than the one used by the service.\n\n## Chrome Failed to Read DnsConfig\n\nIf you see this error in the logs of the chrome container, it's a benign error and you can safely ignore it. Whatever problems you're having, is unrelated to this error.\n\n## AI Tagging not working (when using OpenAI)\n\nCheck the logs of the container and this will usually tell you what's wrong. Common problems are:\n1. Typo in the env variable `OPENAI_API_KEY` name resulting into logs saying something like \"skipping inference as it's not configured\".\n2. You forgot to call `docker compose up` after configuring open ai.\n3. OpenAI requires pre-charging the account with credits before using it, otherwise you'll get an error like \"insufficient funds\".\n\n## AI Tagging not working (when using Ollama)\n\nCheck the logs of the container and this will usually tell you what's wrong. Common problems are:\n1. Typo in the env variable `OLLAMA_BASE_URL` name resulting into logs saying something like \"skipping inference as it's not configured\".\n2. You forgot to call `docker compose up` after configuring ollama.\n3. You didn't change the `INFERENCE_TEXT_MODEL` env variable, resulting into karakeep attempting to use gpt models with ollama which won't work.\n4. Ollama server is not reachable by the karakeep container. This can be caused by:\n    1. Ollama server being in a different docker network than the karakeep container.\n    2. You're using `localhost` as the `OLLAMA_BASE_URL` instead of the actual address of the ollama server. `localhost` points to the container itself, not the docker host. Check this [stackoverflow answer](https://stackoverflow.com/questions/24319662/from-inside-of-a-docker-container-how-do-i-connect-to-the-localhost-of-the-mach) to find how to correctly point to the docker host address instead.\n\n## Crawling not working\n\nCheck the logs of the container and this will usually tell you what's wrong. Common problems are:\n1. You changed the name of the chrome container but didn't change the `BROWSER_WEB_URL` env variable.\n\n## Upgrading Meilisearch - Migrating the Meilisearch db version\n\n[Meilisearch](https://www.meilisearch.com/) is the database used by karakeep for searching in your bookmarks. The version used by karakeep is `1.13.3` and it is advised not to upgrade it without good reasons. If you do, you might see errors like `Your database version (1.11.1) is incompatible with your current engine version (1.13.3). To migrate data between Meilisearch versions, please follow our guide on https://www.meilisearch.com/docs/learn/update_and_migration/updating.`.\n\nLuckily we can easily workaround this:\n1. Stop the Meilisearch container.\n2. Inside the Meilisearch volume bound to `/meili_data`, erase/rename the folder called `data.ms`.\n3. Launch Meilisearch again.\n4. Login to karakeep as administrator and go to (as of v0.24.1) `Admin Settings > Background Jobs` then click on `Reindex All Bookmarks`.\n5. When the reindexing has finished, Meilisearch should be working as usual.\n\nIf you run into issues, the official documentation can be found [there](https://www.meilisearch.com/docs/learn/update_and_migration/updating).\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/06-administration/06-server-migration.md",
    "content": "# Migrating Between Servers\n\nThis guide explains how to migrate all of your data from one Karakeep server to another using the official CLI.\n\n## What the command does\n\nThe migration copies user-owned data from a source server to a destination server in this order:\n\n- User settings\n- Lists (preserving hierarchy and settings)\n- RSS feeds\n- AI prompts (custom prompts and their enabled state)\n- Webhooks (URL and events)\n- Tags (ensures tags by name exist)\n- Rule engine rules (IDs remapped to destination equivalents)\n- Bookmarks (links, text, and assets)\n  - After creation, attaches the correct tags and adds to the correct lists\n\nNotes:\n- Webhook tokens cannot be read via the API, so tokens are not migrated. Re‑add them on the destination if needed.\n- Asset bookmarks are migrated by downloading the original asset and re‑uploading it to the destination. Only images and PDFs are supported for asset bookmarks.\n- Link bookmarks on the destination may be de‑duplicated if the same URL already exists.\n\n## Prerequisites\n\n- Install the CLI:\n  - NPM: `npm install -g @karakeep/cli`\n  - Docker: `docker run --rm ghcr.io/karakeep-app/karakeep-cli:release --help`\n- Collect API keys and base URLs for both servers:\n  - Source: `--server-addr`, `--api-key`\n  - Destination: `--dest-server`, `--dest-api-key`\n\n## Quick start\n\n```\nkarakeep --server-addr https://src.example.com --api-key <SOURCE_API_KEY> migrate \\\n  --dest-server https://dest.example.com \\\n  --dest-api-key <DEST_API_KEY>\n```\n\nThe command is long‑running and shows live progress for each phase. You will be prompted for confirmation; pass `--yes` to skip the prompt.\n\n### Options\n\n- `--server-addr <url>`: Source server base URL\n- `--api-key <key>`: API key for the source server\n- `--dest-server <url>`: Destination server base URL\n- `--dest-api-key <key>`: API key for the destination server\n- `--batch-size <n>`: Page size for bookmark migration (default 50, max 100)\n- `-y`, `--yes`: Skip the confirmation prompt\n\n## What to expect\n\n- Lists are recreated parent‑first and retain their hierarchy.\n- Feeds, prompts, webhooks, and tags are recreated by value.\n- Rules are recreated after IDs (tags, lists, feeds) are remapped to their corresponding destination IDs.\n- After each bookmark is created, the command attaches the correct tags and adds it to the correct lists.\n\n## Caveats and tips\n\n- Webhook auth tokens must be re‑entered on the destination after migration.\n- If your destination already contains data, duplicate links may be de‑duplicated; tags and list membership are still applied to the existing bookmark.\n\n## Troubleshooting\n\n- If the command exits early, you can re‑run it, but note:\n  - Tags and lists that already exist are reused.\n  - Link de‑duplication avoids duplicate link bookmarks. Notes and assets will get re-created.\n  - Rules, webhooks, rss feeds will get re-created and you'll have to manually clean them up afterwards.\n  - The progress log indicates how far it got.\n- Use a smaller `--batch-size` if your source or destination is under heavy load.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/06-administration/07-legacy-container-upgrade.md",
    "content": "# Legacy Container Upgrade\n\nKarakeep's 0.16 release consolidated the web and worker containers into a single container and also dropped the need for the redis container. The legacy containers will stop being supported soon, to upgrade to the new container do the following:\n\n1. Remove the redis container and its volume if it had one.\n2. Move the environment variables that you've set exclusively to the `workers` container to the `web` container.\n3. Delete the `workers` container.\n4. Rename the web container image from `hoarder-app/hoarder-web` to `hoarder-app/hoarder`.\n\n```diff\ndiff --git a/docker/docker-compose.yml b/docker/docker-compose.yml\nindex cdfc908..6297563 100644\n--- a/docker/docker-compose.yml\n+++ b/docker/docker-compose.yml\n@@ -1,7 +1,7 @@\n version: \"3.8\"\n services:\n   web:\n-    image: ghcr.io/hoarder-app/hoarder-web:${KARAKEEP_VERSION:-release}\n+    image: ghcr.io/karakeep-app/karakeep:${KARAKEEP_VERSION:-release}\n     restart: unless-stopped\n     volumes:\n       - data:/data\n@@ -10,14 +10,10 @@ services:\n     env_file:\n       - .env\n     environment:\n-      REDIS_HOST: redis\n       MEILI_ADDR: http://meilisearch:7700\n+      BROWSER_WEB_URL: http://chrome:9222\n+      # OPENAI_API_KEY: ...\n       DATA_DIR: /data\n-  redis:\n-    image: redis:7.2-alpine\n-    restart: unless-stopped\n-    volumes:\n-      - redis:/data\n   chrome:\n     image: gcr.io/zenika-hub/alpine-chrome:123\n     restart: unless-stopped\n@@ -37,24 +33,7 @@ services:\n       MEILI_NO_ANALYTICS: \"true\"\n     volumes:\n       - meilisearch:/meili_data\n-  workers:\n-    image: ghcr.io/hoarder-app/hoarder-workers:${KARAKEEP_VERSION:-release}\n-    restart: unless-stopped\n-    volumes:\n-      - data:/data\n-    env_file:\n-      - .env\n-    environment:\n-      REDIS_HOST: redis\n-      MEILI_ADDR: http://meilisearch:7700\n-      BROWSER_WEB_URL: http://chrome:9222\n-      DATA_DIR: /data\n-      # OPENAI_API_KEY: ...\n-    depends_on:\n-      web:\n-        condition: service_started\n\n volumes:\n-  redis:\n   meilisearch:\n   data:\n```\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/06-administration/08-hoarder-to-karakeep-migration.md",
    "content": "# Hoarder to Karakeep Migration\n\nHoarder is rebranding to Karakeep. Due to github limitations, the old docker image might not be getting new updates after the rebranding. You might need to update your docker image to point to the new karakeep image instead by applying the following change in the docker compose file.\n\n```diff\ndiff --git a/docker/docker-compose.yml b/docker/docker-compose.yml\nindex cdfc908..6297563 100644\n--- a/docker/docker-compose.yml\n+++ b/docker/docker-compose.yml\n@@ -1,7 +1,7 @@\n version: \"3.8\"\n services:\n   web:\n-    image: ghcr.io/hoarder-app/hoarder:${HOARDER_VERSION:-release}\n+    image: ghcr.io/karakeep-app/karakeep:${HOARDER_VERSION:-release}\n```\n\nYou can also change the `HOARDER_VERSION` environment variable but if you do so remember to change it in the `.env` file as well.\n\n## Migrating a Baremetal Installation\n\nIf you previously used the [Debian/Ubuntu install script](../02-installation/06-debuntu.md) to install Hoarder, there is an option to migrate your installation to Karakeep.\n\n```bash\nbash karakeep-linux.sh migrate\n```\n\nThis will migrate your installation with no user input required. After the migration, the script will also check for an update.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/06-administration/_category_.json",
    "content": "{\n  \"label\": \"Administration\",\n  \"position\": 6\n}\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/07-community/01-community-projects.md",
    "content": "# Community Projects\n\nThis page lists community projects that are built around Karakeep, but not officially supported by the development team.\n\n:::warning\nThis list comes with no guarantees about security, performance, reliability, or accuracy. Use at your own risk.\n:::\n\n### Raycast Extension\n\n_By [@luolei](https://github.com/foru17)._\n\nA user-friendly Raycast extension that seamlessly integrates with Karakeep, bringing powerful bookmark management to your fingertips. Quickly save, search, and organize your bookmarks, texts, and images—all through Raycast's intuitive interface.\n\nGet it [here](https://www.raycast.com/luolei/karakeep).\n\n### Alfred Workflow\n\n_By [@yinan-c](https://github.com/yinan-c)_\n\nAn Alfred workflow to quickly hoard stuff or access your hoarded bookmarks!\n\nGet it [here](https://www.alfredforum.com/topic/22528-hoarder-workflow-for-self-hosted-bookmark-management/).\n\n### Obsidian Plugin\n\n_By [@jhofker](https://github.com/jhofker)_\n\nAn Obsidian plugin that syncs your Karakeep bookmarks with Obsidian, creating markdown notes for each bookmark in a designated folder.\n\nGet it [here](https://github.com/jhofker/obsidian-hoarder/), or install it directly from Obsidian's community plugin store ([link](https://obsidian.md/plugins?id=hoarder-sync)).\n\n### Telegram Bot\n\n_By [@Madh93](https://github.com/Madh93)_\n\nA Telegram Bot for saving bookmarks to Karakeep directly through Telegram.\n\nGet it [here](https://github.com/Madh93/karakeepbot).\n\n### Hoarder's Pipette\n\n_By [@DanSnow](https://github.com/DanSnow)_\n\nA chrome extension that injects karakeep's bookmarks into your search results.\n\nGet it [here](https://dansnow.github.io/hoarder-pipette/guides/installation/).\n\n### Karakeep-Python-API\n\n_By [@thiswillbeyourgithub](https://github.com/thiswillbeyourgithub/)_\n\nA python package to simplify access to the karakeep API. Can be used as a library or from the CLI. Aims for feature completeness and high test coverage but do check its feature matrix before relying too much on it.\n\nIts repository also hosts the [Community Script](https://github.com/thiswillbeyourgithub/karakeep_python_api/tree/main/community_scripts), for example:\n\n| Community Script | Description | Documentation |\n|----------------|-------------|---------------|\n| **Karakeep-Time-Tagger** | Automatically adds time-to-read tags (`0-5m`, `5-10m`, etc.) to bookmarks based on content length analysis. Includes systemd service and timer files for automated periodic execution. | [`Link`](https://github.com/thiswillbeyourgithub/karakeep_python_api/tree/main/community_scripts/karakeep-time-tagger) |\n| **Karakeep-List-To-Tag** | Converts a Karakeep list into tags by adding a specified tag to all bookmarks within that list. | [`Link`](https://github.com/thiswillbeyourgithub/karakeep_python_api/tree/main/community_scripts/karakeep-list-to-tag) |\n| **Omnivore2Karakeep-Highlights** | Imports highlights from Omnivore export data to Karakeep, with intelligent position detection and bookmark matching. Supports dry-run mode for testing. | [`Link`](https://github.com/thiswillbeyourgithub/karakeep_python_api/tree/main/community_scripts/omnivore2karakeep-highlights) |\n\n\nGet it [here](https://github.com/thiswillbeyourgithub/karakeep_python_api).\n\n### FreshRSS_to_Karakeep\n\n_By [@thiswillbeyourgithub](https://github.com/thiswillbeyourgithub/)_\n\nA python script to automatically create Karakeep bookmarks from your [FreshRSS](https://github.com/FreshRSS/FreshRSS) *favourites/saved* RSS item. Made to be called periodically. Based on the community project `Karakeep-Python-API` above, by the same author.\n\nGet it [here](https://github.com/thiswillbeyourgithub/freshrss_to_karakeep).\n\n### karakeep-sync\n_By [@sidoshi](https://github.com/sidoshi/)_\n\nSync links from Hacker News upvotes, Reddit Saves to Karakeep for centralized bookmark management.\n\nGet it [here](https://github.com/sidoshi/karakeep-sync)\n\n### Home Assistant Integration\n\n_By [@sli-cka](https://github.com/sli-cka)_\n\nA custom integration that brings Karakeep data into Home Assistant. It exposes your Karakeep statistics data (like lists, bookmarks, tag, etc.) as Home Assistant entities, enabling dashboards, automations, and notifications based on your Karakeep data.\n\nGet it [here](https://github.com/sli-cka/karakeep-homeassistant)\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/07-community/02-community-channels.md",
    "content": "# Community Channels\n\nStay connected with the Karakeep team and community for updates, support, and feature discussions.\n\n## Discord\n\n- Join the official server: [discord.gg/NrgeYywsFh](https://discord.gg/NrgeYywsFh)\n- Great for getting help, sharing setups, and chatting with the team and other users.\n\n## Twitter / X\n\n- Follow [@karakeep_app](https://twitter.com/karakeep_app) for release announcements, tips, and product news.\n- DM or tag us with feedback or things you'd like to see next.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/07-community/_category_.json",
    "content": "{\n  \"label\": \"Community\",\n  \"position\": 7\n}\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/08-development/01-setup.md",
    "content": "# Setup\n\n## Quick Start\n\nFor the fastest way to get started with development, use the one-command setup script:\n\n```bash\n./start-dev.sh\n```\n\nThis script will automatically:\n- Start Meilisearch in Docker (on port 7700)\n- Start headless Chrome in Docker (on port 9222) \n- Install dependencies with `pnpm install` if needed\n- Start both the web app and workers in parallel\n- Provide cleanup when you stop with Ctrl+C\n\n**Prerequisites:**\n- Docker installed and running\n- pnpm installed (see manual setup below for installation instructions)\n\nThe script will output the running services:\n- Web app: http://localhost:3000\n- Meilisearch: http://localhost:7700  \n- Chrome debugger: http://localhost:9222\n\nPress Ctrl+C to stop all services and clean up Docker containers.\n\n## Manual Setup\n\nKarakeep uses `node` version 24. To install it, you can use `nvm` [^1]\n\n```\n$ nvm install  24\n```\n\nVerify node version using this command:\n```\n$ node --version\nv24.14.0\n```\n\nKarakeep also makes use of `corepack`[^2]. If you have `node` installed, then `corepack` should already be\ninstalled on your machine, and you don't need to do anything. To verify the `corepack` is installed run:\n\n```\n$ command -v corepack\n/home/<user>/.nvm/versions/node/v24.14.0/bin/corepack\n```\n\nTo enable `corepack` run the following command:\n\n```\n$ corepack enable\n```\n\nThen, from the root of the repository, install the packages and dependencies using:\n\n```\n$ pnpm install\n```\n\nOutput of a successful `pnpm install` run should look something like:\n\n```\nScope: all 20 workspace projects\nLockfile is up to date, resolution step is skipped\nPackages: +3129\n+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++\nProgress: resolved 0, reused 2699, downloaded 0, added 3129, done\n\ndevDependencies:\n+ @karakeep/prettier-config 0.1.0 <- tooling/prettier\n\n. prepare$ husky\n└─ Done in 45ms\nDone in 5.5s\n```\n\nYou can now continue with the rest of this documentation.\n\n### First Setup\n\n- You'll need to prepare the environment variables for the dev env.\n- Easiest would be to set it up once in the root of the repo and then symlink it in each app directory (e.g. `/apps/web`, `/apps/workers`) and also `/packages/db`.\n- Start by copying the template by `cp .env.sample .env`.\n- The most important env variables to set are:\n  - `DATA_DIR`: Where the database and assets will be stored. This is the only required env variable. You can use an absolute path so that all apps point to the same dir.\n  - `NEXTAUTH_SECRET`: Random string used to sign the JWT tokens. Generate one with `openssl rand -base64 36`. Logging in will not work if this is missing!\n  - `MEILI_ADDR`: If not set, search will be disabled. You can set it to `http://127.0.0.1:7700` if you run meilisearch using the command below.\n  - `OPENAI_API_KEY`: If you want to enable auto tag inference in the dev env.\n- run `pnpm run db:migrate` in the root of the repo to set up the database.\n\n### Dependencies\n\n#### Meilisearch\n\nMeilisearch is the provider for the full text search (and at some point embeddings search too). You can get it running with `docker run -p 7700:7700 getmeili/meilisearch:v1.13.3`.\n\nMount persistent volume if you want to keep index data across restarts. You can trigger a re-index for the entire items collection in the admin panel in the web app.\n\n#### Chrome\n\nThe worker app will automatically start headless chrome on startup for crawling pages. You don't need to do anything there.\n\n### Web App\n\n- Run `pnpm web` in the root of the repo.\n- Go to `http://localhost:3000`.\n\n> NOTE: The web app kinda works without any dependencies. However, search won't work unless meilisearch is running. Also, new items added won't get crawled/indexed unless workers are running.\n\n### Workers\n\n- Run `pnpm workers` in the root of the repo.\n\n### Mobile App (iOS & Android)\n\n#### Prerequisites\n\nTo build and run the mobile app locally, you'll need:\n\n- **For iOS development**: \n  - macOS computer\n  - Xcode installed from the App Store\n  - iOS Simulator (comes with Xcode)\n\n- **For Android development**:\n  - Android Studio installed\n  - Android SDK configured\n  - Android Emulator or physical device\n\nFor detailed setup instructions, refer to the [Expo documentation](https://docs.expo.dev/guides/local-app-development/).\n\n#### Running the app\n\n- `cd apps/mobile`\n- `pnpm exec expo prebuild --no-install` to build the app.\n\n**For iOS:**\n- `pnpm exec expo run:ios`\n- The app will be installed and started in the simulator.\n\n**Troubleshooting iOS Setup:**\nIf you encounter an error like `xcrun: error: SDK \"iphoneos\" cannot be located`, you may need to set the correct Xcode developer directory:\n```bash\nsudo xcode-select -s /Applications/Xcode.app/Contents/Developer\n```\n\n**For Android:**\n- Start the Android emulator or connect a physical device.\n- `pnpm exec expo run:android`\n- The app will be installed and started on the emulator/device.\n\nChanging the code will hot reload the app. However, installing new packages requires restarting the expo server.\n\n### Browser Extension\n\n- `cd apps/browser-extension`\n- `pnpm dev`\n- This will generate a `dist` package\n- Go to extension settings in chrome and enable developer mode.\n- Press `Load unpacked` and point it to the `dist` directory.\n- The plugin will pop up in the plugin list.\n\nIn dev mode, opening and closing the plugin menu should reload the code.\n\n\n## Docker Dev Env\n\nIf the manual setup is too much hassle for you. You can use a docker based dev environment by running `docker compose -f docker/docker-compose.dev.yml up` in the root of the repo. This setup wasn't super reliable for me though.\n\n\n[^1]: [nvm](https://github.com/nvm-sh/nvm) is a node version manager. You can install it following [these\ninstructions](https://github.com/nvm-sh/nvm?tab=readme-ov-file#installing-and-updating).\n\n[^2]: [corepack](https://nodejs.org/api/corepack.html) is an experimental tool to help with managing versions of your\npackage managers.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/08-development/02-directories.md",
    "content": "# Directory Structure\n\n## Apps\n\n| Directory                | Description                                            |\n| ------------------------ | ------------------------------------------------------ |\n| `apps/web`               | The main web app                                       |\n| `apps/workers`           | The background workers logic                           |\n| `apps/mobile`            | The react native based mobile app                      |\n| `apps/browser-extension` | The browser extension                                  |\n| `apps/landing`           | The landing page of [karakeep.app](https://karakeep.app) |\n\n## Shared Packages\n\n| Directory         | Description                                                                  |\n| ----------------- | ---------------------------------------------------------------------------- |\n| `packages/db`     | The database schema and migrations                                           |\n| `packages/trpc`   | Where most of the business logic lies built as TRPC routes                   |\n| `packages/shared` | Some shared code between the different apps (e.g. loggers, configs, assetdb) |\n\n## Toolings\n\n| Directory            | Description             |\n| -------------------- | ----------------------- |\n| `tooling/typescript` | The shared tsconfigs    |\n| `tooling/eslint`     | ESlint configs          |\n| `tooling/prettier`   | Prettier configs        |\n| `tooling/tailwind`   | Shared tailwind configs |\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/08-development/03-database.md",
    "content": "# Database Migrations\n\n- The database schema lives in `packages/db/schema.ts`.\n- Changing the schema, requires a migration.\n- You can generate the migration by running `pnpm run db:generate --name description_of_schema_change` in the root dir.\n- You can then apply the migration by running `pnpm run db:migrate`.\n\n## Drizzle Studio\n\nYou can start the drizzle studio by running `pnpm run db:studio` in the root of the repo.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/08-development/04-architecture.md",
    "content": "# Architecture\n\n![Architecture Diagram](/img/architecture/arch.png)\n\n- Webapp: NextJS based using sqlite for data storage.\n- Workers: Consume the jobs from sqlite based job queue and executes them, there are three job types:\n  1. Crawling: Fetches the content of links using a headless chrome browser running in the workers container.\n  2. OpenAI: Uses OpenAI APIs to infer the tags of the content.\n  3. Indexing: Indexes the content in meilisearch for faster retrieval during search.\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/08-development/_category_.json",
    "content": "{\n  \"label\": \"Development\",\n  \"position\": 8\n}\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/_category_.json",
    "content": "{ \"label\": \"API\" }\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/add-bookmark-to-list.api.mdx",
    "content": "---\nid: add-bookmark-to-list\ntitle: \"Add a bookmark to a list\"\ndescription: \"Add a bookmark to a manual list. This operation is idempotent — adding an already-present bookmark has no effect.\"\nsidebar_label: \"Add a bookmark to a list\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzFVm2LGzcQ/iuD8iUxe/alpDTsh4LTNnBNKEfio5SLwePVrFexVlIk7Z0ds9Af0V/YX1JG+2I7uaYtlPbT+bQzo2eemXlGB2EdeYzKmispcoFSvrB2W6PfLuxrFaLIhKRQeOXYRuRiLiUgrHsjiBYQajQNatAqxCksKhVgjAoqgJJUOxvJRPj9198ApVRmA2gAtSeU+wvnKfDXMWqFAYwFKksq4lRkIuImiPxWMKQglpkIVDRexb3Ibw9iTejJz5tYifx22S4z4dBjTZF8SAahqKhGkR9E3DsSuQjRK7P5LLlFRdAY9aEhBm2iKhV5sCXEirr0RCZoh7XTHEWRknq339T3759/Y3cfv652MdriOQNWMZkw4Csp2kx4+tAoT1Lk0TeUCYM1G+jOIBOKATiMlWizfwnxwOc/Rj30wBeRr49GZ+iX7BGcNYECJ/DV5TP+c477JwuFNWNPnKKFewzcJCS5uwbmITRFQSGUjdb7KcN6dvn087g3BptYWa8+khwjv0j9AdFuKTVkrUJQZpOBMneolczAeqCd4ySZqx5ZYp92ceY0cn5fqsqR3lMEom3bhPQBBrgz+N4xbWMjlLYxn0JA57Qq0jTN3gf7MBC7fk8FT6vzPHtRddwXVtJfN9EcaiwqZeiC5xHXmoC8tx7YPXFdUwi4+VuhqqZG82mg3n8q2rN+uu0AHuMvjz34AzsmAtP9sbIsUK5JSXKj5WLGjRFmh26G2tlAZZgdjs3ZClYLfzdoQeO1yMUBpfQUQjtDp2Z3T0Um7tArhpyI6z93ZSux0VHkoorRhXw2i34/3aLHLZGbonMPTmUfYZjFV709dFg4sRMZe8v17G4+FbORbb6Z80hmPHvJSGT9j5fW18gIf/x5kShWprTszll3kJ5OL6eXJyM+4plfX32Gf/yoAiAE0uVFZUNMBR2Y7TRcAlf6QsULjZF8yk4V1K+B+fUVoNb2PsDeNjzONRrcHIOELA13yIAVPoNKbSqtNhWfYAiU/hoJayy2jQvgvN14rGuMqkAWgnfmnXn0CJguVr9uTPhwrjWQkc4qEwP0LQd4rgWO75CgTCrRat4PbgqygopQkp/CL7aBAg1syPBWI15dnNmW9lB6W5/X957WcHMFjZHkYTJ5SzEqswnwbfJ5RfswmQywr3GjzAg5KcIRc2icsz5C0fhg/cUaGaobPeBOIay6j6tE0kqrWsUVfGjI7+G4ArkYBIMmgzKFbiRxZVeGdvG7PkSpSHeaybSAioCh42W4ZAzJlSwpFlX6zkEYGE1hDivTaL2CO9QNQWn9+R3KSK4RpcBcD0+86GvrE8BGxzBwM+wgWOwdBT4cTkKqxprAGurGyxMBT0rI35kLmEy0MtvJJOUyh5s3r092i4oV2NTnqKHweK9JQk0RJUacdu4s+qN7En/gI1Zo6k1Scw42BhqnLfLGKpUmeKxqbnLr4fr7l0+SgDobYo1Ju/v1+dBDSj/w4jocd8H/9fjqNOhkFbZZJ6OHXolv0zMmiEzk43tmHHE+PXkrLDPBWsJOhwM39Y3XbcvHqW/5BXfU4qTYUgX+LUVeog70BX4ev+lXyxP4M9z9IZp9knzd8H8iE1vaH19j/AL7D289YaddtpnohCfl3hnMi4JcPHE9jb48WY/XNwuRCTxfHp8sixT3QUCHQ2exYHFs2xFfEkuG1rZ/AKDKROU=\nsidebar_class_name: \"put api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Add a bookmark to a list\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"put\"}\n  path={\"/lists/{listId}/bookmarks/{bookmarkId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nAdd a bookmark to a manual list. This operation is idempotent — adding an already-present bookmark has no effect.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the list.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"ListId\"},\"required\":true,\"name\":\"listId\",\"in\":\"path\"},{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the bookmark.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"204\":{\"description\":\"No content — the bookmark was added to the list successfully.\"},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"List or bookmark not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/admin-update-user.api.mdx",
    "content": "---\nid: admin-update-user\ntitle: \"Update a user (admin)\"\ndescription: \"Update a user's role, bookmark quota, storage quota, or browser crawling setting. Requires admin role. You cannot update your own user account via this endpoint.\"\nsidebar_label: \"Update a user (admin)\"\nhide_title: true\nhide_table_of_contents: true\napi: eJztV82OGzcMfhVCPTQxZm1vkl58KOD8AdvksE12ERSbRU2PaI9ijTTRz3pdY4A+RJ+wT1JQ82N77QRFkUuB+LBrSxRFfqTIj1thK3IYlDUXUkwEylKZ60pioGtPTmRCks+dqlhCTESzBQjRk/vRg7OaMphbuyrRreBztAEz8ME6XFL30zqYO7v25CB3uNbKLMFTCMosh/COPkflyEO6Oikcwm82Qo7G2ACxuXFjowO7NuliwDy30QS4UwihUB7IyMoqE4YiEwGXXkxuxJQVittMeMqjU2EjJjdbMSd05KYxFGJyc1vfZqJChyUFcj4J+LygEsVkK8KmIjERPjhllkdQXBUEFy/BLiAU1JgVbGsum0H3WFaaFfDe7+dPnoo6E65xV4pJcJEyYbDsRC6kyIRi1RWGQrBpLE0+PLdywwYdG7BQpKXfuximWner6AhskkYNf//5F1ijN1A5e6ckyU5qrbSGOUFeoFmSZNNzawKZwFdiVWmVpwQZffJ87wmE7PwT5UFkonKcTkGR512O5SkcycSSAxSbDMMmTnUmujz6lfNm76QygZZJ1kStcc5qG/hKZVTJ2sZ1Jtq0+4+n2xR90WboK8OSck/P3FpNaI701PXRK+FkaBNXYsD9bOhgad0+cvp8PB4/dOWncfep+cN54StrfAPzk/H4ODn2TJDgY56T94uo9eYbxrdVexKiQ2s+FBQKfiD8Uhpc1uj37BqK+uBx3PTKbxuPn51y8jlKaJ9Iym9l7lArCcpUMSTkufZgCFRWjEP/TPpK8g3RyK08me2HJk+hxLxQhs4coeQkAnLOOuDjQ64QJXmPy3+lqoglmoeK2vPHiCYDd/pvMxFUSKn4ig+KDujzE9lkMIbCOvUHyYQ0B/J5qqQQ7IoMKA+l8l6ZZdaFIRV+uq/4/gc4B7oPo0qjOo3wrlL0NXTfgt7Sp8eWvrZurqQkk8xsegqmTIIOi+8xP475sy9UEO7ACxvNd9AegJbuD4Vl0lTF5CQ37YkYpZwbcXHxo23T12vBJMTddRQjOi0mYotSOvK+HmGlRnfnIhN36BRbmHBqt5vILDDqICaiCKHyk9EouM1whQ5XRNUQq+okPWk1dBzlTSsPjS3sxx47es/ha27e50g9uHwz+5HEuNYnIW5g6ctr60pkC3/5cJUQ5bR4t+Mvr75RA2SKtLCshuFsfD0fjodjsQtT7+j08uIImH5TeUDwpBdnhfUhJUZnCjNUNNxcUJ6pcKYxkEuwqZyGcMWUc3p5Aai1XXsmp9xbSjTMejslPgOtfPAZMCPNoFDLQqtlwSvoPaX/RsIc81WsPBOzpcOyxKBy5E790Xw0P/wAHAcyoX1uvMgcr2O8fV0DPKzIFd/BvTDFfjZty2dSMoOCUJLrmTYsyfAcQIAmebaiDSycLQ8TZ01zuL6AaCQ5GAzeNyTew8/pzBva+MGgM/sSl8r0Jr9VPuzZ7GNVWRcgj85bdzZHNrXqTyRiP2s2ZwmkmValCjP4HMltYEfZORgEHR8CZXIdJU8TMDN0H160KhLXTS2BYQEVAH2DS3dJr5IjuaCQF2mflbBhzKxhxsRvBneoI8HCusM7lJEcI0qKOR6OwFgorUsGRh18h83zbmC62lTkebFb8SkacwJrqHm3jgj4CfrJR3MGg4FWZjUYJF+mcP3u7W76WqtQ7Ah/GrVIQkkBmQoNm+PcevvjqQUDL3Glp1YkJWcnYyBW2mIzMGiCR6rkJLcOLl++fpwKcWV9KDH1gHacORgT4VF66Y8fvsTtrqH8P+bKpg7ucZc6a0r5ti3+N31NS+VfZGLSDna3meAiwyLbLWf7tdN1zcspoXkU3VX/1COk8u0AskDt6SvYPWpdlY/hS1a2i2g2qcnoyL9EJla02Q2fNY9gTV1IFjSbL5p7zjhT9w4fEYA6605M85yq8FXZ273ueXl9lZpAM+KWiRwIh2uRpb/Jziapm6GS17ZCo1nG1P1Fo5JbDh52rAcdKjl1EonttpG44sJZ1z0wqZAyLnX9DxwY/7Q=\nsidebar_class_name: \"put api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Update a user (admin)\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"put\"}\n  path={\"/admin/users/{userId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nUpdate a user's role, bookmark quota, storage quota, or browser crawling setting. Requires admin role. You cannot update your own user account via this endpoint.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The ID of the user to update.\",\"example\":\"user_123\"},\"required\":true,\"name\":\"userId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The fields to update. All fields are optional — only provided fields will be changed.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"role\":{\"type\":\"string\",\"enum\":[\"user\",\"admin\"]},\"bookmarkQuota\":{\"type\":\"integer\",\"nullable\":true,\"minimum\":0},\"storageQuota\":{\"type\":\"integer\",\"nullable\":true,\"minimum\":0},\"browserCrawlingEnabled\":{\"type\":\"boolean\",\"nullable\":true}},\"description\":\"User update data\",\"example\":{\"role\":\"admin\",\"bookmarkQuota\":1000,\"storageQuota\":5000000000}}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"User updated successfully.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"success\":{\"type\":\"boolean\",\"description\":\"Whether the update was successful.\"}},\"required\":[\"success\"]}}}},\"400\":{\"description\":\"Bad request — invalid input data or attempted to update own user.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"403\":{\"description\":\"Forbidden — admin access required.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}},\"404\":{\"description\":\"User not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/attach-asset-to-bookmark.api.mdx",
    "content": "---\nid: attach-asset-to-bookmark\ntitle: \"Attach asset to a bookmark\"\ndescription: \"Attach a previously uploaded asset to a bookmark. The asset must be uploaded first via the POST /assets endpoint.\"\nsidebar_label: \"Attach asset to a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzdV9uKG0cQ/ZWi/WKLWckODjF6CMg3srFJlr0QwnpBpZkaTVs93ePuHmllMZCPyBfmS0LVXKRdrY0hhEBeFm1PdXXVqepzqnfKVeQxamdPMzVVGCOmxSwEipfupXOrEv1KJSqjkHpdsZ2aqplYAULlaa1dHcwW6so4zCgD5M0QHSAsOgdjuCyo+1DWIcKC9va59iHCWiPEguDs14tLmIhpALJZ5bSNY5WoiMugpteqDyqom0QFSmuv41ZNr3dqQejJz+pYqOn1TXOTqAo9lhTJBzEIaUElqulOxW1FaqpC9Nouj9LjYGurP9UEOiMbda7Jg8slwCEnlSi6xbIy7EmTzsztdlluPr74wd1+/r64jdGlLzhwHcWkD/w0U02iPH2qtadMTaOvKVEWSzZa7I0SpTmYCmOhOBneQSG+dNmWUzgOucX39DWgzYAzlCJIqTjY1NlINvJerCqjU6n65GNgBw+A4xYfKY0qUZXnHomaAn/V2bcBePq6R+yLTTJmIOTXpbg7dku2LrnoRtvVT7E0r7ocEhVST2RD4STCLFedp4vD9QVaS/60xCWpROW1MWe4pJlPC73mlbXOyLFdh7r0vWRMqceNoWxvWwfyV134fNgaI3petyvrNpa78RgEqUJqMASdd4BD7jzEQocBgzvdcM0AH6Jy0zStRaicDW0Nvnv67GstsMHQ1Z0yCHWaUgic/PZfboP/UzWbROXa0C9yLY9TsbUxuOB7zdf320r4/KGqXVmsY+G8/kwZ/PXHn3JjXgqTQXQrsqADlDoEbZcJaLtGo7MEnAe6rfi8ezWNdBsnlUH9cDX3tRio6zAC1Uf6/DjSnr/Augi5q+39o/9JO6UuexDne7oDJaaFtnTiCTPGH8h754G3C5uUFAL3xze4KuoS7X1H3f7jWykB7v3f7Hn9DW8U4OT8WDgW0soFyZLZe6omfU+GyW5P8U2nc3wByK97maq9UVO1wyzzFEIzwUpP1s+4wdFrjlUQ6z63dcqxNlFNVRFjFaaTSfTb8Qo9roiqMVbVgwzdeehp+l1nD20snNGBwl5wIduTD3V2gJlPlovMZixkYsS3UX68db5EjvDn3y4FW26Q872ivenbsWWWfdUOCOWINxqWyNzxJgavzezZ+On46YHsDmnNzk6PYBg+Mh1DIJOfFC5EaYi+TtouRVG5U050PDEYyQtIOiWebHRg34DGuE2AratZd0u0uNw7CQkYHWJIgOeYBAq9LIxeFrzSNkEihywwXdVVgMq7pceyxKhTZOb+YD/YR4+AUeeJpL1mvDgzZpiTAnQtC3iXQyo+IwNtpdLzWXfhxckcCsKM/Bh+dzWkaGFJlmdCArSS2Yq2kHtX3m2TDS3g6hRqm5GH0eiCYtR2GeBH2fOOtmE06sM+w6W2Q8jvdYgHMYe6qpyPkNY+OH+yQA61GnbIaDhvP84FpLnRpY5z+FST38J+yGvHzF4pQdvU1BlxZeeWbuOrzkWuybRcy7CAjoChxaU/ZHDJlcwppoV8ZyccGI1hBnOWgDms0dQkmn7nDG0zrhGJY66HJ7AOSuclwNrE0GMz8Cp3eeDFYcSVaiwInKX2lnpqZ4ow/WBPYDTiGzEaSS4zuDp/P/QbbHQswEmfo4FO/6CkiBlGHLfbWSyG7SIawEvM8NSZSHP2NvZwajcEjzWLMYvR2eu3T4SAmfdKFO7vRtr+sXD8LLh/HXd7NflvnhgtzRxIaJO0bLzriPx6GC6YtKd3pvWOy28SxRTCtrsd9/KVN03Dy9Ku/DTZM7nwfaYD/87UNEcT6CugPD7vFOkJfCncbhHtVgTD1PyfStSKtnefFw2POO3Nlyhag45aT4Rx9w6OpL1J+h2zNKUqftX25kAXGXyZ0dpXTCm6rzxuVCJ/Jdi2b0VsZG2nDNplLcKuWp+sIXhXgu5JjmT1IBy7XWtxydzYNAM6wpUMTNP8Dd4ASw8=\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Attach asset to a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/bookmarks/{bookmarkId}/assets\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nAttach a previously uploaded asset to a bookmark. The asset must be uploaded first via the POST /assets endpoint.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the bookmark.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The asset ID and type to attach.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\",\"description\":\"The ID of the previously uploaded asset.\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"pdf\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"avatar\",\"unknown\"],\"description\":\"The type classification for this asset.\"}},\"required\":[\"id\",\"assetType\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"201\":{\"description\":\"The asset was attached successfully.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"pdf\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"avatar\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Bookmark not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/attach-tags-to-bookmark.api.mdx",
    "content": "---\nid: attach-tags-to-bookmark\ntitle: \"Attach tags to a bookmark\"\ndescription: \"Attach one or more tags to a bookmark. Tags can be identified by ID or name. If a tag name is provided and the tag doesn't exist, it will be created automatically.\"\nsidebar_label: \"Attach tags to a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzVV9uO2zYQ/ZUB89DE0NqbIkUDPRRwboCbIF0kXhTFZgGPxbHEmCIVkrKtGAb6Ef3Cfkk7lCV7126QFnmpH9YWNTOcy5kzs1thK3IYlDUTKVKBIWBWTDH3U/vM2mWJbikSIclnTlUsJlIxjkJgDYF1UFpHEDD3ECwgzPdaQ2ArkKGBOYGSZIJaKJIwb2DyghUNljSEyQKQ1eMjKA+VsyslSQIaCaGItkFa8ua7ALRRPiSgAqyV1mw5c4SBpetgSwwqQ62boUgEuyTSG9GF4cVtIjxltVOhEenNVswJHblxHQqR3tzubhNRocOSAjkfBXxWUIki3YrQVCRS4YNTJj9JyLQgqI36VB8F6sAuovt9QkQiaINlpdmSIiX1psnL9cenP9rN5x+KTQg2e8qOqxBFOscnUuwS4ehTrRxJkQZXUyI4XSIV84NQIhQ7U2EoBAfDGuTDMysbDuHU5b5osZ5DeMlV5WyXtQ9Q4IqAVCjIAcIsYD6RM65b+/AWS5pxTJk1gUzgK7CqtMoimkYfPd9zJod2/pGyIBJROcZeUOTj21iuXgqdw4ZDClT6r9Jm/N4v1C7C4G1M1Zl3beAknzXnakymLhlAqEQiirpEwwiStMBaB5Huj3b8OS7PTRvKbXfuK2t86+X3l5fnKzF54Tu4tEUpMMCaHEHn4jfMdGfyK7L9X/AeMP/XUJ/G8p0ksvd0n8wnl49P83dtsA6Fderz3+Ty5+9/RCeexdaGYJdkmFRK5b0yeQLKrFArmTCQaVPxTfdSG2gTRpVGdT6pB3j0AR57IDpPn5x62jU0GBtgYWvzLauaWXkG5CfUDSVmhTJ04QglzjUBOWcdsPqQe6Ik7zH/KlOxAe4b2usPxf1iRgcP9m8P1X/Jil0jlRQKy6Oosj5GyXSWilFHdH60PXDebhR7jXndrTrWrp0WqdiilI68342wUqPVY5GIFTrFnrZd0L5uq9S3dAiVT0ej4JrhEh0uiaohVtXZDthb6HD/ei8PrS8cz9HAec9lbG8+Hjt9kvlmjiOKMa9HIZHsf7yyrkT28OdfpzGzDI93B4J/2YGxI9KbnhMP9eup8HB0zIAdod3ueJIsLBvjpLYRPx5eDi+PWrYPd3w1OUlP/1J5QPCkFxeF9SHCpKueMnkc8YyfCxUuNAZyMXkqoyFMC+XZNqDWdu2hsTXPqhIN5gcjPgGtfPBJ5M0ECpUXWuUFn6D3FL+NhDlmy7qKy0XusDwsCh/MB/PgAXA1mMja5uPDsdZARlZWmeBhD2TAu8xS8R0SlIkImI33NBCNzKAglOSG8Jut4x6Uk+FdiwBNjGxJDSycLe/CZ01zuJ5AbSQ5GAzeUwjK5B5+ijqvqfGDQef2FebK9C6/UT4c+ezrqrIuQFY7b93FHNnVqteAlUKYtS9nMUkzrUoVZvCpJtfAYRfiYhB0kwyUyXQtiSs7M7QJz/cmFop0y8CcFt7R0Ld56S7pTXIlFxR43SgI2Ag7RkMYw8zUWs9ghbomWFh39w5lJNeIomGuhyMwtt1BHflaB9/lpmfbaVOR58N+E+y20rjBcvc6IuBG9OkHcwGDgVZmORjEWMZw/e5NjzdYq1CAjThHDZnDtSYJJQWUGHDYqvMI6dXjKAE+Yt6nvUgEZydjoK60RV56F0oTPFQlg9w6uHrx6lGkZWZD7s50221++y38dPO+343bw4j5v6zuLSceTeJd0tL6dj8PbvrFl9k/vbMFt+tXIphxWHK7ZehfO73b8XFENy/8h4EQ6VIqz7+lSBeoPX0hiQ/f7cfaI/gnZ7u9yjRx7uian0QiltTcXdoj27ZEEb1oBZ63d10wdI8MnOwHu6TTGGcZVeGLsrdHw/Xql/dTHi77/w3KuDwIh2uRxL/R2RbmcWbFs63QaPI6bgeitcmjCO9OsnuTqx1F59Kx3bYSU6bS3a7PTqRWTsxu9xcThBgt\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Attach tags to a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/bookmarks/{bookmarkId}/tags\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nAttach one or more tags to a bookmark. Tags can be identified by ID or name. If a tag name is provided and the tag doesn't exist, it will be created automatically.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the bookmark.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The tags to attach. Each tag must have either a `tagId` or a `tagName`.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"tagId\":{\"type\":\"string\"},\"tagName\":{\"type\":\"string\"},\"attachedBy\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\"],\"default\":\"human\"}}}}},\"required\":[\"tags\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The IDs of the tags that were attached.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"attached\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"description\":\"The unique identifier of the tag.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"TagId\"}}},\"required\":[\"attached\"]}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Bookmark not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/check-bookmark-url.api.mdx",
    "content": "---\nid: check-bookmark-url\ntitle: \"Check if a URL exists in bookmarks\"\ndescription: \"Check if a URL is already bookmarked. Uses substring matching to find candidates, then normalizes URLs (ignoring hash fragments and trailing slashes) for exact comparison.\"\nsidebar_label: \"Check if a URL exists in bookmarks\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzVVtuOGzcM/RVCeUmMWXtT9GkeAjhJU2wToEGyi6LYLGB6RM8o1kiKqNldxzDQj+gX9ksKai7rvfSh6FOfPNaFPDwkD7VXPlDEZLw706pUVUPV9rX32xbj9iJaVShNXEUT5Igq1Rs5AGYDCBefPoBhQBsJ9Q7Wwy3Sc7hgYuBuzSkaV0OLqWrkI3nYGKehQqeNxkRcQGrIgfOxRWu+E4tZhuemdj7fbZAb2ESsW3KJAZ2GFNFY2WOL3BC/gI2PQLdYJah8GzAa9m6uCpWwZlVeqjEiVleFYqq6aNJOlZd7tSaMFJddalR5eXW4KlTAiC0lipwPcNVQi6rcq7QLpErVx/SImPOGMiPJQyYRsEbjOAHdGk6CdiSI5+pQqEjfOhNJqzLFjv6TtUI5bAValxNmxMC3juJOSTyROHjHxBLDD6en8nPf2a/rr1QlME6bCrPxm4ZSQ1FyM6b5OL2r8c+ZXsneynXWrqQqnE+w8Z3TgqryLpFL4hBDsNm4d4uvLF6fYNZnHKpQIUpVJtNjvnP2VBbENa4t/TOPZ2/Bb3Ioj8grwEcQC4L9KFgJ4yhgdbiXsMtjSFeHg+z+ePryMbEXDrvU+Gi+k4a//vgzu3idKw6S35ITX61hNq4uwLhrtEZnTHQbxNcDFhPdpkWwaJ7mb+KEbrENQsk9BOrQQ20pNV6avaZMNkrtq8VUUItccSd9MTHF67EVZKVUe9Q6EvNhgcEsrl+qQl1jNJKDnK5hu2djg51NqlRNSoHLxSLF3XyLEbdEYY4hPNlGg4Uxa++H89BjkTCOuvizsDAUylEvT6SIZ4kjH1PlcEgVw8c7ER5B+Mtv5znNxm28XJeoe0gv56fzUxETkzKnE57lx7NH+KdNUUZgspuTxnMSdqaKkgoUHRPdPDHpxGKimKMzFc3hvDEstgGt9TcMO9+JDrTosL4zwgVYw0kEFGsuoDF1Y03dyAoyU/51GtZYbbvAEKKvI7YtJlOhtbv5F/fFPXsGQhe5NDSnLC6tBXI6eCOCO5Q94P3CDeJDg3E5RavlUGXZyAoaQk1xDr/7TrQeanIyZQjQ5ci2tINN9O39/N7QGi7OoHOaIsxmnylJszK8ynfe045nsxH2R6yNmyB/MCKNE2buQvAxQdVF9vFkjQI1TDfg2iCs+s1VJmllTWvSCrJqwt0EkGQQjAoKxlW20ySZXTm6TW8GExtDtm9woQVMAuSel9HJZDKPQEpVk/fFiACjOSxHFb1G21GeaPd8DOpM2bDkIxI4D62PGWBnE4/cjNMOzneBWBan+ZezsSbwjvr2ikQgncLlF3cCs5k1bjub5ViWWQ3HeoMbkxrwuc7RQhXxxpKGlhJqTDjvr4tCTdezUoEsiaDScCQX53jGQResR00aNsYSPDetFLmP8PHtuxd5UAbPqcWseMOYe/AGyarOUopTbzxsy/2div7fXjC9ih0p/6HohXg/KPfdNJKw77T7qlCiPHJgv5cWuIj2cJDl/m0giq4NizJpVW7Q8sPpecza80/D+HsB//Jt8mQEW9pN75Vc8KpU+bUyjpL8GitULyQZa39lWVUU0tGtRy8LsTKNuJ9/OleFwvsj4cEIyNaHLXS7I9v7fX/iXCTvcFAj7iyB6iCj/2/jS/aa\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Check if a URL exists in bookmarks\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/bookmarks/check-url\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nCheck if a URL is already bookmarked. Uses substring matching to find candidates, then normalizes URLs (ignoring hash fragments and trailing slashes) for exact comparison.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The URL to check against existing bookmarks.\"},\"required\":true,\"description\":\"The URL to check against existing bookmarks.\",\"name\":\"url\",\"in\":\"query\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Object indicating whether the URL is bookmarked. `bookmarkId` is `null` if not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarkId\":{\"type\":\"string\",\"nullable\":true,\"description\":\"The ID of the existing bookmark, or null if the URL is not bookmarked.\"}},\"required\":[\"bookmarkId\"]}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/create-backup.api.mdx",
    "content": "---\nid: create-backup\ntitle: \"Trigger a new backup\"\ndescription: \"Trigger a new full account backup. The backup is created asynchronously — use GET /backups/{backupId} to check its status.\"\nsidebar_label: \"Trigger a new backup\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzFVsGO2zYQ/ZUBc0hraO2k6EmHAt60KbZJ0UXjRVFsFvBYHEuMKVLhkLvrGAL6Ef3CfkkxlOy1Nz701pMEznD4ZubNI3fKdxQwGu+utCpVFQgjXWK1SZ0qlCaugunErEq1CKauKQCCowdYJ2sBq8onF2GVd0xh0dD4D4ZhiKYBeeuqJnjnE9st/PPX35CY4OefFjAbvHm2G36udA/RQ9VQtQETGThiTDxVhYpYsypv1YCO1V2hmKoUTNyq8nanVoSBwjzFRpW3d/1doQJx5x0Tq3Knvnv1Wj6nKQ2xBqDGO3hAhjikSfokHb/6RFWUrALFFBxpeDCxAYSXHTltXP3yCGvlXSQX5UDsOmuqHH72ieXUneKqoRblL247UqUaoqtCdUEaEs2A2egjH47BuFr1hUpM4eq8CZkpnrMVyiVrcWVJlTEk6oux2Xoez0Zi84WODC61KwpiWHm/aTFs3kjnz3oMdTiHgVxqpYdjyVShOFUVMatCrdHYFEjd9YWiEHz4lZixpv+QSy+9/pxMIC3RjVaHIj2V5DjhMb3nyRyg3/W9BP3+HGduHKbY+GC+kM5cjg3BZSYfRL8hJyRpDbNxdQHG3aM1ugAfgB47gfiMIJEe46yzaM5T46l2j9h2kvIJAtUPUFuKjZcR7jxnIqHMgdrPl6RG4Z4C51lJwapS7VDrQMz9DDszu3+tCnWPwUhhc/dG81CCNSYbVamaGDsuZ7MYttMNBtwQdVPszshFQzBGAL/OVXo3+sOARbAfTfEHSX04+XiWD5WQkyWP7KbK0UmamH/e+tCiIPzlj4WSkhi39rJdsh4gvZ6+mr4SKTExF/KAZ3599RX+g9EwIDDZ9UXjOUp1YE8b42pApyEQ6gsTLyxGCjk7U5Hoh2GJDWitf2DY+iTq1qLD+ikIF2ANRy5AJK6AxtSNNXUjK5m88nV6VCKGLvg6YNtiNBVau51+dB/dixcg5SIXR7GRxbm1QE533rgowpVHBPCUrZ2cocG43KLlfKRWDrKEhlBTmMKfPkGFDmpycmEQoMuZbWgL6+Db0/4+0ApuriA5TQEmkw8Uo3E1ww95zzva8mSyh32NtXEHyO8NxyPMnLrOhwhVCuzDxQoFanfYAfcGYTkYl7lIS2taE5fwOVHYQocBW4oUeBDz/Y0AxlU2aZLOLh09xjdjiLUhO0y1lAVMBLkRpC77Qw4hpZNrilWT7RJEgNEU5rAUfVrCPdpEsPbh9AzjtPSIcmDpRyBwHlofMsBkI+9rczlSBBbbjlgW9yucu7Ei8I6G8QpEIJPC5Ud3AZOJNW4zmeRc5nDz+/sD34aLy2eeo4Uq4IMlDS1F1BhxOmwXWTpsz/IEsgTORxpdMjn3Pg5SZz1q0rA2luAb0wrJfYDrH99+O5V7QaSpxSxzDvMInz4pVmffHbsnsfzfnyCDEB0pttzGoqW7UXBv1erpeSJyIUu7nfD2Jti+l+VMTXmjPMltfrEUahi2rNAb2qpSzauKuph12SY5+qvXhAjoQfuvf/uwkBvvVDef6WQOP5rQbY+C73aDx0J0oe9VMaLIOqF6uRT/Bbudlic=\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Trigger a new backup\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/backups\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nTrigger a new full account backup. The backup is created asynchronously — use GET /backups/\\{backupId\\} to check its status.\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"201\":{\"description\":\"Backup creation was triggered. The backup object is returned with a 'pending' status.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"assetId\":{\"type\":\"string\",\"nullable\":true},\"createdAt\":{\"type\":\"string\"},\"size\":{\"type\":\"number\"},\"bookmarkCount\":{\"type\":\"number\"},\"status\":{\"type\":\"string\",\"enum\":[\"pending\",\"success\",\"failure\"]},\"errorMessage\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"userId\",\"assetId\",\"createdAt\",\"size\",\"bookmarkCount\",\"status\"]}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/create-bookmark.api.mdx",
    "content": "---\nid: create-bookmark\ntitle: \"Create a new bookmark\"\ndescription: \"Create a new bookmark. The bookmark type (link, text, or asset) is determined by the `type` field in the request body. For link bookmarks, if the URL already exists, the existing bookmark is returned with a 200 status.\"\nsidebar_label: \"Create a new bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJztWuFuI7kNfhVC9+cumDjeRX/5xwHO9hbd3rYXXBIURdaA6Rnao7NGmpM0SbyGgT5En/Ce5EBpPDNOxln7Nijawlhg4UikRJHUR3LEtTAlWfTS6A+ZGInUEnq6NGZZoF2KRGTkUitLJhAj8S5MA4KmB5jVVAO4yan5C/yqJPhWSb1MwNOjT8BYQOfIfwfSQUaebCE1ZTBbgc8JpswxhbkklYHUYczSrxU5DzOTrQbw3ljgBZtNXAJyHghvf/4IqCxhtgJ6lM67JIyH31IvWrmkA0u+srzzg/Q5ILwdDsF59JUbiER4XDgxuhPb0zsxSYSjtLLSr8Tobi1mhJbsuPK5GN1NNpNE1HJemmwlRusn2tpVi4GoXN4qNdqT9syCZalkGixw8YtjvrVwaU4FhlmlfpqHvVlJYiTM7BdKvUhEadlyXpJjOi+9ovAjkjlvpV6IROhKKZzxnLcVJaLAx4+kF3yCN8PhcJMItGku7ynrcM+MUYRabBIxx3vDCtg3r43v2XeTCFcVBdpV71xURDb2X5Y4UOODurLSRDs85yBdFWw4ZR6Y39gClZhsEiGL0lh/Tc7V/t0np6ls2qu67bJYSpGIB5qx4RT/LsxMKmKSR0+aFxeJcFIvFM3jhHVObPcXk81mk6yF0XSYLcPsC8eUehmOV1nVRzdnBXgxEpWVfMLSUlAhZeNo6l5NbKIzS8uWvouzcYsJC/91IjMOBJHDj/1muO070R7Rtmt+rWwBmoJw4dfNF8hlgQvevszmLdMe32Jn+DsWey7I0Sdu5Wu3nWz43yZyuNJoF0/9djh8DkjjFo4CAvpcuh4EjYD+IoIeBWIv20f26+4FjNjwDczkXB4MIQfC41ejocfFQurFdYgph8Dx1qtclaYUMGOOUlU2eBjpjLkmDZrKz0HFr798P4r3qGcvqvfR7kXWfWK+AtImonJk99zHGOGbCbQWV8zsqXB/2FH1vguO3mOaU3b5YtBCPmZeFag5Tuzee5mJev2d1QJd5/b9ZwLLUddoB3cOoA+o2ouF+4jH+3C3zxNTS6RdbvwxXGU2P4Z8Xil1xYJFBDlqp6cR+hjme5mROUpQvJfpgXbJfaHetZ725Vwt0h4jzvbkRySDrw+AWPnc2MOMVc2UdDkdRp2hp6ua4zB9MMff6vB2AMN/b952oKj/03nc3rB3KJY5+bm7rK6KGdkXbtYfVnN/8vjVWq/0UpuHntgVGDg3rbd7/cB7kJk5pv2lA2LdYFBbv17pujs+Q63Jfqh95Am0ixp0ma7OjQPgiR4kFzEhuS2VwYw4nuM9emQTt6o7ysP6coRWFZNNL0GbTe/kzp18dye5fZrJ9iegTa5VZ1atkzY2nzRJQ/NZRcRq5e3wzRe+mDygqz+ZZFDjNxtidao9TrXHqfY41R6n2uNUe5xqj1Ptcao9TrXHqfY41R6H1x5/6nscucSseXb+7V//BqnvUUl+kS4rDxl6fMXCIzVZr76fvtcUmOZS0zk/z7AdgKw1Fph9EGoRco795IClQp73dKGaf/DsySkI2K7f0eUPzNgosqeIu9UxosnPlAVN8nv8ZXg6B2+WpPkhqZCOU+lkq+bQJUCPJe//RM8M0helQtmv4fbaPWJRBhG7EgRJg6Z8brjFoTQu2AP5DVxcNC0F7Ghk78m6kM6GPFSsMcssObe5wFJe3L/h64dWsgaDHevpqIQ5VorfXXPvSze6uPB2NViixSVROcCyfGYULnfrFcDEfoYfa3qIsrD0nS6Eaz583Lnbi9DogncOMMNkXB4GIsaK8OP99mX4r/+4CRZnt/25bWH4YavBpqGg1W5bnMbspluOxpFYyLUcTb3WDnUK6+7gzvt+/Yz/7O2+s3Bd3NU1W332UEHU9UNL2/v43QFyqeeGz8t2j0Z5MxgOhqL198Yi46sPzyzYTEoHCI7U/Dw3zocbtvUsfkNFzeCC2bn05wo92WBfmRK/tUrHawMqZR4crEzFzSIFalxQt+NF1d0tuHAJ5HKRK7nIeSTiXRI2mWG6rEoHpTULi0WBXqbIH00+6U/6m2+AHYa0r3GLB8dKAemsNFJ7BzUGAO7e2JL3aLpzpuP6eoVFppATZmQH8E9TQYoaFqS5qYgAdTjZklYwt6bY9fAHmsHtB6h0RhbOzq7J83Ozg+8Dz4+0cmdnW7GvcCF1I/JH6XxHZleV7CuQVtYZez5DFrVsOOBeIkzj5DQoaapkIf0Ufq3IrqBEiwV3JdUP39t3dJA6VVVGbNmppkf/rl4idioxsrFaQHpAF/Wy3aRZki05J5/mYZ4XYcFoAGOYcmydwj2qimBu7O4eUmdsIwoLsz0sgTZQGBsErJR3W91sgxtwEHY82HQwBWvMCIymCDCWKDRoudEnfQ5nZ3xpzs7CWcahE2C3QcAEP0cF9R2CgjyGUBjZGZob9gDRoeULGApqkuCcWxoNVZ2MAGcd8G3IhBn6r/78/rsQ0Rie+YPEaF1/3ehvOnt6E9dtxPg/71KLaNcJiu1HkxjT7sSs28HGeMSD6zVfjFurNhseDr7PbWxtRAtNbYmItzkEwSUxINf563mdwQePDej7JP/hjD5yjNOUSv8i7aQTkq9+ur4JCW3soitCciQschzg/0dCJCL6Yoh9YWwtFOpFFbIfEdfc1NV09wPpbgTsfqlCvepIuF5HihvGu81GJPVRAv6J0GPzO29NTqA=\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Create a new bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/bookmarks\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nCreate a new bookmark. The bookmark type (link, text, or asset) is determined by the `type` field in the request body. For link bookmarks, if the URL already exists, the existing bookmark is returned with a 200 status.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The bookmark to create.\",\"content\":{\"application/json\":{\"schema\":{\"allOf\":[{\"type\":\"object\",\"properties\":{\"title\":{\"type\":\"string\",\"nullable\":true,\"maxLength\":1000},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"note\":{\"type\":\"string\"},\"summary\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\",\"nullable\":true},\"crawlPriority\":{\"type\":\"string\",\"enum\":[\"low\",\"normal\"]},\"importSessionId\":{\"type\":\"string\"},\"source\":{\"type\":\"string\",\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]}}},{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\",\"format\":\"uri\"},\"precrawledArchiveId\":{\"type\":\"string\"}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\"}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\"}},\"required\":[\"type\",\"assetType\",\"assetId\"]}]}]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"A bookmark with this URL already exists. The existing bookmark is returned.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"userId\":{\"type\":\"string\"},\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"attachedBy\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\"]}},\"required\":[\"id\",\"name\",\"attachedBy\"]}},\"content\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true},\"description\":{\"type\":\"string\",\"nullable\":true},\"imageUrl\":{\"type\":\"string\",\"nullable\":true},\"imageAssetId\":{\"type\":\"string\",\"nullable\":true},\"screenshotAssetId\":{\"type\":\"string\",\"nullable\":true},\"pdfAssetId\":{\"type\":\"string\",\"nullable\":true},\"fullPageArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"precrawledArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"videoAssetId\":{\"type\":\"string\",\"nullable\":true},\"favicon\":{\"type\":\"string\",\"nullable\":true},\"htmlContent\":{\"type\":\"string\",\"nullable\":true},\"contentAssetId\":{\"type\":\"string\",\"nullable\":true},\"crawledAt\":{\"type\":\"string\",\"nullable\":true},\"crawlStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\",\"nullable\":true},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true},\"size\":{\"type\":\"number\",\"nullable\":true},\"content\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"assetType\",\"assetId\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"unknown\"]}},\"required\":[\"type\"]}]},\"assets\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"pdf\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"avatar\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"userId\",\"tags\",\"content\",\"assets\"],\"title\":\"Bookmark\"}}}},\"201\":{\"description\":\"The bookmark was created successfully.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"userId\":{\"type\":\"string\"},\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"attachedBy\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\"]}},\"required\":[\"id\",\"name\",\"attachedBy\"]}},\"content\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true},\"description\":{\"type\":\"string\",\"nullable\":true},\"imageUrl\":{\"type\":\"string\",\"nullable\":true},\"imageAssetId\":{\"type\":\"string\",\"nullable\":true},\"screenshotAssetId\":{\"type\":\"string\",\"nullable\":true},\"pdfAssetId\":{\"type\":\"string\",\"nullable\":true},\"fullPageArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"precrawledArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"videoAssetId\":{\"type\":\"string\",\"nullable\":true},\"favicon\":{\"type\":\"string\",\"nullable\":true},\"htmlContent\":{\"type\":\"string\",\"nullable\":true},\"contentAssetId\":{\"type\":\"string\",\"nullable\":true},\"crawledAt\":{\"type\":\"string\",\"nullable\":true},\"crawlStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\",\"nullable\":true},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true},\"size\":{\"type\":\"number\",\"nullable\":true},\"content\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"assetType\",\"assetId\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"unknown\"]}},\"required\":[\"type\"]}]},\"assets\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"pdf\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"avatar\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"userId\",\"tags\",\"content\",\"assets\"],\"title\":\"Bookmark\"}}}},\"400\":{\"description\":\"Bad request — invalid input data.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/create-highlight.api.mdx",
    "content": "---\nid: create-highlight\ntitle: \"Create a new highlight\"\ndescription: \"Create a new text highlight on a bookmark. Highlights are defined by character offsets within the bookmark's content and support color coding.\"\nsidebar_label: \"Create a new highlight\"\nhide_title: true\nhide_table_of_contents: true\napi: eJztV9tuGzcQ/ZUB89BWWF1S5GkfCig31E2AGImNonAMaLQ70jLikgzJta0KC/Qj+oX9kna4V9lKkAc/FEXfFkty5syZ4ZnhQRhLDoM0+iwXqcgcYaCf5bZQclsEkYicfOak5R0iFS/iOiBouoVAdwGKbi8YDQhrY3Ylut0MeiMe0BHktJGacljvISvQYRbIgdlsPAUPtzIUUkMoqDfwnYfM6EA6AOocfGWtcQEyo4yDzORSb2ciEQG3XqRXYvAmrhPhKaucDHuRXh3EmtCRW1ahEOnVdX2dCEefK/Lhucn3Ij3cC/GioFFQwUDDSQJSZ6piv0c44exl0jDRBpNEvCaaQ9UAnmsTiOG2IbFXtFbJLFI//+TZ9UH4rKAS+SvsLYlUmPUnyjgN1nGigiTPq51zzlm/1wcn9VbUifABXXgX4YzWdVWuyfE66fwrqxHxQ7t8rCqZ6z0pZW4F85iLRGwdkRaJWKuKmPycNlipINJuY50IJuiUSV0phWtFIg2uojoRTNQ3bKybJEpGkF6N+TiOfhxri6L1cV3XjRVvjfYNrT8unp6uh6YE8qEu/s/l4+UyEfJ06JUn9wVW2oQsTwB5hNqIiHr/Y2/XiQgyMPRBcURTSc8Wi4fF8xxzaNUG/vrjT5D6BpXMe+EzDkrpPYtKhxk2klTuH7HCMpOfzMMx1CWUmBVS09QR5pwfIOcasaUZk16S97j9JlNFVaK+b6g9P3uQoghwsD8i+RUf7Ak+cTsvNVahME7+/k9vYYZZm59HxYdgdqRB+o7ipKM/Yd7pzrL/ezxzGcytQnma4eH+3GFpI8Qxgh7psxOl0DUMbQJsTKUHvN5SJjeSu2NfrJAb8nEv3Un/mILznyiH6D8UhocWa3yMErnDi3kxjAI8Cbgbcj4OApVTIhUHzHNH3tdztHJ+81Qk4gadZGCRnna5SWCnfkUI1qfzeXD72Q4d7ojsDK19ECt3i9YCmE3M7pt2PzRYGP5oRPnAWWs7wWhQ6TllzxxH3CbSdhNLdPx4bVyJjPCXXy8ikVwN74f55lVXp8eNZkjVUX9ZHPWTRd8/hjbRiP9Iy6PGj5RZ6o1hb8x6Q8nT2WK2EEMSez6W52cP+OsXpQcET2ozLYwPsWy6AFgtecbieprKMFXIsySzKzOawUUhPdsGZMwe9qbiMa5EjdvBiE9ASc/jGo+QydDaeYDzwyC3xmxXWQ/Wma3DssQgM1RqP/uoP+onT4DTRTq0l5F/LpUC0rk1UgffyTrgsSpZ9pFDO/Sulq2ERCMrKAhzcjP4zVSQoYYtaZ7SCVDHyHa0h40z5XF93dIaLs+g0jk5mEw+UAhSbz38FM+8ob2fTDrY57iVuof8VvowwtwP25Xzxk3XyFBtfwJuJMKqWVxFklZKljKs4HNFbg8WHZYUyHlOBkE3Y7UTNHFmV5ruwovWROx4UQ2ZFpAB0De8dE56k5zJDYWsiOtshIHRDJaw4rliBTeoKoKNccc+pM45RxQNcz4cgTZQGhcBVir4jpteqS/2ljz/7P74mI01gdHUXG9HBHxTffpRT2EyUVLvJpMYyxIu378d3gn8xhk9CxzeKsqhpIA5Bpw1x/l69cdjG2reFvH90GyJxdnt0VBZZTCPY4Mi+F6WXOTGwfnL1z9EmWZ1LDF2CI1RQo4eccWX3nqHod/8C599jTiOujXPi6zvh7YPXIni6EnIKsJ/Dwcu50un6pp/x4rld+HQBeIrMRHNHYyNY0d7JqHBNeWqiE2DZ+T0YSuuk+7EMsvIhq/uvR71sfN3Hy5Y2NtnaRn7tHAYx3O8FakQiWgqKPaL+O8gFOptFRuxaGxyG8DjLnKva8So2iXU+xHCw6HZccEqVdciaUOJqiVqfjX9DdD6rAk=\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Create a new highlight\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/highlights\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nCreate a new text highlight on a bookmark. Highlights are defined by character offsets within the bookmark's content and support color coding.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The highlight to create, including the bookmark ID, text offsets, and optional color/note.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarkId\":{\"type\":\"string\"},\"startOffset\":{\"type\":\"number\"},\"endOffset\":{\"type\":\"number\"},\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"],\"default\":\"yellow\"},\"text\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"bookmarkId\",\"startOffset\",\"endOffset\",\"text\",\"note\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"201\":{\"description\":\"The created highlight.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarkId\":{\"type\":\"string\"},\"startOffset\":{\"type\":\"number\"},\"endOffset\":{\"type\":\"number\"},\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"],\"default\":\"yellow\"},\"text\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\",\"nullable\":true},\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"}},\"required\":[\"bookmarkId\",\"startOffset\",\"endOffset\",\"text\",\"note\",\"id\",\"userId\",\"createdAt\"],\"title\":\"Highlight\"}}}},\"400\":{\"description\":\"Bad request — invalid offsets or missing required fields.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Bookmark not found — the specified bookmarkId does not exist.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/create-list.api.mdx",
    "content": "---\nid: create-list\ntitle: \"Create a new list\"\ndescription: \"Create a new bookmark list. Lists can be manual (bookmarks are added explicitly) or smart (bookmarks are matched automatically by a search query).\"\nsidebar_label: \"Create a new list\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzNV19vGzcM/yqE+pIal4szbC9+GOBkLdC1wII2wTCkBkzf0T7VOkmVdHE844B9iH3CfZKBuj8+O266AR2wl8QnkRT5I/UjtRPGksMgjX6Ti4nIHGGgd9IHkYicfOak5U0xEddxCxA0bWBhzLpEtwYlfUiBFTxkqGFBUKKuUMFZJ+MBHQHmOeVAj1bJTAa1fQnGgS/RhWPJEkNWUA5YBVNikBkqtYXFFhA8ocsK+FyR275MRSICrryY3IvogJglwlNWORm2YnK/EwtCR25ahUJM7mf1LBGOPlfkw5XJt2KyO4rwtqAYDwQDDRApvO695B2fAMI8Hj+HpSSVg/TARqWjvJFuw2/FO+Gy8gG0CQyQp8C+Z0YH0oH9QMuwxDRcfPLszE74rKAS+VfYWhITYRafKOO8WMdJC5I872osaSDlg5N6JRJRSv2O9Ipjv0xEiY/913hcHyX3We3xgfYPUVtmp9TqpF14ao90VXKiGnREIiKonLGcllipICbdXp2ICNrXgqoTYdGRDly5T0V1pRQuFIlJcBXVdZN8zhP7EVFr45jVdbPtrdG+QfW78eXpAmkKI28K/9tlUZ6IoU6+kNyvp+8o+GcS9i8g/M+z++Q8Wy2UzAaiC2MUoWYrBfprw/LGYTDOn5aqPLn3Rj3rtdlocvydy2D4x4OkTVxpz58dV4/MRXJQQgMce60TLg78mSUiyMCORfISTQ1+Px4/LbsrzKElLvjrjz9B6gdUMgepbRUgx4BwRukqTQZMBaX0XupVw5UJHBITbGQoAAdE+o3KODP5SagPA5pCiVkhNZ07wpwzDuScccDqKaetJO9x9Y9MFVWJ+thQq5+K48xFB/f2B1l4xYp9Gk7c/juNVSiMk79THvMQCoKr2GIgmDVp7gUt7EmXpIg8PdrYHw5xDvQYLqxCeRrhfZk+Ymmji0MPoqcRqVAYbt3WxKZtkclRXMT+wzeR3APx7bjficopMRE7zHNH3tcXaOXFwyUXPDrJ6MUcttsNAN3tLUKwfnJxEdw2XaPDNZFN0donCWGObC2AWUaM3rby0PjCng869QcOvDl52K97HPhkjiOK8eWOQiJpf7w2rkT28Odfb2O2uWTf79v8qw69rlN+qZL65YYq99+tHz23tfS1F9hT6IBXpV4aPpTBbw64TMfpWOwLrodlevPmiTf9pvRx7lHL88L4EEu8G5f4eqNmbsD8XIZzhYFcBFlmlMJtIT3bBlTKbDxsTcWTTYkaV3sjPulGFZ6lEijkqlByVcRZx3uK/3UOC8zWlfVgnVk5LPvBLP2oP+oXL4CzRjq0xMGLU6WAdG6N1KEfkgAPr4zlM5jKYqXMp219RyNzKAhzcin8Zqo4Xa5I87RKgDpGtqYtLJ0pD8tsQwu4ewOVzsnBaPSBQpB65eHHqPOWtn406ty+wZXUvcvMxAOffWWtcQGyynnjzhfIrtpeAx4kwrzZnEeQ5kqWMswbXgWLDksK5Dwng6AbMEDqTFU5cWbnmh7DdWuimSiZWhgWkAHQN7h0h/QmOZNLClkR99kIO0YpTGHObXQOD6gqgqVxh2dInXOOKBrmfDgCbaA0LjpYqeA7bK66If92a8nz4lU/qLezvtHU3HJHBHxR/OSjPofRSEm9Ho1iLFO4e/9u/2KIrcfEOkcFmcONohxKCsidLG3UmRt79ciRwEs8QVMrEouzk9FQWWWQnxhLqQjOZMlFbhzc/PT6ZWwpzI8l6gERHLxo1Ik3z25P1//T509DTYNOwiMPs/yubQT3QnVPI+YPXtjtuJDvnKprXm7p7H62bwPxtZSI5vbFzrEmZrzrBo1zrofYNVTFxz8ZGOqk05hmGdnwrOxs0MNufvlwy8zePs/KOE0IhxuRxL8TIRLR1E5sGHFtJxTqVRXHBdHY5D6Ah23kqG3EqNot1NuBh7tdI3HL/FTXImlDiXwlan4s/A0IJEx+\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Create a new list\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/lists\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nCreate a new bookmark list. Lists can be manual (bookmarks are added explicitly) or smart (bookmarks are matched automatically by a search query).\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The list to create. For smart lists, a `query` field is required. For manual lists, `query` must not be set.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\",\"minLength\":1,\"maxLength\":100},\"description\":{\"type\":\"string\",\"minLength\":0,\"maxLength\":500},\"icon\":{\"type\":\"string\"},\"type\":{\"type\":\"string\",\"enum\":[\"manual\",\"smart\"],\"default\":\"manual\"},\"query\":{\"type\":\"string\",\"minLength\":1},\"parentId\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"name\",\"icon\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"201\":{\"description\":\"The created list.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"description\":{\"type\":\"string\",\"nullable\":true},\"icon\":{\"type\":\"string\"},\"parentId\":{\"type\":\"string\",\"nullable\":true},\"type\":{\"type\":\"string\",\"enum\":[\"manual\",\"smart\"],\"default\":\"manual\"},\"query\":{\"type\":\"string\",\"nullable\":true},\"public\":{\"type\":\"boolean\"},\"hasCollaborators\":{\"type\":\"boolean\"},\"userRole\":{\"type\":\"string\",\"enum\":[\"owner\",\"editor\",\"viewer\",\"public\"]}},\"required\":[\"id\",\"name\",\"icon\",\"parentId\",\"public\",\"hasCollaborators\",\"userRole\"],\"title\":\"List\"}}}},\"400\":{\"description\":\"Bad request — invalid input data (e.g., smart list missing query, or manual list with a query).\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/create-tag.api.mdx",
    "content": "---\nid: create-tag\ntitle: \"Create a new tag\"\ndescription: \"Create a new tag. Tag names are normalized (trimmed and converted to the user's preferred tag style).\"\nsidebar_label: \"Create a new tag\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy9VlFvGzcM/iuE+rDWuNjtsKd7GOB2K9C1wILVwTCkAUyf6DvVOkmVeE5uxgH7EfuF+yUDdWfHSYNhD8NeYoeiyI/kx08+KB8oIhvv3mlVqioSMq2wVoXSlKpogpypUr3JJ4Dg6BYY6zmssAaHLSXASOB8bNGa30nDc46mbUkDOg2Vd3uKTBrYAzcEXaL4TYIQaUsxih1rSNxbejFXhWKskyqv1Uo+bwqVqOqi4V6V1we1IYwUlx03qry+GW4KFelLR4lfe92r8vAI86qhHF1QSvqxOslSecfkWK5gCNZUuQWLz0nuHVSqGmpRvnEfSJXKbz5TxapQIUrD2FCSUwl85pU4GlerYRhxmUhaSsleN8Mw2lPwLo3Xv3356mnQI9Dcmv8QrdFPYC3+ZRFGq+JBJd89Bf7KYceNj5kHf/3xZ5746zw1YL8jByZBa1Iyri7AuD1aowvwEeguSKpH5TLd8SJYNE8XOmEtFN1hGyw9QqCGEWpL3Hihd/AptwWFQGqRuSYUi3uKKTOsi1aV6oBaR0ppWGAwi/0rVag9RoMbO3ZyOh7r32JnWZWqYQ6pXCw49vMdRtwRhTmG8NUqyYSnCOC3uUXvJ38YsQjwM+5/lLrHzOcbcGqDZJY6spsqJydVTF/eymoKwp9+XeW5CnF+uV+cH4/NOxL6jBzGbb0cSING9K/mL+cvZVEN54afoC8v331V6unQJEBIZLcXjU8sjYSN97sW4864OitFJNQXhi8sMsXcCFPRHFaNSRIb0Fp/m6D3nexyiw7r+yCpAGsSp0J2JhXQmLqxpm7EgilR/nQaNljtuiDy4+uIbYtsKrS2n39yn9yzZyCdJcfTiolxaS2Q08EbxwmmlQB8yOogOTQYl6e5Xk4UzEHW0BBqinP4zXdQoYOanIguAbpc2Y562EbfPqTCLW3g6h10TlOE2ewjMRtXJ/g+33lPfZrNjrAvsTbuBPmDSXyGOXUh+MhQdTH5eLFBgRpON2BvENbj4To3aW1Na3gNXzqKPQSM2BJTTDIMgqOEgXGV7bToP6wd3fGbKcTWkB23X9oChgHT2JdjklNImeSWuGryuQQRYDSHJaxdZ+0a9mg7gq2PD3MYp2VGlAPLPPITBK2PGWBnOR1783qiCKz6QEmMR0vK09gQeEfjJkYikKVK5Sd3AbOZNW43m+ValnD1y4cT3+DWcAM+8xwtVBFvLWloiVEj43y8LvJ1up5lDMQEzjNNLpmcRx8HXbAeNWnYGkvw3LRCch/h8oe3L+aykCJhLbqzZX38Mj9ewsO9oP5/r/goTGfyPRSjth4m9b0eX/qbQokgyP+HgzDzKtphEHMmnzzz99qbH/1CjeuU5XpHvVQ11nchA85SbTvJ/tVbORTHG8uqosD/6Htz9m5c/vxxJXI6/cpovZY7EW9Vkf+WShVqJENW6Ww7KIuu7rAW3zGmiC8+1O5HWp2rmo7Q9WcID4fRYyWCMwyqmErJAqQGeZX/Bt1jblI=\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Create a new tag\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/tags\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nCreate a new tag. Tag names are normalized (trimmed and converted to the user's preferred tag style).\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The tag name to create.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"}},\"required\":[\"name\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"201\":{\"description\":\"The created tag.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"}},\"required\":[\"id\",\"name\"]}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/delete-backup.api.mdx",
    "content": "---\nid: delete-backup\ntitle: \"Delete a backup\"\ndescription: \"Permanently delete a backup and its associated archive file.\"\nsidebar_label: \"Delete a backup\"\nhide_title: true\nhide_table_of_contents: true\napi: eJylVttuGzcQ/ZUB85IIa8kuUiTYhwJK4wBugsJIbBSFI0Cj5Ug7FpfckFxZirBAP6Jf2C8phru62WlaoE9akTPDM5dzyK1yNXmM7OyVVrnSZCjSGyyWTa0ypSkUnmvZVrm6Jl+hJRvNBjpDQJglW0CrgWMADMEVjJE0oC9KXhHM2dBQZSriIqj8TnXRg5pkKlDReI4bld9t1YzQkx83sVT53aSdZKpGjxVF8iEZhKKkClW+VXFTk8pViJ7t4gnOm5KgsfylIWBNNvKcyYObQyypxyt4aI1VbSQOE2uz3iyqh/vXr9z664/lOkZXvBbQHJNJB/pKqzZTnr407EmrPPqGMmWxEpPZziRTLDBqjKWSNDyF2tlAQaD/cP5Sfk4R/+qgcDaSjfDXH38e4YQHDH2tNYSmKCiEeWPMZihAXp5fPI11a7GJpfP8lfQ+2ptUW4huSRY4QMUhsF1kwHaFhnUGzgOta0lLatOjSbWmdRzVBiWn7/XgUM5jBKpt24T0G1l3NQXrIsxdYx8fjHVtuEizOboP7tvHu9k9FVFlqvYyyZG7KhdO078PyhgqLEq2dOYJNc4MAXnvPIh7qnBFIeDiP4Uqmwrt40C9/1C1J3Nz1wE8xJ8cJu1SHFPZ0vmxdAdiSp4yVbkadfMRRtvd2LVK+ORXO7Y03qhcbVFrTyG0I6x5tLpQmVqhZ4GYCtVvd82ZY2OiylUZYx3y0Sj6zXCJHpdE9RDrp4ogTOsj7Pj1vreHDoskckT0T9K/7uRjuu+rKydLHslMOJWMVNZ/vHO+QkH4y283qaRs507cJesO0sXwfHh+RNw9nvH11RP8+00OgBDIzM9KF2Jq4My5ZYV+yXaR1E06e8bxzGAkn7LjgoZwU3KQ2IDGuIcAG9dAdFChxcUhSMjAcIghA1HBDEpelIYXpaxgCJR+re5ZH6D2buGxqjBygUL3z/azffYMpFyiaB0tZHFsDJDVtWMbA/QjBnjK+FrO0MA2tWg67umZgkyhJNTkh/C7a6BACwuycicQoE2ZLWkDc++q0/4+0Axur6CxmjwMBp8oRraLAD8ln/e0CYPBDvY1LtjuIX/gEI8wh6aunY9QND44fzZDgVrvPWDFCNNuc5qKNDVccZzCl4b8Bg6XhDSDYKe2wLYwjSbp7NTSOv7ch5gzmU4ZpSzAETB0ddkdsg8pnZxTLMq0L0EEGA1hDFPbGDOFFZqGYO786RlstfSIUmDphyewDirnE8DGxLCrzZt+ROBmU1OQxd1KSN2YEThLHb08EQhTQv7ZnsFgYNguB4OUyxhuP37Yzxs8cCzBpTlHA4XHB0MaKoqoMeKwcxdp37sniQdZEkWm3iQN587GQlMbh5p0utThOVcy5M7D9dt3L5Jg1i7ECpNW99fi29OXwmMObg+S/78fGJ2GHF1YbdbJ4LaXzbv+kg4qU/n+vp5kSlgv29utjN+tN20ry2nC5DVyUM2krZqDfGuVz9EE+k5Kzz/2ov8C/glhv4h2k8TZNPJPZWpJm+NXRTtpM9VRNWHotsdFQXU8cjyOPTm6QN5efri8uVSZwlPFfaSwKfQ3EW23ncWNKErb7gEmhRF0bfs3Y8WbRQ==\nsidebar_class_name: \"delete api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Delete a backup\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"delete\"}\n  path={\"/backups/{backupId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nPermanently delete a backup and its associated archive file.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the backup.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BackupId\"},\"required\":true,\"name\":\"backupId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"204\":{\"description\":\"No content — the backup was deleted successfully.\"},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Backup not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/delete-bookmark.api.mdx",
    "content": "---\nid: delete-bookmark\ntitle: \"Delete a bookmark\"\ndescription: \"Permanently delete a bookmark and all its associated data (tags, highlights, assets).\"\nsidebar_label: \"Delete a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy1Vt1u2zYUfpUD9qY1FDsdOqzQxQB3TYGsxRC0CYYhNeBj8chiTZEsSSVWDQF7iD3hnmQ4lCzbadrtZleWyfPznb/vcCesI49RWXMpRS4kaYr0ytpNjX4jMiEpFF45FhC5uCJfoyETdQu9KCCsBmlAIwG1BhUDYAi2UBhJgsSI8DTiOmRQqXWl1bqKIWMRiuHZVGSCL0V+K/aOg1hkIlDReBVbkd/uxIrQk583sRL57aJbZMKhx5oi+ZAEQlFRjSLfidg6ErkI0Suz/iqE64qgMepzQ6AkmahKRR5sCbGiMRTGRFusnWZLipTU23Zd3396+ZPdfvmx2sZoi5cMXMUksgd+KUWXCU+fG+VJijz6hjJhsGah1UEoE4rBOIyV4GA8BWdNoMAB/HD+gn9Ocf9mobAmkonw959/naCFewxDOSSEpigohLLRup0ymBfnz7+2dmOwiZX16gvJ0d6rlGOIdkMGVIBahaDMOgNl7lArmYH1QFvHoXGGBjwp57SNM6eRo/peLQ5JPUYguq5LSB+Je59ZMDZCaRvz0DU6p1WRWnj2KdjHAdjVJyqiyITz3PBR9ZkurKR/b5k51FhUytCZJ5S40gTkvfXA6inHNYWA6/9kqmpqNA8NDfpT0Z10z20P8GB/cei4C1ZMiUv+Y2UP88txcmflYrbvkTDbHdqvEzxd/m4/O43XIhc7lNJTCN0MnZrdPReZuEOvGGZK1nDdl6jERkeRiypGF/LZLPp2ukGPGyI3RecenbvBwn7a3g7y0GPhYI7G/gPXsPd8PPxjhtkzx5HEeLqSkMiGjzfW18gIf/39OqVVmdKyOkfdQ3o+PZ+eHw3xiGd+dfkV/vFSBUAIpMuzyoaYirjPrDLrRIJc3TMVzzRG8ik6VdAUrisV2DaTpL0P0NoGooUaDa4PRkIGWgUmyG9RZpacrLDYNC6A83btsa4xqgJ56D+aj+bJE+B0Mb/1o8GHc62BjHRWmRhgaDPA07l37EOCMqlEy/kwpMnIEipCSX4Kf9gGCjSwJsPrgwBNimxDLZTe1qf1vacV3FxCYyR5mEw+UIzKrAP8nHTeUhsmkz3sK1wrM0J+p0I8whwa56yPUDQ+WH+2QobqRg24UwjL/nKZkrTUqlZxCZ8b8i0cVgYXg2DPuqBMoRtJXNmloW38ZTBRKtI9P3JaQEXA0Odl72Q0yZUsKRZVumcjDIymMIelabRewh3qhqC0/tSHMpJrRMkw18MTGAu19Qlgo2PY52bkwuvWUeDDcWGmaqwIrKF+vDwR8KSE/KM5g8lEK7OZTFIsc7h5/+5oe6hYgU19jhoKj/eaJNQUkTf3tFdngh/VE9EDHzEr0yCSmnMvY6Bx2qIkCaXSBE9VzU1uPVy9fvMskaazIdaY+HpYkK8fPikeTuHuQPz/31ukp5ejjdZlPUPuBla9HTd5EJnIj9b6IhNMCiyy23F33njddXycGpCfLgdSTdQrVeBvKfISdaDvxPv0/bAXnsG3UA6HaNrE3brhfyITG2pPnx/dostEP8sJRS8wLwpy8Uj12PriaMu8vnh3cX0hMoGnlPyAgpPpRzHtdr3ENVNO140QEwUxuq77BxUht/M=\nsidebar_class_name: \"delete api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Delete a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"delete\"}\n  path={\"/bookmarks/{bookmarkId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nPermanently delete a bookmark and all its associated data (tags, highlights, assets).\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the bookmark.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"204\":{\"description\":\"No content — the bookmark was deleted successfully.\"},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Bookmark not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/delete-highlight.api.mdx",
    "content": "---\nid: delete-highlight\ntitle: \"Delete a highlight\"\ndescription: \"Delete a highlight by its ID.\"\nsidebar_label: \"Delete a highlight\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVsGO2zYQ/ZUBc0kMrb0pUjTQoYDT3aDbBGiQ7KIoNgt4LI4lxhSpkNSuFUNAP6Jf2C8phpJlee2kOfRkQxwO38x788itsBU5DMqaKylSIUlToF9VXmiVF0EkQpLPnKo4QqTiIq4DQrELgWUDKni4upiKRATMvUhvxZDBi7tEeMpqp0Ij0tutWBI6cvM6FCK9vWvvElGhw5ICOR8DfFZQiSLditBUJFLhg1MmP8JyXRDURn2uCZQkE9RKkQO7glDQHh+jog2WleZUipTUmyYvHz69/MluvvxYbEKw2UuGrkIMGaBfSdEmwtHnWjmSIg2upkQYLDmqGEUlQjGeCkMhuB5HvrLGk+cafjg/559j6F2r5aiTyoOjUDtDklFn1gQygXdjVWmVRZpmnzynONEmu/xEGVNWOSY1qA7A0tp1iW7N/D5uaZsIH9CF31crT2G0bupySY7XychvrGZWW3eKKjJ1yUJoSGv7ILgp3KjcERmRiKWuiZUhaYW1DiLdBbaJCLQJp1KaWmtcMkfMRJsIYwN9V6A6XXrtyX2lK5kjDCTnJ4C0B6K4Hff3sJvj3vVV9ZgjouH88Wl3J3Qo2pbPfHH+/FhINwbrUFinvpCEf/76O2r/VZwwCHZNhkVVKu+VyRNQ5h61kglYB7SpuIJHQmOUs0qjOi2xPb3DRI0RDEhfHCMdygFjA6xsbf5PkWdWnpTCIYY5lJgVytCZI5QsESDnrAPePmXeS/Ie8+9KVdQlmseJ+v3TI5VEgPv8I54veWPsXDw/FHZvxFwnu0oqZoNN+Nl2ZD4ti47c/c48a6dFKrYopSPv2xlWanb/XCTiHp1ioLFd/XLH0m4CixAqn85mwTXTNTpcE1VTrKqTxttn2Nntmz4eOixczsj3PzCLvRuN3H/oMZ/MdcQwkfZBbBPxz2vrSmSEv/1xHRurzMrydq66g/R8ej49H5n4gGf+7uoI/7CoPCB40quzwvoQadxNszI5oJHA/J6pcKYxkIvVqYymcF0oz7kB2bU8NLaGYKFEg/k+iU9AKx98AnwvJnur9wmg9xR/jYQlZuu68lA5mzssSwwqQ62b6Ufz0Tx5AtwuvuC64eCPc62BjKysMoFvjSg0wMPRr/gMCcpEihbzfk5jkgUUhJLcFP60NWRoICfDLwECNLGyNTWwcrY85PeBlnBzBbWR5GAy+UAhKJN7+DnueUONn0x2sN9hrswA+a3yYYTZ11VlXYCsdt66syUy1GrYAfcKYdEtLmKTFlqVKizgc02ugf2bgckg2N25oEyma0nM7MLQJvzSp1gp0p1FcltABUDf9WV3yJCSmVxRyIq4zkkYGE1hDgu+WxZwj7omWFl3eIYykjmimJj5cATGQmldBFjr4He9edVLBK6bijx/3H3xkY0lgTXUjZcjAp4Un340ZzCZaGXWk0msZQ43798OeoMHFQqwUeeoIXP4oElCSQElBpx229njh+3R64E/sS9THxLFuYsxUFfaoiQJK6UJnqqSRW4dvLt4/SzaZmV9KDE6dv8+On4oPh7D7d77//tZ2fnE6Hbi65utbtsb5O3+ReZFItLx++wuETzfHLTdstBunG5b/hy1xM/QvT9GF5XK838p0hVqT99A/vR9b/LP4Gs4+49ommjD/PJJhUjEmppHD8n2rk1EN5cRRhcxzzKqwmjv0UXJXjvcHBeXby+vL0Ui8NBkH5lqPOAktO22i7hmE2nbAWk0FcbYtv8CRuRA+A==\nsidebar_class_name: \"delete api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Delete a highlight\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"delete\"}\n  path={\"/highlights/{highlightId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nDelete a highlight by its ID.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the highlight.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"HighlightId\"},\"required\":true,\"name\":\"highlightId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The deleted highlight is returned.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarkId\":{\"type\":\"string\"},\"startOffset\":{\"type\":\"number\"},\"endOffset\":{\"type\":\"number\"},\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"],\"default\":\"yellow\"},\"text\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\",\"nullable\":true},\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"}},\"required\":[\"bookmarkId\",\"startOffset\",\"endOffset\",\"text\",\"note\",\"id\",\"userId\",\"createdAt\"],\"title\":\"Highlight\"}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Highlight not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/delete-list.api.mdx",
    "content": "---\nid: delete-list\ntitle: \"Delete a list\"\ndescription: \"Delete a list. This removes the list only — bookmarks within it are not deleted.\"\nsidebar_label: \"Delete a list\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy1VlFv2zYQ/isH9qU1FDsdOrTQwwBnSYGsxRC0CYYhNeCzeLZYUyRLUo5VQ8B+xH7hfslwlCzbadbtZU9xyOPdd9/dfaedsI48RmXNtRS5kKQp0nsVosiEpFB45fhS5OIyXQGCViGO4bZUATxVdkMBYknpGKzRDfz1x5+wsHZdoV8HeFCxVAZUBPQExkbogsixyETEVRD5veCIQcwyEaiovYqNyO93YkHoyU/rWIr8ftbOMuHQY0WRfEgGoSipQpHvRGwciVyE6JVZfYP9tiSojfpSEyhJJqqlIg92OQBnLLTFymn2okhJvW1W1cPnN6/t9uuP5TZGW7xhwComEwZ8LUWbCU9fauVJijz6mjJhsGID3RlkQjEAh7EUnICn4KwJFBj0D+ev+M8p1l8tFNZEMjEROVD7gGHPHIS6KCiEZa11M2YQr85ffuvpzmAdS+vVV5KDr4vEKUS7JgMqQKVCUGaVgTIb1EpmYD3Q1nFKzEqPJXFM2zhxGjmj73F/IPIYgWjbNiF9ImdmM7XG0tbmcVh0Tqsi9ejkc7BPB7eLz1Rw0zrPHR1Vx3BhJf17e0yhwqJUhs48ocSFJiDvrQd+nvitKARc/SdXZV2heeyofz8W7UnH3HcAD/5nhw674oeJtBQ/lvYwoJwnd1QuJtwbYbLr2q0VPEF+s5+P2muRix1K6SmEdoJOTTYvRSY26BXDSyT1111ZlljrKHJRxuhCPplE34zX6HFN5Mbo3JOz1XvYT9S73h46LJzE0Wh/5Np1kY8HfGCWI3MeyUzkvZHI+h9vra+QEf7y222iU5ml5eecdQfp5fh8fH40rAOe6c31N/iHSxUAIZBenpU2xFS8vYopswI0EriqZyqeaYzkU3aqoF4LpzfXgFrbhwCNrSFaqNDg6uAkZGmQQwasehmUalVqtSr5BEOg9NdIWGCxrl0A5+3KY1VhVAXyoH8yn8yzZ8B0sYZ1I8GHU62BjHRWmciinNoL8HTWHceQoEwq0XzaD2ZyMoeSUJIfw++2hgINrMjwXiBAkzJbUwNLb6vT+j7QAu6uoTaSPIxGHylGZVYBfkpv3lETRqM97BtcKTNAThN/wBxq56yPUNQ+WH+2QIbqhhewUQjz7nKeSJprVak4hy81+QYOa4GLQbBXWVCm0LUkruzc0Db+3LtYKtKdJjItaTl1W2wIMrjkSi4pFmW6ZycMjMYwhbmptZ7DBnVNsLT+NIYykmvUrUeuR1p/UFmfANY6hj03F32LwG3jKPDhxbA/uRoLAmuoGy9PBDwpIf9kzmA00sqsR6OUyxTuPrwf+i1tXrCpz1FD4fFBk4SKIkqMOO6es6gPz5O4Ax+xGlNvkppzb2OgdtqiJAlLpQmeq4qb3Hq4uXz7IomlsyFWmHS6X4Yn3w6PJ3B3EPv/6SOj05Wj9dVmnTTuehm9T+s6iEzk/d6eZYJVgK92O27HO6/blo9Tx/H3yEFFk9ZKFfi3FPkSdaDvJPn8Q78AXsA/oesP0TRJrHXN/4lMrKk5fFu0szYT3eAmBN3ltCjIxaNnx55nR6vk8ur91e2VyASe6u8jvU2un8Sz23UWt6wvbTvAS3rD6Nr2b2ZfomE=\nsidebar_class_name: \"delete api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Delete a list\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"delete\"}\n  path={\"/lists/{listId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nDelete a list. This removes the list only — bookmarks within it are not deleted.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the list.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"ListId\"},\"required\":true,\"name\":\"listId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"204\":{\"description\":\"No content — the list was deleted successfully.\"},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"List not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/delete-tag.api.mdx",
    "content": "---\nid: delete-tag\ntitle: \"Delete a tag\"\ndescription: \"Delete a tag. This removes the tag from all bookmarks it was attached to.\"\nsidebar_label: \"Delete a tag\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVttuGzcQ/ZUB85IIa8kpUiTYhwJy7QBugsJIZBSFI0Cj5WiXEZdkSK4sRVigH9Ev7JcUw12tZMdN+9CnlTjk8MzlnOFeWEceo7LmWopcSNIUaYalyISkUHjl2CZycZksgBCxHMOsUgE81XZDAWJFvAorb2tArWFp7bpGvw6gItxjAIwRi4okRDsWmYhYBpHfiRl/55kIVDRexZ3I7/ZiSejJT5tYifxu3s4z4dBjTZF8SBtCUVGNIt+LuHMkchGiV+ZbxLOKoDHqS0OgJJmoVoo82NUBLyOhLdZOsxNFSurtrqzvP795bbdff6y2MdriDcNVMW2ZYXktRZsJT18a5UmKPPqGMmGwZntM9kwovt1hrASj9xScNYECI/7h/BV/HgL91UJhTSQT4a8//hzSyYnr6iEhNEVBIawarXdjhvDq/OW3jm4NNrGyXn0lObi6SPmEaNdkQAWoVQjKlBkos0GtZAbWA20dB8Qp6aGk/NI2TpxGDuh7eT9m8RSBaNs2IX0i5BmWYGyElW3M41vROa2K1JOTz8E+fbddfqYiikw4zx0cVZffwkr6986YQo1FpQydeUKJS01A3lsPfDylt6YQsPxPrqqmRvPYUX9+LNoH7XLXATz6nx+764oPppyl+2Nlj4TkOLmfcjFh7kz2qdVawdTxmwMxGq9FLvYopacQ2gk6Ndm8FJnYoFcMLqWoN3c1WWGjo8hFFaML+WQS/W68Ro9rIjdG554kVe/hQKV3/X7osHAIJ5z+yJXrbj5l9pBXvpnjSNtE3m8SWf/jrfU1MsJffpulZCqzsnyco+4gvRyfj89PaDrgmd5cf4N/MKoACIH06qyyIabSHWRLmRLQSOCanql4pjGST9Gpgnrtm95cs9TZ+wA720C0UKPB8ugkZKBViCFjLocMKlVWWpUVr2AIlL5GwhKLdeMCOG9Lj3WNURXILP9kPplnz4DTxeLVEYIXp1oDGemsMpFFODUX4EOiO75DgjKpRItpz8rkZAEVoSQ/ht9tAwUaKMnwFCBAkyJb065T8wf1vacl3F5DYyR5GI0+UozKlAF+Smfe0S6MRgfYN1gqM0B+r0I8wRwa56yPUDQ+WH+2RIbqhhOwUQiLzrhISVpoVau4gC8N+R0c5wEXg+CgsKBMoRtJXNmFoW38uXexUqQ7QeS08FDCbmoNlwwuuZIrikWV7OyEgdEYprAwjdYL2KBuCFbWP7xDGck16sYh18MTGAu19Qlgo2M45OaibxGY7RwFXrwYBiZXY0lgDXX08kTATAn5J3MGo5FWZj0apVimcPvh/dBvcK9iBTb1OWooPN5rklBTRIkRx91xVvTheFJ24CXWYuq3pOY87DHQOG1RkoSV0gTPVc1Nbj3cXL59kaTS2RBrTCrdz8HTp8JjAu6PSv//Pik6NTmZWG3WCeK+l8677tmRibyb0/NMMPPZsN9zC9563ba8nLqMHx9H5Uz6KlXg31LkK9SBvhPZ8w+95L+Af8LWL6LZJYHWDf8TmVjTbnhKtPM2Ex1XE4DONi0KcvHk1Knj+cnsuLx6fzW7EpnAh5L7SGKT6yfh7PfdjhlLStse0fF/Rte2fwPHe5L9\nsidebar_class_name: \"delete api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Delete a tag\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"delete\"}\n  path={\"/tags/{tagId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nDelete a tag. This removes the tag from all bookmarks it was attached to.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the tag.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"TagId\"},\"required\":true,\"name\":\"tagId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"204\":{\"description\":\"No content — the tag was deleted successfully.\"},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Tag not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/detach-asset-from-bookmark.api.mdx",
    "content": "---\nid: detach-asset-from-bookmark\ntitle: \"Detach asset from a bookmark\"\ndescription: \"Detach an asset from a bookmark.\"\nsidebar_label: \"Detach asset from a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy9Vm1v2zYQ/isH9ktrKHY6dFihDwPcNQWyFkPRJhiG1IDP4kliTZEsSSV2DQH7EfuF+yXDUZJf0qzrgGGf4lB3x+eeu3uOO2EdeYzKmkspciEpYlHPQ6D4ytvmhbXrBv1aZEJSKLxybCly8TLZARpAtoXS2wYQVoP9VGQiYhVEfiPGGEEsMhGoaL2KW5Hf7MSK0JOft7EW+c2iW2TCoceGIvmQDEJRU4Mi34m4dSRyEaJXpvoCzVVN0Br1qSVQkkxUpSIPtoRY0wkm2mDjNEdSpKTebKvm7uPzH+zm8/f1JkZbPGfgKiaTEfilFF0mPH1qlScp8uhbyoTBho1WB6NMKAbjMNaiy/4j9Inefw09FfCruHGwOAG9YPPgrAkUGPd358/4zyncXywU1kQyEf78/Y+h/ncYoO8dkhDaoqAQylbr7ZQxPDt/+mWca4NtrK1Xn0mmSJzui9QSEO2aDKgAjQpBmSoDZW5RK5mB9UAbxxkxKwOSRDJt4sxp5Hy+Rv6ByGMEouu6hPSBjMdG4Lv7dI2NUNrW3MeAzmlVpHGafQz2YSR29ZGKKDLhPA9fVD3ZhZX0z80yhwaLWhk684QSV5qAvLce2D2R3VAIWH1TqLpt0NwPNPhPRXfSPTc9wEP8xaHdLtgxMZjuj7XttURTZI/UXLmYjbMSZrvD2HSzRGmY7YaW7ATLhL8dRaD1WuRih1J6CqGboVOz26ciE7foFeNO7A2f++KV2OooclHH6EI+m0W/na7R45rITdG5B0dwiDAO3uvBHnosnN2Rfr3novY3H6vYnnK+mfNIZiwTyUhkw49X1jfICH/+9SrxrExp2Z2z7iE9nZ5Pz49Geo9n/vbyC/z7jyoAQiBdntU2xFTVkWplKkAjgct9puKZxkg+ZacKmsJVrQLHBtTa3gXY2haihQYNVocgIQOtQgwZsLxnUKuq1qqq+aSvY5YuWWGxbl0A523lsWkwqgJZDj6YD+bRI2C6WOr6WeHDudZARjqrTAww9B3gqSI4vkOCMqlEy/kwvinIEmpCSX4Kv9kWCjRQkeHdRrymOLM1bftFdVLfO1rB9SW0RpKHyeQ9xahMFeDH5POatmEyGWG/xUqZPeQ3KsQjzKF1zvoIReuD9WcrZKhu7wG3CmHZf1wmkpZaNSou4VNLfguH3cfFIBiVGJQpdCuJK7s0tIk/DSFKRbpXTqYFVAQMPS/jJfuQXMmSYlGn7xyEgdEU5rA0rdZLuEXdEpTWn96hjOQaUQrM9fAExkJjfQLY6hhGbvYqebV1FPhwv/lTNVYE1lA/Xp4IeFJC/sGcwWSilVlPJimXOVy/e7PvN7hTsQab+hw1FB7vNEloKKLEiNPenaV/755WAPARyzQNJqk5RxsDrdMWJUkolSZ4rBpucuvh7ctXT5KKOhtig0nAh405Pnkeeu/cH8jdYSl801OpF42jDdZlve7tBvG82T80gshEfvLq6OeOj8elvsgEjz+77Xbch9dedx0fp1bj19ZBPpPIShX4txR5iTrQV9J5/G5YCU/g75APh2i2SaV1y/+JTKxpe/pi4lfS/3jzSE+36DLRi0VKvv86Lwpy8cjvOPTiaK+9vHhzcXXBxJ9q/j2NT6EfBLTb9RZXrGldt8eXNI7Rdd1fn98Mqw==\nsidebar_class_name: \"delete api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Detach asset from a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"delete\"}\n  path={\"/bookmarks/{bookmarkId}/assets/{assetId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nDetach an asset from a bookmark.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the bookmark.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"},{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the asset.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"AssetId\"},\"required\":true,\"name\":\"assetId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"204\":{\"description\":\"No content — asset was detached successfully.\"},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Bookmark or asset not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/detach-tags-from-bookmark.api.mdx",
    "content": "---\nid: detach-tags-from-bookmark\ntitle: \"Detach tags from a bookmark\"\ndescription: \"Detach one or more tags from a bookmark. Tags can be identified by ID or name.\"\nsidebar_label: \"Detach tags from a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy1VttuGzcQ/ZUB85IIa8kpUjTYhwJy7ABugiBwZBSFY0Cj5WiXEZfckFzbG2GBfkS/sF/SDvci2VaDtEj9IEvkzHAuZ87MVtiKHAZlzbkUqZAUMCsWmPvXzpYn1m5KdBuRCEk+c6piQZGK0ygG1hBYB6V1BAFzD2tnS0BY9XpTYEuQoYEVgZJkglorkrBq4PyUVQ2WNBWJYG2RXonhRS+uE+Epq50KjUivtmJF6MjN61CI9Oq6vU5EhQ5LCuR8FPBZQSWKdCtCU5FIhQ9OmfyR74uCoDbqc73nkQO7hlDQznORCLrDstJsSZGS+q7Jy9tPL3+yd19+LO5CsNlLdlyFKDI4fi5FmwhHn2vlSIo0uJoSwWGKVKx2QolQ7EyFoRAcDGuQDydWNhzCY5djfoOFrkJTOOMCBMyhrH2AAm8ISIWCHCAsA+bncskJ7n68w5KWHFNmTSAT+AmsKq2yWPrZJ8/vHMihXX2iLIhEVI6BEhT5eBvLNUqhc9hwSIFK/03aDLaHhWojDN7FVB24w8CBkzxpDtWYTF0ygFCJRBR1iYYRJGmNtQ4i7Y9a/tsvz1UXyvVw7itrfOflD8fHhytxfuoHuHRFKTDALTnqa0PyO2Z6MPkN2f4veA+Y/2uoL2L5HiVy9LRP5ovj54/zd2mwDoV16svfLPDn739EJ05ia0OwGzKgPJTKe2XyBJS5Qa1kwkCmu4pfepDaQHdhVmlUh5O6g8cY4L4HYvD0xWNPh4YGYwOsbW2+Z1UzKw+A/GHl5lBiVihDR45Q4koTkHPWAatPuSdK8h7zbzIVG+ChoV5/Kh4WMzq4s3+9q/4ZKw6NVFIobDc3NAXWiISWitlAdX623bFeO4vdxszubgberp0WqdiilI68b2dYqdnNc5GIG3SKfY0Z66+7Oo1NHULl09ksuGa6QYcbomqKVXWwB3oLA/Lf9PLQ+cIR7Y2cD1zI7uX9wTOmmV/mOKIYM3sUEkn/5bV1JbKHv/y6iLllgFzsKP5sgONApVcjK+4qOJLh7mifAwdKu255lqwtG+OkdhE/nx5Pj/eadgx3/v78UXrGS+UBwZNeHxXWhwiUoXrK5IBGAiPoSIUjjYFcTJ7KaAqLQnm2Dai1vfXQ2JqnVYkG850Rn4BWPvgkMmcChcoLrfKCT9B7iv+NhBVmm7ryUDmbOyxLDCpDrZvpR/PRPHkCXA2msq79+HCuNZCRlVUmeOihDHifWyp+Q4IyEQHLeU8E0cgSCkJJbgq/2TquLDkZXo0I0MTINtR0C849+NzSCi7PoTaSHEwmHygEZXIPP0edN9T4yWRw+z3myowuv1U+7Pns66qyLkBWO2/d0QrZ1WrUgBuFsOwulzFJS61KFZbwuSbXwG4b4mIQDLMMlMl0LYkruzR0F171JtaKdMfBnBZQAdB3eRkeGU1yJdcUeOEoCNgIO0ZTmMPS1Fov4QZ1TbC27v4bykiuEUXDXA9HYGy3MDrytQ5+yM3It4umIs+H4y44LJBx3eTudUTAjejTj+YIJhOtzGYyibHM4fLi7Yg3uFWhABtxjhoyh7eaJJQUUGLAaafOQ2RUj8ME+IiZn3qRCM5BxkBdaYuSJKyVJniqSga5dfD+9PWzSMyV9YG7M90Ou1+/Mh9akx/243Y3Zv6HTbsjsL3B2SYdB2978r4a91Sm6vTe0tptS4lgemDJ7ZZxeul02/JxhCLv5zv2jtwmlefvUqRr1J6+Eu/Ti34KPYN/cnZYg0wTh4Su+ZdIxIaa+zt2pMauq6MXncCr7q0jxtmegUfjvE0GjXmWURW+Knu9NwtPz96eLc54FvTLfBmnvXB4K5L4Gd3tUBlHTDzbCo0mr+M4F51Vnhx4f/A8GDTd5DiUkO22k1gw87XtmJ/IhJyatv0LslDeoA==\nsidebar_class_name: \"delete api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Detach tags from a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"delete\"}\n  path={\"/bookmarks/{bookmarkId}/tags\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nDetach one or more tags from a bookmark. Tags can be identified by ID or name.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the bookmark.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The tags to detach. Each tag must have either a `tagId` or a `tagName`.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"tagId\":{\"type\":\"string\"},\"tagName\":{\"type\":\"string\"},\"attachedBy\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\"],\"default\":\"human\"}}}}},\"required\":[\"tags\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The IDs of the tags that were detached.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"detached\":{\"type\":\"array\",\"items\":{\"type\":\"string\",\"description\":\"The unique identifier of the tag.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"TagId\"}}},\"required\":[\"detached\"]}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Bookmark not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/download-backup.api.mdx",
    "content": "---\nid: download-backup\ntitle: \"Download a backup\"\ndescription: \"Download a completed backup as a zip archive. The backup must have a 'success' status.\"\nsidebar_label: \"Download a backup\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy1VttuGzcQ/ZUB85BEWEtOkaLBPhRQmqRwk4cgsVEUjgGNdke7jLgkw4ssWVigH9Ev7Je0w71IttK0L33SipzLmdsZ7oWx5DBIoy9KkYvS3GplsHyJxTpakYmSfOGkZQGRi1f9NSAUprGKApWwTLKAHhDupAV0RS03NIXLmobLJvoANW4IEB77WBTk/WPwAUP0U5GJgJUX+bXo/HpxkwlPRXQy7ER+vRdLQkduHkMt8uub9iYTFh02FMj5JOCLmhoU+V6EnSWRCx+c1NVJBIwpavklEsiSdJArSQ7MCsIIlvHQFjk8kQtJslTbXdXcfn7xg9nefV9vQzDFCwYtQxLpQF+Uos2Eoy9ROipFHlykTGhsWGQ5iGRCMgyLoRYchiNvjfbkGfp35+f8c4q4z+JKKjrNs8hEYXQgHVgZrVWySBWd3UnLR2Nu2rZtM/H8/NmplyuNMdTGybu/K/rn73+kfLxMWYdg1qRBemik91JXGUi9QSXLDIwD2loO+AGMQNswswo52m9V55DoYwRiQPr8FGmXbdAmwMpE/dDxcfyfvfm6e7P8TEUQmbCO+z/ILv+FKenfW2gODRa11HTmCEtcKgJyzjhg9Sk3QUPeY/WfTNWxQf3QUK8/Fe29jrruAB7s3xx68DUrprQl/6E2PM4VpSC52XIx67rIz/ZDN7azYd4Fz5vbDNMUnRK52GNZOvK+naGVs80zkYkNOslAU7r6665EK4wqiFzUIVifz2bB7aZrdLgmslO0p1zCfd1bGObvbS8PHRYO54gIPnIVO8/HdDDmmD1zHEmMZy4Jiaz/eGNcg4zwl18vU2KlXhlW56g7SM+m59Pzo8Ee8czfX5zgHy8lD6QntTqrjQ+pjEtj1g26tdQVoC6B63smw5nCQC5FJ4tEkNKzbUClzK2HnYkQDDSosToY8Rko6YPPgFkyg1pWtZJVzSfoPaVfPfCwB+tM5bBpMMgCldpNP+lP+tEj4HQx43XDwYdzpYB0aY3UwUPfaID3596yjxKkTiVazPshTUYWUBOW5Kbwm4lQoIaKNO8TAtQpsjXtYOVMc7++t7SEqwuIuiQHk8lHCkHqysOPSect7fxkMsB+j5XUI+R30ocjzD5aa1yAIjpv3NkSGaodNWAjERbd5SIlaaFkI8MCvkRyOzgskW5bDWwMUhcqlsSVXWjahp96EytJquNHTgvIwHSc8jI4GU1yJVcUijrdsxEGRlOYw0JHpRawQRUJVsbd9yF1yTWiZJjr4Qi0gca4BDCq4IfcvOxbBC53ljwfDic+VWNJYDR14+WIgCfF55/0GUwmSur1ZJJimcPVh3djv8GtDDWY1OeooHB4q6iEhgKWGHDaqTPBj+qJ6IGPmJepF0nNOchoiJaZhspujT2RDTe5cfD+1ZuniTat8aHBxNj92jx6byy/+iLZH6j/f3ycdPxytNLarKPIfc+t1/2C9yIT+dGuH+n1JhNMDiy533OXXjnVtnycGpEfNQdyTRRcSs/fpchXqDx9I+4nH/oN8RT+CWx/iHqXOFxF/icysabd8eOkvWkz0U10wtBdz4uCbDhSfPjEYJIeV87Pry9FJvA+NT+g4mT8q5j2+07ikqmnbUeIiYoYX9v+BSrqvTM=\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Download a backup\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/backups/{backupId}/download\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nDownload a completed backup as a zip archive. The backup must have a 'success' status.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the backup.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BackupId\"},\"required\":true,\"name\":\"backupId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The backup file as a zip archive.\",\"content\":{\"application/zip\":{\"schema\":{}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Backup not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/get-asset.api.mdx",
    "content": "---\nid: get-asset\ntitle: \"Get a single asset\"\ndescription: \"Download an asset's binary content. The response Content-Type header is set based on the asset's MIME type.\"\nsidebar_label: \"Get a single asset\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy9VduO2zYQ/ZUB89DE0NqbokUDPRRwrtgmAYJkF0WxWcBjaSSxpkiGHO1aMQT0I/qF/ZJiKNnrzW7zWL/YJofDM2cOz+yU8xSQtbNnpcpVTbyMkVhlqqRYBO1lS+XqpbuxxmEJaAEl4ocIa20x9FA4y2R5DucNQaDonY0EL8bVk/PeEzSEJQXQESIxrDFSCc4CN3RI9v7s/Svg3tNcZYqxjiq/VAlLVFeZilR0QXOv8sudWhMGCsuOG5VfXg1XmfIYsCWmEFNALBpqUeU7JRlVriIHbet7VQnizuovHYEuybKuNAVw1S0yQUNbbL2RNJp0abZ93d78+ewXt/36c7NldsUzgaw5hSTIZ6UaMhXoS6cDlSrn0FGmLLYSgVNEprRg8MiNkhr21EXB/ePpqXzdh/s98h/iPFBlqOD4MNnwmOb1PAPdYk0Lb+sM0HujiySJhS+rJ3Mp5afTp/fhXFjsuHFBf6US/vnr73TF89QbYLchKw1vdYxa8mp7jUaXGbgAtPVCjJA7FZB6RVteeINCy/d6eNuPYwRqkE+mWuLGTVpW2Uhvrhap9LjYTewPSjQVrveK6YJRudphWQaKcVig14vrpypT1xg0rs3YlWl7pKLCzrDKVcPsY75YcOjnGwy4IfJz9P5BtU0Z9hp7O8XDiEVqOBL7J6FgvPlY8gdG5GapI4WpfApS2fTjtQstCsLffj9XQo22lZPjUvUI6en8dH56pN4DnuWHs3v4D5s6AkIkU500LrKwA2vnNi2GjbY1oC0hEJYnmk8MMoVUnS5IVKqj5AY0xt1E6F0H7KBFi/VtkpiB0ZFjBmIEGTS6boyuG1kZ+5ilS9ZYbDofwQdXB2xbZF2gMf38s/1sHz0CoUte9ahmWVwaA2RL77TlCNMDBbyrWi93lKBHg1otJ4mlJKvpXc3hD9dBgRZqsuKgJM4olW2ohyq49m5/b2gNF2fQWXmTs9knYta2jvBrOvOW+jib7WF/wFrbA+R3OvIR5th57wJD0YXowsnopf5wAq41wmrcXCWSVka3mlfwpaPQw61RfuPX2hamK0k6u7K05RdTikqTGV+30AKaAUcvOVxySCmdrIiLJu1LEgFGc1jCynbGrOAaTUdQuXD3Dm1L6RGlxNKPQGAdtC4kgJ3huOfm+SQREI+LsrhfiakbawJnaXxegSiZXMw/2xOYzYy2m9ks1bKEi4/vDnqDG80NuKRzNFAEvDFUQkuMJTLOx+NiT4fjyaZAlsA6pikkiXMfY6HzMjKphEobgsfJZMX+Prx8Pdqqd5FbTHY3DYc3xPK2tK3NZNffPsPdrWX+31N5dJ0jmx6y0Th3k81ejsMtqkzl+zF3lSlxCdnc7eSSi2CGQZaTImWC37ps8uJSR/ldqrxCE+k79T/+OI3YJ/Bf+KZFtH0yc9PJP5WpDfVHs3i4GjI18pEgjLvLoiDPR+eOU18djZo3r85VpvCuO3/jxinvg2h2uzHiXNxnGA7gkhsJtGH4F4TkX+M=\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get a single asset\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/assets/{assetId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nDownload an asset's binary content. The response Content-Type header is set based on the asset's MIME type.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the asset.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"AssetId\"},\"required\":true,\"name\":\"assetId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The asset's binary content. The Content-Type header reflects the asset's MIME type (e.g., image/png, application/pdf).\"},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/get-backup.api.mdx",
    "content": "---\nid: get-backup\ntitle: \"Get a single backup\"\ndescription: \"Retrieve metadata for a single backup, including its current status (pending, success, or failure).\"\nsidebar_label: \"Get a single backup\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy9Vu+KGzcQf5VB+ZKYPftSUhr2Q8FJk3BNCiG5o5TLgce7Y69irbSRRnfnmIU+RJ+wT9KOdr322U4pFPrJa41m9Jt/v5mNcg15ZO3sRalytSR+gcUqNipTJYXC60ZkKlcfiL2mW4KaGEtkhIXzgBC0XRqCedLKQNvCxFLbJWgOUETvyTIERo4BHjdkRZZBiEVBIWTgPCxQm+jpyVhlinEZVH6tOhBB3WQqUBG95rXKrzdqTujJTyNXKr++aW8y1aDHmph8SBdCUVGNKt8oXjekchXYa7s8cueyIohWf4kEuiTLeqHJg1sAV1tnBA/dY90YsaNJl+Z+vazvPj//wd1//b66Z3bFcwGtOV3pQF+Uqs2Upy9ReypVzj5SpizWcmW+vZIpLTAa5EqJG55C42ygINC/Oz+Xn2PEYpUCU7kHsXCWybIoYNMYXaRsTj4H0ToREDf/TAWrTDVecs+6e1OXx0FrMxUD+YvTIgyB+JQsUzYag3MJinjfZqrwhEzllE9aCvor7QlsrOfkRTB3blWjX7100fLJG11lncJANtZSSn3NqUz1Racy1ZecumkzRd47/wuFgEv6F748yO21hG0I0i4k+w737h06M0C/aVsx+uz86XHOryxGrpzXX/9O+Z+//5Gq80XqAWC3Igs6QK1DSE2l7S0aXaamovtGIB4UCNM9TxqD+nRp7GI3lP0+ArVF+uwYaVf7YB3DwkV7+PB/qczClScT8xDAFGosKm3pzBOWkjBImQVRH0up1N/O8aGpKtZoDw31+mN1WAMJ4M7+zY4RXoliClt6nyvXk6w4Ka2fq0nXyWGy2XJDK7VB/nZLadEblasNlqWnENoJNnpy+1Rl6ha9FnwpSr24y8wCo2GVq4q5Cflkwn49XqHHFVEzxuaY3YVcegtbEnzb34cOi3ixx8YfJXndy/ucPIRWXhY/0jUhvnRJuiB9vHa+RkH486+XKZ7aLpyoi9cdpKfj8/H5HrsOeKbvL47wD0IdZCaRWZxVLnDK3rbvZCihLUHSeqb5zCCTT97pgsZwWekgtgGNcXcB1i4CO6jR4nJnJGRgdOCQgYyqDCq9rIxeVnKSul9+7ZafAzTeLT3WNbIu0Jj1+JP9ZB89AgmXjJ2uJ+RwagyQLRunLQfo6wvwYbs38kYJ2qYUzaZ9byYjM6gIS/Jj+M1FKNDCkqwMdwK0ybMVrWHhXf0wv3c0h6sLiLYkD6PRR2LWdhngx6TzltZhNNrCfo9LbQfI73TgPcwhNo3zLFM/OH82R4HaDBpwqxFmnXCWgjQzutY8gy+R/Bp2k1ySIcOuG4n9TkGS2Zmle37Zm1hoMh0tSlhAM2Do4rJ9ZDApmVwQF1WSixEBRmOYwkwIfga3aCKlnebBG9qWkiNKhiUfnsA6qJ1PAKPhsI3Ni75E4HLdUJDD7UlI2ZgTOEtde3kikE4J+Sd7BqOR0XY1GiVfpnD14d1Qb3CnuQKX6hwNFB7vDJXDEjbu1IXXB/XE7yBHQsfUX0nFub1jITbGYUklLLQheKxrKXLn4f1Pr58ktmxc4BoTUfe7yxviw33vsA83O87/vxbGjm72BpssLcKYm55hr/ulS+Z+PuxfN5kSghDxZiOVeuVN28pxKkbZLncEm2i41EG+S5Uv0AT6B88ff+iHwxP4FsL+EO068biJ8k9lakXr/S2xlQWl6+qEoRNPi4Ia3lM8mq7C1MO4efPqUlaTh/x8wMfJ+klQm01341L4p20HjImPBGDb/gWcCEi2\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get a single backup\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/backups/{backupId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nRetrieve metadata for a single backup, including its current status (pending, success, or failure).\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the backup.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BackupId\"},\"required\":true,\"name\":\"backupId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The requested backup.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"assetId\":{\"type\":\"string\",\"nullable\":true},\"createdAt\":{\"type\":\"string\"},\"size\":{\"type\":\"number\"},\"bookmarkCount\":{\"type\":\"number\"},\"status\":{\"type\":\"string\",\"enum\":[\"pending\",\"success\",\"failure\"]},\"errorMessage\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"userId\",\"assetId\",\"createdAt\",\"size\",\"bookmarkCount\",\"status\"]}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Backup not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/get-bookmark-highlights.api.mdx",
    "content": "---\nid: get-bookmark-highlights\ntitle: \"Get highlights of a bookmark\"\ndescription: \"Retrieve all text highlights within the specified bookmark.\"\nsidebar_label: \"Get highlights of a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJylVm2P2zYM/iuE+qUNfMl16LDCHwakW9vdOmCH9g7DcD0gjE3bamTJleTcuYGB/Yj9wv2SjfJLnEvaFeinBBZFPSQfPuROmIosemn0RSpikZN/YcymRLv5ReaFknnhnYhESi6xsmI7EYu35K2kLQEqBZ7uPRSjMdxJX0gNviBwFSUyk5TCunc6F5HwmDsR34jhISduI+Eoqa30jYhvdmJNaMkua1+I+Oa2vY1EhRZL8mRdMHBJQSWKeCd8U5GIhfNW6vwI6FVBUGv5sSaQKWnPYCyYLKCbYqJ7LCvFniTJVN03eXn34fkP5v7T98W99yZ5zsClDyYD8ItUtJGw9LGWllIRe1tTJDSWbLTeG0VCMpgKfSE4GEuuMtqR4wC+Oz/nn2Pcp1Iq3QHqxGhP2vN9rColk1DIxQfHTk5kyaw/UOJFJCrLZfeyg7B/aWKL1mLD0D2V7v99TMI9qkobCefR+t+zzJGfnOu6XJPlc9LpF04To4w9VW3SdclUakgpcyc4tZzu3BJpEYm1qonJlVKGtfIiHgzbSDBtT7nUtVK45jpzOdtIaOPpqwzl6dBrR/YzWUksoad0eQJIe8Csm0M6TbM5zV0fVY85IBrfn752u+fy2OWiffjkhBa3bTh9dv70mKvXGmtfGCs//dfn//z1d+itF6GDwZsNaZAOSumc1HkEUm9RyTQCY4HuK37rAZM5hEWlUJ7m8L72Y8tOEYgB6bNjpEPfgjYeMlPrh09/SxMlJj1Jk0MISygxKaSmM0uYMn2ArDUW+PqcOVGSc5h/lauiLlE/dNTfnx8xKADc+59w4CVfDIkL7/vC9KOAg2TRisVi4J9b7PZUbBfFdEg4sttBoWurRCx2mKaWnGsXWMnF9qmIxBatZLghaf1xV6qhRwvvKxcvFt428w1a3BBVc6yqk+reexg0/U1vDx0WDmoyXN5xLXu9moyYMdP8MscRzFjDgxELSfjzytgSGeGvf1yF9EqdGb7OUXeQns7P5+eTUTHiWV5eHOEfD6UDBEcqOyuM86GYQ5KlzgF1ClzlM+nPFHqyITqZ0ByueCQsLy94Eps7B42pwRsoUWO+d+IiUNJ5FwHP3mgyWiJA5yj86hTWmGzqykFlTW6xLNHLBJVq5u/1e/3oEXC6eIp2LcIfl0oB6bQyUnsHPd0AD/u/4jdS6JeC1bJv1uBkBQVhSnYOf5oaEtSQk+aNhAB1iGxDDWTWlIf1vaM1XF9ArVOyMJu9I++lzh38GO68ocbNZgPsS8ylHiH/Jp2fYHZ1VRnrIamtM/ZsjQy1Gm/AViKsusNVSNJKyVL6FXysyTawX0y4GATDbAepE1WnxJVdabr3P/UuMkmq00lOC0gP6Lq8DI+MLrmSGfmkCOfshIHRHJaw4umzgi2qmiAz9vANqVOuEQXHXA9LoA2UxgaAtfJuyM2oiVdNRY4/jmtZqMaawGjq2ssSAXeKi9/rM5jNlNSb2SzEsoTrt7+NfAsLC5jAc1SQWLxTlEJJHlP0OO+us9CP14Pgd7skD6/eJJBzsNFQV8pgSilkUhE8liWT3Fi4/PnVkyCelXG+xKDb/Rr2mg6WU5MBjjAfNuRuPwu+dcXt9GQyyngRYEnc9Yq6n+isnPHBeJ8O3kiwJLD9bsfcvLaqbflzoB+vx3tJDcKbSsf/UxFnqBx9IcTHb/vp8AQ+B3nYBXUTlJvXqViISGyoOVxx29s2El0nBxSdwTJJqPKTq0cDltV5nDivX16JSOChJj/Q4OD9JKzdrrO4Ys1p2xFl0CAG2Lb/AidXhhI=\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get highlights of a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/bookmarks/{bookmarkId}/highlights\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nRetrieve all text highlights within the specified bookmark.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the bookmark.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The highlights within this bookmark.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"highlights\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"bookmarkId\":{\"type\":\"string\"},\"startOffset\":{\"type\":\"number\"},\"endOffset\":{\"type\":\"number\"},\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"],\"default\":\"yellow\"},\"text\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\",\"nullable\":true},\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"}},\"required\":[\"bookmarkId\",\"startOffset\",\"endOffset\",\"text\",\"note\",\"id\",\"userId\",\"createdAt\"],\"title\":\"Highlight\"}}},\"required\":[\"highlights\"]}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Bookmark not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/get-bookmark-lists.api.mdx",
    "content": "---\nid: get-bookmark-lists\ntitle: \"Get lists of a bookmark\"\ndescription: \"Retrieve all lists that contain the specified bookmark.\"\nsidebar_label: \"Get lists of a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJylVttuGzcQ/ZUB85IIa8kpUjTQQwElTQI3fTAcG0XhCNBoOdIy4pIMyZWtCAv0I/qF/ZJ2uBddmwbIk1bkXA7ncma2wjryGJU1V1KMxZLiK2tXJfrVbyrEIDIhKeReORYRY3FD0StaE6DWoFkEYoERcmsiKgOxIAiOcrVQJGHe2hqKTERcBjG+F539IKaZCJRXXsWNGN9vxZzQk59UsRDj+2k9zYRDjyVF8iEJhLygEsV4K+LGkRiLEL0yyxOQtwVBZdTnikBJMpHBeLCLhG4fEz1i6TRbUqSkftwsy4dPL3+yj19+LB5jtPlLBq5iEumAX0lRZ8LT50p5kmIcfUWZMFiy0HwnlAnFYBzGQvBjPAVnTaDAD/jh8pJ/TnGfjakKB7D5gkxkA+icVnlK4OhTYCtnwmTnnyiPIhPOc7qjajAkV3ti6D1uGHakMvy/upKnmai7QJy5OHjraQpNpTXOOdIc0DoTKj8nWKeqIBOvzrg/Y6UROBUkU5VcjiWaCrXIRCjRR65JSQusdBTj7q7OxOeK/Oab/LlqrlW+Jzq3VhMatlJgeG1Z3nqM1ofzUlUgf2P1V1HbB0Oe/0sVLX+sFT2kk9b/tD6o0XvOVpubNrJ7cey1zkDcwzPd9QKTg6iPfTQFNa3TxYvL56cVfmewioX16su/7PD3n3+ljnyV+h6iXZEBFaBUISizzECZNWolM7Ae6NGxm6Pyj/QYR06jOl/4u7j1jb6PQHRIX5wi7bodjI2wsJU5dv09nZdbeTa9hxAmUGJeKEMXnlBylQF5bz2w+pBLpaQQcPlNpoqqRHNsqNUfiuNMJoA7+3uZf8OKKXDJfyxsOzdSQTF1i1HHVWG03fFhPdLtRAnk1x2lV16LsdiilJ5CqEfo1Gj9nAsavWKkKV7tdZOlrjuLGF0Yj0bRb4Yr9LgickN07uw4aC10Q+B9Kw8NFn7P3jT6wGlsPO/PpD7I7JnfkcS4eZOQyNqPt9aXyAh//f02RVaZhWV1fnUD6fnwcni5N1t6PJPrqxP8/aUKgBBILy4KG2LKYxdfZZaARgIn+ELFC42RfHqdymkItzxCJtdXPLbtQ4CNrSBaKNHgcmckZM34yYCHdQaFWhZaLQs+wRAo/RoJc8xXlQvgvF16LEuMKketN8OP5qN58gQ4XDx2m+7gw4nWQEY6q0wM0FYa4GHrO/Yhod0iZpO2T5ORGRSEkvwQ/rAV5GhgSYY3FwI06WUr2sDC2/Iwvw80h7srqIwkD4PBB4pRmWWAn5POe9qEwaCDfY1LZXrITHF7mEPlnPUR8soH6y/myFBdrwFrhTBrLmcpSDOtShVnkEYH7DYZTgZBtwyAMrmuJHFmZ4Ye4+vWxEKRbiiSwwIqAoYmLp2T3iRnckExL9I9G2FgNIQJzHg+zWCNuiJYWH/oQxnJOaJkmPPhCYyF0voEsNIxdLHp6fB24yjwYb/HpWzMCayhpr08EXCnhPFHcwGDgVZmNRikt0zg7ua3vt7gQcUCbKpz1JB7fNAkoaSIEiMOG3Xm+F49cT3wERMztSKpODsZA5XTFiVJWChN8FSVXOTWw/Uvb58l3nQ2xBITZbd72zuK7eplF4A9wuNe3O4mwPeswg2N7A0vHvvMhNuWQ+/7RZIJc3ywVbZTNhNMAiy63XI13nld13zc7ir30x2JJqqVKvC3FOMF6kBfednTm3YUPIP/QtutjGaTuFpX/E9kYkWbwy24nvLik3o3oWgEJnlOLu6pnkxT5uN+vLx7cysygYcsfMS6yfpZWNttI3HLLFPXPcrEOgywrv8BsYyNGA==\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get lists of a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/bookmarks/{bookmarkId}/lists\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nRetrieve all lists that contain the specified bookmark.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the bookmark.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The lists that contain this bookmark.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"lists\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"description\":{\"type\":\"string\",\"nullable\":true},\"icon\":{\"type\":\"string\"},\"parentId\":{\"type\":\"string\",\"nullable\":true},\"type\":{\"type\":\"string\",\"enum\":[\"manual\",\"smart\"],\"default\":\"manual\"},\"query\":{\"type\":\"string\",\"nullable\":true},\"public\":{\"type\":\"boolean\"},\"hasCollaborators\":{\"type\":\"boolean\"},\"userRole\":{\"type\":\"string\",\"enum\":[\"owner\",\"editor\",\"viewer\",\"public\"]}},\"required\":[\"id\",\"name\",\"icon\",\"parentId\",\"public\",\"hasCollaborators\",\"userRole\"],\"title\":\"List\"}}},\"required\":[\"lists\"]}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Bookmark not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/get-bookmark.api.mdx",
    "content": "---\nid: get-bookmark\ntitle: \"Get a single bookmark\"\ndescription: \"Retrieve a single bookmark by its ID, including its tags, content, and assets.\"\nsidebar_label: \"Get a single bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzVWNtuGzkS/ZUC52EToy07i1nMQA8LKJmbdzI7RmxjMXAEqNRdUjNikx2SLVkRGtiP2C/cL9kt9lVSeyInmYd5ktAssk5dWFU8O2Fysuil0VeJGIsl+ZfGrDK0KxGJhFxsZc6rYizekLeS1gQITuqlIpjXojDfgvQOrr6LQOpYFYnUy/DF49JFEBvtSfsIUCeAzpF3IxEJXhTje9FodGIaCUdxYaXfivH9TswJLdlJ4VMxvp+W00jkaDEjT9YFARenlKEY74Tf5iTGwnkr9fII+21KUGj5viCQCWkvF5IsmAX4tDODMdEDZrnikyTJRD1sl9nm3bffmIcPf0sfvDfxtwxc+iDSAL9KRBkJS+8LaSkRY28LioTGjIXmnVAkJIPJ0aeijAbQz41RhDrAX2ChvBgvUDk6NOdqAY48eANB1Z4Vf3GwKJRqnA7Pfrr95XUEnh58BOTj0XPYSKVgTnWsKAGpwxGWXG60oxHcVKcH5bAwFpRcpp5sK+Jgk5IGo9UWMvKYoEeQDjRRQslo3x9/UhvaENY6XlVomjC+L8huBSdleyCH8q+Xl/xznH/sD3Kekr2Eq03kLZjnSsbhMl68c7xvIEXM/B3FjCG3fHW9rLTK5PgSlJGILaGnZOIHVzOT8EUYXI6ELpTCOSc6B6hs0/4ESbRxKteUDKR2GYkFrg3f8cfWPS6XUi9vPPrCfVxfJEgXGRcSV8QxOSdYhVSFJXYTaa5GYlpGwhVZhlZ+CC7+8sdr409zT4Vje5qsKWx8wrEdTMyliMSG5pxdiv9nZi4VhermSTtOyUhUNXxRLdhglsxyY32wpXBkr4Zzqirb7QJai1ve7Clzn5yo1U0bWEDvMU4peTnortZmNjMtMtRiWu7VnnvWWJ+/d1qQ690+o+nXRWgqv29Atfo4FCX1qnKhVcP+O/ka7dWQE+Rlhku6G1L7mPCEm/FQnIcyMbZE2qXGP2VXniyeIs5l/5qBVRXkSZosxRY3ipJP2LyWCZknAcW1jE+MS+oz9arLtI/K11n5FDiN5ScqYOkvXwCx8KmxpwWrmCvpUjpNOkFP1/WO0/zBO36p29sJGw5KRpCubvCUB7XPqwg8tgT/hD9DJaEq8qfd3GGojY7PxRqm8yqY/O/2I+KhiIhwy7tNj/QNbjX/HCzzj7a9U2uZkx/6x+oim5P9nZv1yW7unNLZ+gW8XuiVNpuB3hU2TMvWtV++8Z4UZu5pP/WKWL8Z1NGvT7rpf5+j1mSv6hw5KO2iLrosV8/DoeCJgUouqoHkLlcGE+J+jmv0yCHuXPekDBuaETpXTMtBgW6a3pude/Pu3nB7OMkOD6DtrFVPVl2StjGfHj85RRkgfn354vixcaerQiw//P+t8d9//ye8i16GpzR4syLNj5xMOp4A+cG+RiWTCIwFesjZ4IN3CdeWi1yhHH6RdNnSvp37CFqkXx8jbawBbTwsTKEPVX/Okyg2yWAm7EOYQIZxKjWdW8KEMwTIWmOBt4d3bEbOcQafcFSYQA8PqvePxGFOBYDd+b0of88bg+OCfp+ampxhI5k9GIuL5tK4i11HMZScZGTXDT8SZlCxwySx5Fx5gbm8WL/gq4dWMsbgqXq5ik/NO4jU+9yNLy683Y5WaHFFlI8wzwe5lfqEhlH5uZaHCgtb0qN2bjiAleY+wdO6lzWHEsNi/DQMQlwnwp8fjM2QEf7jX7fBp1IvDG9nqytIL0aXo8seUdPimVxfHeFvF6VjaovU4jw1zocINp5lPovJKw7tufTnCplIYOtkTCO4TaXjswGVMhsHW1Mw9ZChxmV3iItASeddVNNiqVymgZNwUc2KVQzZHONVkTvIrVlazDL0MkaltqO3+q3+6itgdzGHVd0L/jhRCkgnuZHaO6hzDHD/0ueso+VJZpP6hoZDZpASJmRH8JspIEYNS9JMDBKgDpataAsLa7L9+G5oDndXUOiELJyd3ZD3Ui8d/D3s+Zm27uysgX2NS6lbyK+l8z3Mrsj57QlxYZ2x53NkqHm7A9YSYVYtzoKTZkpm0s8gsDDQ0YIcjI4FaughjuxM04N/VR+xkKSq4shuAekBXeWXRkl7ZCCRyMdpWOdDGBiNYAIz7iozWKMqKoppT4fUCceIwsEcD0ugDWTGBoCF8q7xTVsIuf04/tiSoiEacwKjqbpelgj4prjxW30OZ2fcnM/Ogi0TuHvzuqNlN9KnYEKeo4K6pbZU16jaztW93R6qfODZuCRTLRKSs5HRUNRtGLjfwrMwA3L3uP7uh+ehYubGeX6Kj3cNg/Yj+WPa+PAm7rrK/weQzVVt6fWy7oleldT7dg7hJjzu8bbTSHBFYJHdjlPzzqqy5M8VB8iVNpGOK8YjbGfftD8D8TnorRVth8jQkP1iLAIV2vSVJ/rk2Zu6Lz6Hx7Q3U6/e9nU2qHrRCtNyVc4CikpgEseU9+EejRYMv+21P35/K6oHbZ+j3G9EfbJoH9ZuV0nccuEtyxZlKMQMsCz/B5wM34s=\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get a single bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/bookmarks/{bookmarkId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nRetrieve a single bookmark by its ID, including its tags, content, and assets.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the bookmark.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"},{\"schema\":{\"type\":\"boolean\",\"default\":false,\"description\":\"If set to true, the bookmark's full content (HTML, text, etc.) will be included in the response. Set to false for lighter responses when only metadata is needed.\"},\"required\":false,\"description\":\"If set to true, the bookmark's full content (HTML, text, etc.) will be included in the response. Set to false for lighter responses when only metadata is needed.\",\"name\":\"includeContent\",\"in\":\"query\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The requested bookmark.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"userId\":{\"type\":\"string\"},\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"attachedBy\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\"]}},\"required\":[\"id\",\"name\",\"attachedBy\"]}},\"content\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true},\"description\":{\"type\":\"string\",\"nullable\":true},\"imageUrl\":{\"type\":\"string\",\"nullable\":true},\"imageAssetId\":{\"type\":\"string\",\"nullable\":true},\"screenshotAssetId\":{\"type\":\"string\",\"nullable\":true},\"pdfAssetId\":{\"type\":\"string\",\"nullable\":true},\"fullPageArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"precrawledArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"videoAssetId\":{\"type\":\"string\",\"nullable\":true},\"favicon\":{\"type\":\"string\",\"nullable\":true},\"htmlContent\":{\"type\":\"string\",\"nullable\":true},\"contentAssetId\":{\"type\":\"string\",\"nullable\":true},\"crawledAt\":{\"type\":\"string\",\"nullable\":true},\"crawlStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\",\"nullable\":true},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true},\"size\":{\"type\":\"number\",\"nullable\":true},\"content\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"assetType\",\"assetId\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"unknown\"]}},\"required\":[\"type\"]}]},\"assets\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"pdf\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"avatar\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"userId\",\"tags\",\"content\",\"assets\"],\"title\":\"Bookmark\"}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Bookmark not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/get-current-user-stats.api.mdx",
    "content": "---\nid: get-current-user-stats\ntitle: \"Get current user stats\"\ndescription: \"Retrieve usage statistics for the currently authenticated user, including bookmark counts by type, top domains, tag usage, bookmarking activity patterns, and storage usage.\"\nsidebar_label: \"Get current user stats\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzVV81u20YQfpXB5tIKtGQXPekQQKmbxE2KBrGNIHAMaESOxI2Wu8zuUDYjCOhD9An7JMUsKYmSpTg/p55EaHZmv/nZb2aWypXkkbWzF5kaqhnxb5X3ZPk6kL9k5KASlVFIvS7llBqqt8Re04KgCjgjCIysA+s0wNR54JwgbUyYGrDinCzrFJkyqAL5BLRNTZVpO4OJc/MC/RxSV1kOMKmB65ISYFdC5grUNiTAOGuuSjYKoowp64XmGkpkJi8n0WYQ2HmBFTX6KlGMs6CGN0ocCuo2UYHSymuu1fBmqSaEnvyo4lwNb25Xt4nyFEpnAwU1XKpfTk/lZzcA58SoTfTnywGIDguG1Fkmy2IKy9JIOLSzg49B7C1VSHMqUL7EfTVUbvKRUlaJKr3kh3WDxlbFszYCoXPaVsWEvFol8vUcF85rpqMHRj7N9YKyY/KrGK/Dstc68FHhSz3LjZ7lR06scxee1VdR9pi3Rtv5QUtM93xQgCHQIclKsvqp0l68vmkMt2bWSrdi15XnTdF1bKD3WKtEaaYiPA66qdrOucBe25mgi0X+OLrWwvq8ACvw/qK5/uw04mQ0I4F9qT/T8Ug8jPQ3OsO72o+70mI7AmvP0Shd2+pq3q465aLtbNS+9K8AnOvwjuhI2eQ6/OmsvPQj0veE/nDt1i9d5b8/kPmu9tbwV9ZE1N9WRER0jvVf0z1nv7VYsf4RVFm8awNqP73rXHQj34nzJqq7zsSHiLNrodbv98xi8W2Vuwc+6n/hCXbI7NJVPv0BqGFfvwUrnGoMTgypIfuKEkW2KgQcllol6o4mgtDId+Em2ghgumeyQdpUooK2M0PTRuCDNHJdlM43Dn1lIFp0x9O805P2OtBuv9l2l04v2e8cD/vEDi8/IL89qjvMG52KOpQ6cWqVqF9Pzx72+msrI4zz+jNl8O/f/8T2/iwODcBuThZ0gEIHCbaMNgs0OkvAeaD7UmK01/6l6QxK0/aIB41/k3u6x6KU1O8gUKsGakGcu3Zek4JCITU1kHEjDAoahHZwC+QXMvXIpFN5o4ZqiVnmKYTVAEs9WJypRC3QaymzWI2tuAnEFCvDaqhy5jIMBwP2dX+OHudEZR/L8sFoeJUTtBbATWOsXrXnocEiHnRmsEsJQHNzdxLbxENuFj/iMTVsD0kW48dz5wsUhH+8u4qlq+3Uibp43UA665/2T6UCNMdwbvCM3lw8wL8R6gAIgcz0JHeBJTq7s6fNwBNmJ5pPDDL56J1OqQ9XuQ5iG9AYdxegdhWwgwKtzIqb4kvASP3H6TYkkG9eQAJNQTfT7ATTeVUGKL2beSwKlFHamLr/wX6wT57AaDtfa2flz5ExQDYrnZaJun2pgLs1W8odGWgbUzQetQUWjYwhJ8zI9+G9qyBFCzOysiMQoI2ezamGqXfFbn7vaALXF1DZjDz0epfErO0swNOo84rq0OutYb/BmbYbyMIEHcyhKoWlZIgOzp9MUKCWGw1YaIRxIxzHII2NLjSP4VNFXtYBjwUx+SDJIFjP8+3eQZLZsaV7WXSiiakm07xtCQtoBgxNXNaXbExKJqfEaR7lYkSAUR9GMBa2HsMCTUVxD9i5Q9ss7kDRsOTDE1gHhfMRYGU4rGOzZlMQQgvy54ZfYzYmBM5S87w8UdyYwvCDPYFeTybbXi/6MoLrt6+3G9ad5hxcrHM0kHq8kwWmIMYMGfuNupDTRj2SFMhfYB1TeyQW5/qMhao0DjPKQNoM/KQLKXLn4c3585/70m9LF7hAu+3J6gXxzn4E4dCWudyS5v9u4Wyoq8P0q6Rh32XL1DcqMrU0bulITQBuEyVcI9LlUor+2pvVSv6OdS3r6Zar47KaqOalRnqfU62GapSmVHIkdVPFYWR/3RT23bSPF79fSQfd5dw9jo3W16ONrTu2l8vmxJVwymqlkhZE5Bi1krb6H6wyvyg=\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get current user stats\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/users/me/stats\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nRetrieve usage statistics for the currently authenticated user, including bookmark counts by type, top domains, tag usage, bookmarking activity patterns, and storage usage.\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"Detailed usage statistics for the current user.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"numBookmarks\":{\"type\":\"number\"},\"numFavorites\":{\"type\":\"number\"},\"numArchived\":{\"type\":\"number\"},\"numTags\":{\"type\":\"number\"},\"numLists\":{\"type\":\"number\"},\"numHighlights\":{\"type\":\"number\"},\"bookmarksByType\":{\"type\":\"object\",\"properties\":{\"link\":{\"type\":\"number\"},\"text\":{\"type\":\"number\"},\"asset\":{\"type\":\"number\"}},\"required\":[\"link\",\"text\",\"asset\"]},\"topDomains\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"domain\":{\"type\":\"string\"},\"count\":{\"type\":\"number\"}},\"required\":[\"domain\",\"count\"]},\"maxItems\":10},\"totalAssetSize\":{\"type\":\"number\"},\"assetsByType\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\"},\"count\":{\"type\":\"number\"},\"totalSize\":{\"type\":\"number\"}},\"required\":[\"type\",\"count\",\"totalSize\"]}},\"bookmarkingActivity\":{\"type\":\"object\",\"properties\":{\"thisWeek\":{\"type\":\"number\"},\"thisMonth\":{\"type\":\"number\"},\"thisYear\":{\"type\":\"number\"},\"byHour\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"hour\":{\"type\":\"number\"},\"count\":{\"type\":\"number\"}},\"required\":[\"hour\",\"count\"]}},\"byDayOfWeek\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"day\":{\"type\":\"number\"},\"count\":{\"type\":\"number\"}},\"required\":[\"day\",\"count\"]}}},\"required\":[\"thisWeek\",\"thisMonth\",\"thisYear\",\"byHour\",\"byDayOfWeek\"]},\"tagUsage\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"},\"count\":{\"type\":\"number\"}},\"required\":[\"name\",\"count\"]},\"maxItems\":10},\"bookmarksBySource\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"count\":{\"type\":\"number\"}},\"required\":[\"source\",\"count\"]}}},\"required\":[\"numBookmarks\",\"numFavorites\",\"numArchived\",\"numTags\",\"numLists\",\"numHighlights\",\"bookmarksByType\",\"topDomains\",\"totalAssetSize\",\"assetsByType\",\"bookmarkingActivity\",\"tagUsage\",\"bookmarksBySource\"]}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/get-current-user.api.mdx",
    "content": "---\nid: get-current-user\ntitle: \"Get current user info\"\ndescription: \"Retrieve profile information for the currently authenticated user, including their name, email, and avatar.\"\nsidebar_label: \"Get current user info\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy9VcGO20YM/RWCObQ1tPam6EmHAk7bBGlyCJJdFMVmAdMSLU08mpnMUN51DQH9iH5hv6TgSOu1sznk1JOEGQ75SD4+HtAHjiTGu9c1ltiw/NLHyE6uE0cssOZURRPUAEt8zxIN7xhC9BtjGYzb+Njl97DxEaRlqEYHdg/US8tOTEXCNfSJYwHGVbavjWvU1kRw1HEB3JGxBZCrgXYkFOdYoFCTsLxBhZLwtsDEVR+N7LG8OeCaKXJc9tJieXM73BYYOQXvEicsD/jj5aV+zvFfPcLLcL5LX8tEY1feCTtRFxSC1RSMd4tPSf0cMFUtd6R/sg+MJfr1J64ECwxRKypmRGHqE5sk0bgGhwI16acXBbreWlpbxlJiz0OBuS7fZGk6ar7Np/UV2dzeR+u195bJ4TBoGT/3JnKtpTc1nj64HQa1+Ony+dPaXjttt4/mL67h37//yVx4kZsE4rfswCToTErGNUqDHVlTF+Aj8H3QeF+UXfheFsGS+XrBj/nxPXVB0ztDgMMItWNp/cRs7Q4pXXChzU+LjlFJFXfKL+VUHy2WeKC6jpzSsKBgFrvnWOCOotEi5qZO12MJNtRbwRJbkZDKxULifr6lSFvmMKcQnsyQcnDyAH6Tq/RmsocRi2I/YfsHTX2MfMr5YyU0suaRzbSX2QiL6edlpjWW+PsfV7m/ynR9rlmPkJ7PL+eXOnBGciGPeJbvXj/Bf7w0CQgS281F65NodWDt/bajuNXx1lmOTPWFkQtLwjFnZyqew1VrkvoGstbfJdj7HsRDR46aRyepAGuSpAJUCApoTdNa07R6Qilx/roa1lRt+5AnuYnU6QxXZO1+/tF9dM+ewfJRhYx3eri0FtjVwRsnCSa+A52zNWiMGozLLVotJ2plJytomWqOc/jT91CRg4ad6igDuZzZlvewib477+8dr+H6NfSu5giz2QcWMa5J8HN+84b3aTZ7gP2OGuOOkN+aJCeYUx+Cj6Jilny8WJNCDccXsDMEq/FylYu0sqYzsoLPPcc9BIrUsXBM2gyGB+Wc1Jm1syvH97oMsouNYTtOtZYFjAClsS4PQY4utZMblqrN9+pEgfEclrBSLVrBjmzPeV2cxTCuzpsiO9Z+RAbnofMxA+ytpIfavJgoAlf7wEkPH05S7saawTsexysyg05KKj+6C5jNrHHb2SznsoTr92+PfIM7Iy34zHOyUEW6s1xDx0I1Cc3H5ypLx+dZnkCPwHnhySST88HGQR+sp5pryHvm+yzUKnvvfn35w1y3QfBJOsoyNy4GfMVytqfydvpyEg+Pavl/L+ZReE4UeihG7TxMCnuDWWFRBVg3tyqEnh4OStXraIdBjzMbdX0/Kmxe5gWO85VFect7LHFZVRwkS7HtNfqTtayaeZT7V79dYYF0rpRfKGP2Pl2R25/4PhxGiytVgmHAYgKRlQEHXYP/ATixQLA=\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get current user info\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/users/me\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nRetrieve profile information for the currently authenticated user, including their name, email, and avatar.\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The current user's profile information.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\",\"nullable\":true},\"email\":{\"type\":\"string\",\"nullable\":true},\"image\":{\"type\":\"string\",\"nullable\":true},\"localUser\":{\"type\":\"boolean\"}},\"required\":[\"id\",\"localUser\"]}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/get-highlight.api.mdx",
    "content": "---\nid: get-highlight\ntitle: \"Get a single highlight\"\ndescription: \"Retrieve a single highlight by its ID.\"\nsidebar_label: \"Get a single highlight\"\nhide_title: true\nhide_table_of_contents: true\napi: eJylVm1v2zYQ/isH9ktrKHY6dFihDwPc9WVZCyxoEwxDGsBn6SyxpkiVPCVxDQH7EfuF+yXDUbIsJ27RYZ8kkHfH5+65e8itcjV5ZO3sWa5SVRD/qovS6KJklaicQuZ1LdsqVe+JvaYbAoSgbWEIyp0tLDegOcDZy6lKFGMRVHqlhlBBXScqUNZ4zRuVXm3VktCTnzdcqvTqur1OVI0eK2LyIRqErKQKVbpVvKlJpSqw17Z4AOqiJGis/twQ6Jws65UmD24FXI7wCSq6w6o2EkqTzs3dpqhuPz3/yd19+bG8Y3bZc4GuOZoM0M9y1SbK0+dGe8pVyr6hRFmsxKocWSVKC54auVSSj6dQOxsoSA4/nJ7K5yF0CUyBKT/EmjnLZFl8sK6NziJFs09BHI8Uxy0/USaM1V4IZd0du3RuXaFfC7f3C9kmKjB6/n21CsSjfdtUS/KyTzb/xm7mjPPHCCLbVEL/hoxxt0pKIeUpPJFViVqahqQfclphY1ilO8M2UUx3fCykbYzBpTAj9W8TZR3Tdxnq46k3gfxXqpJ5QqZ8fgRIe9AKV+P6HlZzXLs+qx5zRDScPz7t+kj3qbaVM5+dPn3YPpcWGy6d118oh3/++jt2/Is4V8BuTRZ0gEoHmdUEtL1Bo/MEnAe6qyWDe40mKGe1QX28xfb0DnM0RjAgffYQ6ZAOWMewco29f/b/afLM5Udb4RDDHCrMSm3pxBPm0iJA3jsP4j4V3isKAYvvClU2Fdr7gXr/6YMuiQD38Uc8vxLHWLl4PpeuF2FJUoQkVbNBF8JsO9KbVjqO/M1OLxtvVKq2mOeeQmhnWOvZzVOVqBv0WlDGWvXbHUW78SuZ65DOZuw30zV6XBPVU6zro1rbR9gp7NveHjoskstI6j8Ihb0UjQR/KLCcLHlEM5X2RqIR8ee18xUKwt/+uIhV1XblxF2y7iA9nZ5OT0e6PeCZn589wD9s6iB3GJnVSekCRw53o6xtAWhzEHJPNJ8YZPIxO53RFC5KHSQ2oEhWgI1rgB1UaLHYBwkJGB04JCBXYbLX9pAAhkDxa3NYYrZu6gC1d4XHqkLWGRqzmX60H+2jRyDlkjutmwxZnBsDZPPaacsB+i4DPJz7Ws7IQdtI0WLeD2kMsoCSMCc/hT9dAxlaKMjKE4AAbcxsTRtYeVcd8ntLS7g8g8bm5GEy+UDM2hYBfo4+b2kTJpMd7HMstB0gv9OBR5hDU9fOM2SND86fLFGg1oMH3GiERbe5iEVaGF1pXsDnhvwG9s8EIUMu0O6aBW0z0+QkzC4s3fEvfYiVJtPpo5QFNAOGri67Q4aQwuSKOCvjvgQRYDSFOSzkYlnADZqGYOX84Rna5sIRxcDChyewDirnI8DGcNjV5kXfInCxqSnI4m4lRDaWBM5SN16eCGRSQvrRnsBkYrRdTyYxlzlcvn839Bvcai7BxT5HA5nHW0M5VMSYI+O0cxeBH9yj0IMsiShTbxKbc2djoamNw5xyWGlD8FhX0uTOw/nL10+iZtYucIVRrvsn0RviI+/D+6O43Yv/f3hWdqIxuqfkIhfd2/ZqebV/kQWVqHT8PrtOlAy7GG230nWX3rStLMfGkmfoXiyjpOY6yH+u0hWaQN9I4fH7Xu6fwNdw9otoN1GT5Q2UKpWoNW3uPSTb6zZR3ZBGGJ3FPMuo5pHvgytThHe4Q968ulCJwkO5vSevMfpRXNttZ3EhctK2A8woLwKwbf8FIl9Czg==\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get a single highlight\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/highlights/{highlightId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nRetrieve a single highlight by its ID.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the highlight.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"HighlightId\"},\"required\":true,\"name\":\"highlightId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The requested highlight.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarkId\":{\"type\":\"string\"},\"startOffset\":{\"type\":\"number\"},\"endOffset\":{\"type\":\"number\"},\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"],\"default\":\"yellow\"},\"text\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\",\"nullable\":true},\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"}},\"required\":[\"bookmarkId\",\"startOffset\",\"endOffset\",\"text\",\"note\",\"id\",\"userId\",\"createdAt\"],\"title\":\"Highlight\"}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Highlight not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/get-list-bookmarks.api.mdx",
    "content": "---\nid: get-list-bookmarks\ntitle: \"Get bookmarks in a list\"\ndescription: \"Retrieve a paginated list of bookmarks within the specified list. For smart lists, bookmarks are computed from the list's query.\"\nsidebar_label: \"Get bookmarks in a list\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzVWdtuGzkS/ZUC5yGJ0ZaTxQx2oIcFlMxkxjvJjhHbWCwcAaK6S2pGbLJDsmUrQgP7EfuF8yWDIvsmuWW3Lw+zT3HUxapTFxaLh1umczTcCa1OEzZmS3QfhHVvtV5l3Kwsi1iCNjYiJxE2Zp/QGYFrBA45XwrFHSYghXWgFzCvl8G1cKlQ4FIEm2MsFqISG8F7bcBm3Dj/fxt1VnGDEOssL0jpwujMKyCxFxa+Fmg2IxYxx5eWja8YAbVsGjGLcWGE27Dx1ZbNkRs0k8KlbHw1LacRy7nhGTo01gvYOMWMs/GWuU2ObMysM0Itb3l6kSIUSnwtEESCypEPhrysMREWvOFZLkmLQJHIm80yu/7y49/1zbcf0hvndPwjARbOixDg04SVETP4tRAGEzZ2psCIKZ6RgAwCERMEIOcuZWV0F2JURUah4Dau8FM8ElzwQjo2Dr/sO3aujQNtEjQw30Bs0KcfEu5wBD+FtRachhe07sVoF/CCS4tPVtm4bLVxv9PC2muf5n63VZHNveCu8Y/8RmRFBuEzZUg4zLw5g64wCnI0VK04xJWHaOskLhNugAcHSu1dYaw2oeI55AbXQhcWDNpcK4tke4EuTn3pKbxxjf26tIKGAe493lTlahws3e/rXGuJXLFOPfYiOl2ARUeG/V7whuuW8MLCopASYq0cKgcvf734+CEChzcuAnTx6BVcCylhjiBULIsEE6jaTu3RCM6Ddm8cFtqAFMvUoWlELFynqEAruYEMHU+44yAsKMQEkyFF89f3oclfZeNdQLOXxyl5WimkVP7t9Wv6Z9fZyZ2dv7/rs4hV/pM+nudSxL5HnHyxpLSnfvT8C8YEMDd0RjkRIDWWOqLcGL4hV2in3q9CJLe3ZBkx37Ywmbjer5lOvD99nyOmCin5nLYiFUDZbMwBktzEqVhj0rN1yogt+FrT4Xbou+PLpVDLc8ddYe+31x4ZtohjtHTAL7iQhUEKE6qEVk3LiNkiy7gR33yWnl+90m5YeAKOzTBZXZh4gNrOwZkLFrFrnFOBSvo703Mh0R/tDpWlko+YFWopcRE+GO+WyHJtnPelsGhO+2sqzCrPXKhhJ/d84M7xOMXkbW+4Gp/JzbTIuGLTcqe3XZHFSv+ONi/X2cBa4e8LP03d7UD4ehiKFGoVQmhkf/wGb6OdHjVAXmR8iZd9Zg8JT6xF15fnvkqMDaKyqXYPWZUni4eI07FyRsBCB3mQJYOx4dcSk0csXosE9YOA8rWIB+YldZl811bavfJVVT4ETu35QAMk/fwNkBcu1WZYsoq5FDbFYdI0dJ9VK4bFg1Z8rI63AQv2WoaXDjt4SoPg0zoCjUU+Pv6PvpYQmvywndsPtbbxVKycai4kk/66uEfcNxHmd3m76MC5QUfNv3rb/MFjb2gvs+JbV21zqTq0sx4d5jYora/PEPVCrZS+7jm7/IJp2YT2+Q/eQWmmM+3XThPrHgZV9itN593f51wpNKdVjey1dlY1XZKr5l/f8FhPJ2dhILnMpeYJ0nnO19xxSnEbugdVWN+M0IZiWvYKtNP0zuzcmXd3htv9SbZ/AG1mrWqyaou0yfm0vRTXLBYjgHSXrW7JA7p4/6VZm91rcQTaAC0FsQClIdPGX9qI7Rix/bDMO6RaB00H8Fl9r2r5t9JH9/vXb27fwy5VOEPEN0zgj//+z2N76+kvcHqFiu5/mbA0vEYg1JpLkXjIeJMTqL1bGbXFk1xy0X8fawu94by6CBqk399GSsQXKO1goQu1b/Ypl8FYJ70FvH9hzXicCoXHBnlCaQY0Rhug5f56n6G1tPEGqPKD876iav3tnHuArf5Orn+mhT5o3r5LdcW/kpPE/I3ZiSdIT7aBFixPugVk0axrRtMPz2zLk8SgteUJz8XJ+g31DG4EofSxqj6H7NQEYepcbscnJ85sRitu+AoxH/E872VDKw01B/pbJQ8BC/nSIWPPKYXVlb1DyTYBJsu+N5IY3Wm9EDU4/8d7bTJOCP/57wsfVaEWmpaT1wHSm9Hr0esOB9bgmZyd3sLffBQWOFiUi+NUW+dzWMdVqCVwlQAl91i4Y8mJYSHvRIwjuEiFJd3ApdTXFja6IE4m44ovWyU2qnlt6lARpGKZerLGRhBaVOSNzHm8KnILudFLw7OMOxFzKTejz+qz+u47oHAR6xx2Bv04kRJQJbkWyhFj56sM+O6Wz8lGQyDNJtX+9EpmkCJP0IzgP7qAmCtYoiL2H4Er79kKNy3v3oTsGudweQqFIn736OgcnRNqaeEffs1vuLFHRzXsqoVVkP3GbzHbIqdLMwQO8XjOCWrerIC14DALH2c+SDNPrM4C9w8tkU/JaOmxmjejzM7azjqDhUAZWiOFBYQDbkNcaiONykPEJ0xgRh1+Bmsui8C97dgQKqEcoVdM+TB46yioYlN3daBz09KPTZ/32ZgjaIVhexlEoJ1ix5/VMRwd0VRxdOR9mcDlpw9NvfmnFtC+zrmEahZoOMBRWE69vVnue7wnIKkpYyXii7OWUVBU8wPQoAAv/fBKZ8fZT+9f+Z6Za+uIQxhva2rxF3S7RCD3W2F/L27b7v+XeE4K/ahz+rV8RGjEV/5hhrruuHmhaXvxNGLUSUhsu6WSvjSyLOnnQKpSh06EpU5zgD7uBuQRLym9Dqxws/e64uuXjZln64cDeth7yB1Y6jeSR+J4whvJYUzNY8YjQf0/8P53uH/rLaANw7SdHh5YwS8/VfPPKzhkvb6UqU3XZlso4ZmUbinhwPIIwsdJHGPehXprfCTozTz1y88XLHAtXfp8d9To8pi7kLbbIHFBR2tZNgj9UUsAy/JP7xni+g==\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get bookmarks in a list\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/lists/{listId}/bookmarks\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nRetrieve a paginated list of bookmarks within the specified list. For smart lists, bookmarks are computed from the list's query.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the list.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"ListId\"},\"required\":true,\"name\":\"listId\",\"in\":\"path\"},{\"schema\":{\"type\":\"string\",\"enum\":[\"asc\",\"desc\"],\"default\":\"desc\",\"description\":\"Sort order by creation date. Defaults to 'desc'.\"},\"required\":false,\"description\":\"Sort order by creation date. Defaults to 'desc'.\",\"name\":\"sortOrder\",\"in\":\"query\"},{\"schema\":{\"type\":\"number\",\"description\":\"Maximum number of items to return per page.\"},\"required\":false,\"description\":\"Maximum number of items to return per page.\",\"name\":\"limit\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"description\":\"Cursor from a previous response to fetch the next page.\",\"title\":\"Cursor\"},\"required\":false,\"description\":\"Cursor from a previous response to fetch the next page.\",\"name\":\"cursor\",\"in\":\"query\"},{\"schema\":{\"type\":\"boolean\",\"default\":false,\"description\":\"If set to true, the bookmark's full content (HTML, text, etc.) will be included in the response. Set to false for lighter responses when only metadata is needed.\"},\"required\":false,\"description\":\"If set to true, the bookmark's full content (HTML, text, etc.) will be included in the response. Set to false for lighter responses when only metadata is needed.\",\"name\":\"includeContent\",\"in\":\"query\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"A paginated list of bookmarks in the specified list.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarks\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"userId\":{\"type\":\"string\"},\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"attachedBy\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\"]}},\"required\":[\"id\",\"name\",\"attachedBy\"]}},\"content\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true},\"description\":{\"type\":\"string\",\"nullable\":true},\"imageUrl\":{\"type\":\"string\",\"nullable\":true},\"imageAssetId\":{\"type\":\"string\",\"nullable\":true},\"screenshotAssetId\":{\"type\":\"string\",\"nullable\":true},\"pdfAssetId\":{\"type\":\"string\",\"nullable\":true},\"fullPageArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"precrawledArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"videoAssetId\":{\"type\":\"string\",\"nullable\":true},\"favicon\":{\"type\":\"string\",\"nullable\":true},\"htmlContent\":{\"type\":\"string\",\"nullable\":true},\"contentAssetId\":{\"type\":\"string\",\"nullable\":true},\"crawledAt\":{\"type\":\"string\",\"nullable\":true},\"crawlStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\",\"nullable\":true},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true},\"size\":{\"type\":\"number\",\"nullable\":true},\"content\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"assetType\",\"assetId\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"unknown\"]}},\"required\":[\"type\"]}]},\"assets\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"pdf\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"avatar\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"userId\",\"tags\",\"content\",\"assets\"],\"title\":\"Bookmark\"}},\"nextCursor\":{\"type\":\"string\",\"nullable\":true,\"description\":\"Cursor for the next page, or null if no more results.\"}},\"required\":[\"bookmarks\",\"nextCursor\"],\"title\":\"PaginatedBookmarks\"}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"List not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/get-list.api.mdx",
    "content": "---\nid: get-list\ntitle: \"Get a single list\"\ndescription: \"Retrieve a single list by its ID.\"\nsidebar_label: \"Get a single list\"\nhide_title: true\nhide_table_of_contents: true\napi: eJylVtuO2zYQ/ZUB85IYWntTpEjghwLOFdvkIdjsoig2BjwWx9bEFKmQ1NqOIaAf0S/slxRDybe1GyzQJ8vkXA7ncmY2ylXkMbKzV1oN1ZziJw5RZUpTyD1XcqOG6pqiZ7onQAhs54bAcIgwXQPHAFdv+ypTEedBDe+UGAhqnKlAee05rtXwbqOmhJ78qI6FGt6Nm3GmKvRYUiQfkkDICypRDTcqritSQxWiZzs/gXJTENSWv9cErMlGnjF5cDOIRYtKsNAKy8qIFSbWZrWel8tvr1661Y9fi1WMLn8lgDkmEQF8pVWTKU/fa/ak1TD6mjJlsRQB0wpkigVAhbFQ8gBPoXI2UBDQv1xeys8pVrFJIZLegcudjWSjiGNVGc5T/AffguicCYSbfqNcclJ5yVbk1iPr02A1W8xnLo6gnUbZ1sbgVAIib28yxfk5wSYljqwE5DFWWoFTQbJ1KdVSoq3RqEyFEn2UstE0w9pENdzeNZn6XpNfP8pfVU8N5weiU+cMoRUrBYY3TuSdx+h8OC9VB/LXzvwUtVta8vJfc3Tycc+0TCed/3FzVE53kq0uN11kD+K40zoD8QDP+LhkVdOIkxeXz08L79ZiHQvn+Qdp+Oevv1NzvE4tCNEtyAIHKDlIM2fA9h4N6wycB1pVAvlBnUZaxUFlkM9X6D4+u747RLBD+uIUqbwErIswc7V96Pb/tEfu9NkUHrsfQYl5wZYuPKGWSgLy3nkQ9b6UQ0kh4PxRpoq6RPvQUKffVw8rIgHc2z/I7jtRTEFL/mPhOm5ORSMMqgZCJmGwaampkf4hf7/l0tobNVQb1NpTCM0AKx7cP5cyRc+CLUWou25zsu25IsYqDAeD6Nf9BXpcEFV9rKqzPNxZ2LLvx04eWizygoMx8EUS13o+HAa7sIpneUcSk5ZMQirrPt47X6Ig/P2PmxRLtjMn6vLqFtLz/mX/8oDYd3hGn69O8O8uOchUIzO7KFyIKXNT5xYl+gXbOaDVICm94HhhMJJPr+Oc+nBTcBDbgMa4ZYC1qyE6KNHifG8kZIn5QwYyITMoeF4YnhdygiFQ+rUappgv6ipA5d3cY1li5ByNWfe/2q/2yROQcMm8a/tBDkfGAFldObYxQFdbgMeNXokPDWxTiiajriuTkQkUhJp8H/50NeRoYU5W9gECtOllC1rDzLvyOL9LmsLtFdRWk4de7wvFyHYe4Lek85HWodfbwv6Mc7Y7yKnd95hDXVXOR8hrH5y/mKJArXYacM8Ik/ZykoI0MVxynEAaCLBfISQZMmvbiQxsc1NrksxOLK3im87EjMm0hChhAY6AoY3L1snOpGRyRjEv0r0YEWDUhxFMZOpM4B5NTTBz/tgHWy05omRY8uEJrIPS+QSwNjFsY/O6KxG4WVcU5HB7ElI2pgTOUttengikU8Lwq72AXs+wXfR66S0juL3+tKs3WHIswKU6RwO5x6UhDSVF1Bix36oLo+/UE7ODHAkVUyeSinMrY6GujENNGmZsCJ5yKUXuPHx++/5ZYsrKhVhiIulucfpA8XhjfNiFmz3bP27HbKniYBzJwBa223TMeJe2NZmbw25tG2dKGluuNhupsFtvmkaOu63ibrwnxkSfmoN8azWcoQn0E8xPrztCfwb/ha47RLtO/Gtq+acytaD1frVsxrKepF5MCNrLUZ5TFQ/UTuah8OtuQHx4d6Myhces+oBFk/WzkDabVuJGWKNpdggTiwjApvkXiRA7OA==\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get a single list\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/lists/{listId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nRetrieve a single list by its ID.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the list.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"ListId\"},\"required\":true,\"name\":\"listId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The requested list.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"description\":{\"type\":\"string\",\"nullable\":true},\"icon\":{\"type\":\"string\"},\"parentId\":{\"type\":\"string\",\"nullable\":true},\"type\":{\"type\":\"string\",\"enum\":[\"manual\",\"smart\"],\"default\":\"manual\"},\"query\":{\"type\":\"string\",\"nullable\":true},\"public\":{\"type\":\"boolean\"},\"hasCollaborators\":{\"type\":\"boolean\"},\"userRole\":{\"type\":\"string\",\"enum\":[\"owner\",\"editor\",\"viewer\",\"public\"]}},\"required\":[\"id\",\"name\",\"icon\",\"parentId\",\"public\",\"hasCollaborators\",\"userRole\"],\"title\":\"List\"}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"List not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/get-tag-bookmarks.api.mdx",
    "content": "---\nid: get-tag-bookmarks\ntitle: \"Get bookmarks with a tag\"\ndescription: \"Retrieve a paginated list of all bookmarks that have the specified tag attached.\"\nsidebar_label: \"Get bookmarks with a tag\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzVWetuG7sRfpUBz48kxlpOinPQA/0o4OTc3JP0BLGMokgEeLQ72mW8S25IrmxFENCH6BP2SYoh9yZ5Fa8vBdpfccThzDcXDoffboQuyaCTWp0lYipScjNMX2t9VaC5siISCdnYyJIlxFR8IGckrQgQSkylQkcJ5NI60EvAPIdFsxVchg4yXBG4jMCWFMulpAQcpoDOYZxRMhGRcJhaMf0oZvzvPBKW4spItxbTjxuxIDRkTiuXienH+XYeiRINFuTIWC9g44wKFNONcOuSxFRYZ6RKbwGfZQSVkl8qApmQcozFMGgG5zBlJHSDRZmzEkkyyW/WaXH9+cc/65uvP2Q3zun4R4YrnReZYXqWiG0kDH2ppKFETJ2pKBIKC153fj0Skq2X6DKxjb4Fl1RVcBjQxjV4DkZCS6xyJ6bhl32vzrVxoE1CBhZriA35TEKCjibwU9hrwWl4xvueTXbxLjG39GiVrcdWG/cHb2y8/lKRWQ+7rapi4QV3jb/DG1lUBYRlTo90VHhzhlxlFJRkuPJojCv30dZ6kctCuhEeHKizN5Wx2sDS6ILPiKGV1JUFQ7bUyhLbXpKLM193im5ca78prKBhhHsPN1W7GgdLd/u60DonVKJXj4OIzpZgybFhfxS84aYhPLOwrPIcYq0cKQfPf5u9exuBoxsXAbl48gKuJTcQAqnivEooAam8isajCZwH7d44LLWBXKaZI9OKWLjOSIFW+RoKcpigQ5AWFFHC7ebuqP7v+9Dmr7bxJqDZy+OcPa0Vcir/9PIl/7Pr7OlAFx/VwRlFHQXWimWZy9h3ipPPllUPVJFefKaYYZaGLx0nA7DWXk8UjcE1O8Tn9W4VMrl9MLeR8M2LklM3uFroxLsztBwJVeU5LvhAchls2+M5QhJNnMkVJQMHaBuJJa4032+H1h2mqVTpuUNX2bvtdReHreKYLF/ZS5R5ZYjDRCrhXfNtJGxVFGjkV5+lp1evtBsXnoBjPU5WVyYeobZ3fZZSROKaFlygOf9d6IXMyV/vjpTlwo+ElSrNaRkWjHdLFqU2zvtSWTJnwzUVppUnLtRwngcWmjnp9WC4Wp/ZzawqUIn5dqfDfWSLtf4dbV6ud4C1oj+WfqD6tgNh9TCUXKqrEEKTD8dv9DHa6VQj5GWBKV0MmT0kfGotuaE8D1VibIiUzbS7z64yWd5HnC+X9wwsdJB7WTIUG7zOKXnA5pVMSN8LKK5kPDIvmSvyN12l3SlfV+V94DSejzTA0k/fALFymTbjklUtcmkzGifNo/f7ese4ePCOd/X1NmLDXsvw0uEEz3kcfFxH4OHIx8f/MdQSQpMfd3KHoTY2HosVueZCMvmv2R3ivokIf8q7TQfuDb5q/jbY5g9ee2N7mZVf+2rbp9Whk/XgMHdB6Xx9gqhX6krp64G7y2+Yb9vQPv3FOyrNfKf91mti/cugzn6t6bz/+wKVInNW18heaxd102W5ev71DU8MdHIRBpKLMteYEN/nuEKHnOIudPeqsKEZoQvFfDso0E3TO7Nzb97dGW73J9nhAbSdterJqivSNufz7mnc8FKCAfKLtn4rj+jiw09nbXYfxxFoA7wV5BKUhkIb/3RjzmMi9sOy6NFkPTQ9wO+b11XHqG19dL9/+er2a+xChTtEfqUE/v3Pf3lsrz0DBk5fkeJXYCEtD68RSLXCXCYeMt2UDGrvVcZt8aTMUQ6/x7pCb3mvPoIW6fe3kc4wBaUdLHWl9q0+5i0Y62SwfvdfrQXGmVR0bAgTzjKQMdoAb/dv/IKs5XM3QpWfm/cV1ftvp9wD7PT3Uv0zb/Qx8/Zdpms+lZ1k+m8qTrjETzaeGdye9KvHklk1jKafnMUGk8SQtdsTLOXJ6hU3DDSSMfpI1cshNQ1HmDlX2unJiTPryRUavCIqJ1iWg2xoraHhQH+v5SFgYU96ZOw5J7B+r/co2Ta8bNk3RhbjB60X4u7m//hFmwIZ4V//PvMxlWqpeTt7HSC9mrycvOzRYC2e0/dnt/C3i9ICgqV8eZxp63wGm7hKlQKqBDi1x9Id58gkC3snY5rALJOWdTNrra8trHXFtEyBCtNOiY08L2IjJj1sBJlMM8/X2AhCf4q8kQXGV1VpoTQ6NVgU6GSMeb6efFKf1HffAYeLWedwLvjH0zwHUkmppXJM2vkaA9w97yXbaDmky9P6cHoll5ARJmQm8A9dQYwKUlLM5ROg8p5d0Tqwgzv5vaYFXJxBpZjiPTo6J+ekSi38xe/5ndb26KiBXfevGvJbZog6zLYq+cUMgUY8XiBDLdsdsJIIl2Hx0gfp0nOrl+AZKuiIfE5Gx5A11Bln9rJrq5ewlJSHvshhAekAbYhLY6RVeYj7hFO45PZ+CSvMq0C/7diQKuEckVfM+TB06x6oY9O0dOBL0/KPbZP32VgQaEXheBki4JNip5/UMRwd8UhxdOR9OYWLD2/beoNr6TLQvs4xh3oQaGnASdjOjb3d7hu85yC5JVMt4ouzkVFQ1cMD8JQAz/3kyhfH+59+eeE7ZqmtYwJhumnYxV/J9bhAjwv5FOwfxk3X/P8rn4dCg+ndZR27EPrqx2Z0mDbfXLrWOo8ENwYW2my4Qi9Mvt3yz4Em5YabSMuN4wAh3HfvAd9GBuFf0Xrve4kvRzEVnn8fD+h+Xzi+gaX56vFAHI/46nEYU/t54oGg/h+Y/G+4f4vd78Iw74aBe1bw8w/1MPMCDllvHlhq3bfZoApHzL/JwvXjAYS10zimso/01ijIyNvZ6NefZyLQJn0mfHdw6FOSu4g2myAx44tyu+0A8v8Z4Hb7HzCJvDs=\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get bookmarks with a tag\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/tags/{tagId}/bookmarks\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nRetrieve a paginated list of all bookmarks that have the specified tag attached.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the tag.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"TagId\"},\"required\":true,\"name\":\"tagId\",\"in\":\"path\"},{\"schema\":{\"type\":\"string\",\"enum\":[\"asc\",\"desc\"],\"default\":\"desc\",\"description\":\"Sort order by creation date. Defaults to 'desc'.\"},\"required\":false,\"description\":\"Sort order by creation date. Defaults to 'desc'.\",\"name\":\"sortOrder\",\"in\":\"query\"},{\"schema\":{\"type\":\"number\",\"description\":\"Maximum number of items to return per page.\"},\"required\":false,\"description\":\"Maximum number of items to return per page.\",\"name\":\"limit\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"description\":\"Cursor from a previous response to fetch the next page.\",\"title\":\"Cursor\"},\"required\":false,\"description\":\"Cursor from a previous response to fetch the next page.\",\"name\":\"cursor\",\"in\":\"query\"},{\"schema\":{\"type\":\"boolean\",\"default\":false,\"description\":\"If set to true, the bookmark's full content (HTML, text, etc.) will be included in the response. Set to false for lighter responses when only metadata is needed.\"},\"required\":false,\"description\":\"If set to true, the bookmark's full content (HTML, text, etc.) will be included in the response. Set to false for lighter responses when only metadata is needed.\",\"name\":\"includeContent\",\"in\":\"query\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"A paginated list of bookmarks that have the specified tag.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarks\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"userId\":{\"type\":\"string\"},\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"attachedBy\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\"]}},\"required\":[\"id\",\"name\",\"attachedBy\"]}},\"content\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true},\"description\":{\"type\":\"string\",\"nullable\":true},\"imageUrl\":{\"type\":\"string\",\"nullable\":true},\"imageAssetId\":{\"type\":\"string\",\"nullable\":true},\"screenshotAssetId\":{\"type\":\"string\",\"nullable\":true},\"pdfAssetId\":{\"type\":\"string\",\"nullable\":true},\"fullPageArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"precrawledArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"videoAssetId\":{\"type\":\"string\",\"nullable\":true},\"favicon\":{\"type\":\"string\",\"nullable\":true},\"htmlContent\":{\"type\":\"string\",\"nullable\":true},\"contentAssetId\":{\"type\":\"string\",\"nullable\":true},\"crawledAt\":{\"type\":\"string\",\"nullable\":true},\"crawlStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\",\"nullable\":true},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true},\"size\":{\"type\":\"number\",\"nullable\":true},\"content\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"assetType\",\"assetId\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"unknown\"]}},\"required\":[\"type\"]}]},\"assets\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"pdf\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"avatar\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"userId\",\"tags\",\"content\",\"assets\"],\"title\":\"Bookmark\"}},\"nextCursor\":{\"type\":\"string\",\"nullable\":true,\"description\":\"Cursor for the next page, or null if no more results.\"}},\"required\":[\"bookmarks\",\"nextCursor\"],\"title\":\"PaginatedBookmarks\"}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Tag not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/get-tag.api.mdx",
    "content": "---\nid: get-tag\ntitle: \"Get a single tag\"\ndescription: \"Retrieve a single tag by its ID, including the number of bookmarks using it.\"\nsidebar_label: \"Get a single tag\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVtuO2zYQ/ZUB85IYWntTpEighwJOc8E2eQgSL4pis4DH4lhiTJEKOdq1YwjoR/QL+yXFULLX63XTAu2TbM5weDhn5gy3yjcUkI13F1rlqiSeYakypSkWwTRiULn6SBwM3RAgRONKS8BYwmIDhiNcvMrAuMK22rgSuCJwbb2gAH4JC+9XNYZVhFY2guGxyhRjGVV+pWbyvc5UpKINhjcqv9qqBWGgMG25UvnVdXedqQYD1sQUYnKIRUU1qnyreNOQylXkYNxD0LOKoHXma0tgNDk2S9ODEoiMpSChNdaNlSCGjLbrTVnffnnx3K+//VitmX3xQuAaTi4zLC+06jIV6GtrAmmVc2gpUw5rsXOyZ8rI6Q1ypQR9oNh4FykK4h/Oz+XzEKiEpMikU2ZvDVfQRiwJIiObyKaIgrfwjsmxxMCmsaZI1E2+RAl0IjV+8YUKVplqghDNpodh9MP0dbt7nDK09csdkwcOPc/HDi83U2YsKtKz5PZPYNCcDFm1NboTlq67x8CVXGaAfgT0u7Cu7/E6hH12/vQhPZcOW658MN9Iw5+//5Hq52WqUmC/IgcmQm2iFLh0wg1aozPwAWjdCMgj4pjWPGksmtOU7av5rjYPEeyRPjtRSFiC8wxL37rjU/9LuRRen6iL436bQo1FZRydBUKNC0tAIfgAsn0slNYUpab/TajE/nGgYf9YHZdAAngX/4Db17Ix5Sydz5UfZE4uKS2aq4nI0WSburdTokbhZqc1bbAqV1vUOlCM3QQbM7l5qjJ1g8EIsr6Ce3NPyBJbyypXFXMT88mEw2a8woAromaMTXNSp4YIO3V6N/hDj0XwH8jkJ6GtP/lQLPdJlZPlHslN5YOTyoYfb3yoURD+8ussZdK4pZftcuse0tPx+fj8QPn2eKYfLh7g3xtNlPlAdnlW+ciJt53+i/aj0yCEnhk+s8gU0u1MQWOYVSZKbEBr/W2EjW+BPdToRAL3QyQDayLHTDQyZlCZsrKmrGQFY6T0dRoWWKzaJkITfBmwrpFNgdZuxp/dZ/foEUi6ZB703SCLU2uBnG68cRxhqCzA+13eyBkajEsUzadDS6Ygc6gINYUx/OZbKNBBSU4GKwG6dLMVbWAZfH2f31tawOUFtE5TgNHoEzEbV0b4Ke15R5s4Gu1gf8DSuD3k9ybyAebYNo0PDEUbog9nCxSozX4H3BiEeW+cpyTNrakNz+FrS2EDdyNWyJBx1A+tYbCTMDt3tOafhxBLQ7ZXQ0kLGAaMfV52h+xDCpNL4qLq3wa0ZgFGY5jC3LXWzuEGbUuw9OH+GcZp4YhSYOEjEDgPtQ8JYGs57nKzU3kQbY+yuNf9xMaCwDvq2ysQgXRKzD+7MxiNrHGr0SjdZQqXH9/v660fwz7VOVooAt5a0lATo0bGcb9d5Hy/Pck6yJIIMQ0uqTh3Pg7axnrUpGFpLMFjU0uR+wAfXr15knSy8ZGH4Tc8Ld4S33t7HTfh9k7q//fHWi8qB1Ory3pd3A4KetU/6DKV9y+g60yJAIhhu5VKvAy262Q5FZs86+4ENMmsNlF+a5Uv0Ub6zuUefxxk/wn8HbZhEd0m6bRt5Z/K1Io2+0dady0vjNSyCUBvmxYFNXyw68HQFBneT5G3r2cqU3hffI/ENkU/iWi77T1mIi5ddwdQ/gvArvsLHy8NAA==\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get a single tag\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/tags/{tagId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nRetrieve a single tag by its ID, including the number of bookmarks using it.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the tag.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"TagId\"},\"required\":true,\"name\":\"tagId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The requested tag with usage statistics.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"numBookmarks\":{\"type\":\"number\"},\"numBookmarksByAttachedType\":{\"type\":\"object\",\"properties\":{\"ai\":{\"type\":\"number\"},\"human\":{\"type\":\"number\"}}}},\"required\":[\"id\",\"name\",\"numBookmarks\",\"numBookmarksByAttachedType\"],\"title\":\"Tag\"}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Tag not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/karakeep-api.info.mdx",
    "content": "---\nid: karakeep-api\ntitle: \"Karakeep API\"\ndescription: \"Karakeep is a self-hostable bookmarking and read-it-later service. This API allows you to manage bookmarks, lists, tags, highlights, assets, and backups programmatically.\"\nsidebar_label: Introduction\nsidebar_position: 0\nhide_title: true\ncustom_edit_url: null\n---\n\nimport ApiLogo from \"@theme/ApiLogo\";\nimport Heading from \"@theme/Heading\";\nimport SchemaTabs from \"@theme/SchemaTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Export from \"@theme/ApiExplorer/Export\";\n\n<span\n  className={\"theme-doc-version-badge badge badge--secondary\"}\n  children={\"Version: 1.0.0\"}\n>\n</span>\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Karakeep API\"}\n>\n</Heading>\n\n\n\nKarakeep is a self-hostable bookmarking and read-it-later service. This API allows you to manage bookmarks, lists, tags, highlights, assets, and backups programmatically.\n\n## Authentication\n\nAll endpoints require a Bearer token passed in the `Authorization` header. You can generate an API key from the Karakeep web UI under **Settings > API Keys**.\n\n## Pagination\n\nList endpoints support cursor-based pagination via `cursor` and `limit` query parameters. The response includes a `nextCursor` field — pass it as the `cursor` parameter to fetch the next page. A `null` value for `nextCursor` indicates there are no more results.\n\n## Bookmark Types\n\nBookmarks can be one of three types:\n- **link** — A URL bookmark with optional crawled metadata.\n- **text** — A plain text note.\n- **asset** — An uploaded file (image or PDF).\n\n<div\n  style={{\"marginBottom\":\"2rem\"}}\n>\n  <Heading\n    id={\"authentication\"}\n    as={\"h2\"}\n    className={\"openapi-tabs__heading\"}\n    children={\"Authentication\"}\n  >\n  </Heading><SchemaTabs\n    className={\"openapi-tabs__security-schemes\"}\n  >\n    <TabItem\n      label={\"HTTP: Bearer Auth\"}\n      value={\"bearerAuth\"}\n    >\n      \n      \n      \n      \n      <div>\n        <table>\n          <tbody>\n            <tr>\n              <th>\n                Security Scheme Type:\n              </th><td>\n                http\n              </td>\n            </tr><tr>\n              <th>\n                HTTP Authorization Scheme:\n              </th><td>\n                bearer\n              </td>\n            </tr><tr>\n              <th>\n                Bearer format:\n              </th><td>\n                JWT\n              </td>\n            </tr>\n          </tbody>\n        </table>\n      </div>\n    </TabItem>\n  </SchemaTabs>\n</div>\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/list-backups.api.mdx",
    "content": "---\nid: list-backups\ntitle: \"Get all backups\"\ndescription: \"Retrieve a list of all backups for the authenticated user, including their status and metadata.\"\nsidebar_label: \"Get all backups\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy9VsGO2zYQ/ZUBc2kNrb0pevKhgDdtgm1SIEh2URTJAh5LY4kxRTLk0LuOIaAf0S/slxRDybJ310B76kkCZzh8nHnzhnvlPAVk7ex1pebK6MhXWG6Sj6pQFcUyaC9WNVcfiIOmLQGCuIFbAxoDq94d1i4ANwSYuCHLukSmClKkUIC2pUmVtrV46ACRkVMEtBW0xFgh41QVirGOav5JHRDcFSpSmYLmnZp/2qsVYaCwSNyo+ae77q5QgaJ3NlJU87364fJSPo9RL86BlcNKZ5ksyw703ghe7ezsS5RtexXLhlqUP955UnPlVl+oZFUoHyRnrPtDh4gnjhgC7lShNFMb/z2Ark58Igdta9UVShJ3fd6EMRKfsxXKJmNwZUjNOSTqClUGkjos+GykqL/RicGmdkVBDCvnNi2GzSuXLJ/16Et4DgPZ1EoVPdmqX4mpLCkKo9aoTQqk7rpCUQgu/EYxYk3/4S6dVPtr0oEqia4rNSbpmJLTCw/Xe3qZEfpd9zTmoZZi6Qr14+XL53y6tUJwF/Q3quDvP//KnL/KxAR2G7KgI7Q6Rm1rIf4Wja4KcAHowctBT7jH9MAzb1CfZ90xqw/YeknGIwSq66G2xI2TBq4pMwylRdRsNbZypLClEHMbpWDUXO2xqgLF2M3Q69n2pSrUFoOWjOeyDuY+A2tMhtVcNcw+zmczDrvpBgNuiPwUvX8mFjeiBH0E6T1J0tvBH3osAv2kwT/KzYeWOmnzMRFystwju6n54CTVzT+vXWhREP76+42SjGi7drJdbt1Dejm9nF6KymjOeRzxLN5fP8M/GnUEhEhmfdG4yJIdOPBJ9EwkLBBWF5ovDDKFfDtd0hRuGh0ltgiPu4+wcwnYQYsW62OQWGSBigWI+hXQ6Loxum5kJbNavrYaZdYHVwdsWxSFNWY3/Ww/2xcvYHGUXe2sLC6MAbKVd9pyhIHngI/J6uWMCrTNJVouBmblIEtoCCsKU/jDJSjRQk1WpgUB2nyzDe1gHVz7uL73tILba0i2ogCTyUdi1raO8FPe85Z2cTI5wH6PtbYj5Hci1UfMMXnvAkOZQnThYoUC1Y87YKsRlr1xmZO0NLrVvISvicIOPAZsiSlEKQbBYVgM44iksktLD/xqCLHWZPqmlrSAZsDY5+VwyBhSKrkmLptslyACjKawgKUI1xK2aBLlqfjoDG2rPBpzYKlHILAOWhcywGQ4HnJzNVAEbnaeoiweVmKuxorAWerbKxCBdEqcf7YXMJkYbTeTSb7LAm4/vBv5BveaG3CZ52igDHhv6GQM99tFlcbtWZ1AlsA6psElk/PgYyF547CiCtbaEHynWyG5C/D+59ffT2VgeBe5xaxyFnMLvyE+HcpPe3B/lMn/4+3Ry8yJHMsQFqXcD3J6Mh8KJWIgS/u9sPI2mK6T5Uw8eZwcxTQ/VQrVt1LW3w3t5GFSluQ5q65J+d3w9BUi8jgK+5tfbmTOPRbFJyKYox9eIXZ3Enu/7z1upOm7ThUDiCwCqpOB9w8jvIXe\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get all backups\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/backups\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nRetrieve a list of all backups for the authenticated user, including their status and metadata.\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"A list of all backups.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"backups\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"assetId\":{\"type\":\"string\",\"nullable\":true},\"createdAt\":{\"type\":\"string\"},\"size\":{\"type\":\"number\"},\"bookmarkCount\":{\"type\":\"number\"},\"status\":{\"type\":\"string\",\"enum\":[\"pending\",\"success\",\"failure\"]},\"errorMessage\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"userId\",\"assetId\",\"createdAt\",\"size\",\"bookmarkCount\",\"status\"]}}},\"required\":[\"backups\"]}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/list-bookmarks.api.mdx",
    "content": "---\nid: list-bookmarks\ntitle: \"Get all bookmarks\"\ndescription: \"Retrieve a paginated list of all bookmarks for the authenticated user. Supports filtering by archived/favourited status and sorting by date.\"\nsidebar_label: \"Get all bookmarks\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzVWc1uIzcSfpUCc5jEaMuexZ50CKCZ7CTezGyM2MZiMSNAVHe1mhGb7JBs2RqhgX2IfcI8SVBk/8lqe1oaH7InG+oq1lfF+iE/7pgu0HAntLpK2JRJYd0brdc5N2vLIpagjY0o6Dubsl/RGYEbBA4FXwnFHSZAKqBT4FLCslGFVBtwGQIvXYbKidjLlhbNBG7KotDGWUiFdGiEWsFyC9zEmdhgcpHyjS6NIHnruCstcJWA1cbVkgl3OGERc3xl2fQj6wDPI2YxJuUtm37csSVyg2ZWuoxNP86recQKbniODo31AjbOMOdsumNuWyCbsqXWErk6cP2dh9rHWYObsCpiBn8vhcGETVMuLR6hHDHFczLcfGERE6Tze4lmy6roJIwHITwO5YB6i7P7NgKpdbS7LGKoypy2itu4hk17lWDKS+nYNPzyGNCNNg60SQKo2KDP07D98EPQteA0vCK9V2N8PHrJ1m/Kv19IcYTbqsyXXnDf+Af+IPIyh/CZakY4zL05g640Cgo0VFk4xpVjVmu9kCIX7piN27f5tjRWG0iNzqkHGNwIXVowaAutLJLtFF2c+dJX+OBa+044ie0KI9w73VTtahwsHVdOdT4OIrpKwaIjw86UGHnDTcN7ZSEtpYRYK4fKwbc/3X54H4HDBxcBunjyHdwLapAIQsWyTDABofwSjUcTuAmre+O+gUqxyqgkGxEL9xkq0EpuIUfHE+44CAsKMcFkTNL89X1o96+28TagebSPc/K0XpC28m+Xl/Rn39nZwJRqJxRZqj0lTV4UkoaU0OriN0vqA5mil79hTFAKQ2PTiWC8XbMnyo3hWwJNNfnlJURyWHxVxHyDwmTmBr/mOhGpGP4cMVVKyZdUdLTVVVuCIyTbWXRYJFXUnwCD3x1frYRa3fjJ8WV73XCwZRyjpWNHyoUsDVKYUCWkNa8iZss850Z89rv08ssr7caFJ+DYjpPVpYlHLNsbkYVgEbvHJSWopP9zvRSS0OKDQ2UpuSNmhVpJTMMH490SOR2rvC900LoazqlwanrhRA01O/CBO8fjDJM3g+FqfSY3szLnis2rvS72kSzW6++t5uV6BawV/pL6M93zDoSvT0ORQq1DCI0cjt/oMtrrRiPkRc5XeDdk9inhmbXohvZ5KBNjg6hspt0xWkWSHiNOA+SagIUOcpQlg7Hh9xKTE5Q3IkF9FFC+EfHIfclcLt92mfZF+Torj4HTeD7SAEm/fAOku5o24zarXEphMxwnTcfr61pjXDxI40M93kYoPGoZXjpU8JyOfF/XEegA5OPj/xlqCaHJj6vcYaiNja/FyinnwmbSf7dfEPdNhPkq75SemBs0av412OafHHtje5kVn/vLttenpyrr5DB3Qel8fYGol2qt9P3A7PIK86oN7csP3lHbTDPtp14T6w+DevfrlW76vy+5Umiu6hx51NpZ3XRJrj7/+obHBjo5CweSu0JqnnjigG+447TFXeiOyrChM0IXink1KNCdpvfOzlGfe9mjN/ZPssMH0PasVZ+suiRt93zeXX8bqooRQLq11vfhEV18+Hpc02ztBTgCbYBUQaSgNOTa+OsZ8RoT9jgsyx7V10PTA3zd3KA6kq3y0f375evDG9edCjNEfMYE/vjv/zy2N56EA6fXqOimlwtLh9cIhNpwKRIPGR8KAvXoVkZt8aKQXAzfx7pEf+B54fH2EXikdE9Cl2liN1chPTnRgeyi77xFs2k4QX/wYzueJAatrS54IS42rynfuRG0I74e688hBg2NlTlX2OnFhTPbyZobvkYsJrwoDoiUW6JGwwp0K6Uw/VzLQ8BC4Ht05g35Xl83e6RmGwqy7OuaxOg+5oWoOP0/77TJOSH8579vfRYIlWpSJ68DpNeTy8llj6lp8cyurw7wtx+FBQ4WZXqeaesoOu0Vm9haYm4N8uRcuHPJiQcg70SME7jNhKW1iTjW9xa2uiTmIOeKr7pFbOSv7jYCqq4IMrHKPKVgIwjlFXkjSx6vy8JCYfTK8DznRDlLuZ18Up/UN9/ArOOhhVb040xKQJUUWihHvJKvCuD76VqQjZbmWMzq3PKLLCBDnhCh/R9dQswVrFARmY7AlfdsjdtAYO3t7z0u4e4KSkUs5NnZDToiti1873V+xq09O2tg1+VXQ35PJEaH2QYeHQLTdb7kBLVoNWAjOCzCx4UP0sLTfwvwJAp0VDhtRkfiNOwO7eyi6woLSAXKUNYUFhAOuA1xaYy0Sz5Fz8EMFtSdFrDhsgwM0Z4NoRL/VuAXpv0weNDG6tg0HQmo51v6se1RfjeWCFphKC+DCFQpdvpJncPZGU3EszPvywzufn3f5hvcC5eB9nnOJdRzrGWqJkGd+lKr7vuTp8mAKIRaxCdnI6OgrGcfvXkgfOsPXtT3rn94950n7gptHd1/p7uGAPsR3f6jyuMq3HWt8i/3NBO6Uq9/dzfq0H/7w2ceMeoe9ONuR2l8Z2RV0c+B7qOunAhL3eUJYrMfimdfWwaBrXG7/wLjk5NNmSeMT7E8+ILyjO29Y8eJ1k9423gGUf+940RAx71QPIOlebU4EcdXvFo8jal9XjgR1P8DE/+M+wfsfBeGeXdS8q+uEQtz0pdx0J7FMRZ9rQP6nVZpz20//uOWBXqizzjvn3D61B9X297au12QuKWJXlWsccFPeFbRZeFPhCDUDw==\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get all bookmarks\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/bookmarks\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nRetrieve a paginated list of all bookmarks for the authenticated user. Supports filtering by archived/favourited status and sorting by date.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"boolean\",\"description\":\"Filter by archived status.\"},\"required\":false,\"description\":\"Filter by archived status.\",\"name\":\"archived\",\"in\":\"query\"},{\"schema\":{\"type\":\"boolean\",\"description\":\"Filter by favourited status.\"},\"required\":false,\"description\":\"Filter by favourited status.\",\"name\":\"favourited\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"enum\":[\"asc\",\"desc\"],\"default\":\"desc\",\"description\":\"Sort order by creation date. Defaults to 'desc'.\"},\"required\":false,\"description\":\"Sort order by creation date. Defaults to 'desc'.\",\"name\":\"sortOrder\",\"in\":\"query\"},{\"schema\":{\"type\":\"number\",\"description\":\"Maximum number of items to return per page.\"},\"required\":false,\"description\":\"Maximum number of items to return per page.\",\"name\":\"limit\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"description\":\"Cursor from a previous response to fetch the next page.\",\"title\":\"Cursor\"},\"required\":false,\"description\":\"Cursor from a previous response to fetch the next page.\",\"name\":\"cursor\",\"in\":\"query\"},{\"schema\":{\"type\":\"boolean\",\"default\":false,\"description\":\"If set to true, the bookmark's full content (HTML, text, etc.) will be included in the response. Set to false for lighter responses when only metadata is needed.\"},\"required\":false,\"description\":\"If set to true, the bookmark's full content (HTML, text, etc.) will be included in the response. Set to false for lighter responses when only metadata is needed.\",\"name\":\"includeContent\",\"in\":\"query\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"A paginated list of bookmarks.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarks\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"userId\":{\"type\":\"string\"},\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"attachedBy\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\"]}},\"required\":[\"id\",\"name\",\"attachedBy\"]}},\"content\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true},\"description\":{\"type\":\"string\",\"nullable\":true},\"imageUrl\":{\"type\":\"string\",\"nullable\":true},\"imageAssetId\":{\"type\":\"string\",\"nullable\":true},\"screenshotAssetId\":{\"type\":\"string\",\"nullable\":true},\"pdfAssetId\":{\"type\":\"string\",\"nullable\":true},\"fullPageArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"precrawledArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"videoAssetId\":{\"type\":\"string\",\"nullable\":true},\"favicon\":{\"type\":\"string\",\"nullable\":true},\"htmlContent\":{\"type\":\"string\",\"nullable\":true},\"contentAssetId\":{\"type\":\"string\",\"nullable\":true},\"crawledAt\":{\"type\":\"string\",\"nullable\":true},\"crawlStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\",\"nullable\":true},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true},\"size\":{\"type\":\"number\",\"nullable\":true},\"content\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"assetType\",\"assetId\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"unknown\"]}},\"required\":[\"type\"]}]},\"assets\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"pdf\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"avatar\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"userId\",\"tags\",\"content\",\"assets\"],\"title\":\"Bookmark\"}},\"nextCursor\":{\"type\":\"string\",\"nullable\":true,\"description\":\"Cursor for the next page, or null if no more results.\"}},\"required\":[\"bookmarks\",\"nextCursor\"],\"title\":\"PaginatedBookmarks\"}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/list-highlights.api.mdx",
    "content": "---\nid: list-highlights\ntitle: \"Get all highlights\"\ndescription: \"Retrieve a paginated list of all highlights across all bookmarks for the authenticated user.\"\nsidebar_label: \"Get all highlights\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy1V1GP2zYM/iuE9rIFvtx12FMeBqTb2nXtsKK9wzBcA4SxmViNLKmUnLssMLAfsV+4XzJQdhznklu7FXvKQZbIj+THj7ydcp4Yo3b2RaEmyugQf9Sr0uhVGYPKVEEhZ+3lgpqoNxRZ04YAweNKW4xUgLwBtwQ0Bsr+LWDOLoR0unBuXSGvAywdQywJsI4l2ajzZKEOxGOVqYiroCa3aoBglqlAec06btXkdqcWhEw8rWOpJrezZpYpj4wVReKQLoS8pArVZKfi1pOaKFtXC+KTUH7Ge13VFbSfBb+OVAWIDphizRY8sURJY9VkiulDrZkKNVmiCfQ51jJlsaKU7EpHlSktFj7UxFvVZGciCJG1XZ1E8F3NwTEs2VVSD6aNdnUApuCdDSS+lxTzMiXc0n3s/UcdDfUWPiG8/+6qCzVvPR3HOhO/rYUg4X59dSU/x66nZ5h2YJm4yJ2NZKM8Re+NcEo7e/k+yPsz6XSL95RL3j0L+aNuvR+MDu4iM24FtlTz4zb2RJdeeli+JlMhIsdflstA8ZSgTabIFv/wNXfG8TlakK0raZstGePulGS1UJlaMZFVmVqYmqSPClpibaKa7C82mYp0H8+ZtLUxuBCSRK6pyZR1kT7poj4furT4I1nJmaS60zNAmiNq3g7ze5zNYe66qDrMCVHvf+htdmiEXnCSRyFw1xofjfixTul0ru+FDByDPAW9BOugckzSP7UREj+MsxxK8ADOAPLrfVcMxLJpxNA3V09O2+jGiuQ61r9TAX/98WdC9zSJKUS3Jgs6QKVD0HaVgbYbNLpIoOneC6wHnSYpvvQG9fkeO3DzHiufAA8RJKRNpiqKpZO5s0p18yiyri6Pwg/Em72412zURO2wKJhCaC7R68vNE5WpDbKWoqQu7D63SdhzvozRh8nlZeTteI2MayI/Ru9PZPVaxlNrQbRG8vSyuw8tFkE/mEtvJfiu/wfTqc+FeJY40jU16S5JY6Y/njmuUBD+9Ot1IoK2SyfPJeoW0pPx1fhqoNs9nunrFyf4+486AEIgs7woXYiSnX4Qa7sCtAUwYXGh44XBSJyi0zmN4brUQWzL8HZ3AbauFomv0OLqYCRkSZBDBjK3s4EqZ4AhUPq1BSwwX9c+gGe3YqwqlLFvzHb8zr6zX3wB08MuoJ2Vw6kxQLbwTtsoUyY1BuAxX734KEDbVKL5tCNXMjKHkrAgHsNvroYcLazIyp5DgDZFtqZtO86O6ntHC7h5AbUtiGE0eksxarsK8G1685K2YTTaw+4asIP8SkbTAXOovXccoZ17FwsUqL5/ARuNMG8/zlOS5mkZmEMajXDYaaQYdBi02uamLkgqOz/owhyWmkzb15IW0BEwtHnZO+lNPjasYQpzEag5bNDUlCTsyIe2RdrXkmGpB9OJknW5edpRBK63noIcPu1XQKnGgsBZatuLiUA6JUze2QsYjYy269EoxTKFmzever7BnY4luMRzNJAz3hkqoKKIBUYct89FmPrnSaBAjkCGQXclkXN/x0LtjcOCClhqQ/ClroTkjuH198++SrufdyFWmISuW2eeU3yw7j5sw91BLP/3rbmVmYEiy7wVpdx1ino0UGaZEj2Q091OiHnDpmnkuF3LRGcLHUQvHlkHh7H9u8X3LNQ1bQfLcCKfmqi0Cn86js9Yhh/H1G+tB1Czw6xJ/4BkqlWalLb21TTPyQ9DOdlKxUo/+p7/cK0yhccz48GMSNb3K6ndDmzvdu2Na9HEplF76EkjVTNrmuZvOTne2g==\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get all highlights\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/highlights\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nRetrieve a paginated list of all highlights across all bookmarks for the authenticated user.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"number\",\"description\":\"Maximum number of items to return per page.\"},\"required\":false,\"description\":\"Maximum number of items to return per page.\",\"name\":\"limit\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"description\":\"Cursor from a previous response to fetch the next page.\",\"title\":\"Cursor\"},\"required\":false,\"description\":\"Cursor from a previous response to fetch the next page.\",\"name\":\"cursor\",\"in\":\"query\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"A paginated list of highlights.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"highlights\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"bookmarkId\":{\"type\":\"string\"},\"startOffset\":{\"type\":\"number\"},\"endOffset\":{\"type\":\"number\"},\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"],\"default\":\"yellow\"},\"text\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\",\"nullable\":true},\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"}},\"required\":[\"bookmarkId\",\"startOffset\",\"endOffset\",\"text\",\"note\",\"id\",\"userId\",\"createdAt\"],\"title\":\"Highlight\"}},\"nextCursor\":{\"type\":\"string\",\"nullable\":true,\"description\":\"Cursor for the next page, or null if no more results.\"}},\"required\":[\"highlights\",\"nextCursor\"],\"title\":\"PaginatedHighlights\"}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/list-lists.api.mdx",
    "content": "---\nid: list-lists\ntitle: \"Get all lists\"\ndescription: \"Retrieve all bookmark lists for the authenticated user, including both manual and smart lists.\"\nsidebar_label: \"Get all lists\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy9Vs1uGzcQfpUBc2mFtWQXPelQQEmbwE0OQWKjKGwBGi1HWkZckiFnZW+FBfoQfcI+STvc1Y9toe2pJ63I4cw3f9/MTvlAEdl4d63VVFmT+INJnFShNKUymiB3aqo+EUdDWwK0Fpbeb2qMGxD5BCsfgSsCbLgix6ZEJg1NoliAcaVttHFrWHquoEbXoAV0GlKNkXsNY1UoxnVS0zvVm58XKlHZRMOtmt7t1JIwUpw1XKnp3bybFypSCt4lSmq6U99dXsrPU8gzaweA/sGRhmULPkKqMJKGB8NVBl02MZLjDFdwlN4xORZ1GIIVZ4x3ky9JdO5UKiuqUb64DaSmyi+/UMmqUCFKLNn0iLLhEzGMEVtVKMNUp39/bvSJTOJo3Fp1hXJY09mLJ54/vy+Ua6zFpSU15dhQVyhTnhPsChVQonF9xvwZLb3AS0FyTS257LOtCpVzLUnVtMLGspru77pCfW0otv/JXmiW1pQnokvvLaETLRWmN17kfUT2MZ2Xkix/8vYfUUu1RPmvDXv52Bp6yCeD/XnXSf19bUwkLS+MVkNuhsiexPHw6gzEEzzzQrFhAZY7QHXPbfQFNe/yxfeXVy/r/dZJA/pofvu72P/8/Y9c3q9z4wD7DTkwCWqTknFracwtWqML6Ql6DGLmWfkzPfIkWDTnC/8Yt0esQ0Z+ikA8EKg1ceWFW9aUyxylhdXEDiyTKG5JsnW3U020aqp2qHWklLoJBjPZXkkCMBophJzU4br3f19NFXNI08mEYzveYMQNURhjCC947EZ4qtcAfpVD9H6Qhx6LAD+hn8/id2/5lIQOYRDL4kcWk2LLQqoYPt76WKMg/PmXGyXxMG7l5bl43UO6Gl+OL9Ux/wc8s4/XL/AfLk0ChER2dVH5xBKdAy0L2wrDRkJ9YfjCIlPM3pmSxnBTmSS6hcr9Q4LWN8BeqBnXRyWp6MmzAGHmAiqzrqxZV3KCKVH+dRqWWG6akCBEv45Y1yj8b207vnf37tUrmB2HgvFODoWXyengjeMEQ40DPi3VIDY0GJdTtJgNdZWVLKAi1BTH8KtvoEQHa3IyxgjQZc821MIq+vppfh9oCbfX0DhNEUajz8Rs3DrBD/nNe2rTaLSH/RHXxh0gS0ueYE5NCD6yzI7k48USBWo4vICtQVj0l4scpIU1teEFZKqDgBFrYopJkkGwH2XDsCTJ7MLRI78ZVKwM2b6lJSxgGDD1cdkbOaiUTK6Iy364iRIBRmOYwUL4dAFbtA3lmf3EhnE6D+6sWPIRCZyH2scMsLGc9rF5vR//N22gJIf7k5SzsSTwjvr2ikQgnZKm9+4CRiNr3GY0yr7M4PbTh+MukSeyz3WOFsqID5Y01MSokXHcPxdOOjzP3ARyBM4zDSK5OPcyDppgPWrSsDKW4BtTS5H7CB9/fPvtOE88n7jGzHH9eFXviPOaY89tQrsjRf4PS1HPMSdMLDNMaHI3MOlhMBRKeEAOdjspyNtou06Oh/F6Nz/yaN6hCtV3UabeDbWyMZUlBc6Ea5u8tTzfgIQZD4z+7qcbVSh8yofP+C9r3+9Arj3Rvdv1EjfS712nigFE7n/VyaT7C6UyuQc=\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get all lists\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/lists\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nRetrieve all bookmark lists for the authenticated user, including both manual and smart lists.\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"All lists owned by or shared with the current user.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"lists\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"description\":{\"type\":\"string\",\"nullable\":true},\"icon\":{\"type\":\"string\"},\"parentId\":{\"type\":\"string\",\"nullable\":true},\"type\":{\"type\":\"string\",\"enum\":[\"manual\",\"smart\"],\"default\":\"manual\"},\"query\":{\"type\":\"string\",\"nullable\":true},\"public\":{\"type\":\"boolean\"},\"hasCollaborators\":{\"type\":\"boolean\"},\"userRole\":{\"type\":\"string\",\"enum\":[\"owner\",\"editor\",\"viewer\",\"public\"]}},\"required\":[\"id\",\"name\",\"icon\",\"parentId\",\"public\",\"hasCollaborators\",\"userRole\"],\"title\":\"List\"}}},\"required\":[\"lists\"]}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/list-tags.api.mdx",
    "content": "---\nid: list-tags\ntitle: \"Get all tags\"\ndescription: \"Retrieve a paginated list of all tags. Supports filtering by name, attached-by source, and sorting by name, usage count, or relevance.\"\nsidebar_label: \"Get all tags\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzNVs2OGzcMfhWCubTGrL0peppDAW/aBGlyCBIvimLXgOkZ2qNYI00kjXcdw0Afok/YJykojf+dzaKnnrw7osiP5KePXKNt2FFQ1rwtMUetfBjR3GOGJfvCqUaOMMePHJziJQNBQ3NlKHAJYg12BqQ1BJr7Pnxqm8a64GGmdGCnzBymKzBUcwYUAhUVl1fTFXjbukK+mRK8deHIsPU0Zyhsa0IG1oFjzUsyBfcxQ4mD+R1GlOMMPRetU2GF+d0ap0yO3bANFeZ34804w4Yc1RzY+Wjgi4prwnyNYdUw5uiDgMRNho6/tMpxifmMtOcMBQzm8eeVNYGUkbIoqcaXlt0KN9m3HWbIpq0FaHSTYUwKJUyXjIAveUatDph3x0/AkCr9l/CkMMOqrclghsYaxvETUbY9ulk9P9YT7orWeeue4cq09ZTF0LRa01Qz5sG1/IRrrWp1WpCxmPvGGs9enP90fS0/x0weXiCwkAoeVKgOueeFboU1gU0QN9Q0WhXxrQw+e/F1IRE7/cyF4GqcvKygEpLE2p0VOUexwoFr//3bqrxY9VSJSwdtfWPtoia38OdFPjG4WQ27po+i2ffAkLroMlHs/GSzOWrinSSTbR/FEdAnYY0zDCoIL+Tlozg1/BheJYJd4P8xkU7lLN2DmXUQKgZxJbTgqDdyFdQMjIXaOgbHvtVCh9NUQlLKAyDjlO/P1y/PiXdrqA2Vdeorl/DPX3/HyDdRsSDYBRtQHmrlvTLzDJRZklZlBMSPjYQ84WPgxzBoNKnLTNwrwSPVTazcIYLYmU2GNYfKivTPOfaaRDtx0KXm2S232tk6jTmuqSwde78ZUKMGy5eY4ZKcklIneqTjlP5W3aoQGp8PBsGt+gtytGBu+tQ0Z2NmVDF0HuK7rBjedfaQsAjuA9n/JGmnyIfiv6uCRJY8ohnmnRFm3R+vratJEP7+xyi2V5mZleuSdYL0sn/dv8Y9/XZ4hh/enuHfHSoPBJ717KqyPkh1YNpRW2adzD3HVF6pcKUpsIvZqYL7MKqUF98yVe2Dh5VtIVioyYgybZ34LIqXz6J2ZVCpeaXVvJIv5D3HX1PClIpF23honJ07qmsKqiCtV/17c29evAApF5vQ6Zp8HGoNbMrGKhM8dHQHOmZqIzFKUCa2aDLsaBWdTKBiKtn14U/bQkEG5mxkyWAgEzNb8ApmztbH/X3gKdy+hdaU7KDX+8RB1gIPv8Q773jle70t7A9JwjvI70XG95h9WkIgTZ+rKQnUZncDlopgkg4nsUiTOEwmEMcI7FcGaUZ8/nGkgDKFbkuWzk72b34CM8U6vWgpC6gA5FNdtkF2LqWTMw5FdSw7fRjCRGRnAkvSLUdhOoqhTCk94uhY+uH4TJ+62mwlFEQ4vXzciWrsxpTBGk7PyzGDvBSf35sr6PW0MoteL+YyhNuP73d8S9PRRp6ThsLRg+YSag5UUqB+ui6StLsepQnkExgbuDOJ5NzaGGgbbankUhZGhh9ULSS3Dj78+vrHvsyVxvrQTZZu8r/hsNs4Tx/gei+Q/6OVNUnRgV5vsqSm605vu1EyzlDUQv5fr4W2t05vNvI5LTiiwqXyoib7fWjBq/MtNbIIc4zb1jeudBvlc0yP1sLnXNgtfs8x3q5ye9vxfqjERT7DJCmxAunSsCi4Obx1tp6Jl910e/PbCDOk4+FwMgyi9+2GZg5TXa+TxUjEb7PBLfIohriRqf8v9k2eOw==\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Get all tags\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/tags\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nRetrieve a paginated list of all tags. Supports filtering by name, attached-by source, and sorting by name, usage count, or relevance.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\"},\"required\":false,\"name\":\"nameContains\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"enum\":[\"name\",\"usage\",\"relevance\"],\"default\":\"usage\"},\"required\":false,\"name\":\"sort\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\",\"none\"]},\"required\":false,\"name\":\"attachedBy\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\"},\"required\":false,\"name\":\"cursor\",\"in\":\"query\"},{\"schema\":{\"type\":\"number\",\"nullable\":true},\"required\":false,\"name\":\"limit\",\"in\":\"query\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"A paginated list of tags with usage counts.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"numBookmarks\":{\"type\":\"number\"},\"numBookmarksByAttachedType\":{\"type\":\"object\",\"properties\":{\"ai\":{\"type\":\"number\"},\"human\":{\"type\":\"number\"}}}},\"required\":[\"id\",\"name\",\"numBookmarks\",\"numBookmarksByAttachedType\"],\"title\":\"Tag\"}},\"nextCursor\":{\"type\":\"string\",\"nullable\":true,\"description\":\"Cursor for the next page, or null if no more results.\"}},\"required\":[\"tags\",\"nextCursor\"]}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/remove-bookmark-from-list.api.mdx",
    "content": "---\nid: remove-bookmark-from-list\ntitle: \"Remove a bookmark from a list\"\ndescription: \"Remove a bookmark from a manual list.\"\nsidebar_label: \"Remove a bookmark from a list\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzlVtuKG0cQ/ZWi/WKLWWkdHGLmISDHa9jYBGPvEsJaoNJMSdNWT3e7L1rJYiAfkS/Ml4TquUjaXTsOhLzkabU9dTl1O1V7YSw5DNLoy1LkwlFtNvTCmHWNbv3KmfqN9EFkoiRfOGlZUOTiXRIDhEUnCUtnakCoUUdUoKQPY5GJgCsv8hvBRryYZcJTEZ0MO5Hf7MWC0JGbxlCJ/GbWzDJh0WFNgZxPAr6oqEaR70XYWRK58MFJvboH56oiiFp+igSyJB3kUpIDs4RQ0YCFtlhbxVYkyVJtd6v69uPzH8z28/fVNgRTPGfAMiQRBnxZiiYTjj5F6agUeXCRMqGxZgHVCmRCMgCLoRJN9i8h7pP6j1H3dfsq8sVB6AT9jDW8NdqT5wC+O3/Gf05x/2KgMDqQDvDn73+coIVb9ND2T9m2Q5998LEoyPtlVGo3ZmjPzs/v2+7Rg/SgTQCpT+rX+WU9tFbJInXt5KNn5QcybxYfqeDWtY57PMg2rMKU9Pf1mUKNRSU1nTnCEheKgJwzDlg9hVCT97j6JlNVrFHfNdTpj0VzUqqbFuDB/uxQ3gtWFE3TpAw+vZ/Ba40xVMbJz1QO9XmRpgyCWZPm1NbSe6lXGUi9QSXLDIwD2lr2fyfPgbZhYhXKhzM8BHxo0mMEA9IH+ojni/0OzcMFX5qo70L435c6+Q+VYXYuSVFgjTSxuZjwbPjJviWjZtJn00/2hylvBNOu2/SkGp0SudhjWTryvpmglZPNU5GJDTrJqFPuus9t5ZYYVRC5qEKwPp9MgtuN1+hwTWTHaO2D9NZZ6EntdScPLRaO7WgfvOeStp6Pt8KQcPbMcSQxJrEkJLLuxyvjamSEP/96lbIs9dKwOkfdQno6Ph+fH3HlgGf69vIe/uGj9IDgSS3PKuNDqmmfWalXgLoELvaZDGcKA7kUnSxoDFeV9GwbUClz62FnIgTDCxJXByM+S/zmM+BVmUElV5WSq4pf0HtKf3UJCyzW0Xqwzqwc1jUGWSCz6Qf9QT96BJwuXiPtpPDjVCkgXVojdWBeTl0HeEoHln2UPdPOp93sJiNzqAhLcmP4zUQoUMOKNN8KBKhTZGvaHYh+SNktLeD6EqIuycFo9J5CkHrl4cek85p2fjTqYb/FldQD5EQKB8w+WmtcgCI6b9zZAhmqHTRgIxHm7cd5StJcyVqGOXyK5HZwuCW4GAT9cgOpCxVL4srONW3DT52JpSTV0ianBWQA9G1eeieDSa7kkkJRpe9shIHRGKYw11GpOWxQRYKlcac+pC65RpQMcz0cgTZQG5cARhV8n5thHV7tLHl+7F98qsaCwGhqx8sRAU+Kzz/oMxiNlNTr0SjFMoXrd2+OlrQMFZjU56igcHirqISaApYYcNyqM+8P6on/gZ+YpKkTSc3Zy2iIVhksefFLRfBY1tzkxsHbl6+eJA61xocaE313d8gXb0j1wMW5P+yEbz8+W+o4WmJN1rLfviPQm3TGeZGJfLjnhsnk16NbaZYJpgBW2u+5F6+dahp+Tu3GF+yBQhPRltLz71LkS1SevhLR43fdUngCX8LdPaLeJaZWkf8TmVjT7nCN8gX6H3o9yk4zazLR8kWKvRWYFgXZcKR6b5vzDhjW28uLNxdXFyITeEr+d8g+OXgQ2X7fSlwxuTXNADSRHWNsmr8ApAaEbg==\nsidebar_class_name: \"delete api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Remove a bookmark from a list\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"delete\"}\n  path={\"/lists/{listId}/bookmarks/{bookmarkId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nRemove a bookmark from a manual list.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the list.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"ListId\"},\"required\":true,\"name\":\"listId\",\"in\":\"path\"},{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the bookmark.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"204\":{\"description\":\"No content — the bookmark was removed from the list successfully.\"},\"400\":{\"description\":\"Bookmark is not in the list.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"List or bookmark not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/replace-asset-on-bookmark.api.mdx",
    "content": "---\nid: replace-asset-on-bookmark\ntitle: \"Replace asset on a bookmark\"\ndescription: \"Replace an existing asset on a bookmark with a different previously uploaded asset.\"\nsidebar_label: \"Replace asset on a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy9VttuGzcQ/ZUB+5IIa8kpUjTYhwLKDXATtIZjoygcARrtzmoZcUmG5FpShAX6Ef3Cfkkx3Isky0lT9PJiyNy5nLmdmZ0wlhwGafRFLlLhyCrMaOo9hZ/1c2NWFbqVSEROPnPSsqBIxVUrBqiBNtIHqZeArANGA8Ki04O1DCUg5LIoyJEOYB3dSVN7tYXaKoM55a3iWCQi4NKL9Fb0br2YJcJTVjsZtiK93YkFoSM3rUMp0ttZM0uERYcVBXI+CvispApFuhNha0mkwgcn9fIkgOuSoNbyY00gc9JBFpIcmAJCSQN8xkQbrKxiS5JkrjbbZbX+8Ox7s/n0XbkJwWTPGLgMUaQHfpGLJhGOPtbSUS7S4GpKhMaKhRZ7oURIBmMxlKJJ/iX0Qzr/FvRY8S/ixk7iCPSsFScfnpt8y8hPkV687KFpWndtEgx0rRY/DE1kNDH0zOhAOrA9tFbJLHbo5INnow/kySw+UBZEIqzjfg6SfNTtIH9VQj8Ds/acUsAeb0U6jEVzlKfbwdOsadpP3hrtWxTfnj89TcxPBrog4Y/ffu/crdH3bnLwdZaR90Wt1HbMdXl6/uTUzo3GOpTGyU+UR0scwPM4JhDMijRID5X0XuplAlLfoZJ5AsYBbSyjv5fuQJswsQrlw4ke8rdvrkMEog3/6UMR98PBvttwtQlQmFrfx/BPSp6ZnP663lOoMCulpjNHmONCEZBzxgGrx2RX5D0uv8pUWVeo7xvq9E87JQLc25/tR/AVK8YMRv+hNMzIto5B8rSlYtKTh5/s9jzSTGI+/WTXtWEjmDfdXc+KtVMiFTvMc0feNxO0cnL3RCTiDp1k0O20tJ/byhVYqyBSUYZgfTqZBLcdr9DhisiO0doHR6iz0M/Rm04eWiwc2gGhv+OKtp4PaX3IN3vmOKIY82YUEkn347VxFTLCH3+5jknmTrnac9GrvkEPaKCvYcMcVhj+xjlqA3gyPh+fHzDigH56eXES7fBRMjV4UsVZaXyIDdAXJi5FnQN3xpkMZwoDuZgLmdEYrkvp2TagUmbtYWtq5psKNS73RnwCSvrgE+DtmEApl6WSy5Jf2qon0ckCs1VtPVhnlg6rCoPMkJnjvX6vv/kGOLm8Kdqx4sepUkA6t0bqwLQTWxTwmDws+8hB6ljQ+bSb9GhkDiVhTm4Mv5oaMtSwJM3HRLwLOLIVbaFwpjruhjUt4OYCap2Tg9HoHQWmfg8/RJ03tPWjUQ/7EpdSD5DfSh8OMPvaWuMCZLXzxp0tkKHaQQPuJMK8/TiPSZorWckwh481uS3sTwcuBkFP2iB1puqcuLJzTZvwojNRSFItyXJaQAZeDDEvvZPBJFeyoJCV3ULZBAZGY5jCXNdKzeEOVU1QGHfsQ+qca0TRMNfDEWgDlXERYK2C73MzEOr11pLnx+FwitVYEK/TdhgdEfBc+fS9PoPRSEm9Go1iLFO4uXp772Qzsc9RQeZwrSiHigLmGHDcqvOWGNTjtgB+YkanTiQ2Zy+j99deIRXBI1lxkxsHly9fP46Ea40PFUau7w6O4cg8PSzvz+Nuvz7+q9u0JaWD9dgkLa/uOnK+HS47LxKRHp157aTy83AoJIIJg9V2O+7cG6eahp9jc/J5u6fnSOK59Pw7F2mBytMXMvDoqts3j+FzyLtH1Nu4BVTN/4lErGh7fKLyWfo/eu7T08yaRLT0EoNvv75oHZ1xwx9on9wLDLrVmGYZ2XAgewhmdrBmL2+uebV0R2wVrwjhcC2S+DdCbKcibqz4thMK9bKOZ4JoHfMiwuM9dm9vxXAeTMJu10pcM/M2zZCTyMSckab5E6WWyl4=\nsidebar_class_name: \"put api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Replace asset on a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"put\"}\n  path={\"/bookmarks/{bookmarkId}/assets/{assetId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nReplace an existing asset on a bookmark with a different previously uploaded asset.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the bookmark.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"},{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the asset.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"AssetId\"},\"required\":true,\"name\":\"assetId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The ID of the new asset to replace the existing one.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"assetId\":{\"type\":\"string\",\"description\":\"The ID of the new asset to use as a replacement.\"}},\"required\":[\"assetId\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"204\":{\"description\":\"No content — asset was replaced successfully.\"},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Bookmark or asset not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/search-bookmarks.api.mdx",
    "content": "---\nid: search-bookmarks\ntitle: \"Search bookmarks\"\ndescription: \"Full-text search across all bookmarks. Searches bookmark titles, content, descriptions, and notes. Results default to relevance sorting.\"\nsidebar_label: \"Search bookmarks\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzVWd1u2zgWfpUDzkXaQHHSxV75YgG3s93JTrsTNAkWi9ZAjqVji2OKVEnKiWsY2IeYJ5wnWRxSluRESZQ0A8xe2RAPzx/PHz9uhCnJopdGn2ZiLByhTfO3xiwLtEsnEpGRS60smUKMxftKqSNPNx4iJWBqjXOASsFst2sE52GRXPMNvPSKXAKp0Z60T6DD1yWAOgNtPLkRfCJXKe8gozlWyoM3YEnRCnVK4Iz1Ui9GIhEeF06MP4tW2WkiHKWVlX4txp83YkZoyU4qn4vx5+l2mogSLRbkybpA4NKcChTjjfDrkth8b6Ve3LH6IqedvV8rsmuIdCOxTYSlr5W0lImxtxUN3pkIjQWL/CoSIZk0rItt8pBapKuCbUaX1koK1qD2Djug9poYdz7fVurcWA/GZmRhbizY6PAR/Bj3Onb5QbP9YASXjuAAXXoAxsIBMzsIOzP0dDRDR1l7MHsumaNyd3zyh4pv/MpffmEhA/yrq2IWCPcV/Yg3sqgKiMtg5iA9FS5GpK+shpIslLigIWY/hVtjhZKF9E+JkH2Z7yrrjIW5NQUglJZW0lSOXV4a7Yhlz8mnOficQHNa7+SHfG04DDDv+aJqU9Mo6XFbZ8YoQi06wd6r0ekcHIX6ETIzCN7VowMH80qpXTmCVz9dfPyQAFe2BMino9dwLbmmEUidqiqjDKQOLHYWcZUL3IPwEJBKLnJPtiFxcJ2TBqPVGgrymKFHkA40UUbZkKD589vQnF8t413U5tY5TtnSmiEf5V9OTvhn39gJh4TU6CkDJZ3nHGmaChTo01zqRTCgW1NZh9oHzBPLUsk0tLTjXx0z7okhM/uVUlaytNwAvYxqNdI6pGgtrtkcztbHWcjsblpuE5FaYrsmvne1MJmcy/7lROhKKZxxOnIQbJvkHEDJXpIrynrSZ5uIOa4Mt8v71j0uFlIvzj36yj0ur+1PrkpTcjw+zFGqynIPKklnvGu6TYSrigKt/BZO6eXZ8yQxyD1Rj/UwWlPZdADbTpcupUjENc04QBX/L8xMKtaWbjxpx2GfCCf1QtE8LthglixKY32wpXJkT/tjKk5ALxyoMZt7FtB7THPK3va6q7GZzcyrArWYbvfq22eWWPPf4xboOglsNP0yD/PZwwbE1ftVUVIvowut6vff4DTaq1MD6GWBC7rsE3sf8cQ58n3n3BeJqSXSLjf+KbvKbP4Ucm4tZ6xYrCBPkmQptXitKHvG5pXMyDxJUVzJdOC55L5Q79pIe5S+jsqnqLOzfKAApn75AoiVz40ddljVTEmX0zBqHrnP6h3D/ME7PtbtbcCGWyUjUMcMnvIw+H0VgUej4J/wp68kxCI/LHP7Vd3J+F5dkWMuHib/u3iEPBQREbK83XRP3+BW86/eMn9v2xtay5z81mXbXKzuy6xnu7l1SmvrC3i90kttrnt6V9gw3TauffnGO+iYuaf91Cli3WZQn37N6bz7fYZakz2tY+RWaRd10WW6ev4NBU/0VHIRB5LLUhnMiPs5rtAjH3HruidFWN+M0Lpiuu0laKfpvdm5M+/uDbe3J9n+AbSZterJqg3S5syn7cV4BzsJVpDvs/VNeUAV7784G7t/NU4Y7eCtIOegDRTGUgOWiNtumXUgu442HYXPdnerFjDbBu/+9eTN3bvYpY49RH6jDH7/729Bt7cBUANvlqT5DlhIx8NrAlKvUMksqEw3JSt161bGZfG4VCj772NtoN9gUQZ9uxoETfmeRD43jFMuYngiQ3viuDH+ON4L+YDJrnYwX5j/xAazzJJz22Ms5fHqDYc9WskHE9KyXo6u2IFoufelGx8fe7seLdHikqgcYVn2QoQ1B762srd+rukh6sI2dBDKc3ZBfevs4JSNR1hySG8m42tZIOIcDX/eG1sga/jPf1+EYJB6bng7Wx1VejM6GZ10oJxGn8nZ6R39m0XpAMGRmh/lxnn2TnMH58s3A7WWMDuS/kghAwVsnUxpBBe5dMybwWBz7WBtKoYWCtS4aJm4JNztXQKcZAnkcpEHzIFR4JBlEQ2eYbqsSgelNQuLRYFepqjUevRFf9E//ADsLtK+vunzx4lSQDorjdSegaeQHID7UVuyjAYHuZrUIRaYXEFOmJEdwX9MBSlqWJBmdJwAdbBsSeuIcO2d7zXN4PIUKs2Q5uHhOXnGIh38Lez5mdbu8HCndp2FtcofGOVodXZVyfc+iFBYDW2WzQ5YSYSruHgVnHQV8MGrGltu0W0+jBbl2cE/fLJXbXG4grkkFbOb3QLSA7rol52QhuV9+B1M4IqL1BWsUFURQtqTIXXGZ0SBMZ+HpTvVrPbNrjABl37HH5tSFU5jRmA0xfSyRMCZ4sZf9BEcHnJjPDwMtkzg8tOH9uXhWvocTIhzVFC3swbKGsXtXJ6a7aFMBRwtvEnUJCE4dzQaqroFAvc6eBXmLy5/Zz++fx2QvdI4z9fg8WaHkMVXEZjd97qyaQvmn+qhJRakTgVv79SxAu+3n7oGTxPBFYRXNxsO5Uurtlv+HDFBrsyZdFxh7kE/u/549aludq/hgTeVXlWXtK7fWUKIirEIuPJw2X/wg8UDSncfMZ6p/NOeHR7QZfcU8Uw9vuMp4n6dmjeDZyr1/wCvP2D+Hci9dcO0nW7C42ciYm8LaRd3T9KUyu6uO8g5c2lGrn/8/UJEZKELFu9PJV3UDvW6w3uziRQX3IW3W7EzIXRlseU5/38FJqeO\nsidebar_class_name: \"get api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Search bookmarks\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"get\"}\n  path={\"/bookmarks/search\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nFull-text search across all bookmarks. Searches bookmark titles, content, descriptions, and notes. Results default to relevance sorting.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The search query string.\"},\"required\":true,\"description\":\"The search query string.\",\"name\":\"q\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"enum\":[\"asc\",\"desc\",\"relevance\"],\"default\":\"relevance\",\"description\":\"Sort order for results. Defaults to 'relevance'. Use 'asc' or 'desc' for date-based sorting.\"},\"required\":false,\"description\":\"Sort order for results. Defaults to 'relevance'. Use 'asc' or 'desc' for date-based sorting.\",\"name\":\"sortOrder\",\"in\":\"query\"},{\"schema\":{\"type\":\"number\",\"description\":\"Maximum number of items to return per page.\"},\"required\":false,\"description\":\"Maximum number of items to return per page.\",\"name\":\"limit\",\"in\":\"query\"},{\"schema\":{\"type\":\"string\",\"description\":\"Cursor from a previous response to fetch the next page.\",\"title\":\"Cursor\"},\"required\":false,\"description\":\"Cursor from a previous response to fetch the next page.\",\"name\":\"cursor\",\"in\":\"query\"},{\"schema\":{\"type\":\"boolean\",\"default\":false,\"description\":\"If set to true, the bookmark's full content (HTML, text, etc.) will be included in the response. Set to false for lighter responses when only metadata is needed.\"},\"required\":false,\"description\":\"If set to true, the bookmark's full content (HTML, text, etc.) will be included in the response. Set to false for lighter responses when only metadata is needed.\",\"name\":\"includeContent\",\"in\":\"query\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"A paginated list of bookmarks matching the search query.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarks\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"userId\":{\"type\":\"string\"},\"tags\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"attachedBy\":{\"type\":\"string\",\"enum\":[\"ai\",\"human\"]}},\"required\":[\"id\",\"name\",\"attachedBy\"]}},\"content\":{\"oneOf\":[{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"link\"]},\"url\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true},\"description\":{\"type\":\"string\",\"nullable\":true},\"imageUrl\":{\"type\":\"string\",\"nullable\":true},\"imageAssetId\":{\"type\":\"string\",\"nullable\":true},\"screenshotAssetId\":{\"type\":\"string\",\"nullable\":true},\"pdfAssetId\":{\"type\":\"string\",\"nullable\":true},\"fullPageArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"precrawledArchiveAssetId\":{\"type\":\"string\",\"nullable\":true},\"videoAssetId\":{\"type\":\"string\",\"nullable\":true},\"favicon\":{\"type\":\"string\",\"nullable\":true},\"htmlContent\":{\"type\":\"string\",\"nullable\":true},\"contentAssetId\":{\"type\":\"string\",\"nullable\":true},\"crawledAt\":{\"type\":\"string\",\"nullable\":true},\"crawlStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"url\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"text\"]},\"text\":{\"type\":\"string\"},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"text\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"asset\"]},\"assetType\":{\"type\":\"string\",\"enum\":[\"image\",\"pdf\"]},\"assetId\":{\"type\":\"string\"},\"fileName\":{\"type\":\"string\",\"nullable\":true},\"sourceUrl\":{\"type\":\"string\",\"nullable\":true},\"size\":{\"type\":\"number\",\"nullable\":true},\"content\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"type\",\"assetType\",\"assetId\"]},{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"enum\":[\"unknown\"]}},\"required\":[\"type\"]}]},\"assets\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"assetType\":{\"type\":\"string\",\"enum\":[\"linkHtmlContent\",\"screenshot\",\"pdf\",\"assetScreenshot\",\"bannerImage\",\"fullPageArchive\",\"video\",\"bookmarkAsset\",\"precrawledArchive\",\"userUploaded\",\"avatar\",\"unknown\"]},\"fileName\":{\"type\":\"string\",\"nullable\":true}},\"required\":[\"id\",\"assetType\"]}}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"userId\",\"tags\",\"content\",\"assets\"],\"title\":\"Bookmark\"}},\"nextCursor\":{\"type\":\"string\",\"nullable\":true,\"description\":\"Cursor for the next page, or null if no more results.\"}},\"required\":[\"bookmarks\",\"nextCursor\"],\"title\":\"PaginatedBookmarks\"}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/sidebar.ts",
    "content": "import type { SidebarsConfig } from \"@docusaurus/plugin-content-docs\";\n\nconst sidebar: SidebarsConfig = {\n  apisidebar: [\n    {\n      type: \"doc\",\n      id: \"api/karakeep-api\",\n    },\n    {\n      type: \"category\",\n      label: \"Bookmarks\",\n      items: [\n        {\n          type: \"doc\",\n          id: \"api/list-bookmarks\",\n          label: \"Get all bookmarks\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/create-bookmark\",\n          label: \"Create a new bookmark\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/search-bookmarks\",\n          label: \"Search bookmarks\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/check-bookmark-url\",\n          label: \"Check if a URL exists in bookmarks\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-bookmark\",\n          label: \"Get a single bookmark\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/delete-bookmark\",\n          label: \"Delete a bookmark\",\n          className: \"api-method delete\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/update-bookmark\",\n          label: \"Update a bookmark\",\n          className: \"api-method patch\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/summarize-bookmark\",\n          label: \"Summarize a bookmark\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/attach-tags-to-bookmark\",\n          label: \"Attach tags to a bookmark\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/detach-tags-from-bookmark\",\n          label: \"Detach tags from a bookmark\",\n          className: \"api-method delete\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-bookmark-lists\",\n          label: \"Get lists of a bookmark\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-bookmark-highlights\",\n          label: \"Get highlights of a bookmark\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/attach-asset-to-bookmark\",\n          label: \"Attach asset to a bookmark\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/replace-asset-on-bookmark\",\n          label: \"Replace asset on a bookmark\",\n          className: \"api-method put\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/detach-asset-from-bookmark\",\n          label: \"Detach asset from a bookmark\",\n          className: \"api-method delete\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"Lists\",\n      items: [\n        {\n          type: \"doc\",\n          id: \"api/list-lists\",\n          label: \"Get all lists\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/create-list\",\n          label: \"Create a new list\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-list\",\n          label: \"Get a single list\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/delete-list\",\n          label: \"Delete a list\",\n          className: \"api-method delete\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/update-list\",\n          label: \"Update a list\",\n          className: \"api-method patch\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-list-bookmarks\",\n          label: \"Get bookmarks in a list\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/add-bookmark-to-list\",\n          label: \"Add a bookmark to a list\",\n          className: \"api-method put\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/remove-bookmark-from-list\",\n          label: \"Remove a bookmark from a list\",\n          className: \"api-method delete\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"Tags\",\n      items: [\n        {\n          type: \"doc\",\n          id: \"api/list-tags\",\n          label: \"Get all tags\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/create-tag\",\n          label: \"Create a new tag\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-tag\",\n          label: \"Get a single tag\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/delete-tag\",\n          label: \"Delete a tag\",\n          className: \"api-method delete\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/update-tag\",\n          label: \"Update a tag\",\n          className: \"api-method patch\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-tag-bookmarks\",\n          label: \"Get bookmarks with a tag\",\n          className: \"api-method get\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"Highlights\",\n      items: [\n        {\n          type: \"doc\",\n          id: \"api/list-highlights\",\n          label: \"Get all highlights\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/create-highlight\",\n          label: \"Create a new highlight\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-highlight\",\n          label: \"Get a single highlight\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/delete-highlight\",\n          label: \"Delete a highlight\",\n          className: \"api-method delete\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/update-highlight\",\n          label: \"Update a highlight\",\n          className: \"api-method patch\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"Assets\",\n      items: [\n        {\n          type: \"doc\",\n          id: \"api/upload-asset\",\n          label: \"Upload a new asset\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-asset\",\n          label: \"Get a single asset\",\n          className: \"api-method get\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"Users\",\n      items: [\n        {\n          type: \"doc\",\n          id: \"api/get-current-user\",\n          label: \"Get current user info\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-current-user-stats\",\n          label: \"Get current user stats\",\n          className: \"api-method get\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"Admin\",\n      items: [\n        {\n          type: \"doc\",\n          id: \"api/admin-update-user\",\n          label: \"Update a user (admin)\",\n          className: \"api-method put\",\n        },\n      ],\n    },\n    {\n      type: \"category\",\n      label: \"Backups\",\n      items: [\n        {\n          type: \"doc\",\n          id: \"api/list-backups\",\n          label: \"Get all backups\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/create-backup\",\n          label: \"Trigger a new backup\",\n          className: \"api-method post\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/get-backup\",\n          label: \"Get a single backup\",\n          className: \"api-method get\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/delete-backup\",\n          label: \"Delete a backup\",\n          className: \"api-method delete\",\n        },\n        {\n          type: \"doc\",\n          id: \"api/download-backup\",\n          label: \"Download a backup\",\n          className: \"api-method get\",\n        },\n      ],\n    },\n  ],\n};\n\nexport default sidebar.apisidebar;\n"
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/summarize-bookmark.api.mdx",
    "content": "---\nid: summarize-bookmark\ntitle: \"Summarize a bookmark\"\ndescription: \"Trigger AI summarization for a bookmark. The summary is generated asynchronously and attached to the bookmark. Returns the updated bookmark record.\"\nsidebar_label: \"Summarize a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzNV9tuGzcQ/ZUB85IIa8kpUjTQQwGlbQA3AWrENorCMaDRcrTLiEtueJGtCAv0I/qF/ZJiuBdJltIaaB/6pDU5nDlzOzPeCluTw6CsuZBiKnysKnTqC72xdlWhW4lMSPK5UzXLiKm4dqooyMHsAnrh9ByW1gHCons3huuSOokNKA8FGTZEEtBvTF46a2z0egNoJGAImJckIVgIJe1p+UAhOuPTaaxlUtDfgqPcOjkWmQhYeDG9FT1sL+4y4SmPToWNmN5uxYLQkZvFUIrp7V1zl4kaHVYUyPkk4POSKhTTrQibmjgWwSlTHAeAkRj1ORIoSSaopSIHdnkIXGSCHrCqNWtSpKR+2BTV/afX39mHL9+WDyHY/DUDVyGJ9MAvpGgy4ehzVI6kmAYXKRMGKxZa7IQyoRhMjaEU7IwjX1vjybMD35yf888x7iFy9yqUBzHtMsW4c2sCmcAasK61ylN+J588qzkRJ7v4RHkQmagdF1NQLQglj2PZZCJ3xAZn4eRtZSXH8+R1JkzUGhccLw5LM0TvCZLo8lKtaR/TwlpNaNjsEteWS+Vr9wGLQpniKmCI/p/tZYJMrLgefcxz8l6wCaWjIw4TGcmv7ppMHLTQf6/e2PC08HTpf5qsjS5/gtodTKyVyMQ9Lbi6NH9XdqE0pSYJZDxXaCa8MoWmZXvhkluqqq0LyZfoyV2cqqmDfrnlutuvsoOa2quDg6Q/zvDpxAwY7pqGrb46f3ncZjcGYygtc6iEP3//I3XZm0Q+EOyKDJNhpTw7m4Eya9RKZmAd0EPNPjxqwUAPYVJrVKebb4j9jm32EYge6atjpD3lgLEBljaax6b/TffnVp4skUMIM6gwL5WhM0couXSAnLMO+Pk4EQJ5j8WTVJWxQvNYUfd+fFQmCeBO/92Oin/ihylwyX4oLU/G2vrkJRPuVEx6JvWT7Y6Vm8kwP7mEyK374RKdFlOxRSkded9MsFaT9UuRiTU6xXBT0LrrNlVLjDqIqShDqP10MgluM16hwxVRPca6PjmYOg39OHrXyUOLhZ3am4tXnMvW8v50HCLNltmPJMaEmIRE1n28ta5CRvjzr9cpvMosLT9nr1tIL8fn4/O9KTfgmV1eHOEfLpUHBE96eVZaH1Iy+xgrU6SFgbN8psKZxkAueady4p1DedYNqLW997CxkVeKCg0WOyU+A6188Bnw2pBBqYpSq6LkE/Se0q+RsMB8FWsPtbOFw6rCoHLUejP+aD6aZ8+Aw8ULQNsifDjTGsjI2ioTPHTlBnjY/zXbkKBMStF81jVrUjKHklCSG8NvNkKOZtibAE3ybEUbWDpbHeb3nhZwcwHRSHIwGl1RCMoUHr5Pb97Rxo9GPexLLJQZIL9XPuxh9rFmxoU8Om/d2QIZaj28gLVCmLeX8xSkuVaVCnP4HMltYLdTtQtgv5aAMrmOkjizc0MP4YdOxVKRbnmSwwIqALbL3mBkUMmZXFLI28WFlTAwGsMM5jx55rBGHSktogc2lJGcI0qKOR+OwFiorEsAow6+j83AidebmjwfDhtlysaCwBpq28sRAXeKn340ZzAaaWVWo1HyZQY3H94/2rZsqnPUkDu81yShooASA47b50z0w/NE+MBHzM7UiaTi7GUMxFpblCSBRyY8VxUXuXVw+ePbF4k8mbMqTLzdbZBXPT/treqPG3G7mwH/122/5ae90cj7AVPstiPo22FX5rE9PVicdxx9lwlmGBbfbrnUb5xuGj5O1cz/KOwYOvG4VJ6/pZguUXv6m8g9/9ANmxfwNcTdIZpNGgQ68l8iEyvaHC77De8/LTEkFK3ALM+pDntPj+Y1k/0wwC5/ubrmBeiQ4x9xelJ/Etd220pcM4c1zQAzcRojbJq/ACkPBf8=\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Summarize a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/bookmarks/{bookmarkId}/summarize\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nTrigger AI summarization for a bookmark. The summary is generated asynchronously and attached to the bookmark. Returns the updated bookmark record.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the bookmark.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={undefined}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The bookmark with the updated summary.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"userId\":{\"type\":\"string\"}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"userId\"]}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Bookmark not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/update-bookmark.api.mdx",
    "content": "---\nid: update-bookmark\ntitle: \"Update a bookmark\"\ndescription: \"Partially update a bookmark. Only the fields provided in the request body will be updated. Supports updating common fields (title, note, archived, favourited) as well as type-specific fields.\"\nsidebar_label: \"Update a bookmark\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzdWNuOG8cR/ZVC+0VazHJXhgMbfAhAKTaytgIvpF0EwWoBFqeLMy32dI/6Qi5FEPBH5AvzJUn1XLmkZQbRU/hCsqequi6nbrMTtiaHQVlzI8VUxFpioNfWrip0K5EJST53qmYCMRW36IJCrbfQEALCoqWdwK9GbyGUBEtFWnqonV0rSRKUSceOPkXyARZWbmGjtIYFtXLkBN7HurYu+OZEmQJyW1XWdNJeBBU0ZWBsoAzQ5aVak8xgiWsbnQokXwJ62JDW/B22NV36mnK1VHkrYyIyEbDwYvogOhO9eMyEp5xFbMX0YScWhI7cLIZSTB8e94+ZqNFhRYGcTwQ+L6lCMd0JvkRMhQ9OmeLIWXclQTTqUyRQkkxQS0UO7DI5o3ebyAQ9YVVrlqRISf20LarNxx++t0+f/1Q+hWDzH1hxNl9Me8VvpNhngn2qHEkxDS5SJgxWTLQYiDKhWJkaQynYmDYKr63csgnHKrf+DraNzXFgtzbCBk1gmrxEUxAYIsl/F9SHnS3LrQlkAl+Eda1VnpB29dHzbSc8aRcfKQ8iE7VjXAZFPvG20R5RLqzVhIZ9MCDg9HMfqwrd9lTATNQaF+xXdt8+E4yuY8J97/4/EpGJCp/ekikYPa+ur6/3mcgdMcRn4SwNotOn6JbWVRg4Q51ifQ7idoZYjKG07izSOi608iWdR80QuW055Nkcf7OS0+E8hkBP5/kOvafwZsDcHzCkD2eEr63xDdS+vb4+nRZtoTpI3K8Eb3XCDV/GzT4TVevBM2F1Jn7Zif9TsgUsCmWK9wFD9OfkC5lYcUH2Mc/Je4Y6Kh0dsZvISOZ67LNYfU4u/vriTyf+Cff8N9XE2+jys6pGpybWSmRiQwtGl+bflV0oTalLBDKeAZkJr0yhadk8cMksVXH7TLZET+7mFKYOGsYD426MsgNMjXBwEPTnET4dmF6HxybFvrt+dZxV96YpSurzf9LqX7/9M7WY16n7QrArMqA8VMqzsRkos0atZAbWAT3VbMOzFOQycVVrVKeTr/f90G7HGohO0++ONe16Lg8fsLTRfM3mllt5EiKHKsygwrxUhi4doWToADlnHTD7JBUE8h6Ls0SVsULzXFDLPzmCSVJwkP84zCI/MqNoy2hFobSyGTXyks3kkWMqrrqK6a92w1yyZ+SQW3dDVWp7YodSOvJ+f4W1ulq/EplYo1OsZTMINI+bCC0xau6IZQi1n15dBbedrNDhiqieYF2fHMhaCd0Y9ktLD40ubMtoHnzPIWxuHk+FvYP5ZrYjkXEdTEQia3/81PXsn/9+l7zK0Hg3zF8/dkAcTzhNPRiX2eakrzujIpJq1vC/C0t/MOogw2Hj6d/BRn/czQvDyWgsGHEfdv/DB0OTH+mYevnonoOWPepvyiwte4YR0uj2anI9uR6Z2cdudntzZEj/UHlA8KSXl6X1IeG9QyEvGWgkcCJcqnCpMZBLSFA5TeCuVJ5lA2ptN83YGyxUaLAYhPgMtPLBZ8CrRQalKkqtipJPknH8bSQsMF/FOm1FhcOqwqBy3qQmH8wH8803wNDiJaGpInw40xrIyNoqEzy0GQl4WCJrvqNfseaztp4lIXMoCSW5CfzDRsjRQEGGtz0CNMmyFW1h6Wx1mAsbWsD9DUQjycHFxXsKvI95+HPi+YW2/uKiU/sWC2V6ld8qH0Y6+2angzw6b93lAlnVuueAtUKYNw/nyUlzrSoV5vApktvCsHdxMHiBbAY1UCbXURJHdm7oKbxpRaT9JLUSdguokDZB9kt3SS+SI7mkkJfpOQthxWgCM5hzc57DGnUkWFp3eIcykmNESTDHwxEYC5V1ScGog+9807eNu21Nng/7rTNFY0FgDTWlyBGlndVPP5hLuLjQyqwuLpItM7h/97bHG2xUKMEmnKOG3OFGk4SKAkoMOGnYOc169tQTgY/S9tySJHB2NAZirS3yts5TBbxQFYPcOrj9y08vU3+prQ8VptbWbpn3z98BPM/C3dAj/x9eHTRFfzRmDOta0+se+sWbR6DpaAt/zASXHybZ7TgP7p3e7/k4QZ3fNAytLjVEqTz/lmK6RO3pC5598a5t1i/h97RsD9FsU0fVkf+JTKxoe/i2YM/zY1M1khYNQVufLxnHIwFHU88+6zhmeU51+CLt42hkuJ3dvfkr9832rUSVZiLhcMPzLW4abRvQp3acznZCoyliGnpEI3Tfrrrj5eSwKSezTvpjt2so7riw7ve9e1KhZc/s9/8GMwq28A==\nsidebar_class_name: \"patch api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Update a bookmark\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"patch\"}\n  path={\"/bookmarks/{bookmarkId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nPartially update a bookmark. Only the fields provided in the request body will be updated. Supports updating common fields (title, note, archived, favourited) as well as type-specific fields.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the bookmark.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"BookmarkId\"},\"required\":true,\"name\":\"bookmarkId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The fields to update. Only the fields you want to change need to be provided.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"summary\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\"},\"title\":{\"type\":\"string\",\"nullable\":true,\"maxLength\":1000},\"createdAt\":{\"type\":\"string\",\"nullable\":true},\"url\":{\"type\":\"string\",\"format\":\"uri\"},\"description\":{\"type\":\"string\",\"nullable\":true},\"author\":{\"type\":\"string\",\"nullable\":true},\"publisher\":{\"type\":\"string\",\"nullable\":true},\"datePublished\":{\"type\":\"string\",\"nullable\":true},\"dateModified\":{\"type\":\"string\",\"nullable\":true},\"text\":{\"type\":\"string\",\"nullable\":true},\"assetContent\":{\"type\":\"string\",\"nullable\":true}}}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The updated bookmark.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"},\"modifiedAt\":{\"type\":\"string\",\"nullable\":true},\"title\":{\"type\":\"string\",\"nullable\":true},\"archived\":{\"type\":\"boolean\"},\"favourited\":{\"type\":\"boolean\"},\"taggingStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"summarizationStatus\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"success\",\"failure\",\"pending\"]},\"note\":{\"type\":\"string\",\"nullable\":true},\"summary\":{\"type\":\"string\",\"nullable\":true},\"source\":{\"type\":\"string\",\"nullable\":true,\"enum\":[\"api\",\"web\",\"cli\",\"mobile\",\"extension\",\"singlefile\",\"rss\",\"import\"]},\"userId\":{\"type\":\"string\"}},\"required\":[\"id\",\"createdAt\",\"modifiedAt\",\"archived\",\"favourited\",\"taggingStatus\",\"summarizationStatus\",\"userId\"]}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Bookmark not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/update-highlight.api.mdx",
    "content": "---\nid: update-highlight\ntitle: \"Update a highlight\"\ndescription: \"Partially update a highlight. Supports changing the color or note.\"\nsidebar_label: \"Update a highlight\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytV+Fu2zYQfpUD+6c1FDsdOqzQjwFu16JZCyxoEwxDGsBn8SyxpkiVpOKohoA9xJ5wTzIcJcty7BYFVv+xQx6P3313/O6yFbYih0FZcyFFKupKYqA3Ki+0yosgEiHJZ05VbCFScYkuKNS6gc4SEIqd8RQ+1FVlXfCQFWhyZXIIBUFmtXVgHRgbaCoSETD3Ir0RwzVe3CbCU1Y7FRqR3mzFktCRm9ehEOnNbXubiAodlhTI+Wjgs4JKFOlWhKYikQofnDL5EeCrgqA26nNNoCSZoFaKHNhVRLaHLhJB91hWml0pUlLfN3m5+fT8F3v/5efiPgSbPWfoKkSTAfqFFG0iHH2ulSMp0uBqSoTBkq2KkVUiFOOpMBSC4+Ej5MMLKxuO4hj1SpGWHoLtqZ7CH0Y3EXe/1dgaNmgC20TGCQyR5D+XBJWzd0qS5NgyawKZwBdhVWmVxYzPPnm+7QSZdvmJMs5+5bg+giLPuzGTpzgnU5ec0Ya0thvB0XHEuSMyIhFLXZO4bRPBFXDqvKm1xiUzy/y18cNOfGWN7+7+6fz8NE8dO/IwmT8o4KW16xLdmt/GQ9RtInxAF/5YrTyF0b6pyyU53icjv7H7/9jkQl9hrYNId4ZtIgLdh+8g+LszkQh1OvTak/sKK5kjTsj8BJD24K3cjPk9ZHPMXR9VjzkiGu4f33Z74nmKrpKenT89Lp5rg3UorFNfSMK/f/8Tn9aLKDwQ7JoMKA+l8l6ZPAFl7lArmbCQ0X3FETwoNEY5qzSq0yW2T+8gNGMEA9Jnx0iHcFhCYWVr82NftTxZCocY5lBiVihDZ45QcokAOWcd8PEp570k7zH/LldFXaJ56Kg/Pz2qkghw73+U51d8UPRyUVIorOw0Nis4TNbaVMwGZfCz7UiSW645cne7llI7LVKxRSkded/OsFKzu6ciEXfoFOOMbPXbXZJ2D7AIofLpbBZcM12jwzVRNcWqOtmOeg+7JvS2t4cOC0cz6oYfOIm9GI164kAx38xxRDOR9kasEvHHa+tKZIS//3kVeeXieL9vPa92tTiI+151OokYPWxlVpYtmbEunKfT8+n5qC0OscwvL45iHzaVBwRPenVWWB9iBeyEgGcGNBK4NM5UONMYyEVmVEZTuCqUZ9+AjLHrgMFCiQbzvROfgFY++AR40kj2ncEngN5T/DYSlpit68pzp8wdliUGlfFsM/1oPppHj4Cp5pGhe1e8ONcayMjKKhM89DUKeKgaFd8hQZmY3sW8f+LRyQIKQkluCn/ZGjI0kJPhAYwATYxsTQ2snC0Pa2NDS7i+gNpIcjCZfKAQlMk9/BrPvKXGTyY72JeYKzNAfqd8GGH23YgGWe28dWdLZKjVcALuFMKi21xEkhZalSos4HNNroH9FMbJINi1aFAm07UkzuzC0H142buIo0pUV6YFVAD0HS+7SwaXnMkVhayI++yEgdEU5rDgtrSAO9Q1wcq6wzuUkZwjio45H47AWCitiwBrHfyOmxd9icBVU5Hnxd2Kj9lYElhD3dN0RMCvzKcfzRlMJlqZ9WQSY5nD9ft3Q73BRoUCbKxz1JA53GiSUFJAiQGn3XFuD8Px2CaAl7qpuDOJxbmzMVBX2qIkCSulCR6rkovcOrj87fWTqLiV9aHEKPb9xHl9NJU/fIbbfdv4QdN8J0Sj7sfjAWvptlfgm/0g7EUi0vFYfJsIFgE22m65Gq+dbltejgXH0/9egKNMS+X5txTpCrWnb4T3+H3fRJ7A13D2i2iaqPM8WaVCJGJNzYP5veX5tXu8EUZn8bK77IzLaeThqB23ye7EPMuoCt+0vR31ssv51cs3LOf9/wllbNbCYZwLcdPB7Wovdom4thUaTV7Hbiw6pyz+eNg7HvSKGNZJQrbbzuKK9a1tB36i3jEzbfsf6cH7pg==\nsidebar_class_name: \"patch api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Update a highlight\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"patch\"}\n  path={\"/highlights/{highlightId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nPartially update a highlight. Supports changing the color or note.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the highlight.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"HighlightId\"},\"required\":true,\"name\":\"highlightId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The fields to update. Only the fields you want to change need to be provided.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"]},\"note\":{\"type\":\"string\",\"nullable\":true}}}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The updated highlight.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"bookmarkId\":{\"type\":\"string\"},\"startOffset\":{\"type\":\"number\"},\"endOffset\":{\"type\":\"number\"},\"color\":{\"type\":\"string\",\"enum\":[\"yellow\",\"red\",\"green\",\"blue\"],\"default\":\"yellow\"},\"text\":{\"type\":\"string\",\"nullable\":true},\"note\":{\"type\":\"string\",\"nullable\":true},\"id\":{\"type\":\"string\"},\"userId\":{\"type\":\"string\"},\"createdAt\":{\"type\":\"string\"}},\"required\":[\"bookmarkId\",\"startOffset\",\"endOffset\",\"text\",\"note\",\"id\",\"userId\",\"createdAt\"],\"title\":\"Highlight\"}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Highlight not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/update-list.api.mdx",
    "content": "---\nid: update-list\ntitle: \"Update a list\"\ndescription: \"Partially update a list. Only the fields provided in the request body will be updated.\"\nsidebar_label: \"Update a list\"\nhide_title: true\nhide_table_of_contents: true\napi: eJy1V+9u2zYQf5UD+6U1FNsZWqzwhwFu1mJZCyxIEwxDasBn8SyxpkiVpGK7hoA9xJ5wTzIcJct27KQb0PVDapHH+/vj744bYUtyGJQ1l1KMRFVKDPRB+SASIcmnTpW8KUbiCl1QqPUaGiFA0MqHPvxm9BpCTjBXpKWH0tl7JUmCMnHZ0ZeKfICZlWtYKq1hRq0O2ReJCJh5MboTbNWLSSI8pZVTYS1GdxsxI3TkxlXIxehuUk8SUaLDggI5HwV8mlOBYrQRYV2SGAkfnDLZkf83OUFl1JeKQEkyQc0VObDz6GOMRCSCVliUmrUoUlKv1lmx/Pz6R7v6+ipfhWDT1+ywClGEHb6Uok4Eh6gcSTEKrqJEGCxYQDcCiVDsQIkhFxxAm5A3Vq7Z7WM320QG26bpOMdrW8ESTWCZNEeTERgiyZ8z6irAEaXWBDKBDWFZapXGYg8+e7Z2Int29plSrn7pGBpBkefdJqLjHBfKfCCTcXXOE1HgqvsaDusHJTg+bSqtccbJbPK2p214oO1V1KbSU2rqCAkynOpv2qgT8aUit/5WLKy0mmmV7gnOrNWERtTxH9fRl9b4JkE/DIeni9kivYPYdyqIOhFsnTxSp/9aie+W60bgWJBMVfCdL9BUqEUifIEu8OWXNMdKBzHa7j1VsSN7j9csETn6C8vy1mGwzp+Wqjy5a6uf9NouDTn+lipY/nGvaBlXWvuT+oAU7rhabW3azO7lsTt1wsU9fyaHxCMaDL4cnh/D7tZgFXLr1FeS8Peff0XmeBOJFIJdkAHloVDeK5MloMw9aiUTsA5oVbLLD3AaaBUGpUZ1GqG7/HTsue9B5+nLY085EjA2wNxW5nvyVWrlyRIemh9DgWmuDJ05QslIAnLOOuDjfYZDQd5j9q9U5VWB5qGi9nxfPEREdHCnf6+6b/mgaDmmoJBb2XSPNI+wYX4SA6YTP9g0LabmG0TuftsTK6fFSGxQSkfe1wMs1eD+nIGKTrF3MUftdlOV7a3LQyj9aDAIbt1foMMFUdnHsjzZT1sN2y76vpWHxheOYa+df+TSNZb3m3qXWLbMcUQxvpRRSCTtj3fWFcge/vr7TcwmQ+J610rfbsG3bVaPVapbbihu971jtt1aSz17Qi3DbGnSzC2b5NQ36s/7w/5wb0rokjK+ujzypdtUHhA86flZbn2IAJpZuyjQLZTJAI0ERtaZCmcaA7mYYpVSH25y5Vk3oNZ22YwGwUKBBrOdEp/EBuQT4HErgVxluVZZzivoPcX/jYQZpouqjENc5rAoMKiUh77+J/PJPHsGXDMenppryYtjrYGMLK0ywUMLccBDvinZRjcRTsctOUQlU8gJJbk+/GErSNFARoaHUgI0MbIFrWHubHEIsiXN4PYSKiPJQa/3kUJQJvPwUzzznta+19u6fYWZMp3LkXV2PvuqLK0LkFbOW3c2Q3a17E7AvUKYNpvTmKSpVoUKU4jggN08ysXgebcZC0CZVFeSuLJTQ6tw0aqIM1zkZU4LqADom7xsjXQquZJzCmke91kJO0Z9GMOUm98U7lFXBHPrDm0oI7lGFBVzPRyBsVBYFx2sdPDb3LxpIQI365I8L25XfKzGjMAaau64IwK+rn70yZxBr6eVWfR6MZYx3F5/6PAGSxVysBHnqCF1uNQkoaCAEgP2m+PcWLrjscEAL3FHoFYkgnMrY6AqtUV+XMyVJniuCga5dXD187sXkbBL60OBZo8GbvefKw9v4GbXcP7HF05DcHttlAcN5uhNy+d38a3A/X7UPhomiWAm4K3NhiF563Rd83JLSXeTHZ1H0pfK828pRnPUnp4I9Pl124hewGPetYto1rFr6Iq/RCIWtN49bOoJj1Xx8kYPms2Lxs4Zw2nv8FE3r5PtiXGaUhmelJ3stcKr8c3FL9wX2gdUEXu9cLgUSfwbPW2wF9tNXNsIjSarYjMXjVLuInjYhB40nRjWyVxsNo3EDfNbXXepiXzHmanrfwD1Vlbw\nsidebar_class_name: \"patch api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Update a list\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"patch\"}\n  path={\"/lists/{listId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nPartially update a list. Only the fields provided in the request body will be updated.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the list.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"ListId\"},\"required\":true,\"name\":\"listId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The fields to update. Only the fields you want to change need to be provided.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\",\"minLength\":1,\"maxLength\":100},\"description\":{\"type\":\"string\",\"nullable\":true,\"minLength\":0,\"maxLength\":500},\"icon\":{\"type\":\"string\"},\"parentId\":{\"type\":\"string\",\"nullable\":true},\"query\":{\"type\":\"string\",\"minLength\":1},\"public\":{\"type\":\"boolean\"}}}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The updated list.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"description\":{\"type\":\"string\",\"nullable\":true},\"icon\":{\"type\":\"string\"},\"parentId\":{\"type\":\"string\",\"nullable\":true},\"type\":{\"type\":\"string\",\"enum\":[\"manual\",\"smart\"],\"default\":\"manual\"},\"query\":{\"type\":\"string\",\"nullable\":true},\"public\":{\"type\":\"boolean\"},\"hasCollaborators\":{\"type\":\"boolean\"},\"userRole\":{\"type\":\"string\",\"enum\":[\"owner\",\"editor\",\"viewer\",\"public\"]}},\"required\":[\"id\",\"name\",\"icon\",\"parentId\",\"public\",\"hasCollaborators\",\"userRole\"],\"title\":\"List\"}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"List not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/update-tag.api.mdx",
    "content": "---\nid: update-tag\ntitle: \"Update a tag\"\ndescription: \"Rename a tag. The new name will be normalized and trimmed.\"\nsidebar_label: \"Update a tag\"\nhide_title: true\nhide_table_of_contents: true\napi: eJytVttuGzcQ/ZUB85IIa8kpUiTYhwJKmqBu8mA4MorCEaDRcrTLiEsyJNeSIizQj+gX9kuK4a4ulpWgQOoHSyKHwzNnZs5wK6wjj1FZcyVFLhonMdIES5EJSaHwyvGeyMUNGawJECKWQ5hUBIZWkNZWSmuYExjra9TqK0lAIyF6VdckhyITEcsg8jsx4c9pJgIVjVdxI/K7rZgTevLjJlYiv5u200w49FhTJB+SQSgqqlHkWxE3jkQuQvTKPIbIoBqjvjQESpKJaqHIg11ArCjBFpmgNdZOsxNFSur1pqxXn1+9tOuvP1frGG3xiuGqmEwmWF5J0WbC05dGeZIij76hTHDYIuewrqTIhOLbHcZKMHo2phBfW7lhzI8xMnERy0QeYyqsiWQiG6NzWhUpH6PPgU+cCd/OP1MRRSac5+xFRYF3O1CnJLXpj1EFZ03oTH+6vDwPrcu/3LH1PyFT8gyu7JuAj/m+48O96bSL5MXl88fgbw02sbI+Vd8/f/2dcv46VRZEuyQDKkCtQlCmzECZe9RKZmA90NrxVSfhRlrHkdOozge6r8BDPR0jEDukL87QzJm3ERa2Mae3/gjJhZVn2DztkTHUWFTK0IUnlDjXBOS99cDHh5yVmkLA8j+5qpoazamj/vzwUSITwIP/6aHP3vJB0RdqTbGysmuoouIwubFyMWIRGW1Tz7WCNcTf7xSi8VrkYotSegqhHaFTo/vnIhP36BVjSwz1211KFtjoKHJRxehCPhpFvxku0eOSyA3RubPq0nvYacr73h46LBzBkbh95MR1Nx9L3J5WvpnjSGYi741E1n95x2rKCH//Y5K45IK4OWjL213l7Tr/qLOUWVjeYII69M+Hl8PLI2nbQx9fXz0Kdb+pAiAE0ouLyoaYkjy3dlmjXypTJpXn7F+oeKExkk9EqIJ4QKjAvgG1tqsAG9tAtFCjwfLgJGSgVYghY8EJGVSqrLQqK17BECh9GglzLJaNC+C8LT3WNUZVoNab4SfzyTx5AswsC37XOrw41hrISGeViQH6MgR8KAmO75CgTMrmbNz3b3Iyg4pQkh/Cn7aBAg2UZHhUEqBJkS1pAwtv64elsKI53F5BYyR5GAw+UozKlAF+SWfe0yYMBjvY11gqs4f8QYV4hDk0zlkfoWh8sP5ijgzV7U/AvUKYdZuzRNJMq1rFGXxpyG/gMEO7ab3Tf1Cm0I0kzuzM0Dq+6V0sFOlOOpkWUBEwdLzsLtm75EwuKBZV2mcnDIyGMIaZabSewT3qhmBh/cM7lJGcI0qOOR+eXw1QW58ANjqGHTev+xKBycZR4MXdSkjZmBNYQ10neiLgpgr5J3MBg4FWZjkYpFjGcHvzYV9vsFKxApvqHDUUHleaJNQUUWLEYXectX9/PM0A4CVWbepNUnHubAw0TluUJGGhNMFTVXORWw/Xv757lkTV2RBrNEfNepsmbfeeOm3A7WEm/OC7q1Oao2HWZp1YbntZveveZpnIu8fMNBPc6ryx3XLN3XrdtrycyopfaAdVTdorVeDvUuQL1IG+E8rTm34aPINvYesX0WySeOuGf4lMLGmzf2+10zYTXXMmAN3em+6aCy6Xo7OPJmqb7U6Mi4Jc/K7t9GgcXY8nb35jde7fdXWat8LjSmTpfwLa1VYS/bS2FRpN2aSBKjqnrOX4cBScSH8K6ywV221nMWH9atsDM/ybmWnbfwG89Bx+\nsidebar_class_name: \"patch api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Update a tag\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"patch\"}\n  path={\"/tags/{tagId}\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nRename a tag. The new name will be normalized and trimmed.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={[{\"schema\":{\"type\":\"string\",\"description\":\"The unique identifier of the tag.\",\"example\":\"ieidlxygmwj87oxz5hxttoc8\",\"title\":\"TagId\"},\"required\":true,\"name\":\"tagId\",\"in\":\"path\"}]}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The new tag name.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"name\":{\"type\":\"string\"}}}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The updated tag.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"}},\"required\":[\"id\",\"name\"]}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}},\"404\":{\"description\":\"Tag not found.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"code\":{\"type\":\"string\",\"description\":\"A machine-readable error code.\"},\"message\":{\"type\":\"string\",\"description\":\"A human-readable error message.\"}},\"required\":[\"code\",\"message\"],\"title\":\"Error\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_docs/version-v0.31.0/api/upload-asset.api.mdx",
    "content": "---\nid: upload-asset\ntitle: \"Upload a new asset\"\ndescription: \"Upload a binary file as a new asset. The uploaded asset can then be attached to a bookmark via the POST /bookmarks/{bookmarkId}/assets endpoint.\"\nsidebar_label: \"Upload a new asset\"\nhide_title: true\nhide_table_of_contents: true\napi: eJzNVttuGzcQ/ZUB89IKK8kp+rQPBZS0AdwkrRHbKIrYgGZ3R1pGXJIhubY3wgL9iH5hv6QYcrW6WAX82DeCHB6eufDMbIWx5DBIoy8rkYvWKoPVwnsKIhMV+dJJy6ciF7fxDBAKqdF1sJKKAD0gaHoE5DszuKkJEghVaQ9K1BBq0lAQYAhY1lRBMAxkzKZBt4EHiWwCV79f38B8t+3n293ysurnEc0D6coaqcNMZCLg2ov8s4iEvbjPhKeydTJ0Iv+8FQWhI7doQy3yz/f9fSYcfW3Jhzem6kS+PXGQuUenghl8YPeaVgVp0YX5yrhmWmFAfro0OpAOjHLGgrd9WVMTV6GzJHJhii9Uclyt46gHSZ5P+cloJQMvxLuBQrGPpOj7xF06qtjheOe+79O+t0b7BPbDxcV5x1IuHtHv0+PbsiTvV61S3Qw+UWid9tBQQHYBsDBtiGk5TuiJ92itkmUsofkXz++93PWIx4U3GvrgpF4/q71YVlp+bQlkRTrIlSTHdORap2o6x7Mfid5E9Je88vHy4y/AZmBWx6Ac84jp5bdDMN02BbmzYGx5FgekhqIL5CMg7/yGzQsZGifXUqNKQBqb/6R6UjS7aB9HZfDngMV9Ntbi7YCYFCHV248Xr5+X2K3GNtTGyW9UwT9//R35vIn/D4LZkAbpoZHeS73OQOoHVLLKwDigJ8sET6oq0FOYW4XyfD2N4aEnbGyiesAgMu0z0VCoDeuaNT5WH7IUiEFK2HVyD+R8VIvWKZGLLVaVI+/7OVo5f3gtMvGATmKhhpJNxykCK2xVELmoQ7A+n8+D62YbdLghsjO09mz+BoRd0t4P9pC4MPUDHbtmz9PLh2o2BoJfZj+imcgHI5ENi3fGNcgMf/3jJtaD1CvD19nrROn17GJ2IfYpH/ksri6f8R8PJeu+J7Wa1sYHjs6o5lKvAXUFjrCayjBVGMhF72RJ3CGkZ2xApcyjh860/H8b1Ljeg/gMlPTBZ8Aan0Et17WS65p3Uvay+EiB5aa1Hqwza4dNg0GWyHp2p+/0q1fA4WK9SALFmwulxh7iYfgfgMfFavmNin8pp2i5GCorgiyhJqzIzeBP08butibNTZQAdfRsQx2snGmO8/tIBdxeQqsrcjCZXFMIUq89/BTvvKfOTyY72lfIX3xH+YP04YCzb601LkDZOm/ctECmascbsZsu0+EyBmmpZCPDEr625Dqw6LChQM6ndr3rHyB1qdqKOLNLTU/h7QCxkqTSp+awgAzcFmNcdo+MkJzJFYWyjucMwsRoBgtY6lapJTygaglWxh2/IXXFOaIIzPlwBNpAY1wk2Krgd7F5s5saWL48b+52fMxGQWD0oImOKIq5z+/0FCYTJfVmMom+LOD204f9CPIoQw0m1jkqKB0+KqrGZjhL11mVxutRnYC3QJtAg0kszp2NPlH972TDRW4cXP387vuo/axMDUaV07EF7Oescaw6/YbbvVL+L6eyJE0HEt5nSV23gwIPvShObKwfvLPdciHfOtX3vB1rlce2vf7GIS4T6fdFyd5QJ3LxNgVjOrSzWGEiPzuW9dnu0qIsyYYD82dzDMvw2EA4Bqyqw+DYmIrvMHDEzfbLRBKPZfpEliP54Qh1d8Biu00WNyxDfS+ygW6UJdHzyPcva0od9w==\nsidebar_class_name: \"post api-method\"\ninfo_path: api/karakeep-api\ncustom_edit_url: null\n---\n\nimport MethodEndpoint from \"@theme/ApiExplorer/MethodEndpoint\";\nimport ParamsDetails from \"@theme/ParamsDetails\";\nimport RequestSchema from \"@theme/RequestSchema\";\nimport StatusCodes from \"@theme/StatusCodes\";\nimport OperationTabs from \"@theme/OperationTabs\";\nimport TabItem from \"@theme/TabItem\";\nimport Heading from \"@theme/Heading\";\n\n<Heading\n  as={\"h1\"}\n  className={\"openapi__heading\"}\n  children={\"Upload a new asset\"}\n>\n</Heading>\n\n<MethodEndpoint\n  method={\"post\"}\n  path={\"/assets\"}\n  context={\"endpoint\"}\n>\n  \n</MethodEndpoint>\n\n\n\nUpload a binary file as a new asset. The uploaded asset can then be attached to a bookmark via the POST /bookmarks/\\{bookmarkId\\}/assets endpoint.\n\n<Heading\n  id={\"request\"}\n  as={\"h2\"}\n  className={\"openapi-tabs__heading\"}\n  children={\"Request\"}\n>\n</Heading>\n\n<ParamsDetails\n  parameters={undefined}\n>\n  \n</ParamsDetails>\n\n<RequestSchema\n  title={\"Body\"}\n  body={{\"description\":\"The file to upload as multipart/form-data.\",\"content\":{\"multipart/form-data\":{\"schema\":{\"type\":\"object\",\"properties\":{\"file\":{\"title\":\"File to be uploaded\"}},\"required\":[\"file\"]}}}}}\n>\n  \n</RequestSchema>\n\n<StatusCodes\n  id={undefined}\n  label={undefined}\n  responses={{\"200\":{\"description\":\"The asset was uploaded successfully. Returns metadata about the uploaded asset.\",\"content\":{\"application/json\":{\"schema\":{\"type\":\"object\",\"properties\":{\"assetId\":{\"type\":\"string\",\"description\":\"The unique identifier assigned to the uploaded asset.\"},\"contentType\":{\"type\":\"string\",\"description\":\"The MIME type of the uploaded file.\"},\"size\":{\"type\":\"number\",\"description\":\"The size of the uploaded file in bytes.\"},\"fileName\":{\"type\":\"string\",\"description\":\"The original file name of the uploaded file.\"}},\"required\":[\"assetId\",\"contentType\",\"size\",\"fileName\"],\"title\":\"UploadedAsset\"}}}},\"401\":{\"description\":\"Unauthorized — the Bearer token is missing, invalid, or expired.\",\"content\":{\"text/plain\":{\"schema\":{\"type\":\"string\",\"example\":\"Unauthorized\"}}}}}}\n>\n  \n</StatusCodes>\n\n\n      "
  },
  {
    "path": "docs/versioned_sidebars/version-v0.28.0-sidebars.json",
    "content": "{\n  \"sidebar\": [\n    {\n      \"type\": \"autogenerated\",\n      \"dirName\": \".\"\n    },\n    {\n      \"API\": [\n        {\n          \"type\": \"doc\",\n          \"id\": \"api/karakeep-api\"\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"Bookmarks\",\n          \"items\": [\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-all-bookmarks\",\n              \"label\": \"Get all bookmarks\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/create-a-new-bookmark\",\n              \"label\": \"Create a new bookmark\",\n              \"className\": \"api-method post\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/search-bookmarks\",\n              \"label\": \"Search bookmarks\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-a-single-bookmark\",\n              \"label\": \"Get a single bookmark\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/delete-a-bookmark\",\n              \"label\": \"Delete a bookmark\",\n              \"className\": \"api-method delete\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/update-a-bookmark\",\n              \"label\": \"Update a bookmark\",\n              \"className\": \"api-method patch\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/summarize-a-bookmark\",\n              \"label\": \"Summarize a bookmark\",\n              \"className\": \"api-method post\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/attach-tags-to-a-bookmark\",\n              \"label\": \"Attach tags to a bookmark\",\n              \"className\": \"api-method post\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/detach-tags-from-a-bookmark\",\n              \"label\": \"Detach tags from a bookmark\",\n              \"className\": \"api-method delete\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-lists-of-a-bookmark\",\n              \"label\": \"Get lists of a bookmark\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-highlights-of-a-bookmark\",\n              \"label\": \"Get highlights of a bookmark\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/attach-asset\",\n              \"label\": \"Attach asset\",\n              \"className\": \"api-method post\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/replace-asset\",\n              \"label\": \"Replace asset\",\n              \"className\": \"api-method put\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/detach-asset\",\n              \"label\": \"Detach asset\",\n              \"className\": \"api-method delete\"\n            }\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"Lists\",\n          \"items\": [\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-all-lists\",\n              \"label\": \"Get all lists\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/create-a-new-list\",\n              \"label\": \"Create a new list\",\n              \"className\": \"api-method post\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-a-single-list\",\n              \"label\": \"Get a single list\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/delete-a-list\",\n              \"label\": \"Delete a list\",\n              \"className\": \"api-method delete\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/update-a-list\",\n              \"label\": \"Update a list\",\n              \"className\": \"api-method patch\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-bookmarks-in-the-list\",\n              \"label\": \"Get bookmarks in the list\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/add-a-bookmark-to-a-list\",\n              \"label\": \"Add a bookmark to a list\",\n              \"className\": \"api-method put\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/remove-a-bookmark-from-a-list\",\n              \"label\": \"Remove a bookmark from a list\",\n              \"className\": \"api-method delete\"\n            }\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"Tags\",\n          \"items\": [\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-all-tags\",\n              \"label\": \"Get all tags\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/create-a-new-tag\",\n              \"label\": \"Create a new tag\",\n              \"className\": \"api-method post\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-a-single-tag\",\n              \"label\": \"Get a single tag\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/delete-a-tag\",\n              \"label\": \"Delete a tag\",\n              \"className\": \"api-method delete\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/update-a-tag\",\n              \"label\": \"Update a tag\",\n              \"className\": \"api-method patch\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-bookmarks-with-the-tag\",\n              \"label\": \"Get bookmarks with the tag\",\n              \"className\": \"api-method get\"\n            }\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"Highlights\",\n          \"items\": [\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-all-highlights\",\n              \"label\": \"Get all highlights\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/create-a-new-highlight\",\n              \"label\": \"Create a new highlight\",\n              \"className\": \"api-method post\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-a-single-highlight\",\n              \"label\": \"Get a single highlight\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/delete-a-highlight\",\n              \"label\": \"Delete a highlight\",\n              \"className\": \"api-method delete\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/update-a-highlight\",\n              \"label\": \"Update a highlight\",\n              \"className\": \"api-method patch\"\n            }\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"Users\",\n          \"items\": [\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-current-user-info\",\n              \"label\": \"Get current user info\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-current-user-stats\",\n              \"label\": \"Get current user stats\",\n              \"className\": \"api-method get\"\n            }\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"Assets\",\n          \"items\": [\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/upload-a-new-asset\",\n              \"label\": \"Upload a new asset\",\n              \"className\": \"api-method post\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-a-single-asset\",\n              \"label\": \"Get a single asset\",\n              \"className\": \"api-method get\"\n            }\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"Admin\",\n          \"items\": [\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/update-user\",\n              \"label\": \"Update user\",\n              \"className\": \"api-method put\"\n            }\n          ]\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "docs/versioned_sidebars/version-v0.29.0-sidebars.json",
    "content": "{\n  \"sidebar\": [\n    {\n      \"type\": \"autogenerated\",\n      \"dirName\": \".\"\n    },\n    {\n      \"API\": [\n        {\n          \"type\": \"doc\",\n          \"id\": \"api/karakeep-api\"\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"Bookmarks\",\n          \"items\": [\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-all-bookmarks\",\n              \"label\": \"Get all bookmarks\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/create-a-new-bookmark\",\n              \"label\": \"Create a new bookmark\",\n              \"className\": \"api-method post\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/search-bookmarks\",\n              \"label\": \"Search bookmarks\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-a-single-bookmark\",\n              \"label\": \"Get a single bookmark\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/delete-a-bookmark\",\n              \"label\": \"Delete a bookmark\",\n              \"className\": \"api-method delete\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/update-a-bookmark\",\n              \"label\": \"Update a bookmark\",\n              \"className\": \"api-method patch\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/summarize-a-bookmark\",\n              \"label\": \"Summarize a bookmark\",\n              \"className\": \"api-method post\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/attach-tags-to-a-bookmark\",\n              \"label\": \"Attach tags to a bookmark\",\n              \"className\": \"api-method post\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/detach-tags-from-a-bookmark\",\n              \"label\": \"Detach tags from a bookmark\",\n              \"className\": \"api-method delete\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-lists-of-a-bookmark\",\n              \"label\": \"Get lists of a bookmark\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-highlights-of-a-bookmark\",\n              \"label\": \"Get highlights of a bookmark\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/attach-asset\",\n              \"label\": \"Attach asset\",\n              \"className\": \"api-method post\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/replace-asset\",\n              \"label\": \"Replace asset\",\n              \"className\": \"api-method put\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/detach-asset\",\n              \"label\": \"Detach asset\",\n              \"className\": \"api-method delete\"\n            }\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"Lists\",\n          \"items\": [\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-all-lists\",\n              \"label\": \"Get all lists\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/create-a-new-list\",\n              \"label\": \"Create a new list\",\n              \"className\": \"api-method post\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-a-single-list\",\n              \"label\": \"Get a single list\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/delete-a-list\",\n              \"label\": \"Delete a list\",\n              \"className\": \"api-method delete\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/update-a-list\",\n              \"label\": \"Update a list\",\n              \"className\": \"api-method patch\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-bookmarks-in-the-list\",\n              \"label\": \"Get bookmarks in the list\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/add-a-bookmark-to-a-list\",\n              \"label\": \"Add a bookmark to a list\",\n              \"className\": \"api-method put\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/remove-a-bookmark-from-a-list\",\n              \"label\": \"Remove a bookmark from a list\",\n              \"className\": \"api-method delete\"\n            }\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"Tags\",\n          \"items\": [\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-all-tags\",\n              \"label\": \"Get all tags\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/create-a-new-tag\",\n              \"label\": \"Create a new tag\",\n              \"className\": \"api-method post\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-a-single-tag\",\n              \"label\": \"Get a single tag\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/delete-a-tag\",\n              \"label\": \"Delete a tag\",\n              \"className\": \"api-method delete\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/update-a-tag\",\n              \"label\": \"Update a tag\",\n              \"className\": \"api-method patch\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-bookmarks-with-the-tag\",\n              \"label\": \"Get bookmarks with the tag\",\n              \"className\": \"api-method get\"\n            }\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"Highlights\",\n          \"items\": [\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-all-highlights\",\n              \"label\": \"Get all highlights\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/create-a-new-highlight\",\n              \"label\": \"Create a new highlight\",\n              \"className\": \"api-method post\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-a-single-highlight\",\n              \"label\": \"Get a single highlight\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/delete-a-highlight\",\n              \"label\": \"Delete a highlight\",\n              \"className\": \"api-method delete\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/update-a-highlight\",\n              \"label\": \"Update a highlight\",\n              \"className\": \"api-method patch\"\n            }\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"Users\",\n          \"items\": [\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-current-user-info\",\n              \"label\": \"Get current user info\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-current-user-stats\",\n              \"label\": \"Get current user stats\",\n              \"className\": \"api-method get\"\n            }\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"Assets\",\n          \"items\": [\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/upload-a-new-asset\",\n              \"label\": \"Upload a new asset\",\n              \"className\": \"api-method post\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-a-single-asset\",\n              \"label\": \"Get a single asset\",\n              \"className\": \"api-method get\"\n            }\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"Admin\",\n          \"items\": [\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/update-user\",\n              \"label\": \"Update user\",\n              \"className\": \"api-method put\"\n            }\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"Backups\",\n          \"items\": [\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-all-backups\",\n              \"label\": \"Get all backups\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/trigger-a-new-backup\",\n              \"label\": \"Trigger a new backup\",\n              \"className\": \"api-method post\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-a-single-backup\",\n              \"label\": \"Get a single backup\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/delete-a-backup\",\n              \"label\": \"Delete a backup\",\n              \"className\": \"api-method delete\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/download-a-backup\",\n              \"label\": \"Download a backup\",\n              \"className\": \"api-method get\"\n            }\n          ]\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "docs/versioned_sidebars/version-v0.30.0-sidebars.json",
    "content": "{\n  \"sidebar\": [\n    {\n      \"type\": \"autogenerated\",\n      \"dirName\": \".\"\n    },\n    {\n      \"API\": [\n        {\n          \"type\": \"doc\",\n          \"id\": \"api/karakeep-api\"\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"Bookmarks\",\n          \"items\": [\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-all-bookmarks\",\n              \"label\": \"Get all bookmarks\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/create-a-new-bookmark\",\n              \"label\": \"Create a new bookmark\",\n              \"className\": \"api-method post\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/search-bookmarks\",\n              \"label\": \"Search bookmarks\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-a-single-bookmark\",\n              \"label\": \"Get a single bookmark\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/delete-a-bookmark\",\n              \"label\": \"Delete a bookmark\",\n              \"className\": \"api-method delete\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/update-a-bookmark\",\n              \"label\": \"Update a bookmark\",\n              \"className\": \"api-method patch\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/summarize-a-bookmark\",\n              \"label\": \"Summarize a bookmark\",\n              \"className\": \"api-method post\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/attach-tags-to-a-bookmark\",\n              \"label\": \"Attach tags to a bookmark\",\n              \"className\": \"api-method post\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/detach-tags-from-a-bookmark\",\n              \"label\": \"Detach tags from a bookmark\",\n              \"className\": \"api-method delete\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-lists-of-a-bookmark\",\n              \"label\": \"Get lists of a bookmark\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-highlights-of-a-bookmark\",\n              \"label\": \"Get highlights of a bookmark\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/attach-asset\",\n              \"label\": \"Attach asset\",\n              \"className\": \"api-method post\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/replace-asset\",\n              \"label\": \"Replace asset\",\n              \"className\": \"api-method put\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/detach-asset\",\n              \"label\": \"Detach asset\",\n              \"className\": \"api-method delete\"\n            }\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"Lists\",\n          \"items\": [\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-all-lists\",\n              \"label\": \"Get all lists\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/create-a-new-list\",\n              \"label\": \"Create a new list\",\n              \"className\": \"api-method post\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-a-single-list\",\n              \"label\": \"Get a single list\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/delete-a-list\",\n              \"label\": \"Delete a list\",\n              \"className\": \"api-method delete\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/update-a-list\",\n              \"label\": \"Update a list\",\n              \"className\": \"api-method patch\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-bookmarks-in-the-list\",\n              \"label\": \"Get bookmarks in the list\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/add-a-bookmark-to-a-list\",\n              \"label\": \"Add a bookmark to a list\",\n              \"className\": \"api-method put\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/remove-a-bookmark-from-a-list\",\n              \"label\": \"Remove a bookmark from a list\",\n              \"className\": \"api-method delete\"\n            }\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"Tags\",\n          \"items\": [\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-all-tags\",\n              \"label\": \"Get all tags\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/create-a-new-tag\",\n              \"label\": \"Create a new tag\",\n              \"className\": \"api-method post\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-a-single-tag\",\n              \"label\": \"Get a single tag\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/delete-a-tag\",\n              \"label\": \"Delete a tag\",\n              \"className\": \"api-method delete\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/update-a-tag\",\n              \"label\": \"Update a tag\",\n              \"className\": \"api-method patch\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-bookmarks-with-the-tag\",\n              \"label\": \"Get bookmarks with the tag\",\n              \"className\": \"api-method get\"\n            }\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"Highlights\",\n          \"items\": [\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-all-highlights\",\n              \"label\": \"Get all highlights\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/create-a-new-highlight\",\n              \"label\": \"Create a new highlight\",\n              \"className\": \"api-method post\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-a-single-highlight\",\n              \"label\": \"Get a single highlight\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/delete-a-highlight\",\n              \"label\": \"Delete a highlight\",\n              \"className\": \"api-method delete\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/update-a-highlight\",\n              \"label\": \"Update a highlight\",\n              \"className\": \"api-method patch\"\n            }\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"Users\",\n          \"items\": [\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-current-user-info\",\n              \"label\": \"Get current user info\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-current-user-stats\",\n              \"label\": \"Get current user stats\",\n              \"className\": \"api-method get\"\n            }\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"Assets\",\n          \"items\": [\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/upload-a-new-asset\",\n              \"label\": \"Upload a new asset\",\n              \"className\": \"api-method post\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-a-single-asset\",\n              \"label\": \"Get a single asset\",\n              \"className\": \"api-method get\"\n            }\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"Admin\",\n          \"items\": [\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/update-user\",\n              \"label\": \"Update user\",\n              \"className\": \"api-method put\"\n            }\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"Backups\",\n          \"items\": [\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-all-backups\",\n              \"label\": \"Get all backups\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/trigger-a-new-backup\",\n              \"label\": \"Trigger a new backup\",\n              \"className\": \"api-method post\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-a-single-backup\",\n              \"label\": \"Get a single backup\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/delete-a-backup\",\n              \"label\": \"Delete a backup\",\n              \"className\": \"api-method delete\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/download-a-backup\",\n              \"label\": \"Download a backup\",\n              \"className\": \"api-method get\"\n            }\n          ]\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "docs/versioned_sidebars/version-v0.31.0-sidebars.json",
    "content": "{\n  \"sidebar\": [\n    {\n      \"type\": \"autogenerated\",\n      \"dirName\": \".\"\n    },\n    {\n      \"type\": \"category\",\n      \"label\": \"API\",\n      \"items\": [\n        {\n          \"type\": \"doc\",\n          \"id\": \"api/karakeep-api\"\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"Bookmarks\",\n          \"items\": [\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/list-bookmarks\",\n              \"label\": \"Get all bookmarks\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/create-bookmark\",\n              \"label\": \"Create a new bookmark\",\n              \"className\": \"api-method post\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/search-bookmarks\",\n              \"label\": \"Search bookmarks\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/check-bookmark-url\",\n              \"label\": \"Check if a URL exists in bookmarks\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-bookmark\",\n              \"label\": \"Get a single bookmark\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/delete-bookmark\",\n              \"label\": \"Delete a bookmark\",\n              \"className\": \"api-method delete\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/update-bookmark\",\n              \"label\": \"Update a bookmark\",\n              \"className\": \"api-method patch\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/summarize-bookmark\",\n              \"label\": \"Summarize a bookmark\",\n              \"className\": \"api-method post\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/attach-tags-to-bookmark\",\n              \"label\": \"Attach tags to a bookmark\",\n              \"className\": \"api-method post\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/detach-tags-from-bookmark\",\n              \"label\": \"Detach tags from a bookmark\",\n              \"className\": \"api-method delete\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-bookmark-lists\",\n              \"label\": \"Get lists of a bookmark\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-bookmark-highlights\",\n              \"label\": \"Get highlights of a bookmark\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/attach-asset-to-bookmark\",\n              \"label\": \"Attach asset to a bookmark\",\n              \"className\": \"api-method post\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/replace-asset-on-bookmark\",\n              \"label\": \"Replace asset on a bookmark\",\n              \"className\": \"api-method put\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/detach-asset-from-bookmark\",\n              \"label\": \"Detach asset from a bookmark\",\n              \"className\": \"api-method delete\"\n            }\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"Lists\",\n          \"items\": [\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/list-lists\",\n              \"label\": \"Get all lists\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/create-list\",\n              \"label\": \"Create a new list\",\n              \"className\": \"api-method post\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-list\",\n              \"label\": \"Get a single list\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/delete-list\",\n              \"label\": \"Delete a list\",\n              \"className\": \"api-method delete\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/update-list\",\n              \"label\": \"Update a list\",\n              \"className\": \"api-method patch\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-list-bookmarks\",\n              \"label\": \"Get bookmarks in a list\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/add-bookmark-to-list\",\n              \"label\": \"Add a bookmark to a list\",\n              \"className\": \"api-method put\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/remove-bookmark-from-list\",\n              \"label\": \"Remove a bookmark from a list\",\n              \"className\": \"api-method delete\"\n            }\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"Tags\",\n          \"items\": [\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/list-tags\",\n              \"label\": \"Get all tags\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/create-tag\",\n              \"label\": \"Create a new tag\",\n              \"className\": \"api-method post\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-tag\",\n              \"label\": \"Get a single tag\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/delete-tag\",\n              \"label\": \"Delete a tag\",\n              \"className\": \"api-method delete\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/update-tag\",\n              \"label\": \"Update a tag\",\n              \"className\": \"api-method patch\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-tag-bookmarks\",\n              \"label\": \"Get bookmarks with a tag\",\n              \"className\": \"api-method get\"\n            }\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"Highlights\",\n          \"items\": [\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/list-highlights\",\n              \"label\": \"Get all highlights\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/create-highlight\",\n              \"label\": \"Create a new highlight\",\n              \"className\": \"api-method post\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-highlight\",\n              \"label\": \"Get a single highlight\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/delete-highlight\",\n              \"label\": \"Delete a highlight\",\n              \"className\": \"api-method delete\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/update-highlight\",\n              \"label\": \"Update a highlight\",\n              \"className\": \"api-method patch\"\n            }\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"Assets\",\n          \"items\": [\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/upload-asset\",\n              \"label\": \"Upload a new asset\",\n              \"className\": \"api-method post\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-asset\",\n              \"label\": \"Get a single asset\",\n              \"className\": \"api-method get\"\n            }\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"Users\",\n          \"items\": [\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-current-user\",\n              \"label\": \"Get current user info\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-current-user-stats\",\n              \"label\": \"Get current user stats\",\n              \"className\": \"api-method get\"\n            }\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"Admin\",\n          \"items\": [\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/admin-update-user\",\n              \"label\": \"Update a user (admin)\",\n              \"className\": \"api-method put\"\n            }\n          ]\n        },\n        {\n          \"type\": \"category\",\n          \"label\": \"Backups\",\n          \"items\": [\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/list-backups\",\n              \"label\": \"Get all backups\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/create-backup\",\n              \"label\": \"Trigger a new backup\",\n              \"className\": \"api-method post\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/get-backup\",\n              \"label\": \"Get a single backup\",\n              \"className\": \"api-method get\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/delete-backup\",\n              \"label\": \"Delete a backup\",\n              \"className\": \"api-method delete\"\n            },\n            {\n              \"type\": \"doc\",\n              \"id\": \"api/download-backup\",\n              \"label\": \"Download a backup\",\n              \"className\": \"api-method get\"\n            }\n          ]\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "docs/versions.json",
    "content": "[\n  \"v0.31.0\",\n  \"v0.30.0\",\n  \"v0.29.0\",\n  \"v0.28.0\"\n]\n"
  },
  {
    "path": "karakeep-linux.sh",
    "content": "#!/usr/bin/env bash\n\n# v3.0.0\n# Copyright 2024-2025\n# Author: vhsdream\n# Adapted from: The Karakeep installation script from https://github.com/community-scripts/ProxmoxVE\n# License: MIT\n\nset -Eeuo pipefail\ntrap 'catch $LINENO \"$BASH_COMMAND\"' SIGINT SIGTERM ERR\nverbose=0\nSPINNER_PID=\"\"\n\nusage() {\n  header\n  cat <<EOF\nThis script has three functions:\n\n'install'   Installs $(app) on a clean Debian 12/Ubuntu 24.04 system.\n'update'    Checks for, and installs updates for $(app) on a system that previously installed $(app) by running this script.\n'migrate'   Performs a full Hoarder ==> $(app) migration on a previous install. Also checks for updates afterwards.\n\nAvailable options:\n\n-h, --help      Print this help and exit\n-v, --verbose   Print script StandardOutput\n--no-color      Disable colours\n\nThis script WILL NOT update or migrate a $(app)/Hoarder install that was installed in any other way.\nPlease back up your existing installation before running this script!\nIf you encounter any errors please create a GitHub issue (https://github.com/karakeep-app/karakeep/issues) and tag vhsdream\n\nUsage: bash $(basename \"${BASH_SOURCE[0]}\") [-h] [-v] [install|update|migrate]\n\nEOF\n  exit\n}\n\nheader() {\n  t_width=$(tput cols 2>/dev/null)\n  if [[ \"$t_width\" -gt 115 ]]; then\n    echo -e \"$(\n      cat <<EOF\n  ${YELLOW}\n  oMMWWWWWMMMMMMMMMMMo    ....                                      ...\n  oMM     'MMMMMMMMMMo    xMMX                                     ;MMM.\n  oMM     'MMOxkXMMMMo    kMMX                                     ;MMM.\n  oMM     'MM'   .OMMo    kMMX   ,;;;. .;lll:.   ';;. :l  ,cloc,   ;MMM.  .;;;.  .:loc,      .:llc'    ;;; .clc,\n  oMM     'MM'    .MMo    kMMX .0MMX, dMMXKWMMO  0MM0NMM'WMNKXMMW' ;MMM. dMMWl 'KMW0OXMWl  ;NMW0ONMN: .MMMXMWMMMWl\n  oMM     .MM'     MMo    kMMNlWMWl    ...',NMM: 0MMWc.   '.',xMMK ;MMMlNMMk. .WMW:'''KMMc;MMW,'''XMM,.MMMk.  ;WMM:\n  oMM     .MM'     MMo    kMMWKMMX.  .xWMN0ONMMo 0MMO   cXMWKOKMMW ;MMM0MMW:  cMMMXXXXXXXddMMWXXXXXXXc.MMM.    KMMx\n  oMM     .MM'.ol. MMo    kMMX xMMWl dMM0  .XMMo 0MMO  .MMM.  dMMW ;MMM.:WMMO .NMMo...lc. .WMWc...o:  .MMMX:',xMMM'\n  oMMXKKKKXMMWMMMMNMMo    kMMX  :WMM0,0MMMWX0MMo 0MMO   dWMMWWxMMW ;MMM. .KMMN,.dNMMMMMNo  .kWMMMMMXc .MMM0WMMMM0'\n  .:cccccccccccccccc:.                  ...               ...                     ..'..       .''.    .MMM, ...\n                                                                                                      .MMM,\n                                                                                                      .OOO.${CLR}\n                                                                               The ${PURPLE}Bookmark${CLR} ${RED}Everything${CLR} ${YELLOW}App${CLR}\n                                                                               script version ${GREEN}3.0.0${CLR}\nEOF\n    )\"\n  fi\n}\n\n# Handling output suppression\nset_verbosity() {\n  if [ \"$verbose\" -eq 1 ]; then\n    shh=\"\"\n  else\n    shh=\"silent_running\"\n  fi\n}\n\nsilent_running() {\n  \"$@\" >/dev/null 2>&1\n}\n\nset_verbosity\n\n# Colour handling\nsetup_colours() {\n  if [[ -t 2 ]] && [[ -z \"${NO_COLOR-}\" ]] && [[ \"${TERM-}\" != \"dumb\" ]]; then\n    CLR='\\033[0m' GREEN='\\033[0;32m' PURPLE='\\033[0;35m' CYAN='\\033[0;36m' YELLOW='\\033[1;33m' RED='\\033[0;31m'\n  else\n    CLR='' GREEN='' PURPLE='' CYAN='' YELLOW='' RED=''\n  fi\n}\nsetup_colours\n\n# Flash and bling\napp() {\n  echo -e \"${CLR}${PURPLE}Karakeep${CLR}\"\n}\n\nspinner() {\n  local frames=('▹▹▹▹▹' '▸▹▹▹▹' '▹▸▹▹▹' '▹▹▸▹▹' '▹▹▹▸▹' '▹▹▹▹▸')\n  local spin_i=0\n  local interval=0.1\n  printf \"\\e[?25l\"\n\n  while true; do\n    printf \"\\r${PURPLE}%s${CLR}\" \"${frames[spin_i]}\"\n    spin_i=$(((spin_i + 1) % ${#frames[@]}))\n    sleep \"$interval\"\n  done\n}\n\nmsg_start() {\n  printf \"       \"\n  echo >&1 -ne \"${CYAN}${1-}${CLR}\"\n  spinner &\n  SPINNER_PID=$!\n}\n\nmsg_done() {\n  if [[ -n \"$SPINNER_PID\" ]] && ps -p \"$SPINNER_PID\" >/dev/null; then kill \"$SPINNER_PID\" >/dev/null; fi\n  printf \"\\e[?25h\"\n  local msg=\"${1-}\"\n  echo -e \"\\r\"\n  echo >&1 -e \"${GREEN}Done ✔ ${msg}${CLR}\"\n}\n\nmsg_info() {\n  if [[ -n \"$SPINNER_PID\" ]] && ps -p \"$SPINNER_PID\" >/dev/null; then kill \"$SPINNER_PID\" >/dev/null; fi\n  printf \"\\e[?25h\"\n  echo >&1 -e \"${1-}\\r\"\n}\n\n# Exception and error handling\nmsg_err() {\n  if [[ -n \"$SPINNER_PID\" ]] && ps -p \"$SPINNER_PID\" >/dev/null; then kill \"$SPINNER_PID\" >/dev/null; fi\n  echo >&2 -e \"${RED}${1-}${CLR}\"\n}\n\ndie() {\n  local err=$1\n  local code=${2-1}\n  msg_err \"$err\"\n  exit \"$code\"\n}\n\ncatch() {\n  local code=$?\n  local line=$1\n  local command=$2\n  msg_err \"Caught error in line $line: exit code $code: while executing $command\"\n}\n\nparse_params() {\n  while :; do\n    case \"${1-}\" in\n    -h | --help) usage ;;\n    -v | --verbose) verbose=1 && set_verbosity ;;\n    --no-color) NO_COLOR=1 ;;\n    -?*) die \"Unknown flag: $1. Use -h|--help for help\" ;;\n    *) break ;;\n    esac\n    shift\n  done\n\n  args=(\"$@\")\n  [[ ${#args[@]} -eq 0 ]] && die \"Missing script arguments. Use -h|--help for help\"\n  return 0\n}\n\nparse_params \"$@\"\n\nOS=\"$(awk -F'=' '/^VERSION_CODENAME=/{ print $NF }' /etc/os-release)\"\nINSTALL_DIR=/opt/karakeep\nAPP_DIR=\"$INSTALL_DIR\"/apps\nexport DATA_DIR=/var/lib/karakeep\nCONFIG_DIR=/etc/karakeep\nLOG_DIR=/var/log/karakeep\nENV_FILE=${CONFIG_DIR}/karakeep.env\n\ninstall_karakeep() {\n  header\n  msg_info \"$(app) installation for Debian 12/Ubuntu 24.04\" && sleep 3\n  echo -e \"\\n\"\n  msg_start \"Installing dependencies...\"\n  $shh apt-get install --no-install-recommends -y \\\n    g++ \\\n    curl \\\n    build-essential \\\n    sudo \\\n    unzip \\\n    gnupg \\\n    graphicsmagick \\\n    ghostscript \\\n    ca-certificates\n  if [[ \"$OS\" == \"noble\" ]]; then\n    $shh apt-get install -y software-properties-common\n    $shh add-apt-repository ppa:xtradeb/apps -y\n    $shh apt-get install --no-install-recommends -y ungoogled-chromium yt-dlp\n    ln -s /usr/bin/ungoogled-chromium /usr/bin/chromium\n  else\n    $shh apt-get install --no-install-recommends -y chromium\n    curl -fsSL https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux -o /usr/bin/yt-dlp && chmod +x /usr/bin/yt-dlp\n  fi\n\n  curl -fsSL https://github.com/Y2Z/monolith/releases/latest/download/monolith-gnu-linux-x86_64 -o /usr/bin/monolith && chmod +x /usr/bin/monolith\n  curl -fsSLO https://github.com/meilisearch/meilisearch/releases/latest/download/meilisearch.deb\n  DEBIAN_FRONTEND=noninteractive $shh apt-get install -y ./meilisearch.deb && rm ./meilisearch.deb\n  msg_done \"Installed dependencies\"\n\n  msg_start \"Installing Node.js...\"\n  mkdir -p /etc/apt/keyrings\n  curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg\n  echo \"deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_24.x nodistro main\" >/etc/apt/sources.list.d/nodesource.list\n  $shh apt-get update\n  $shh apt-get install -y nodejs\n  # https://github.com/karakeep-app/karakeep/issues/967\n  $shh npm install -g corepack@0.31.0\n  msg_done \"Installed Node.js\"\n\n  msg_start \"Installing $(app)${CYAN}, please wait...${CLR}\"\n  mkdir -p {\"$DATA_DIR\",\"$CONFIG_DIR\",\"$LOG_DIR\"}\n  M_DATA_DIR=/var/lib/meilisearch\n  M_CONFIG_FILE=/etc/meilisearch.toml\n  RELEASE=\"$(curl -s https://api.github.com/repos/karakeep-app/karakeep/releases/latest | grep \"tag_name\" | awk '{print substr($2, 3, length($2)-4) }')\"\n  cd /tmp\n  curl -fsSLO \"https://github.com/karakeep-app/karakeep/archive/refs/tags/v${RELEASE}.zip\"\n  unzip -q v\"$RELEASE\".zip\n  mv karakeep-\"$RELEASE\" \"$INSTALL_DIR\" && cd \"$APP_DIR\"/web\n  corepack enable\n  export NEXT_TELEMETRY_DISABLED=1\n  export PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=\"true\"\n  export CI=\"true\"\n  $shh pnpm i --frozen-lockfile\n  $shh pnpm build\n  cd \"$APP_DIR\"/workers\n  $shh pnpm i --frozen-lockfile\n  $shh pnpm build\n  cd \"$APP_DIR\"/cli\n  $shh pnpm i --frozen-lockfile\n  $shh pnpm build\n  cd \"$INSTALL_DIR\"/packages/db\n  $shh pnpm migrate\n  msg_done \"Installed $(app)\"\n\n  msg_start \"Creating configuration files...\"\n  cd \"$INSTALL_DIR\"\n  MASTER_KEY=\"$(openssl rand -base64 12)\"\n  cat <<EOF >\"$M_CONFIG_FILE\"\nenv = \"production\"\nmaster_key = \"$MASTER_KEY\"\ndb_path = \"${M_DATA_DIR}/data\"\ndump_dir = \"${M_DATA_DIR}/dumps\"\nsnapshot_dir = \"${M_DATA_DIR}/snapshots\"\nno_analytics = true\nEOF\n  chmod 600 \"$M_CONFIG_FILE\"\n\n  karakeep_SECRET=\"$(openssl rand -base64 36 | cut -c1-24)\"\n  cat <<EOF >\"$ENV_FILE\"\nNODE_ENV=production\nSERVER_VERSION=${RELEASE}\nNEXTAUTH_SECRET=\"${karakeep_SECRET}\"\nNEXTAUTH_URL=\"http://localhost:3000\"\nDATA_DIR=${DATA_DIR}\nMEILI_ADDR=\"http://127.0.0.1:7700\"\nMEILI_MASTER_KEY=\"${MASTER_KEY}\"\nBROWSER_WEB_URL=\"http://127.0.0.1:9222\"\n# CRAWLER_VIDEO_DOWNLOAD=true\n# CRAWLER_VIDEO_DOWNLOAD_MAX_SIZE=\n# OPENAI_API_KEY=\n# OLLAMA_BASE_URL=\n# INFERENCE_TEXT_MODEL=\n# INFERENCE_IMAGE_MODEL=\nEOF\n  chmod 600 \"$ENV_FILE\"\n  echo \"$RELEASE\" >\"$INSTALL_DIR\"/version.txt\n  msg_done \"Configuration complete\"\n\n  msg_start \"Creating users and modifying permissions...\"\n  if ! id -u meilisearch >/dev/null 2>&1; then\n    useradd -U -s /usr/sbin/nologin -r -m -d \"$M_DATA_DIR\" meilisearch\n  fi\n  if ! id -u karakeep >/dev/null 2>&1; then\n    useradd -U -s /usr/sbin/nologin -r -M -d \"$INSTALL_DIR\" karakeep\n  fi\n  chown meilisearch:meilisearch \"$M_CONFIG_FILE\"\n  touch \"$LOG_DIR\"/{karakeep-workers.log,karakeep-web.log}\n  chown -R karakeep:karakeep \"$INSTALL_DIR\" \"$CONFIG_DIR\" \"$DATA_DIR\" \"$LOG_DIR\"\n  msg_done \"Users created, permissions modified\"\n\n  msg_start \"Creating service files and configuring log rotation...\"\n  cat <<EOF >/etc/systemd/system/meilisearch.service\n[Unit]\nDescription=MeiliSearch is a RESTful search API\nDocumentation=https://docs.meilisearch.com/\nAfter=network.target\n\n[Service]\nUser=meilisearch\nGroup=meilisearch\nRestart=on-failure\nWorkingDirectory=${M_DATA_DIR}\nExecStart=/usr/bin/meilisearch --config-file-path ${M_CONFIG_FILE}\nNoNewPrivileges=true\nProtectHome=true\nReadWritePaths=${M_DATA_DIR}\nProtectSystem=full\nProtectHostname=true\nProtectControlGroups=true\nProtectKernelModules=true\nProtectKernelTunables=true\nProtectKernelLogs=true\nProtectClock=true\nLockPersonality=true\nRestrictRealtime=yes\nRestrictNamespaces=yes\nMemoryDenyWriteExecute=yes\nPrivateDevices=yes\nPrivateTmp=true\nCapabilityBoundingSet=\nRemoveIPC=true\n\n[Install]\nWantedBy=multi-user.target\nEOF\n\n  cat <<EOF >/etc/systemd/system/karakeep-browser.service\n[Unit]\nDescription=Karakeep headless browser\nAfter=network.target\n\n[Service]\nUser=root\nRestart=on-failure\nExecStart=/usr/bin/chromium --headless --no-sandbox --disable-gpu --disable-dev-shm-usage --remote-debugging-address=127.0.0.1 --remote-debugging-port=9222 --hide-scrollbars\nTimeoutStopSec=5\nSyslogIdentifier=karakeep-browser\n\n[Install]\nWantedBy=multi-user.target\nEOF\n\n  cat <<EOF >/etc/systemd/system/karakeep-workers.service\n[Unit]\nDescription=Karakeep workers\nWants=network.target karakeep-browser.service meilisearch.service\nAfter=network.target karakeep-browser.service meilisearch.service\n\n[Service]\nUser=karakeep\nGroup=karakeep\nRestart=always\nEnvironmentFile=${ENV_FILE}\nWorkingDirectory=${APP_DIR}/workers\nExecStart=/usr/bin/node dist/index.js\nStandardOutput=append:${LOG_DIR}/karakeep-workers.log\nStandardError=append:${LOG_DIR}/karakeep-workers.log\nTimeoutStopSec=5\nSyslogIdentifier=karakeep-workers\n\n[Install]\nWantedBy=multi-user.target\nEOF\n\n  cat <<EOF >/etc/systemd/system/karakeep-web.service\n[Unit]\nDescription=Karakeep web\nWants=network.target karakeep-workers.service\nAfter=network.target karakeep-workers.service\n\n[Service]\nUser=karakeep\nGroup=karakeep\nRestart=on-failure\nEnvironmentFile=${ENV_FILE}\nWorkingDirectory=${APP_DIR}/web\nExecStart=/usr/bin/pnpm start\nStandardOutput=append:${LOG_DIR}/karakeep-web.log\nStandardError=append:${LOG_DIR}/karakeep-web.log\nTimeoutStopSec=5\nSyslogIdentifier=karakeep-web\n\n[Install]\nWantedBy=multi-user.target\nEOF\n\n  cat <<EOF >/etc/systemd/system/karakeep.target\n[Unit]\nDescription=Karakeep Services\nAfter=network-online.target\nWants=meilisearch.service karakeep-browser.service karakeep-workers.service karakeep-web.service\n\n[Install]\nWantedBy=multi-user.target\nEOF\n\n  cat <<EOF >/etc/logrotate.d/karakeep\n/var/log/karakeep/*.log\n{\n  su karakeep karakeep\n  weekly\n  missingok\n  rotate 4\n  compress\n  notifempty\n}\nEOF\n\n  msg_done \"Service files created, log rotation configured\"\n\n  msg_start \"Enabling and starting services, please wait...\" && sleep 3\n  systemctl enable -q --now meilisearch.service karakeep.target\n  service_check install\n  exit 0\n}\n\nupdate_karakeep() {\n  msg_info \"${YELLOW}Checking for an update...${CLR}\" && sleep 1\n  if [[ ! -d ${INSTALL_DIR} ]]; then\n    die \"Is Karakeep even installed?\"\n  fi\n  RELEASE=\"$(curl -s https://api.github.com/repos/karakeep-app/karakeep/releases/latest | grep \"tag_name\" | awk '{print substr($2, 3, length($2)-4) }')\"\n  PREV_RELEASE=\"$(cat \"$INSTALL_DIR\"/version.txt)\"\n  if [[ \"$RELEASE\" != \"$PREV_RELEASE\" ]]; then\n    if [[ \"$(systemctl is-active karakeep-web)\" == \"active\" ]]; then\n      msg_start \"Stopping affected services...\"\n      systemctl stop karakeep-web karakeep-workers\n      msg_done \"Stopped services\"\n    fi\n    if [[ \"$OS\" == \"bookworm\" ]]; then\n      $shh yt-dlp -U\n    fi\n    msg_start \"Updating $(app) ${CYAN}to v${RELEASE}...${CLR}\"\n    sed -i \"s|SERVER_VERSION=${PREV_RELEASE}|SERVER_VERSION=${RELEASE}|\" \"$ENV_FILE\"\n    rm -R \"$INSTALL_DIR\"\n    cd /tmp\n    curl -fsSLO \"https://github.com/karakeep-app/karakeep/archive/refs/tags/v${RELEASE}.zip\"\n    unzip -q v\"$RELEASE\".zip\n    mv karakeep-\"$RELEASE\" \"$INSTALL_DIR\"\n    # https://github.com/karakeep-app/karakeep/issues/967\n    if [[ \"$(corepack -v)\" < \"0.31.0\" ]]; then\n      $shh npm install -g corepack@0.31.0\n    fi\n    corepack enable\n    export NEXT_TELEMETRY_DISABLED=1\n    export PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=\"true\"\n    export CI=\"true\"\n    cd \"$APP_DIR\"/web && $shh pnpm i --frozen-lockfile\n    $shh pnpm build\n    cd \"$APP_DIR\"/workers && $shh pnpm i --frozen-lockfile\n    $shh pnpm build\n    cd \"$APP_DIR\"/cli && $shh pnpm i --frozen-lockfile\n    $shh pnpm build\n    cd \"$INSTALL_DIR\"/packages/db && $shh pnpm migrate\n    echo \"$RELEASE\" >\"$INSTALL_DIR\"/version.txt\n    chown -R karakeep:karakeep \"$INSTALL_DIR\" \"$DATA_DIR\"\n    msg_done \"Updated $(app) ${CYAN}to v${RELEASE}${CLR}\"\n    msg_start \"Restarting services and cleaning up...\"\n    rm /tmp/v\"$RELEASE\".zip\n\n    # Migrations\n    # 0.27 changed the worker compiled file from .mjs to .js\n    if grep -q '^ExecStart=/usr/bin/node\\s\\+dist/index\\.mjs$' /etc/systemd/system/karakeep-workers.service; then\n      sed -i -E 's#^(ExecStart=/usr/bin/node\\s+dist/)index\\.mjs$#\\1index.js#' /etc/systemd/system/karakeep-workers.service\n      systemctl daemon-reload\n    fi\n    # End migrations\n\n    systemctl restart karakeep.target\n    service_check update\n  else\n    msg_info \"${YELLOW}No update required.${CLR}\"\n  fi\n  exit 0\n}\n\nmigrate_karakeep() {\n  if [[ ! -d /opt/karakeep ]]; then\n    msg_start \"Migrating your Hoarder installation to $(app), ${CYAN}then checking for an update...${CLR}\"\n    systemctl stop hoarder-browser hoarder-workers hoarder-web\n    sed -i -e \"s|hoarder|karakeep|g\" /etc/hoarder/hoarder.env /etc/systemd/system/hoarder-{browser,web,workers}.service /etc/systemd/system/hoarder.target \\\n      -e \"s|Hoarder|Karakeep|g\" /etc/systemd/system/hoarder-{browser,web,workers}.service /etc/systemd/system/hoarder.target\n    for path in /etc/systemd/system/hoarder*.service; do\n      new_path=\"${path//hoarder/karakeep}\"\n      mv \"$path\" \"$new_path\"\n    done\n    mv /etc/systemd/system/hoarder.target /etc/systemd/system/karakeep.target\n    mv /opt/hoarder \"$INSTALL_DIR\"\n    mv /var/lib/hoarder \"$DATA_DIR\"\n    mv /etc/hoarder \"$CONFIG_DIR\"\n    mv /var/log/hoarder \"$LOG_DIR\"\n    mv \"$CONFIG_DIR\"/hoarder.env \"$ENV_FILE\"\n    mv \"$LOG_DIR\"/hoarder-web.log \"$LOG_DIR\"/karakeep-web.log\n    mv \"$LOG_DIR\"/hoarder-workers.log \"$LOG_DIR\"/karakeep-workers.log\n    usermod -l karakeep hoarder -d \"$INSTALL_DIR\"\n    groupmod -n karakeep hoarder\n    chown -R karakeep:karakeep \"$INSTALL_DIR\" \"$CONFIG_DIR\" \"$DATA_DIR\" \"$LOG_DIR\"\n    systemctl daemon-reload\n    systemctl -q enable --now karakeep.target\n    service_check migrate\n  else\n    msg_info \"${YELLOW}There is no need for a migration: $(app) ${YELLOW}is already installed.${CLR}\"\n  fi\n}\n\nservice_check() {\n  local services=(\"karakeep-browser\" \"karakeep-workers\" \"karakeep-web\" \"meilisearch\")\n  readarray -t status < <(for service in \"${services[@]}\"; do\n    systemctl is-active \"$service\" | grep \"^active\" -\n  done)\n  if [[ \"${#status[@]}\" -eq 4 ]]; then\n    if [[ \"$1\" == \"install\" ]]; then\n      msg_done \"$(app) ${CYAN}is running!${CLR}\"\n      sleep 1\n      LOCAL_IP=\"$(hostname -I | awk '{print $1}')\"\n      msg_info \"Go to ${YELLOW}http://$LOCAL_IP:3000 ${CLR}to create your account\"\n      msg_info \"Change settings at ${YELLOW}'/etc/karakeep/karakeep.env'${CLR}\"\n      exit 0\n    elif [[ \"$1\" == \"update\" ]]; then\n      msg_done \"$(app) ${CYAN}is updated and running!${CLR}\"\n      sleep 1\n      exit 0\n    elif [[ \"$1\" == \"migrate\" ]]; then\n      msg_done \"$(app) ${CYAN}migration complete!${CLR}\"\n      sleep 1\n      exit 0\n    fi\n  else\n    die \"Some services have failed. Check 'journalctl -xeu <service-name>' to see what is going on\"\n  fi\n}\n\n[ \"$(id -u)\" -ne 0 ] && die \"This script requires root privileges. Please run with sudo or as the root user.\"\n\ncase \"${args[0]}\" in\ninstall)\n  install_karakeep\n  ;;\nupdate)\n  update_karakeep\n  ;;\nmigrate)\n  migrate_karakeep && update_karakeep\n  ;;\n*)\n  die \"Unknown command. Choose 'install', 'update' or 'migrate.'\"\n  ;;\nesac\n"
  },
  {
    "path": "kubernetes/.env_sample",
    "content": "# Put your configuration options here\nNEXTAUTH_URL=http://localhost:3000\nKARAKEEP_VERSION=release"
  },
  {
    "path": "kubernetes/.gitignore",
    "content": "_manifest.yaml\n"
  },
  {
    "path": "kubernetes/.secrets_sample",
    "content": "# Use `openssl rand -base64 36` to generate the random strings\nNEXTAUTH_SECRET=generated_secret\nMEILI_MASTER_KEY=generated_secret\nNEXT_PUBLIC_SECRET=\"my-super-duper-secret-string\"\n"
  },
  {
    "path": "kubernetes/Makefile",
    "content": "# Define the output file\nOUTPUT_FILE := _manifest.yaml\n\n# Define the Kustomize build command\nKUSTOMIZE_BUILD := kustomize build .\n\n# The default target\nall: build\n\n$(OUTPUT_FILE):\n\t$(KUSTOMIZE_BUILD) > $(OUTPUT_FILE)\n\n# Build the Kustomize configuration into the output file\nbuild: clean $(OUTPUT_FILE)\n\n# Deploy the manifest using kubectl apply\ndeploy: $(OUTPUT_FILE)\n\tkubectl apply -f $(OUTPUT_FILE)\n\n# Clean up the output file\nclean:\n\trm -f $(OUTPUT_FILE)\n\n.PHONY: all build deploy clean\n"
  },
  {
    "path": "kubernetes/README.md",
    "content": "# Kubernetes installation with Kustomize\n\nYou can:\n\n- edit the configuration in `.env`.\n\nThen run `make deploy`.\n"
  },
  {
    "path": "kubernetes/chrome-deployment.yaml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: chrome\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: chrome\n  template:\n    metadata:\n      labels:\n        app: chrome\n    spec:\n      containers:\n        - name: chrome\n          image: gcr.io/zenika-hub/alpine-chrome:124\n          command:\n            - chromium-browser\n            - --headless\n            - --no-sandbox\n            - --disable-gpu\n            - --disable-dev-shm-usage\n            - --remote-debugging-address=0.0.0.0\n            - --remote-debugging-port=9222\n            - --hide-scrollbars\n"
  },
  {
    "path": "kubernetes/chrome-service.yaml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: chrome\nspec:\n  selector:\n    app: chrome\n  ports:\n    - protocol: TCP\n      port: 9222\n      targetPort: 9222\n  type: ClusterIP"
  },
  {
    "path": "kubernetes/data-pvc.yaml",
    "content": "apiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n  name: data-pvc\nspec:\n  accessModes:\n    - ReadWriteOnce\n  resources:\n    requests:\n      storage: 1Gi"
  },
  {
    "path": "kubernetes/ingress_sample.yaml",
    "content": "apiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n  name: karakeep-web-ingress\n  namespace: karakeep\nspec:\n  rules:\n  - host: \"karakeep.example.com\"\n    http:\n      paths:\n      - path: \"/\"\n        pathType: Prefix\n        backend:\n          service:\n            name: \"web\"\n            port:\n              number: 3000\n"
  },
  {
    "path": "kubernetes/kustomization.yaml",
    "content": "apiVersion: kustomize.config.k8s.io/v1beta1\nkind: Kustomization\n\nnamespace: karakeep\n\nsecretGenerator:\n- envs:\n  - .secrets\n  name: karakeep-secrets\n\nconfigMapGenerator:\n  - envs:\n    - .env\n    name: karakeep-configuration\n\nresources:\n- namespace.yaml\n- web-deployment.yaml\n- web-service.yaml\n- chrome-deployment.yaml\n- chrome-service.yaml\n- meilisearch-deployment.yaml\n- meilisearch-service.yaml\n- meilisearch-pvc.yaml\n- data-pvc.yaml\n\nreplacements:\n- source:\n    fieldPath: data.KARAKEEP_VERSION\n    kind: ConfigMap\n    name: karakeep-configuration\n    version: v1\n  targets:\n  - fieldPaths:\n    - spec.template.spec.containers.0.image\n    options:\n      delimiter: ':'\n      index: 1\n    select:\n      group: apps\n      kind: Deployment\n      name: web\n      version: v1"
  },
  {
    "path": "kubernetes/meilisearch-deployment.yaml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: meilisearch\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: meilisearch\n  template:\n    metadata:\n      labels:\n        app: meilisearch\n    spec:\n      containers:\n        - name: meilisearch\n          image: getmeili/meilisearch:v1.11.1\n          env:\n            - name: MEILI_NO_ANALYTICS\n              value: \"true\"\n          volumeMounts:\n            - mountPath: /meili_data\n              name: meilisearch\n          envFrom:\n            - secretRef:\n                name: karakeep-secrets\n            - configMapRef:\n                name: karakeep-configuration\n      volumes:\n        - name: meilisearch\n          persistentVolumeClaim:\n            claimName: meilisearch-pvc"
  },
  {
    "path": "kubernetes/meilisearch-pvc.yaml",
    "content": "apiVersion: v1\nkind: PersistentVolumeClaim\nmetadata:\n  name: meilisearch-pvc\nspec:\n  accessModes:\n    - ReadWriteOnce\n  resources:\n    requests:\n      storage: 1Gi"
  },
  {
    "path": "kubernetes/meilisearch-service.yaml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: meilisearch\nspec:\n  selector:\n    app: meilisearch\n  ports:\n    - protocol: TCP\n      port: 7700\n      targetPort: 7700"
  },
  {
    "path": "kubernetes/namespace.yaml",
    "content": "apiVersion: v1\nkind: Namespace\nmetadata:\n  name: karakeep\n"
  },
  {
    "path": "kubernetes/web-deployment.yaml",
    "content": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: web\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: karakeep-web\n  template:\n    metadata:\n      labels:\n        app: karakeep-web\n    spec:\n      containers:\n        - name: web\n          image: ghcr.io/karakeep-app/karakeep\n          imagePullPolicy: Always\n          ports:\n            - containerPort: 3000\n          env:\n            - name: MEILI_ADDR\n              value: http://meilisearch:7700\n            - name: BROWSER_WEB_URL\n              value: http://chrome:9222\n            - name: DATA_DIR\n              value: /data\n            # Add OPENAI_API_KEY to the ConfigMap if necessary\n          volumeMounts:\n            - mountPath: /data\n              name: data\n          envFrom:\n            - secretRef:\n                name: karakeep-secrets\n            - configMapRef:\n                name: karakeep-configuration\n      volumes:\n        - name: data\n          persistentVolumeClaim:\n            claimName: data-pvc\n"
  },
  {
    "path": "kubernetes/web-service.yaml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: web\nspec:\n  selector:\n    app: karakeep-web\n  ports:\n    - protocol: TCP\n      port: 3000\n      targetPort: 3000\n  type: LoadBalancer"
  },
  {
    "path": "package.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/package.json\",\n  \"name\": \"karakeep\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"build\": \"turbo --no-daemon build\",\n    \"dev\": \"turbo --no-daemon dev --parallel\",\n    \"clean\": \"git clean -xdf node_modules\",\n    \"clean:workspaces\": \"turbo --no-daemon clean\",\n    \"db:generate\": \"pnpm --filter @karakeep/db run generate\",\n    \"db:migrate\": \"pnpm --filter @karakeep/db run migrate\",\n    \"db:studio\": \"pnpm --filter @karakeep/db studio\",\n    \"workers\": \"pnpm --filter @karakeep/workers run start\",\n    \"web\": \"pnpm --filter @karakeep/web run dev\",\n    \"android\": \"pnpm --filter @karakeep/mobile android\",\n    \"ios\": \"pnpm --filter @karakeep/mobile ios\",\n    \"prepare\": \"husky\",\n    \"format\": \"turbo --no-daemon format --continue\",\n    \"format:fix\": \"turbo --no-daemon format:fix --continue\",\n    \"lint\": \"turbo --no-daemon lint --continue\",\n    \"lint:fix\": \"turbo --no-daemon lint:fix --continue\",\n    \"test\": \"turbo --no-daemon test\",\n    \"typecheck\": \"turbo --no-daemon typecheck\",\n    \"preflight\": \"turbo run --no-daemon typecheck lint format\",\n    \"preflight:fix\": \"turbo run --no-daemon typecheck lint:fix format:fix\"\n  },\n  \"dependencies\": {\n    \"husky\": \"^9.0.11\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^24\",\n    \"oxfmt\": \"^0.35.0\",\n    \"oxlint\": \"^1.50.0\",\n    \"sherif\": \"^1.6.1\",\n    \"turbo\": \"^2.5.5\"\n  },\n  \"packageManager\": \"pnpm@9.15.9\",\n  \"pnpm\": {\n    \"patchedDependencies\": {\n      \"xcode@3.0.1\": \"patches/xcode@3.0.1.patch\",\n      \"playwright-extra@4.3.6\": \"patches/playwright-extra@4.3.6.patch\"\n    }\n  }\n}\n"
  },
  {
    "path": "packages/api/.oxlintrc.json",
    "content": "{\n  \"$schema\": \"../../node_modules/oxlint/configuration_schema.json\",\n  \"extends\": [\n    \"../../tooling/oxlint/oxlint-base.json\"\n  ],\n  \"env\": {\n    \"builtin\": true,\n    \"commonjs\": true\n  },\n  \"ignorePatterns\": [\n    \"**/*.config.js\",\n    \"**/*.config.cjs\",\n    \"**/.eslintrc.cjs\",\n    \"**/.next\",\n    \"**/dist\",\n    \"**/build\",\n    \"**/pnpm-lock.yaml\"\n  ]\n}\n"
  },
  {
    "path": "packages/api/index.ts",
    "content": "import { httpInstrumentationMiddleware } from \"@hono/otel\";\nimport { Hono } from \"hono\";\nimport { cors } from \"hono/cors\";\nimport { logger as loggerMiddleware } from \"hono/logger\";\nimport { poweredBy } from \"hono/powered-by\";\n\nimport { loadAllPlugins } from \"@karakeep/shared-server\";\nimport serverConfig from \"@karakeep/shared/config\";\nimport logger from \"@karakeep/shared/logger\";\nimport { Context } from \"@karakeep/trpc\";\n\nimport trpcAdapter from \"./middlewares/trpcAdapter\";\nimport admin from \"./routes/admin\";\nimport assets from \"./routes/assets\";\nimport backups from \"./routes/backups\";\nimport bookmarks from \"./routes/bookmarks\";\nimport health from \"./routes/health\";\nimport highlights from \"./routes/highlights\";\nimport lists from \"./routes/lists\";\nimport metrics, { registerMetrics } from \"./routes/metrics\";\nimport publicRoute from \"./routes/public\";\nimport rss from \"./routes/rss\";\nimport tags from \"./routes/tags\";\nimport trpc from \"./routes/trpc\";\nimport users from \"./routes/users\";\nimport version from \"./routes/version\";\nimport webhooks from \"./routes/webhooks\";\n\nawait loadAllPlugins();\n\nconst v1 = new Hono<{\n  Variables: {\n    ctx: Context;\n  };\n}>()\n  .route(\"/highlights\", highlights)\n  .route(\"/bookmarks\", bookmarks)\n  .route(\"/lists\", lists)\n  .route(\"/tags\", tags)\n  .route(\"/users\", users)\n  .route(\"/assets\", assets)\n  .route(\"/admin\", admin)\n  .route(\"/rss\", rss)\n  .route(\"/backups\", backups);\n\nconst app = new Hono<{\n  Variables: {\n    // This is going to be coming from the web app\n    ctx: Context;\n  };\n}>()\n  .use(\n    loggerMiddleware((str: string) => {\n      logger.info(str);\n    }),\n  )\n  .use(poweredBy());\n\n// Add OpenTelemetry middleware if tracing is enabled\nif (serverConfig.tracing.enabled) {\n  app.use(\n    \"*\",\n    httpInstrumentationMiddleware({\n      serviceName: `${serverConfig.tracing.serviceName}-api`,\n      serviceVersion: serverConfig.serverVersion ?? \"unknown\",\n    }),\n  );\n}\n\napp\n  .use(\n    cors({\n      origin: \"*\",\n      allowHeaders: [\"Authorization\", \"Content-Type\"],\n      credentials: true,\n    }),\n  )\n  .use(\"*\", registerMetrics)\n  .use(async (c, next) => {\n    // Ensure that the ctx is set\n    if (!c.var.ctx) {\n      throw new Error(\"Context is not set\");\n    }\n    await next();\n  })\n  .use(trpcAdapter)\n  .route(\"/health\", health)\n  .route(\"/version\", version)\n  .route(\"/trpc\", trpc)\n  .route(\"/v1\", v1)\n  .route(\"/assets\", assets)\n  .route(\"/public\", publicRoute)\n  .route(\"/metrics\", metrics)\n  .route(\"/webhooks\", webhooks);\n\nexport default app;\n"
  },
  {
    "path": "packages/api/middlewares/auth.ts",
    "content": "import { createMiddleware } from \"hono/factory\";\nimport { HTTPException } from \"hono/http-exception\";\n\nimport { AuthedContext, Context, createCallerFactory } from \"@karakeep/trpc\";\nimport { appRouter } from \"@karakeep/trpc/routers/_app\";\n\nconst createCaller = createCallerFactory(appRouter);\n\nexport const unauthedMiddleware = createMiddleware<{\n  Variables: {\n    ctx: Context;\n    api: ReturnType<typeof createCaller>;\n  };\n}>(async (c, next) => {\n  if (!c.var.ctx) {\n    throw new HTTPException(401, {\n      message: \"Unauthorized\",\n    });\n  }\n  c.set(\"api\", createCaller(c.get(\"ctx\")));\n  await next();\n});\n\nexport const authMiddleware = createMiddleware<{\n  Variables: {\n    ctx: AuthedContext;\n    api: ReturnType<typeof createCaller>;\n  };\n}>(async (c, next) => {\n  if (!c.var.ctx || !c.var.ctx.user || c.var.ctx.user === null) {\n    throw new HTTPException(401, {\n      message: \"Unauthorized\",\n    });\n  }\n  c.set(\"api\", createCaller(c.get(\"ctx\")));\n  await next();\n});\n\nexport const adminAuthMiddleware = createMiddleware<{\n  Variables: {\n    ctx: AuthedContext;\n    api: ReturnType<typeof createCaller>;\n  };\n}>(async (c, next) => {\n  if (!c.var.ctx || !c.var.ctx.user || c.var.ctx.user === null) {\n    throw new HTTPException(401, {\n      message: \"Unauthorized\",\n    });\n  }\n\n  if (c.var.ctx.user.role !== \"admin\") {\n    throw new HTTPException(403, {\n      message: \"Forbidden - Admin access required\",\n    });\n  }\n\n  c.set(\"api\", createCaller(c.get(\"ctx\")));\n  await next();\n});\n"
  },
  {
    "path": "packages/api/middlewares/trpcAdapter.ts",
    "content": "import { TRPCError } from \"@trpc/server\";\nimport { createMiddleware } from \"hono/factory\";\nimport { HTTPException } from \"hono/http-exception\";\n\nfunction trpcCodeToHttpCode(code: TRPCError[\"code\"]) {\n  switch (code) {\n    case \"BAD_REQUEST\":\n    case \"PARSE_ERROR\":\n      return 400;\n    case \"UNAUTHORIZED\":\n      return 401;\n    case \"FORBIDDEN\":\n      return 403;\n    case \"NOT_FOUND\":\n      return 404;\n    case \"METHOD_NOT_SUPPORTED\":\n      return 405;\n    case \"TIMEOUT\":\n      return 408;\n    case \"PAYLOAD_TOO_LARGE\":\n      return 413;\n    case \"TOO_MANY_REQUESTS\":\n      return 429;\n    case \"INTERNAL_SERVER_ERROR\":\n      return 500;\n    default:\n      return 500;\n  }\n}\n\nconst trpcAdapter = createMiddleware(async (c, next) => {\n  await next();\n  const e = c.error;\n  if (e instanceof TRPCError) {\n    const code = trpcCodeToHttpCode(e.code);\n    const isInternalError = e.code === \"INTERNAL_SERVER_ERROR\";\n    const isProd = process.env.NODE_ENV === \"production\";\n    throw new HTTPException(code, {\n      message: isInternalError && isProd ? \"Internal server error\" : e.message,\n      cause: isInternalError && isProd ? undefined : e.cause,\n    });\n  }\n});\n\nexport default trpcAdapter;\n"
  },
  {
    "path": "packages/api/package.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/package.json\",\n  \"name\": \"@karakeep/api\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"typecheck\": \"tsc --noEmit\",\n    \"format\": \"oxfmt --check .\",\n    \"format:fix\": \"oxfmt .\",\n    \"lint\": \"oxlint .\",\n    \"lint:fix\": \"oxlint . --fix\",\n    \"test\": \"vitest\"\n  },\n  \"dependencies\": {\n    \"@hono/otel\": \"^1.1.0\",\n    \"@hono/prometheus\": \"^1.0.2\",\n    \"@hono/trpc-server\": \"^0.4.0\",\n    \"@hono/zod-validator\": \"^0.5.0\",\n    \"@karakeep/db\": \"workspace:*\",\n    \"@karakeep/shared\": \"workspace:*\",\n    \"@karakeep/shared-server\": \"workspace:*\",\n    \"@karakeep/trpc\": \"workspace:*\",\n    \"@trpc/server\": \"^11.9.0\",\n    \"drizzle-orm\": \"^0.44.2\",\n    \"file-type\": \"^21.2.0\",\n    \"hono\": \"^4.10.6\",\n    \"prom-client\": \"^15.1.3\",\n    \"rss\": \"^1.2.2\",\n    \"zod\": \"^3.24.2\"\n  },\n  \"devDependencies\": {\n    \"@karakeep/tsconfig\": \"workspace:^0.1.0\",\n    \"@types/bcryptjs\": \"^2.4.6\",\n    \"@types/deep-equal\": \"^1.0.4\",\n    \"@types/rss\": \"^0.0.32\",\n    \"vite-tsconfig-paths\": \"^4.3.1\",\n    \"vitest\": \"^3.2.4\"\n  }\n}\n"
  },
  {
    "path": "packages/api/routes/admin.ts",
    "content": "import { zValidator } from \"@hono/zod-validator\";\nimport { Hono } from \"hono\";\nimport { z } from \"zod\";\n\nimport { updateUserSchema } from \"@karakeep/shared/types/admin\";\n\nimport { adminAuthMiddleware } from \"../middlewares/auth\";\n\nconst app = new Hono()\n  .use(adminAuthMiddleware)\n\n  // PUT /admin/users/:userId\n  .put(\"/users/:userId\", zValidator(\"json\", updateUserSchema), async (c) => {\n    const userId = c.req.param(\"userId\");\n    const body = c.req.valid(\"json\");\n\n    // Ensure the userId from the URL matches the one in the body\n    const input = { ...body, userId };\n\n    await c.var.api.admin.updateUser(input);\n\n    return c.json({ success: true }, 200);\n  })\n\n  // POST /admin/jobs/trigger/recrawl\n  .post(\n    \"/jobs/trigger/recrawl\",\n    zValidator(\n      \"json\",\n      z.object({\n        crawlStatus: z\n          .enum([\"success\", \"failure\", \"pending\", \"all\"])\n          .default(\"all\"),\n        runInference: z.boolean().default(false),\n      }),\n    ),\n    async (c) => {\n      const body = c.req.valid(\"json\");\n      await c.var.api.admin.recrawlLinks(body);\n      return c.json({ success: true }, 200);\n    },\n  )\n\n  // POST /admin/jobs/trigger/reindex\n  .post(\"/jobs/trigger/reindex\", async (c) => {\n    await c.var.api.admin.reindexAllBookmarks();\n    return c.json({ success: true }, 200);\n  })\n\n  // POST /admin/jobs/trigger/inference\n  .post(\n    \"/jobs/trigger/inference\",\n    zValidator(\n      \"json\",\n      z.object({\n        type: z.enum([\"tag\", \"summarize\"]),\n        status: z.enum([\"success\", \"failure\", \"pending\", \"all\"]).default(\"all\"),\n      }),\n    ),\n    async (c) => {\n      const body = c.req.valid(\"json\");\n      await c.var.api.admin.reRunInferenceOnAllBookmarks(body);\n      return c.json({ success: true }, 200);\n    },\n  );\n\nexport default app;\n"
  },
  {
    "path": "packages/api/routes/assets.ts",
    "content": "import { zValidator } from \"@hono/zod-validator\";\nimport { Hono } from \"hono\";\nimport { z } from \"zod\";\n\nimport { Asset } from \"@karakeep/trpc/models/assets\";\n\nimport { authMiddleware } from \"../middlewares/auth\";\nimport { serveAsset } from \"../utils/assets\";\nimport { uploadAsset } from \"../utils/upload\";\n\nconst app = new Hono()\n  .use(authMiddleware)\n  .post(\n    \"/\",\n    zValidator(\n      \"form\",\n      z\n        .object({ file: z.instanceof(File) })\n        .or(z.object({ image: z.instanceof(File) })),\n    ),\n    async (c) => {\n      const body = c.req.valid(\"form\");\n      const up = await uploadAsset(c.var.ctx.user, c.var.ctx.db, body);\n      if (\"error\" in up) {\n        return c.json({ error: up.error }, up.status);\n      }\n      return c.json({\n        assetId: up.assetId,\n        contentType: up.contentType,\n        size: up.size,\n        fileName: up.fileName,\n      });\n    },\n  )\n  .get(\"/:assetId\", async (c) => {\n    const assetId = c.req.param(\"assetId\");\n\n    const asset = await Asset.fromId(c.var.ctx, assetId);\n    await asset.ensureCanView();\n\n    return await serveAsset(c, assetId, asset.asset.userId);\n  });\n\nexport default app;\n"
  },
  {
    "path": "packages/api/routes/backups.ts",
    "content": "import { Hono } from \"hono\";\n\nimport { authMiddleware } from \"../middlewares/auth\";\n\nconst app = new Hono()\n  .use(authMiddleware)\n\n  // GET /backups\n  .get(\"/\", async (c) => {\n    const backups = await c.var.api.backups.list();\n    return c.json(backups, 200);\n  })\n\n  // POST /backups\n  .post(\"/\", async (c) => {\n    const backup = await c.var.api.backups.triggerBackup();\n    return c.json(backup, 201);\n  })\n\n  // GET /backups/[backupId]\n  .get(\"/:backupId\", async (c) => {\n    const backupId = c.req.param(\"backupId\");\n    const backup = await c.var.api.backups.get({ backupId });\n    return c.json(backup, 200);\n  })\n\n  // GET /backups/[backupId]/download\n  .get(\"/:backupId/download\", async (c) => {\n    const backupId = c.req.param(\"backupId\");\n    const backup = await c.var.api.backups.get({ backupId });\n    if (!backup.assetId) {\n      return c.json({ error: \"Backup not found\" }, 404);\n    }\n    return c.redirect(`/api/assets/${backup.assetId}`);\n  })\n\n  // DELETE /backups/[backupId]\n  .delete(\"/:backupId\", async (c) => {\n    const backupId = c.req.param(\"backupId\");\n    await c.var.api.backups.delete({ backupId });\n    return c.body(null, 204);\n  });\n\nexport default app;\n"
  },
  {
    "path": "packages/api/routes/bookmarks.ts",
    "content": "import { zValidator } from \"@hono/zod-validator\";\nimport { Hono } from \"hono\";\nimport { z } from \"zod\";\n\nimport {\n  BookmarkTypes,\n  zAssetSchema,\n  zManipulatedTagSchema,\n  zNewBookmarkRequestSchema,\n  zUpdateBookmarksRequestSchema,\n} from \"@karakeep/shared/types/bookmarks\";\n\nimport { authMiddleware } from \"../middlewares/auth\";\nimport { adaptPagination, zPagination } from \"../utils/pagination\";\nimport {\n  zGetBookmarkQueryParamsSchema,\n  zGetBookmarkSearchParamsSchema,\n  zIncludeContentSearchParamsSchema,\n  zStringBool,\n} from \"../utils/types\";\nimport { uploadAsset } from \"../utils/upload\";\n\nconst app = new Hono()\n  .use(authMiddleware)\n\n  // GET /bookmarks\n  .get(\n    \"/\",\n    zValidator(\n      \"query\",\n      z\n        .object({\n          favourited: zStringBool.optional(),\n          archived: zStringBool.optional(),\n        })\n        .and(zGetBookmarkQueryParamsSchema)\n        .and(zPagination),\n    ),\n    async (c) => {\n      const searchParams = c.req.valid(\"query\");\n      const bookmarks = await c.var.api.bookmarks.getBookmarks(searchParams);\n      return c.json(adaptPagination(bookmarks), 200);\n    },\n  )\n\n  // POST /bookmarks\n  .post(\"/\", zValidator(\"json\", zNewBookmarkRequestSchema), async (c) => {\n    const body = c.req.valid(\"json\");\n    const bookmark = await c.var.api.bookmarks.createBookmark({\n      ...body,\n      source: body.source || \"api\",\n    });\n    return c.json(bookmark, bookmark.alreadyExists ? 200 : 201);\n  })\n\n  // GET /bookmarks/search\n  .get(\n    \"/search\",\n    zValidator(\n      \"query\",\n      z\n        .object({\n          q: z.string(),\n          limit: z.coerce.number().optional(),\n          cursor: z\n            .string()\n            .optional()\n            .transform((val) =>\n              val ? { ver: 1 as const, offset: parseInt(val) } : undefined,\n            ),\n        })\n        .and(zGetBookmarkSearchParamsSchema),\n    ),\n    async (c) => {\n      const searchParams = c.req.valid(\"query\");\n      const bookmarks = await c.var.api.bookmarks.searchBookmarks({\n        text: searchParams.q,\n        cursor: searchParams.cursor,\n        limit: searchParams.limit,\n        includeContent: searchParams.includeContent,\n      });\n      return c.json(\n        {\n          bookmarks: bookmarks.bookmarks,\n          nextCursor: bookmarks.nextCursor\n            ? `${bookmarks.nextCursor.offset}`\n            : null,\n        },\n        200,\n      );\n    },\n  )\n\n  // GET /bookmarks/check-url\n  .get(\n    \"/check-url\",\n    zValidator(\n      \"query\",\n      z.object({\n        url: z.string(),\n      }),\n    ),\n    async (c) => {\n      const { url } = c.req.valid(\"query\");\n      const result = await c.var.api.bookmarks.checkUrl({ url });\n      return c.json(result, 200);\n    },\n  )\n\n  .post(\n    \"/singlefile\",\n    zValidator(\n      \"query\",\n      z.object({\n        ifexists: z\n          .enum([\n            \"skip\",\n            \"overwrite\",\n            \"overwrite-recrawl\",\n            \"append\",\n            \"append-recrawl\",\n          ])\n          .optional()\n          .default(\"skip\"),\n      }),\n    ),\n    zValidator(\n      \"form\",\n      z.object({\n        url: z.string(),\n        file: z.instanceof(File),\n      }),\n    ),\n    async (c) => {\n      const form = c.req.valid(\"form\");\n      const up = await uploadAsset(c.var.ctx.user, c.var.ctx.db, form);\n      if (\"error\" in up) {\n        return c.json({ error: up.error }, up.status);\n      }\n      const bookmark = await c.var.api.bookmarks.createBookmark({\n        type: BookmarkTypes.LINK,\n        url: form.url,\n        precrawledArchiveId: up.assetId,\n        source: \"singlefile\",\n      });\n      if (bookmark.alreadyExists) {\n        const ifexists = c.req.valid(\"query\").ifexists;\n        switch (ifexists) {\n          case \"skip\":\n            break;\n          case \"overwrite-recrawl\":\n          case \"overwrite\": {\n            const existingPrecrawledArchiveId = bookmark.assets\n              .filter((a) => a.assetType == \"precrawledArchive\")\n              .at(-1)?.id;\n            if (existingPrecrawledArchiveId) {\n              await c.var.api.assets.replaceAsset({\n                bookmarkId: bookmark.id,\n                oldAssetId: existingPrecrawledArchiveId,\n                newAssetId: up.assetId,\n              });\n            } else {\n              await c.var.api.assets.attachAsset({\n                bookmarkId: bookmark.id,\n                asset: {\n                  id: up.assetId,\n                  assetType: \"precrawledArchive\",\n                },\n              });\n            }\n            if (ifexists == \"overwrite-recrawl\") {\n              await c.var.api.bookmarks.recrawlBookmark({\n                bookmarkId: bookmark.id,\n              });\n            }\n            break;\n          }\n          case \"append-recrawl\":\n          case \"append\": {\n            await c.var.api.assets.attachAsset({\n              bookmarkId: bookmark.id,\n              asset: {\n                id: up.assetId,\n                assetType: \"precrawledArchive\",\n              },\n            });\n            if (ifexists == \"append-recrawl\") {\n              await c.var.api.bookmarks.recrawlBookmark({\n                bookmarkId: bookmark.id,\n              });\n            }\n            break;\n          }\n        }\n        return c.json(bookmark, 200);\n      } else {\n        return c.json(bookmark, 201);\n      }\n    },\n  )\n\n  // GET /bookmarks/[bookmarkId]\n  .get(\n    \"/:bookmarkId\",\n    zValidator(\"query\", zIncludeContentSearchParamsSchema),\n    async (c) => {\n      const bookmarkId = c.req.param(\"bookmarkId\");\n      const searchParams = c.req.valid(\"query\");\n      const bookmark = await c.var.api.bookmarks.getBookmark({\n        bookmarkId,\n        includeContent: searchParams.includeContent,\n      });\n      return c.json(bookmark, 200);\n    },\n  )\n\n  // PATCH /bookmarks/[bookmarkId]\n  .patch(\n    \"/:bookmarkId\",\n    zValidator(\n      \"json\",\n      zUpdateBookmarksRequestSchema.omit({ bookmarkId: true }),\n    ),\n    async (c) => {\n      const bookmarkId = c.req.param(\"bookmarkId\");\n      const body = c.req.valid(\"json\");\n      const bookmark = await c.var.api.bookmarks.updateBookmark({\n        bookmarkId,\n        ...body,\n      });\n      return c.json(bookmark, 200);\n    },\n  )\n\n  // DELETE /bookmarks/[bookmarkId]\n  .delete(\"/:bookmarkId\", async (c) => {\n    const bookmarkId = c.req.param(\"bookmarkId\");\n    await c.var.api.bookmarks.deleteBookmark({ bookmarkId });\n    return c.body(null, 204);\n  })\n\n  // GET /bookmarks/[bookmarkId]/lists\n  .get(\"/:bookmarkId/lists\", async (c) => {\n    const bookmarkId = c.req.param(\"bookmarkId\");\n    const resp = await c.var.api.lists.getListsOfBookmark({ bookmarkId });\n    return c.json(resp, 200);\n  })\n\n  // GET /bookmarks/[bookmarkId]/assets\n  .get(\"/:bookmarkId/assets\", async (c) => {\n    const bookmarkId = c.req.param(\"bookmarkId\");\n    const resp = await c.var.api.bookmarks.getBookmark({ bookmarkId });\n    return c.json({ assets: resp.assets }, 200);\n  })\n\n  // POST /bookmarks/[bookmarkId]/assets\n  .post(\"/:bookmarkId/assets\", zValidator(\"json\", zAssetSchema), async (c) => {\n    const bookmarkId = c.req.param(\"bookmarkId\");\n    const body = c.req.valid(\"json\");\n    const asset = await c.var.api.assets.attachAsset({\n      bookmarkId,\n      asset: body,\n    });\n    return c.json(asset, 201);\n  })\n\n  // PUT /bookmarks/[bookmarkId]/assets/[assetId]\n  .put(\n    \"/:bookmarkId/assets/:assetId\",\n    zValidator(\"json\", z.object({ assetId: z.string() })),\n    async (c) => {\n      const bookmarkId = c.req.param(\"bookmarkId\");\n      const assetId = c.req.param(\"assetId\");\n      const body = c.req.valid(\"json\");\n      await c.var.api.assets.replaceAsset({\n        bookmarkId,\n        oldAssetId: assetId,\n        newAssetId: body.assetId,\n      });\n      return c.body(null, 204);\n    },\n  )\n\n  // DELETE /bookmarks/[bookmarkId]/assets/[assetId]\n  .delete(\"/:bookmarkId/assets/:assetId\", async (c) => {\n    const bookmarkId = c.req.param(\"bookmarkId\");\n    const assetId = c.req.param(\"assetId\");\n    await c.var.api.assets.detachAsset({ bookmarkId, assetId });\n    return c.body(null, 204);\n  })\n\n  // POST /bookmarks/[bookmarkId]/tags\n  .post(\n    \"/:bookmarkId/tags\",\n    zValidator(\"json\", z.object({ tags: z.array(zManipulatedTagSchema) })),\n    async (c) => {\n      const bookmarkId = c.req.param(\"bookmarkId\");\n      const body = c.req.valid(\"json\");\n      const resp = await c.var.api.bookmarks.updateTags({\n        bookmarkId,\n        attach: body.tags,\n        detach: [],\n      });\n      return c.json({ attached: resp.attached }, 200);\n    },\n  )\n\n  // DELETE /bookmarks/[bookmarkId]/tags\n  .delete(\n    \"/:bookmarkId/tags\",\n    zValidator(\"json\", z.object({ tags: z.array(zManipulatedTagSchema) })),\n    async (c) => {\n      const bookmarkId = c.req.param(\"bookmarkId\");\n      const body = c.req.valid(\"json\");\n      const resp = await c.var.api.bookmarks.updateTags({\n        bookmarkId,\n        detach: body.tags,\n        attach: [],\n      });\n      return c.json({ detached: resp.detached }, 200);\n    },\n  )\n\n  // POST /bookmarks/[bookmarkId]/summarize\n  .post(\"/:bookmarkId/summarize\", async (c) => {\n    const bookmarkId = c.req.param(\"bookmarkId\");\n    const bookmark = await c.var.api.bookmarks.summarizeBookmark({\n      bookmarkId,\n    });\n    return c.json(bookmark, 200);\n  })\n\n  // GET /bookmarks/[bookmarkId]/highlights\n  .get(\"/:bookmarkId/highlights\", async (c) => {\n    const bookmarkId = c.req.param(\"bookmarkId\");\n    const resp = await c.var.api.highlights.getForBookmark({ bookmarkId });\n    return c.json(resp, 200);\n  });\n\nexport default app;\n"
  },
  {
    "path": "packages/api/routes/health.ts",
    "content": "import { Hono } from \"hono\";\n\nimport { Context } from \"@karakeep/trpc\";\n\nconst health = new Hono<{\n  Variables: {\n    ctx: Context;\n  };\n}>().get(\"/\", (c) => {\n  return c.json({\n    status: \"ok\",\n    message: \"Web app is working\",\n  });\n});\n\nexport default health;\n"
  },
  {
    "path": "packages/api/routes/highlights.ts",
    "content": "import { zValidator } from \"@hono/zod-validator\";\nimport { Hono } from \"hono\";\n\nimport {\n  zNewHighlightSchema,\n  zUpdateHighlightSchema,\n} from \"@karakeep/shared/types/highlights\";\n\nimport { authMiddleware } from \"../middlewares/auth\";\nimport { adaptPagination, zPagination } from \"../utils/pagination\";\n\nconst app = new Hono()\n  .use(authMiddleware)\n  .get(\"/\", zValidator(\"query\", zPagination), async (c) => {\n    const searchParams = c.req.valid(\"query\");\n    const resp = await c.var.api.highlights.getAll({\n      ...searchParams,\n    });\n    return c.json(adaptPagination(resp));\n  })\n  .post(\"/\", zValidator(\"json\", zNewHighlightSchema), async (c) => {\n    const body = c.req.valid(\"json\");\n    const resp = await c.var.api.highlights.create(body);\n    return c.json(resp, 201);\n  })\n  .get(\"/:highlightId\", async (c) => {\n    const highlightId = c.req.param(\"highlightId\");\n    const highlight = await c.var.api.highlights.get({\n      highlightId,\n    });\n    return c.json(highlight, 200);\n  })\n  .patch(\n    \"/:highlightId\",\n    zValidator(\"json\", zUpdateHighlightSchema.omit({ highlightId: true })),\n    async (c) => {\n      const highlightId = c.req.param(\"highlightId\");\n      const body = c.req.valid(\"json\");\n      const highlight = await c.var.api.highlights.update({\n        highlightId,\n        ...body,\n      });\n      return c.json(highlight, 200);\n    },\n  )\n  .delete(\"/:highlightId\", async (c) => {\n    const highlightId = c.req.param(\"highlightId\");\n    const highlight = await c.var.api.highlights.delete({\n      highlightId,\n    });\n    return c.json(highlight, 200);\n  });\n\nexport default app;\n"
  },
  {
    "path": "packages/api/routes/lists.ts",
    "content": "import { zValidator } from \"@hono/zod-validator\";\nimport { Hono } from \"hono\";\n\nimport {\n  zEditBookmarkListSchema,\n  zNewBookmarkListSchema,\n} from \"@karakeep/shared/types/lists\";\n\nimport { authMiddleware } from \"../middlewares/auth\";\nimport { adaptPagination, zPagination } from \"../utils/pagination\";\nimport { zGetBookmarkQueryParamsSchema } from \"../utils/types\";\n\nconst app = new Hono()\n  .use(authMiddleware)\n  .get(\"/\", async (c) => {\n    const lists = await c.var.api.lists.list();\n    return c.json(lists, 200);\n  })\n  .post(\"/\", zValidator(\"json\", zNewBookmarkListSchema), async (c) => {\n    const body = c.req.valid(\"json\");\n    const list = await c.var.api.lists.create(body);\n    return c.json(list, 201);\n  })\n  .get(\"/:listId\", async (c) => {\n    const listId = c.req.param(\"listId\");\n    const list = await c.var.api.lists.get({ listId });\n    return c.json(list, 200);\n  })\n  .patch(\n    \"/:listId\",\n    zValidator(\"json\", zEditBookmarkListSchema.omit({ listId: true })),\n    async (c) => {\n      const listId = c.req.param(\"listId\");\n      const body = c.req.valid(\"json\");\n      const list = await c.var.api.lists.edit({ ...body, listId });\n      return c.json(list, 200);\n    },\n  )\n  .delete(\"/:listId\", async (c) => {\n    const listId = c.req.param(\"listId\");\n    await c.var.api.lists.delete({ listId });\n    return c.body(null, 204);\n  })\n  .get(\n    \"/:listId/bookmarks\",\n    zValidator(\"query\", zPagination.and(zGetBookmarkQueryParamsSchema)),\n    async (c) => {\n      const listId = c.req.param(\"listId\");\n      const searchParams = c.req.valid(\"query\");\n      const bookmarks = await c.var.api.bookmarks.getBookmarks({\n        listId,\n        ...searchParams,\n      });\n      return c.json(adaptPagination(bookmarks), 200);\n    },\n  )\n  .put(\"/:listId/bookmarks/:bookmarkId\", async (c) => {\n    const listId = c.req.param(\"listId\");\n    const bookmarkId = c.req.param(\"bookmarkId\");\n    await c.var.api.lists.addToList({ listId, bookmarkId });\n    return c.body(null, 204);\n  })\n  .delete(\"/:listId/bookmarks/:bookmarkId\", async (c) => {\n    const listId = c.req.param(\"listId\");\n    const bookmarkId = c.req.param(\"bookmarkId\");\n    await c.var.api.lists.removeFromList({ listId, bookmarkId });\n    return c.body(null, 204);\n  });\n\nexport default app;\n"
  },
  {
    "path": "packages/api/routes/metrics.ts",
    "content": "// Import stats to register Prometheus metrics\nimport \"@karakeep/trpc/stats\";\n\nimport { prometheus } from \"@hono/prometheus\";\nimport { Hono } from \"hono\";\nimport { bearerAuth } from \"hono/bearer-auth\";\nimport { register } from \"prom-client\";\n\nimport serverConfig from \"@karakeep/shared/config\";\n\nexport const { printMetrics, registerMetrics } = prometheus({\n  registry: register,\n  prefix: \"karakeep_\",\n  collectDefaultMetrics: true,\n});\n\nconst app = new Hono().get(\n  \"/\",\n  bearerAuth({ token: serverConfig.prometheus.metricsToken }),\n  printMetrics,\n);\n\nexport default app;\n"
  },
  {
    "path": "packages/api/routes/public/assets.ts",
    "content": "import { zValidator } from \"@hono/zod-validator\";\nimport { and, eq } from \"drizzle-orm\";\nimport { Hono } from \"hono\";\nimport { z } from \"zod\";\n\nimport { assets } from \"@karakeep/db/schema\";\nimport serverConfig from \"@karakeep/shared/config\";\nimport { verifySignedToken } from \"@karakeep/shared/signedTokens\";\nimport { zAssetSignedTokenSchema } from \"@karakeep/shared/types/assets\";\n\nimport { unauthedMiddleware } from \"../../middlewares/auth\";\nimport { serveAsset } from \"../../utils/assets\";\n\nconst app = new Hono()\n  // Public assets, they require signed token for auth\n  .get(\n    \"/:assetId\",\n    unauthedMiddleware,\n    zValidator(\n      \"query\",\n      z.object({\n        token: z.string(),\n      }),\n    ),\n    async (c) => {\n      const assetId = c.req.param(\"assetId\");\n      const tokenPayload = verifySignedToken(\n        c.req.valid(\"query\").token,\n        serverConfig.signingSecret(),\n        zAssetSignedTokenSchema,\n      );\n      if (!tokenPayload) {\n        return c.json({ error: \"Invalid or expired token\" }, { status: 403 });\n      }\n      if (tokenPayload.assetId !== assetId) {\n        return c.json({ error: \"Invalid or expired token\" }, { status: 403 });\n      }\n      const userId = tokenPayload.userId;\n\n      const assetDb = await c.var.ctx.db.query.assets.findFirst({\n        where: and(eq(assets.id, assetId), eq(assets.userId, userId)),\n      });\n\n      if (!assetDb) {\n        return c.json({ error: \"Asset not found\" }, { status: 404 });\n      }\n      return await serveAsset(c, assetId, userId);\n    },\n  );\n\nexport default app;\n"
  },
  {
    "path": "packages/api/routes/public.ts",
    "content": "import { Hono } from \"hono\";\n\nimport assets from \"./public/assets\";\n\nconst app = new Hono().route(\"/assets\", assets);\n\nexport default app;\n"
  },
  {
    "path": "packages/api/routes/rss.ts",
    "content": "import { zValidator } from \"@hono/zod-validator\";\nimport { Hono } from \"hono\";\nimport { z } from \"zod\";\n\nimport serverConfig from \"@karakeep/shared/config\";\nimport { MAX_NUM_BOOKMARKS_PER_PAGE } from \"@karakeep/shared/types/bookmarks\";\nimport { List } from \"@karakeep/trpc/models/lists\";\n\nimport { unauthedMiddleware } from \"../middlewares/auth\";\nimport { toRSS } from \"../utils/rss\";\n\nconst app = new Hono().get(\n  \"/lists/:listId\",\n  zValidator(\n    \"query\",\n    z.object({\n      token: z.string().min(1).optional(),\n      limit: z.coerce\n        .number()\n        .min(1)\n        .max(MAX_NUM_BOOKMARKS_PER_PAGE)\n        .optional(),\n    }),\n  ),\n  unauthedMiddleware,\n  async (c) => {\n    const listId = c.req.param(\"listId\");\n    const searchParams = c.req.valid(\"query\");\n    const token = searchParams.token;\n\n    const res = await List.getPublicListContents(\n      c.var.ctx,\n      listId,\n      token ?? null,\n      {\n        limit: searchParams.limit ?? 20,\n        order: \"desc\",\n        cursor: null,\n      },\n    );\n    const list = res.list;\n\n    const rssFeed = toRSS(\n      {\n        title: `Bookmarks from ${list.icon} ${list.name}`,\n        feedUrl: `${serverConfig.publicApiUrl}/v1/rss/lists/${listId}`,\n        siteUrl: `${serverConfig.publicUrl}/dashboard/lists/${listId}`,\n        description: list.description ?? undefined,\n      },\n      res.bookmarks,\n    );\n\n    c.header(\"Content-Type\", \"application/rss+xml\");\n    return c.body(rssFeed);\n  },\n);\n\nexport default app;\n"
  },
  {
    "path": "packages/api/routes/tags.ts",
    "content": "import { zValidator } from \"@hono/zod-validator\";\nimport { Hono } from \"hono\";\nimport { z } from \"zod\";\n\nimport {\n  zCreateTagRequestSchema,\n  zTagListApiResultSchema,\n  zTagListQueryParamsSchema,\n  zUpdateTagRequestSchema,\n} from \"@karakeep/shared/types/tags\";\n\nimport { authMiddleware } from \"../middlewares/auth\";\nimport { adaptPagination, zPagination } from \"../utils/pagination\";\nimport { zGetBookmarkQueryParamsSchema } from \"../utils/types\";\n\nconst app = new Hono()\n  .use(authMiddleware)\n\n  // GET /tags\n  .get(\"/\", zValidator(\"query\", zTagListQueryParamsSchema), async (c) => {\n    const searchParams = c.req.valid(\"query\");\n    const tags = await c.var.api.tags.list({\n      nameContains: searchParams.nameContains,\n      attachedBy: searchParams.attachedBy,\n      sortBy: searchParams.sort,\n      cursor: searchParams.cursor,\n      limit: searchParams.limit,\n    });\n\n    const resp: z.infer<typeof zTagListApiResultSchema> = {\n      tags: tags.tags,\n      nextCursor: tags.nextCursor\n        ? Buffer.from(JSON.stringify(tags.nextCursor)).toString(\"base64url\")\n        : null,\n    };\n    return c.json(resp, 200);\n  })\n\n  // POST /tags\n  .post(\"/\", zValidator(\"json\", zCreateTagRequestSchema), async (c) => {\n    const body = c.req.valid(\"json\");\n    const tags = await c.var.api.tags.create(body);\n    return c.json(tags, 201);\n  })\n\n  // GET /tags/[tagId]\n  .get(\"/:tagId\", async (c) => {\n    const tagId = c.req.param(\"tagId\");\n    const tag = await c.var.api.tags.get({ tagId });\n    return c.json(tag, 200);\n  })\n\n  // PATCH /tags/[tagId]\n  .patch(\n    \"/:tagId\",\n    zValidator(\"json\", zUpdateTagRequestSchema.omit({ tagId: true })),\n    async (c) => {\n      const tagId = c.req.param(\"tagId\");\n      const body = c.req.valid(\"json\");\n      const tag = await c.var.api.tags.update({ tagId, ...body });\n      return c.json(tag, 200);\n    },\n  )\n\n  // DELETE /tags/[tagId]\n  .delete(\"/:tagId\", async (c) => {\n    const tagId = c.req.param(\"tagId\");\n    await c.var.api.tags.delete({ tagId });\n    return c.body(null, 204);\n  })\n\n  // GET /tags/[tagId]/bookmarks\n  .get(\n    \"/:tagId/bookmarks\",\n    zValidator(\"query\", zPagination.and(zGetBookmarkQueryParamsSchema)),\n    async (c) => {\n      const tagId = c.req.param(\"tagId\");\n      const searchParams = c.req.valid(\"query\");\n      const bookmarks = await c.var.api.bookmarks.getBookmarks({\n        tagId,\n        ...searchParams,\n      });\n      return c.json(adaptPagination(bookmarks), 200);\n    },\n  );\n\nexport default app;\n"
  },
  {
    "path": "packages/api/routes/trpc.ts",
    "content": "import { trpcServer } from \"@hono/trpc-server\";\nimport { Hono } from \"hono\";\n\nimport logger from \"@karakeep/shared/logger\";\nimport { Context } from \"@karakeep/trpc\";\nimport { appRouter } from \"@karakeep/trpc/routers/_app\";\n\nconst trpc = new Hono<{\n  Variables: {\n    ctx: Context;\n  };\n}>().use(\n  \"/*\",\n  trpcServer({\n    endpoint: \"/api/trpc\",\n    router: appRouter,\n    createContext: (_, c) => {\n      return c.var.ctx;\n    },\n    onError: ({ path, error }) => {\n      if (error.code === \"INTERNAL_SERVER_ERROR\") {\n        logger.error(`tRPC failed on ${path}: ${error.message}`);\n        if (error.stack) {\n          logger.error(error.stack);\n        }\n      }\n    },\n  }),\n);\n\nexport default trpc;\n"
  },
  {
    "path": "packages/api/routes/users.ts",
    "content": "import { Hono } from \"hono\";\n\nimport { authMiddleware } from \"../middlewares/auth\";\n\nconst app = new Hono()\n  .use(authMiddleware)\n\n  // GET /users/me\n  .get(\"/me\", async (c) => {\n    const user = await c.var.api.users.whoami();\n    return c.json(user, 200);\n  })\n\n  // GET /users/me/stats\n  .get(\"/me/stats\", async (c) => {\n    const stats = await c.var.api.users.stats();\n    return c.json(stats, 200);\n  });\n\nexport default app;\n"
  },
  {
    "path": "packages/api/routes/version.ts",
    "content": "import { Hono } from \"hono\";\n\nimport serverConfig from \"@karakeep/shared/config\";\nimport { Context } from \"@karakeep/trpc\";\n\nconst version = new Hono<{\n  Variables: {\n    ctx: Context;\n  };\n}>().get(\"/\", (c) => {\n  return c.json({\n    version: serverConfig.serverVersion ?? \"unknown\",\n  });\n});\n\nexport default version;\n"
  },
  {
    "path": "packages/api/routes/webhooks.ts",
    "content": "import { Hono } from \"hono\";\n\nimport { Context, createCallerFactory } from \"@karakeep/trpc\";\nimport { appRouter } from \"@karakeep/trpc/routers/_app\";\n\nconst createCaller = createCallerFactory(appRouter);\n\nconst app = new Hono<{\n  Variables: {\n    ctx: Context;\n  };\n}>().post(\"/stripe\", async (c) => {\n  const body = await c.req.text();\n  const signature = c.req.header(\"stripe-signature\");\n\n  if (!signature) {\n    return c.json({ error: \"Missing stripe-signature header\" }, 400);\n  }\n\n  try {\n    const api = createCaller(c.get(\"ctx\"));\n    const result = await api.subscriptions.handleWebhook({\n      body,\n      signature,\n    });\n\n    return c.json(result);\n  } catch (error) {\n    console.error(\"Webhook processing failed:\", error);\n\n    if (error instanceof Error) {\n      if (error.message.includes(\"Invalid signature\")) {\n        return c.json({ error: \"Invalid signature\" }, 400);\n      }\n      if (error.message.includes(\"not configured\")) {\n        return c.json({ error: \"Stripe is not configured\" }, 400);\n      }\n    }\n\n    return c.json({ error: \"Internal server error\" }, 500);\n  }\n});\n\nexport default app;\n"
  },
  {
    "path": "packages/api/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@karakeep/tsconfig/node.json\",\n  \"include\": [\n    \"**/*.ts\"\n  ],\n  \"exclude\": [\n    \"node_modules\"\n  ],\n  \"compilerOptions\": {\n    \"tsBuildInfoFile\": \"node_modules/.cache/tsbuildinfo.json\"\n  }\n}\n"
  },
  {
    "path": "packages/api/utils/assets.ts",
    "content": "import { Context } from \"hono\";\nimport { stream } from \"hono/streaming\";\n\nimport {\n  createAssetReadStream,\n  getAssetSize,\n  readAssetMetadata,\n} from \"@karakeep/shared/assetdb\";\n\nimport { toWebReadableStream } from \"./upload\";\n\nexport async function serveAsset(c: Context, assetId: string, userId: string) {\n  const [metadata, size] = await Promise.all([\n    readAssetMetadata({\n      userId,\n      assetId,\n    }),\n\n    getAssetSize({\n      userId,\n      assetId,\n    }),\n  ]);\n\n  // Default Headers\n  c.header(\"Content-type\", metadata.contentType);\n  c.header(\"X-Content-Type-Options\", \"nosniff\");\n  c.header(\"Cache-Control\", \"private, max-age=31536000, immutable\");\n  c.header(\n    \"Content-Security-Policy\",\n    [\n      \"sandbox\",\n      \"default-src 'none'\",\n      \"base-uri 'none'\",\n      \"form-action 'none'\",\n      \"img-src https: data: blob:\",\n      \"style-src 'unsafe-inline' https:\",\n      \"connect-src 'none'\",\n      \"media-src https: data: blob:\",\n      \"object-src 'none'\",\n      \"frame-src 'none'\",\n    ].join(\"; \"),\n  );\n\n  const range = c.req.header(\"Range\");\n  if (range) {\n    const parts = range.replace(/bytes=/, \"\").split(\"-\");\n    const start = parseInt(parts[0], 10);\n    const end = parts[1] ? parseInt(parts[1], 10) : size - 1;\n\n    const fStream = await createAssetReadStream({\n      userId,\n      assetId,\n      start,\n      end,\n    });\n    c.status(206); // Partial Content\n    c.header(\"Content-Range\", `bytes ${start}-${end}/${size}`);\n    c.header(\"Accept-Ranges\", \"bytes\");\n    c.header(\"Content-Length\", (end - start + 1).toString());\n    return stream(c, async (stream) => {\n      await stream.pipe(toWebReadableStream(fStream));\n    });\n  } else {\n    const fStream = await createAssetReadStream({\n      userId,\n      assetId,\n    });\n    c.status(200);\n    c.header(\"Content-Length\", size.toString());\n    return stream(c, async (stream) => {\n      await stream.pipe(toWebReadableStream(fStream));\n    });\n  }\n}\n"
  },
  {
    "path": "packages/api/utils/pagination.ts",
    "content": "import { z } from \"zod\";\n\nimport { MAX_NUM_BOOKMARKS_PER_PAGE } from \"@karakeep/shared/types/bookmarks\";\nimport { zCursorV2 } from \"@karakeep/shared/types/pagination\";\n\nexport const zPagination = z.object({\n  limit: z.coerce.number().max(MAX_NUM_BOOKMARKS_PER_PAGE).optional(),\n  cursor: z\n    .string()\n    .refine((val) => val.includes(\"_\"), \"Must be a valid cursor\")\n    .transform((val) => {\n      const [id, createdAt] = val.split(\"_\");\n      return { id, createdAt };\n    })\n    .pipe(z.object({ id: z.string(), createdAt: z.coerce.date() }))\n    .optional(),\n});\n\nexport function adaptPagination<\n  T extends { nextCursor: z.infer<typeof zCursorV2> | null },\n>(input: T) {\n  const { nextCursor, ...rest } = input;\n  if (!nextCursor) {\n    return input;\n  }\n  return {\n    ...rest,\n    nextCursor: `${nextCursor.id}_${nextCursor.createdAt.toISOString()}`,\n  };\n}\n"
  },
  {
    "path": "packages/api/utils/rss.ts",
    "content": "import RSS from \"rss\";\n\nimport serverConfig from \"@karakeep/shared/config\";\nimport {\n  BookmarkTypes,\n  ZPublicBookmark,\n} from \"@karakeep/shared/types/bookmarks\";\nimport { getAssetUrl } from \"@karakeep/shared/utils/assetUtils\";\n\nexport function toRSS(\n  params: {\n    title: string;\n    description?: string;\n    feedUrl: string;\n    siteUrl: string;\n  },\n  bookmarks: ZPublicBookmark[],\n) {\n  const feed = new RSS({\n    title: params.title,\n    feed_url: params.feedUrl,\n    site_url: params.siteUrl,\n    description: params.description,\n    generator: \"Karakeep\",\n  });\n\n  bookmarks\n    .filter(\n      (b) =>\n        b.content.type === BookmarkTypes.LINK ||\n        b.content.type === BookmarkTypes.ASSET,\n    )\n    .forEach((bookmark) => {\n      feed.item({\n        date: bookmark.createdAt,\n        title: bookmark.title ?? \"\",\n        url:\n          bookmark.content.type === BookmarkTypes.LINK\n            ? bookmark.content.url\n            : bookmark.content.type === BookmarkTypes.ASSET\n              ? `${serverConfig.publicUrl}${getAssetUrl(bookmark.content.assetId)}`\n              : \"\",\n        guid: bookmark.id,\n        author:\n          bookmark.content.type === BookmarkTypes.LINK\n            ? (bookmark.content.author ?? undefined)\n            : undefined,\n        categories: bookmark.tags,\n        description: bookmark.description ?? \"\",\n      });\n    });\n\n  return feed.xml({ indent: true });\n}\n"
  },
  {
    "path": "packages/api/utils/types.ts",
    "content": "import { z } from \"zod\";\n\nimport { zSortOrder } from \"@karakeep/shared/types/bookmarks\";\n\nexport const zStringBool = z\n  .string()\n  .refine((val) => val === \"true\" || val === \"false\", \"Must be true or false\")\n  .transform((val) => val === \"true\");\n\nexport const zIncludeContentSearchParamsSchema = z.object({\n  includeContent: zStringBool.optional().default(\"false\"),\n});\n\nexport const zGetBookmarkQueryParamsSchema = z\n  .object({\n    sortOrder: zSortOrder\n      .exclude([zSortOrder.Enum.relevance])\n      .optional()\n      .default(zSortOrder.Enum.desc),\n  })\n  .merge(zIncludeContentSearchParamsSchema);\n\nexport const zGetBookmarkSearchParamsSchema = z\n  .object({\n    sortOrder: zSortOrder.optional().default(zSortOrder.Enum.relevance),\n  })\n  .merge(zIncludeContentSearchParamsSchema);\n"
  },
  {
    "path": "packages/api/utils/upload.ts",
    "content": "import * as fs from \"fs\";\nimport * as os from \"os\";\nimport * as path from \"path\";\nimport { Readable } from \"stream\";\nimport { pipeline } from \"stream/promises\";\nimport { fileTypeFromBlob, supportedMimeTypes } from \"file-type\";\n\nimport { assets, AssetTypes } from \"@karakeep/db/schema\";\nimport { QuotaService, StorageQuotaError } from \"@karakeep/shared-server\";\nimport {\n  newAssetId,\n  saveAssetFromFile,\n  SUPPORTED_UPLOAD_ASSET_TYPES,\n} from \"@karakeep/shared/assetdb\";\nimport serverConfig from \"@karakeep/shared/config\";\nimport { AuthedContext } from \"@karakeep/trpc\";\n\nconst MAX_UPLOAD_SIZE_BYTES = serverConfig.maxAssetSizeMb * 1024 * 1024;\n\n// Helper to convert Web Stream to Node Stream (requires Node >= 16.5 / 14.18)\nexport function webStreamToNode(\n  webStream: ReadableStream<Uint8Array>,\n): Readable {\n  // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any\n  return Readable.fromWeb(webStream as any); // Type assertion might be needed\n}\n\nexport function toWebReadableStream(\n  nodeStream: NodeJS.ReadableStream,\n): ReadableStream<Uint8Array> {\n  const reader = nodeStream as unknown as Readable;\n\n  return new ReadableStream({\n    start(controller) {\n      reader.on(\"data\", (chunk) => controller.enqueue(new Uint8Array(chunk)));\n      reader.on(\"end\", () => controller.close());\n      reader.on(\"error\", (err) => controller.error(err));\n    },\n  });\n}\n\nexport async function uploadAsset(\n  user: AuthedContext[\"user\"],\n  db: AuthedContext[\"db\"],\n  formData: { file: File } | { image: File },\n): Promise<\n  | { error: string; status: 400 | 413 | 403 }\n  | {\n      assetId: string;\n      contentType: string;\n      fileName: string;\n      size: number;\n    }\n> {\n  let data: File;\n  if (\"file\" in formData) {\n    data = formData.file;\n  } else {\n    data = formData.image;\n  }\n\n  const detectedType = await fileTypeFromBlob(data);\n  const fallbackType =\n    data.type && data.type.trim().length > 0 ? data.type : null;\n  // Security: reject browser-provided MIME when we cannot sniff a valid type.\n  if (fallbackType && supportedMimeTypes.has(fallbackType) && !detectedType) {\n    return { error: \"Unsupported asset type\", status: 400 };\n  }\n  const contentType =\n    detectedType?.mime ?? fallbackType ?? \"application/octet-stream\";\n\n  // Replace all non-ascii characters with underscores\n  const fileName = data.name.replace(/[^\\x20-\\x7E]/g, \"_\");\n  if (!SUPPORTED_UPLOAD_ASSET_TYPES.has(contentType)) {\n    return { error: \"Unsupported asset type\", status: 400 };\n  }\n  if (data.size > MAX_UPLOAD_SIZE_BYTES) {\n    return { error: \"Asset is too big\", status: 413 };\n  }\n\n  let quotaApproved;\n  try {\n    quotaApproved = await QuotaService.checkStorageQuota(\n      db,\n      user.id,\n      data.size,\n    );\n  } catch (error) {\n    if (error instanceof StorageQuotaError) {\n      return { error: error.message, status: 403 };\n    }\n    throw error;\n  }\n\n  let tempFilePath: string | undefined;\n\n  try {\n    tempFilePath = path.join(os.tmpdir(), `karakeep-upload-${Date.now()}`);\n    await pipeline(\n      webStreamToNode(data.stream()),\n      fs.createWriteStream(tempFilePath),\n    );\n    const [assetDb] = await db\n      .insert(assets)\n      .values({\n        id: newAssetId(),\n        // Initially, uploads are uploaded for unknown purpose\n        // And without an attached bookmark.\n        assetType: AssetTypes.UNKNOWN,\n        bookmarkId: null,\n        userId: user.id,\n        contentType,\n        size: data.size,\n        fileName,\n      })\n      .returning();\n\n    await saveAssetFromFile({\n      userId: user.id,\n      assetId: assetDb.id,\n      assetPath: tempFilePath,\n      metadata: { contentType, fileName },\n      quotaApproved,\n    });\n\n    return {\n      assetId: assetDb.id,\n      contentType,\n      size: data.size,\n      fileName,\n    };\n  } finally {\n    if (\n      tempFilePath &&\n      (await fs.promises\n        .access(tempFilePath)\n        .then(() => true)\n        .catch(() => false))\n    ) {\n      await fs.promises.unlink(tempFilePath).catch(() => ({}));\n    }\n  }\n}\n"
  },
  {
    "path": "packages/benchmarks/.gitignore",
    "content": "# Docker logs captured during test runs\nsetup/docker-logs/\n"
  },
  {
    "path": "packages/benchmarks/README.md",
    "content": "# Karakeep Benchmarks\n\nThis package spins up a production-like Karakeep stack in Docker, seeds it with a sizeable dataset, then benchmarks a handful of high-signal APIs.\n\n## Usage\n\n```bash\npnpm --filter @karakeep/benchmarks bench\n```\n\nThe command will:\n\n- Start the docker-compose stack on a random free port\n- Create a dedicated benchmark user, tags, lists, and hundreds of bookmarks\n- Run a suite of benchmarks (create, list, search, and list metadata calls)\n- Print a table with ops/sec and latency percentiles\n- Tear down the containers and capture logs (unless you opt out)\n\n## Configuration\n\nControl the run via environment variables:\n\n- `BENCH_BOOKMARKS` (default `400`): number of bookmarks to seed\n- `BENCH_TAGS` (default `25`): number of tags to seed\n- `BENCH_LISTS` (default `6`): number of lists to seed\n- `BENCH_SEED_CONCURRENCY` (default `12`): concurrent seeding operations\n- `BENCH_TIME_MS` (default `1000`): time per benchmark case\n- `BENCH_WARMUP_MS` (default `300`): warmup time per case\n- `BENCH_NO_BUILD=1`: reuse existing docker images instead of rebuilding\n- `BENCH_KEEP_CONTAINERS=1`: leave the stack running after the run\n\nThe stack uses the package-local `docker-compose.yml` and serves a tiny HTML fixture from `setup/html`.\n"
  },
  {
    "path": "packages/benchmarks/docker-compose.yml",
    "content": "services:\n  web:\n    build:\n      dockerfile: docker/Dockerfile\n      context: ../../\n      target: aio\n    restart: unless-stopped\n    ports:\n      - \"${KARAKEEP_PORT:-3000}:3000\"\n    environment:\n      DATA_DIR: /tmp\n      NEXTAUTH_SECRET: secret\n      NEXTAUTH_URL: http://localhost:${KARAKEEP_PORT:-3000}\n      MEILI_MASTER_KEY: dummy\n      MEILI_ADDR: http://meilisearch:7700\n      BROWSER_WEB_URL: http://chrome:9222\n      CRAWLER_NUM_WORKERS: 6\n      CRAWLER_ALLOWED_INTERNAL_HOSTNAMES: nginx\n  meilisearch:\n    image: getmeili/meilisearch:v1.37.0\n    restart: unless-stopped\n    environment:\n      MEILI_NO_ANALYTICS: \"true\"\n      MEILI_MASTER_KEY: dummy\n  chrome:\n    image: gcr.io/zenika-hub/alpine-chrome:124\n    restart: unless-stopped\n    command:\n      - --no-sandbox\n      - --disable-gpu\n      - --disable-dev-shm-usage\n      - --remote-debugging-address=0.0.0.0\n      - --remote-debugging-port=9222\n      - --hide-scrollbars\n  nginx:\n    image: nginx:alpine\n    restart: unless-stopped\n    volumes:\n      - ./setup/html:/usr/share/nginx/html\n  minio:\n    image: minio/minio:latest\n    restart: unless-stopped\n    ports:\n      - \"9000:9000\"\n      - \"9001:9001\"\n    environment:\n      MINIO_ROOT_USER: minioadmin\n      MINIO_ROOT_PASSWORD: minioadmin\n    command: server /data --console-address \":9001\"\n    volumes:\n      - minio_data:/data\n\nvolumes:\n  minio_data:\n"
  },
  {
    "path": "packages/benchmarks/package.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/package.json\",\n  \"name\": \"@karakeep/benchmarks\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"bench\": \"tsx src/index.ts\",\n    \"lint\": \"oxlint .\",\n    \"lint:fix\": \"oxlint . --fix\",\n    \"format\": \"oxfmt --check .\",\n    \"format:fix\": \"oxfmt .\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@karakeep/shared\": \"workspace:^0.1.0\",\n    \"@karakeep/trpc\": \"workspace:^0.1.0\",\n    \"@trpc/client\": \"^11.9.0\",\n    \"p-limit\": \"^7.2.0\",\n    \"superjson\": \"^2.2.1\",\n    \"tinybench\": \"^6.0.0\",\n    \"zod\": \"^3.24.2\"\n  },\n  \"devDependencies\": {\n    \"@karakeep/tsconfig\": \"workspace:^0.1.0\",\n    \"oxlint\": \"^1.50.0\",\n    \"tsx\": \"^4.8.1\"\n  }\n}\n"
  },
  {
    "path": "packages/benchmarks/setup/html/hello.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Benchmarks Fixture</title>\n  </head>\n  <body>\n    <h1>Karakeep Benchmarks</h1>\n    <p>This page is served by the nginx container during benchmarks.</p>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/benchmarks/src/benchmarks.ts",
    "content": "import type { TaskResult } from \"tinybench\";\nimport { Bench } from \"tinybench\";\n\nimport type { SeedResult } from \"./seed\";\nimport { logInfo, logStep, logSuccess } from \"./log\";\nimport { formatMs, formatNumber } from \"./utils\";\n\n// Type guard for completed task results\ntype CompletedTaskResult = Extract<TaskResult, { state: \"completed\" }>;\n\nexport interface BenchmarkRow {\n  name: string;\n  ops: number;\n  mean: number;\n  p75: number;\n  p99: number;\n  samples: number;\n}\n\nexport interface BenchmarkOptions {\n  timeMs?: number;\n  warmupMs?: number;\n}\n\nexport async function runBenchmarks(\n  seed: SeedResult,\n  options?: BenchmarkOptions,\n): Promise<BenchmarkRow[]> {\n  const bench = new Bench({\n    time: options?.timeMs ?? 1000,\n    warmupTime: options?.warmupMs ?? 300,\n  });\n\n  const sampleTag = seed.tags[0];\n  const sampleList = seed.lists[0];\n  const sampleIds = seed.bookmarks.slice(0, 50).map((b) => b.id);\n\n  bench.add(\"bookmarks.getBookmarks (page)\", async () => {\n    await seed.trpc.bookmarks.getBookmarks.query({\n      limit: 50,\n    });\n  });\n\n  if (sampleTag) {\n    bench.add(\"bookmarks.getBookmarks (tag filter)\", async () => {\n      await seed.trpc.bookmarks.getBookmarks.query({\n        limit: 50,\n        tagId: sampleTag.id,\n      });\n    });\n  }\n\n  if (sampleList) {\n    bench.add(\"bookmarks.getBookmarks (list filter)\", async () => {\n      await seed.trpc.bookmarks.getBookmarks.query({\n        limit: 50,\n        listId: sampleList.id,\n      });\n    });\n  }\n\n  if (sampleList && sampleIds.length > 0) {\n    bench.add(\"lists.getListsOfBookmark\", async () => {\n      await seed.trpc.lists.getListsOfBookmark.query({\n        bookmarkId: sampleIds[0],\n      });\n    });\n  }\n\n  bench.add(\"bookmarks.searchBookmarks\", async () => {\n    await seed.trpc.bookmarks.searchBookmarks.query({\n      text: seed.searchTerm,\n      limit: 20,\n    });\n  });\n\n  bench.add(\"bookmarks.getBookmarks (by ids)\", async () => {\n    await seed.trpc.bookmarks.getBookmarks.query({\n      ids: sampleIds.slice(0, 20),\n      includeContent: false,\n    });\n  });\n\n  // Benchmark with cursor (without listId)\n  {\n    const firstPage = await seed.trpc.bookmarks.getBookmarks.query({\n      limit: 50,\n    });\n    bench.add(\"bookmarks.getBookmarks (with cursor)\", async () => {\n      if (firstPage.nextCursor) {\n        await seed.trpc.bookmarks.getBookmarks.query({\n          limit: 50,\n          cursor: firstPage.nextCursor,\n        });\n      }\n    });\n  }\n\n  // Benchmark with cursor and listId\n  if (sampleList) {\n    const firstPage = await seed.trpc.bookmarks.getBookmarks.query({\n      limit: 50,\n      listId: sampleList.id,\n    });\n    bench.add(\"bookmarks.getBookmarks (cursor + list filter)\", async () => {\n      if (firstPage.nextCursor) {\n        await seed.trpc.bookmarks.getBookmarks.query({\n          limit: 50,\n          listId: sampleList.id,\n          cursor: firstPage.nextCursor,\n        });\n      }\n    });\n  }\n\n  // Benchmark with archived filter\n  bench.add(\"bookmarks.getBookmarks (archived filter)\", async () => {\n    await seed.trpc.bookmarks.getBookmarks.query({\n      limit: 50,\n      archived: true,\n    });\n  });\n\n  // Benchmark with favourited filter\n  bench.add(\"bookmarks.getBookmarks (favourited filter)\", async () => {\n    await seed.trpc.bookmarks.getBookmarks.query({\n      limit: 50,\n      favourited: true,\n    });\n  });\n\n  // Benchmark with archived and list filter combined\n  if (sampleList) {\n    bench.add(\"bookmarks.getBookmarks (archived + list filter)\", async () => {\n      await seed.trpc.bookmarks.getBookmarks.query({\n        limit: 50,\n        archived: true,\n        listId: sampleList.id,\n      });\n    });\n  }\n\n  // Benchmark with favourited and list filter combined\n  if (sampleList) {\n    bench.add(\"bookmarks.getBookmarks (favourited + list filter)\", async () => {\n      await seed.trpc.bookmarks.getBookmarks.query({\n        limit: 50,\n        favourited: true,\n        listId: sampleList.id,\n      });\n    });\n  }\n\n  logStep(\"Running benchmarks\");\n  await bench.run();\n  logSuccess(\"Benchmarks complete\");\n\n  const rows = bench.tasks\n    .map((task) => {\n      const result = task.result;\n\n      // Check for errored state\n      if (\"error\" in result) {\n        console.error(`\\n⚠️  Benchmark \"${task.name}\" failed with error:`);\n        console.error(result.error);\n        return null;\n      }\n\n      // Check if task completed successfully\n      if (result.state !== \"completed\") {\n        console.warn(\n          `\\n⚠️  Benchmark \"${task.name}\" did not complete. State: ${result.state}`,\n        );\n        return null;\n      }\n\n      return toRow(task.name, result);\n    })\n    .filter(Boolean) as BenchmarkRow[];\n\n  renderTable(rows);\n  logInfo(\n    \"ops/s uses tinybench's hz metric; durations are recorded in milliseconds.\",\n  );\n\n  return rows;\n}\n\nfunction toRow(name: string, result: CompletedTaskResult): BenchmarkRow {\n  // The statistics are now in result.latency and result.throughput\n  const latency = result.latency;\n  const throughput = result.throughput;\n\n  return {\n    name,\n    ops: throughput.mean, // ops/s is the mean throughput\n    mean: latency.mean,\n    p75: latency.p75,\n    p99: latency.p99,\n    samples: latency.samplesCount,\n  };\n}\n\nfunction renderTable(rows: BenchmarkRow[]): void {\n  const headers = [\"Benchmark\", \"ops/s\", \"avg\", \"p75\", \"p99\", \"samples\"];\n\n  const data = rows.map((row) => [\n    row.name,\n    formatNumber(row.ops, 1),\n    formatMs(row.mean),\n    formatMs(row.p75),\n    formatMs(row.p99),\n    String(row.samples),\n  ]);\n\n  const columnWidths = headers.map((header, index) =>\n    Math.max(header.length, ...data.map((row) => row[index].length)),\n  );\n\n  const formatRow = (cells: string[]): string =>\n    cells.map((cell, index) => cell.padEnd(columnWidths[index])).join(\"  \");\n\n  console.log(\"\");\n  console.log(formatRow(headers));\n  console.log(columnWidths.map((width) => \"-\".repeat(width)).join(\"  \"));\n  data.forEach((row) => console.log(formatRow(row)));\n  console.log(\"\");\n}\n"
  },
  {
    "path": "packages/benchmarks/src/index.ts",
    "content": "import { runBenchmarks } from \"./benchmarks\";\nimport { logInfo, logStep, logSuccess, logWarn } from \"./log\";\nimport { seedData } from \"./seed\";\nimport { startContainers } from \"./startContainers\";\n\ninterface CliConfig {\n  bookmarkCount: number;\n  tagCount: number;\n  listCount: number;\n  concurrency: number;\n  userCount: number;\n  keepContainers: boolean;\n  timeMs: number;\n  warmupMs: number;\n}\n\nfunction numberFromEnv(key: string, fallback: number): number {\n  const raw = process.env[key];\n  if (!raw) return fallback;\n  const parsed = Number(raw);\n  return Number.isFinite(parsed) ? parsed : fallback;\n}\n\nfunction loadConfig(): CliConfig {\n  return {\n    bookmarkCount: numberFromEnv(\"BENCH_BOOKMARKS\", 400),\n    tagCount: numberFromEnv(\"BENCH_TAGS\", 25),\n    listCount: numberFromEnv(\"BENCH_LISTS\", 6),\n    concurrency: numberFromEnv(\"BENCH_SEED_CONCURRENCY\", 12),\n    userCount: numberFromEnv(\"BENCH_USERS\", 3),\n    keepContainers: process.env.BENCH_KEEP_CONTAINERS === \"1\",\n    timeMs: numberFromEnv(\"BENCH_TIME_MS\", 1000),\n    warmupMs: numberFromEnv(\"BENCH_WARMUP_MS\", 300),\n  };\n}\n\nasync function main() {\n  const config = loadConfig();\n\n  logStep(\"Benchmark configuration\");\n  logInfo(`Users:        ${config.userCount}`);\n  logInfo(`Bookmarks:    ${config.bookmarkCount} per user`);\n  logInfo(`Tags:         ${config.tagCount} per user`);\n  logInfo(`Lists:        ${config.listCount} per user`);\n  logInfo(`Seed concur.: ${config.concurrency}`);\n  logInfo(`Time per case:${config.timeMs}ms (warmup ${config.warmupMs}ms)`);\n  logInfo(`Keep containers after run: ${config.keepContainers ? \"yes\" : \"no\"}`);\n\n  const running = await startContainers();\n\n  const stopContainers = async () => {\n    if (config.keepContainers) {\n      logWarn(\n        `Skipping docker compose shutdown (BENCH_KEEP_CONTAINERS=1). Port ${running.port} stays up.`,\n      );\n      return;\n    }\n    await running.stop();\n  };\n\n  const handleSignal = async (signal: NodeJS.Signals) => {\n    logWarn(`Received ${signal}, shutting down...`);\n    await stopContainers();\n    process.exit(1);\n  };\n\n  process.on(\"SIGINT\", handleSignal);\n  process.on(\"SIGTERM\", handleSignal);\n\n  try {\n    const seedResult = await seedData({\n      bookmarkCount: config.bookmarkCount,\n      tagCount: config.tagCount,\n      listCount: config.listCount,\n      concurrency: config.concurrency,\n      userCount: config.userCount,\n    });\n\n    await runBenchmarks(seedResult, {\n      timeMs: config.timeMs,\n      warmupMs: config.warmupMs,\n    });\n    logSuccess(\"All done\");\n  } catch (error) {\n    logWarn(\"Benchmark run failed\");\n    console.error(error);\n  } finally {\n    await stopContainers();\n  }\n}\n\nmain();\n"
  },
  {
    "path": "packages/benchmarks/src/log.ts",
    "content": "const ICONS = {\n  step: \"==\",\n  info: \"--\",\n  success: \"OK\",\n  warn: \"!!\",\n};\n\nexport function logStep(title: string): void {\n  console.log(`\\n${ICONS.step} ${title}`);\n}\n\nexport function logInfo(message: string): void {\n  console.log(`  ${ICONS.info} ${message}`);\n}\n\nexport function logSuccess(message: string): void {\n  console.log(`  ${ICONS.success} ${message}`);\n}\n\nexport function logWarn(message: string): void {\n  console.log(`  ${ICONS.warn} ${message}`);\n}\n"
  },
  {
    "path": "packages/benchmarks/src/seed.ts",
    "content": "import pLimit from \"p-limit\";\n\nimport type { ZBookmarkList } from \"@karakeep/shared/types/lists\";\nimport type { ZTagBasic } from \"@karakeep/shared/types/tags\";\nimport { BookmarkTypes } from \"@karakeep/shared/types/bookmarks\";\n\nimport { logInfo, logStep, logSuccess } from \"./log\";\nimport { getTrpcClient, TrpcClient } from \"./trpc\";\nimport { waitUntil } from \"./utils\";\n\nexport interface SeedConfig {\n  bookmarkCount: number;\n  tagCount: number;\n  listCount: number;\n  concurrency: number;\n  userCount: number;\n}\n\nexport interface SeededBookmark {\n  id: string;\n  tags: ZTagBasic[];\n  listId?: string;\n  title: string | null | undefined;\n}\n\nexport interface UserSeedData {\n  apiKey: string;\n  trpc: TrpcClient;\n  email: string;\n  tags: ZTagBasic[];\n  lists: ZBookmarkList[];\n  bookmarks: SeededBookmark[];\n}\n\nexport interface SeedResult {\n  users: UserSeedData[];\n  searchTerm: string;\n  // For backwards compatibility, expose the first user's data\n  apiKey: string;\n  trpc: TrpcClient;\n  tags: ZTagBasic[];\n  lists: ZBookmarkList[];\n  bookmarks: SeededBookmark[];\n}\n\nconst TOPICS = [\n  \"performance\",\n  \"search\",\n  \"reading\",\n  \"workflow\",\n  \"api\",\n  \"workers\",\n  \"backend\",\n  \"frontend\",\n  \"productivity\",\n  \"cli\",\n];\n\nasync function seedUserData(\n  authlessClient: TrpcClient,\n  userIndex: number,\n  config: SeedConfig,\n  timestamp: number,\n): Promise<UserSeedData> {\n  const email = `benchmarks+${timestamp}+user${userIndex}@example.com`;\n  const password = \"benchmarks1234\";\n\n  logStep(`Creating user ${userIndex + 1}/${config.userCount}`);\n  await authlessClient.users.create.mutate({\n    name: `Benchmark User ${userIndex + 1}`,\n    email,\n    password,\n    confirmPassword: password,\n  });\n  const { key } = await authlessClient.apiKeys.exchange.mutate({\n    email,\n    password,\n    keyName: `benchmark-key-${userIndex}`,\n  });\n\n  const trpc = getTrpcClient(key);\n  logSuccess(`User ${userIndex + 1} ready`);\n\n  logStep(`Creating ${config.tagCount} tags for user ${userIndex + 1}`);\n  const tags: ZTagBasic[] = [];\n  for (let i = 0; i < config.tagCount; i++) {\n    const tag = await trpc.tags.create.mutate({\n      name: `user${userIndex}-topic-${i + 1}`,\n    });\n    tags.push(tag);\n  }\n  logSuccess(`Tags created for user ${userIndex + 1}`);\n\n  logStep(`Creating ${config.listCount} lists for user ${userIndex + 1}`);\n  const lists: ZBookmarkList[] = [];\n  for (let i = 0; i < config.listCount; i++) {\n    const list = await trpc.lists.create.mutate({\n      name: `User ${userIndex + 1} List ${i + 1}`,\n      description: `Auto-generated benchmark list #${i + 1} for user ${userIndex + 1}`,\n      icon: \"bookmark\",\n    });\n    lists.push(list);\n  }\n  logSuccess(`Lists created for user ${userIndex + 1}`);\n\n  logStep(\n    `Creating ${config.bookmarkCount} bookmarks for user ${userIndex + 1}`,\n  );\n  const limit = pLimit(config.concurrency);\n  const bookmarks: SeededBookmark[] = [];\n\n  await Promise.all(\n    Array.from({ length: config.bookmarkCount }).map((_, index) =>\n      limit(async () => {\n        const topic = TOPICS[index % TOPICS.length];\n        const createdAt = new Date(Date.now() - index * 3000);\n        const bookmark = await trpc.bookmarks.createBookmark.mutate({\n          type: BookmarkTypes.LINK,\n          url: `https://example.com/user${userIndex}/${topic}/${index}`,\n          title: `User ${userIndex + 1} ${topic} article ${index}`,\n          source: \"api\",\n          summary: `Benchmark dataset entry about ${topic} for user ${userIndex + 1}.`,\n          favourited: index % 7 === 0,\n          archived: index % 11 === 0,\n          createdAt,\n        });\n\n        const primaryTag = tags[index % tags.length];\n        const secondaryTag = tags[(index + 5) % tags.length];\n        const attachedTags = [primaryTag, secondaryTag];\n        await trpc.bookmarks.updateTags.mutate({\n          bookmarkId: bookmark.id,\n          attach: attachedTags.map((tag) => ({\n            tagId: tag.id,\n            tagName: tag.name,\n          })),\n          detach: [],\n        });\n\n        let listId: string | undefined;\n        if (lists.length > 0) {\n          const list = lists[index % lists.length];\n          await trpc.lists.addToList.mutate({\n            listId: list.id,\n            bookmarkId: bookmark.id,\n          });\n          listId = list.id;\n        }\n\n        bookmarks.push({\n          id: bookmark.id,\n          tags: attachedTags,\n          listId,\n          title: bookmark.title,\n        });\n      }),\n    ),\n  );\n  logSuccess(`Bookmarks created for user ${userIndex + 1}`);\n\n  return {\n    apiKey: key,\n    trpc,\n    email,\n    tags,\n    lists,\n    bookmarks,\n  };\n}\n\nexport async function seedData(config: SeedConfig): Promise<SeedResult> {\n  const authlessClient = getTrpcClient();\n  const timestamp = Date.now();\n\n  logInfo(`Seeding data for ${config.userCount} users`);\n  const users: UserSeedData[] = [];\n\n  // Create all users sequentially to avoid race conditions\n  for (let i = 0; i < config.userCount; i++) {\n    const userData = await seedUserData(authlessClient, i, config, timestamp);\n    users.push(userData);\n  }\n\n  const searchTerm = \"benchmark\";\n  logStep(\"Waiting for search index to be ready\");\n  // Use the first user's client to check search readiness\n  await waitUntil(\n    async () => {\n      const results = await users[0].trpc.bookmarks.searchBookmarks.query({\n        text: searchTerm,\n        limit: 1,\n      });\n      return results.bookmarks.length > 0;\n    },\n    \"search data to be indexed\",\n    120_000,\n    2_000,\n  );\n  logSuccess(\"Search index warmed up\");\n\n  const totalBookmarks = users.reduce(\n    (sum, user) => sum + user.bookmarks.length,\n    0,\n  );\n  const totalTags = users.reduce((sum, user) => sum + user.tags.length, 0);\n  const totalLists = users.reduce((sum, user) => sum + user.lists.length, 0);\n\n  logInfo(\n    `Seeded ${totalBookmarks} bookmarks across ${totalTags} tags and ${totalLists} lists for ${config.userCount} users`,\n  );\n\n  // Return first user's data for backwards compatibility\n  const firstUser = users[0];\n  return {\n    users,\n    searchTerm,\n    apiKey: firstUser.apiKey,\n    trpc: firstUser.trpc,\n    tags: firstUser.tags,\n    lists: firstUser.lists,\n    bookmarks: firstUser.bookmarks,\n  };\n}\n"
  },
  {
    "path": "packages/benchmarks/src/startContainers.ts",
    "content": "import { execSync } from \"child_process\";\nimport net from \"net\";\nimport path from \"path\";\nimport { fileURLToPath } from \"url\";\n\nimport { logInfo, logStep, logSuccess, logWarn } from \"./log\";\nimport { sleep, waitUntil } from \"./utils\";\n\nasync function getRandomPort(): Promise<number> {\n  const server = net.createServer();\n  return new Promise<number>((resolve, reject) => {\n    server.unref();\n    server.on(\"error\", reject);\n    server.listen(0, () => {\n      const port = (server.address() as net.AddressInfo).port;\n      server.close(() => resolve(port));\n    });\n  });\n}\n\nasync function waitForHealthy(port: number): Promise<void> {\n  await waitUntil(\n    async () => {\n      const res = await fetch(`http://localhost:${port}/api/health`);\n      return res.status === 200;\n    },\n    \"Karakeep stack to become healthy\",\n    60_000,\n    1_000,\n  );\n}\n\nasync function captureDockerLogs(composeDir: string): Promise<void> {\n  const logsDir = path.join(composeDir, \"setup\", \"docker-logs\");\n  try {\n    execSync(`mkdir -p \"${logsDir}\"`, { cwd: composeDir });\n  } catch {\n    // ignore\n  }\n\n  const services = [\"web\", \"meilisearch\", \"chrome\", \"nginx\", \"minio\"];\n  for (const service of services) {\n    try {\n      execSync(\n        `/bin/sh -c 'docker compose logs ${service} > \"${logsDir}/${service}.log\" 2>&1'`,\n        {\n          cwd: composeDir,\n          stdio: \"ignore\",\n        },\n      );\n      logInfo(`Captured logs for ${service}`);\n    } catch (error) {\n      logWarn(`Failed to capture logs for ${service}: ${error}`);\n    }\n  }\n}\n\nexport interface RunningContainers {\n  port: number;\n  stop: () => Promise<void>;\n}\n\nexport async function startContainers(): Promise<RunningContainers> {\n  const __dirname = path.dirname(fileURLToPath(import.meta.url));\n  const composeDir = path.join(__dirname, \"..\");\n  const port = await getRandomPort();\n  const skipBuild =\n    process.env.BENCH_NO_BUILD === \"1\" || process.env.BENCH_SKIP_BUILD === \"1\";\n  const buildArg = skipBuild ? \"\" : \"--build\";\n\n  logStep(`Starting docker compose on port ${port}`);\n  execSync(`docker compose up ${buildArg} -d`, {\n    cwd: composeDir,\n    stdio: \"inherit\",\n    env: { ...process.env, KARAKEEP_PORT: String(port) },\n  });\n\n  logInfo(\"Waiting for services to report healthy...\");\n  await waitForHealthy(port);\n  await sleep(5_000);\n  logSuccess(\"Containers are ready\");\n\n  process.env.KARAKEEP_PORT = String(port);\n\n  let stopped = false;\n  const stop = async (): Promise<void> => {\n    if (stopped) return;\n    stopped = true;\n    logStep(\"Collecting docker logs\");\n    await captureDockerLogs(composeDir);\n    logStep(\"Stopping docker compose\");\n    execSync(\"docker compose down\", { cwd: composeDir, stdio: \"inherit\" });\n  };\n\n  return { port, stop };\n}\n"
  },
  {
    "path": "packages/benchmarks/src/trpc.ts",
    "content": "import { createTRPCClient, httpBatchLink } from \"@trpc/client\";\nimport superjson from \"superjson\";\n\nimport type { AppRouter } from \"@karakeep/trpc/routers/_app\";\n\nexport type TrpcClient = ReturnType<typeof getTrpcClient>;\n\nexport function getTrpcClient(apiKey?: string) {\n  if (!process.env.KARAKEEP_PORT) {\n    throw new Error(\"KARAKEEP_PORT is not set. Did you start the containers?\");\n  }\n\n  return createTRPCClient<AppRouter>({\n    links: [\n      httpBatchLink({\n        transformer: superjson,\n        url: `http://localhost:${process.env.KARAKEEP_PORT}/api/trpc`,\n        headers() {\n          return {\n            authorization: apiKey ? `Bearer ${apiKey}` : undefined,\n          };\n        },\n      }),\n    ],\n  });\n}\n"
  },
  {
    "path": "packages/benchmarks/src/utils.ts",
    "content": "export function sleep(ms: number): Promise<void> {\n  return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nexport async function waitUntil(\n  fn: () => Promise<boolean>,\n  description: string,\n  timeoutMs = 60000,\n  intervalMs = 1000,\n): Promise<void> {\n  const start = Date.now();\n  while (Date.now() - start < timeoutMs) {\n    try {\n      if (await fn()) {\n        return;\n      }\n    } catch {\n      // Ignore and retry\n    }\n    await sleep(intervalMs);\n  }\n  throw new Error(`${description} timed out after ${timeoutMs}ms`);\n}\n\nexport function formatNumber(num: number, fractionDigits = 2): string {\n  return num.toFixed(fractionDigits);\n}\n\nexport function formatMs(ms: number): string {\n  return `${formatNumber(ms, ms >= 10 ? 1 : 2)} ms`;\n}\n"
  },
  {
    "path": "packages/benchmarks/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@karakeep/tsconfig/node.json\",\n  \"include\": [\"src/**/*.ts\"],\n  \"exclude\": [\"node_modules\"],\n  \"compilerOptions\": {\n    \"tsBuildInfoFile\": \"node_modules/.cache/tsbuildinfo.json\"\n  }\n}\n"
  },
  {
    "path": "packages/db/.oxlintrc.json",
    "content": "{\n  \"$schema\": \"../../node_modules/oxlint/configuration_schema.json\",\n  \"extends\": [\n    \"../../tooling/oxlint/oxlint-base.json\"\n  ],\n  \"env\": {\n    \"builtin\": true,\n    \"commonjs\": true\n  },\n  \"ignorePatterns\": [\n    \"**/*.config.js\",\n    \"**/*.config.cjs\",\n    \"**/.eslintrc.cjs\",\n    \"**/.next\",\n    \"**/dist\",\n    \"**/build\",\n    \"**/pnpm-lock.yaml\"\n  ]\n}\n"
  },
  {
    "path": "packages/db/drizzle/0000_luxuriant_johnny_blaze.sql",
    "content": "CREATE TABLE `account` (\n\t`userId` text NOT NULL,\n\t`type` text NOT NULL,\n\t`provider` text NOT NULL,\n\t`providerAccountId` text NOT NULL,\n\t`refresh_token` text,\n\t`access_token` text,\n\t`expires_at` integer,\n\t`token_type` text,\n\t`scope` text,\n\t`id_token` text,\n\t`session_state` text,\n\tPRIMARY KEY(`provider`, `providerAccountId`),\n\tFOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nCREATE TABLE `apiKey` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`name` text NOT NULL,\n\t`createdAt` integer NOT NULL,\n\t`keyId` text NOT NULL,\n\t`keyHash` text NOT NULL,\n\t`userId` text NOT NULL,\n\tFOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nCREATE TABLE `bookmarkLinks` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`url` text NOT NULL,\n\t`title` text,\n\t`description` text,\n\t`imageUrl` text,\n\t`favicon` text,\n\t`crawledAt` integer,\n\tFOREIGN KEY (`id`) REFERENCES `bookmarks`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nCREATE TABLE `bookmarkTags` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`name` text NOT NULL,\n\t`createdAt` integer NOT NULL,\n\t`userId` text NOT NULL,\n\tFOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nCREATE TABLE `bookmarks` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`createdAt` integer NOT NULL,\n\t`archived` integer DEFAULT false NOT NULL,\n\t`favourited` integer DEFAULT false NOT NULL,\n\t`userId` text NOT NULL,\n\tFOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nCREATE TABLE `session` (\n\t`sessionToken` text PRIMARY KEY NOT NULL,\n\t`userId` text NOT NULL,\n\t`expires` integer NOT NULL,\n\tFOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nCREATE TABLE `tagsOnBookmarks` (\n\t`bookmarkId` text NOT NULL,\n\t`tagId` text NOT NULL,\n\t`attachedAt` text,\n\t`attachedBy` text,\n\tPRIMARY KEY(`bookmarkId`, `tagId`),\n\tFOREIGN KEY (`bookmarkId`) REFERENCES `bookmarks`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`tagId`) REFERENCES `bookmarkTags`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nCREATE TABLE `user` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`name` text NOT NULL,\n\t`email` text NOT NULL,\n\t`emailVerified` integer,\n\t`image` text,\n\t`password` text\n);\n--> statement-breakpoint\nCREATE TABLE `verificationToken` (\n\t`identifier` text NOT NULL,\n\t`token` text NOT NULL,\n\t`expires` integer NOT NULL,\n\tPRIMARY KEY(`identifier`, `token`)\n);\n--> statement-breakpoint\nCREATE UNIQUE INDEX `apiKey_name_unique` ON `apiKey` (`name`);--> statement-breakpoint\nCREATE UNIQUE INDEX `apiKey_keyId_unique` ON `apiKey` (`keyId`);--> statement-breakpoint\nCREATE UNIQUE INDEX `apiKey_name_userId_unique` ON `apiKey` (`name`,`userId`);--> statement-breakpoint\nCREATE UNIQUE INDEX `bookmarkTags_userId_name_unique` ON `bookmarkTags` (`userId`,`name`);--> statement-breakpoint\nCREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`);"
  },
  {
    "path": "packages/db/drizzle/0001_dapper_trauma.sql",
    "content": "CREATE TABLE `bookmarkTexts` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`text` text,\n\tFOREIGN KEY (`id`) REFERENCES `bookmarks`(`id`) ON UPDATE no action ON DELETE cascade\n);\n"
  },
  {
    "path": "packages/db/drizzle/0002_worried_beyonder.sql",
    "content": "CREATE TABLE `bookmarkLists` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`name` text NOT NULL,\n\t`icon` text NOT NULL,\n\t`createdAt` integer NOT NULL,\n\t`userId` text NOT NULL,\n\tFOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nCREATE TABLE `bookmarksInLists` (\n\t`bookmarkId` text NOT NULL,\n\t`listId` text NOT NULL,\n\t`addedAt` integer,\n\tPRIMARY KEY(`bookmarkId`, `listId`),\n\tFOREIGN KEY (`bookmarkId`) REFERENCES `bookmarks`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`listId`) REFERENCES `bookmarkLists`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nCREATE UNIQUE INDEX `bookmarkLists_name_userId_unique` ON `bookmarkLists` (`name`,`userId`);"
  },
  {
    "path": "packages/db/drizzle/0003_parallel_supernaut.sql",
    "content": "ALTER TABLE user ADD `role` text DEFAULT 'user';"
  },
  {
    "path": "packages/db/drizzle/0004_skinny_vengeance.sql",
    "content": "ALTER TABLE bookmarkLinks ADD `content` text;"
  },
  {
    "path": "packages/db/drizzle/0005_quiet_gunslinger.sql",
    "content": "DROP INDEX IF EXISTS `apiKey_name_unique`;"
  },
  {
    "path": "packages/db/drizzle/0006_funny_mac_gargan.sql",
    "content": "ALTER TABLE bookmarks ADD `taggingStatus` text DEFAULT 'pending';\n--> statement-breakpoint\nUPDATE bookmarks SET taggingStatus = 'success';\n"
  },
  {
    "path": "packages/db/drizzle/0007_messy_raza.sql",
    "content": "ALTER TABLE bookmarkLinks ADD `htmlContent` text;"
  },
  {
    "path": "packages/db/drizzle/0008_cloudy_skin.sql",
    "content": "CREATE INDEX `bookmarkLists_userId_idx` ON `bookmarkLists` (`userId`);--> statement-breakpoint\nCREATE INDEX `bookmarkTags_name_idx` ON `bookmarkTags` (`name`);--> statement-breakpoint\nCREATE INDEX `bookmarkTags_userId_idx` ON `bookmarkTags` (`userId`);--> statement-breakpoint\nCREATE INDEX `bookmarks_userId_idx` ON `bookmarks` (`userId`);--> statement-breakpoint\nCREATE INDEX `bookmarks_archived_idx` ON `bookmarks` (`archived`);--> statement-breakpoint\nCREATE INDEX `bookmarks_favourited_idx` ON `bookmarks` (`favourited`);--> statement-breakpoint\nCREATE INDEX `bookmarksInLists_bookmarkId_idx` ON `bookmarksInLists` (`bookmarkId`);--> statement-breakpoint\nCREATE INDEX `bookmarksInLists_listId_idx` ON `bookmarksInLists` (`listId`);--> statement-breakpoint\nCREATE INDEX `tagsOnBookmarks_tagId_idx` ON `tagsOnBookmarks` (`bookmarkId`);--> statement-breakpoint\nCREATE INDEX `tagsOnBookmarks_bookmarkId_idx` ON `tagsOnBookmarks` (`bookmarkId`);"
  },
  {
    "path": "packages/db/drizzle/0009_cuddly_cammi.sql",
    "content": "CREATE INDEX `bookmarks_createdAt_idx` ON `bookmarks` (`createdAt`);"
  },
  {
    "path": "packages/db/drizzle/0010_curved_sharon_ventura.sql",
    "content": "CREATE TABLE `assets` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`userId` text NOT NULL,\n\t`createdAt` integer NOT NULL,\n\t`contentType` text NOT NULL,\n\t`encoding` text NOT NULL,\n\t`blob` blob NOT NULL,\n\tFOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nCREATE INDEX `assets_userId_idx` ON `assets` (`userId`);"
  },
  {
    "path": "packages/db/drizzle/0011_ordinary_phalanx.sql",
    "content": "CREATE TABLE `bookmarkAssets` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`assetType` text NOT NULL,\n\t`assetId` text NOT NULL,\n\tFOREIGN KEY (`id`) REFERENCES `bookmarks`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`assetId`) REFERENCES `assets`(`id`) ON UPDATE no action ON DELETE cascade\n);\n"
  },
  {
    "path": "packages/db/drizzle/0012_noisy_grim_reaper.sql",
    "content": "CREATE TABLE `bookmarkAssets2` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`assetType` text NOT NULL,\n\t`assetId` text NOT NULL,\n\tFOREIGN KEY (`id`) REFERENCES `bookmarks`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nDROP TABLE `assets`;--> statement-breakpoint\nDROP TABLE `bookmarkAssets`;"
  },
  {
    "path": "packages/db/drizzle/0013_square_lady_ursula.sql",
    "content": "ALTER TABLE `bookmarkAssets2` RENAME TO `bookmarkAssets`;\n"
  },
  {
    "path": "packages/db/drizzle/0014_lonely_thaddeus_ross.sql",
    "content": "ALTER TABLE bookmarks ADD `note` text;"
  },
  {
    "path": "packages/db/drizzle/0015_first_reavers.sql",
    "content": "ALTER TABLE bookmarkAssets ADD `content` text;--> statement-breakpoint\nALTER TABLE bookmarkAssets ADD `metadata` text;--> statement-breakpoint\nALTER TABLE bookmarkAssets ADD `info` text;"
  },
  {
    "path": "packages/db/drizzle/0016_shallow_rawhide_kid.sql",
    "content": "ALTER TABLE bookmarkAssets ADD `fileName` text;--> statement-breakpoint\nALTER TABLE `bookmarkAssets` DROP COLUMN `info`;"
  },
  {
    "path": "packages/db/drizzle/0017_slippery_senator_kelly.sql",
    "content": "ALTER TABLE bookmarkLinks ADD `crawlStatus` text DEFAULT 'pending';--> statement-breakpoint\nUPDATE bookmarkLinks SET crawlStatus = 'failure' where htmlContent is null;--> statement-breakpoint\nUPDATE bookmarkLinks SET crawlStatus = 'success' where htmlContent is not null;"
  },
  {
    "path": "packages/db/drizzle/0018_bright_infant_terrible.sql",
    "content": "ALTER TABLE bookmarks ADD `title` text;"
  },
  {
    "path": "packages/db/drizzle/0019_many_vertigo.sql",
    "content": "DROP INDEX IF EXISTS `bookmarkLists_name_userId_unique`;--> statement-breakpoint\nALTER TABLE bookmarkLists ADD `parentId` text REFERENCES bookmarkLists(id) ON DELETE SET NULL;\n"
  },
  {
    "path": "packages/db/drizzle/0020_sudden_dagger.sql",
    "content": "ALTER TABLE bookmarkLinks ADD `screenshotAssetId` text;--> statement-breakpoint\nALTER TABLE bookmarkLinks ADD `imageAssetId` text;"
  },
  {
    "path": "packages/db/drizzle/0021_magical_firebrand.sql",
    "content": "CREATE INDEX `bookmarkLinks_url_idx` ON `bookmarkLinks` (`url`);"
  },
  {
    "path": "packages/db/drizzle/0022_tough_nextwave.sql",
    "content": "ALTER TABLE bookmarkLinks ADD `fullPageArchiveAssetId` text;"
  },
  {
    "path": "packages/db/drizzle/0023_late_night_nurse.sql",
    "content": "ALTER TABLE bookmarkAssets ADD `sourceUrl` text;"
  },
  {
    "path": "packages/db/drizzle/0024_premium_hammerhead.sql",
    "content": "CREATE TABLE `assets` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`assetType` text NOT NULL,\n\t`bookmarkId` text NOT NULL,\n\tFOREIGN KEY (`bookmarkId`) REFERENCES `bookmarks`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nCREATE INDEX `assets_bookmarkId_idx` ON `assets` (`bookmarkId`);\n--> statement-breakpoint\nCREATE INDEX `assets_assetType_idx` ON `assets` (`assetType`);\n--> statement-breakpoint\nINSERT INTO `assets` (`id`, `assetType`, `bookmarkId`)\nSELECT `screenshotAssetId`, 'linkScreenshot', `id`\nFROM `bookmarkLinks`\nWHERE screenshotAssetId IS NOT NULL;\n--> statement-breakpoint\nINSERT INTO `assets` (`id`, `assetType`, `bookmarkId`)\nSELECT `fullPageArchiveAssetId`, 'linkFullPageArchive', `id`\nFROM `bookmarkLinks`\nWHERE `fullPageArchiveAssetId` IS NOT NULL;\n--> statement-breakpoint\nINSERT INTO `assets` (`id`, `assetType`, `bookmarkId`)\nSELECT `imageAssetId`, 'linkBannerImage', `id`\nFROM `bookmarkLinks`\nWHERE `imageAssetId` IS NOT NULL;\n--> statement-breakpoint\nALTER TABLE `bookmarkLinks` DROP COLUMN `screenshotAssetId`;\n--> statement-breakpoint\nALTER TABLE `bookmarkLinks` DROP COLUMN `fullPageArchiveAssetId`;\n--> statement-breakpoint\nALTER TABLE `bookmarkLinks` DROP COLUMN `imageAssetId`;\n"
  },
  {
    "path": "packages/db/drizzle/0025_aspiring_skaar.sql",
    "content": "ALTER TABLE bookmarks ADD `type` text NOT NULL DEFAULT \"text\";--> statement-breakpoint\r\n-- Fill in the bookmark type\r\nUPDATE bookmarks\r\nSET type = CASE\r\n   WHEN EXISTS (SELECT 1 FROM bookmarkLinks WHERE bookmarkLinks.id = bookmarks.id)\r\n       THEN 'link'\r\n   WHEN EXISTS (SELECT 1 FROM bookmarkTexts WHERE bookmarkTexts.id = bookmarks.id)\r\n       THEN 'text'\r\n   WHEN EXISTS (SELECT 1 FROM bookmarkAssets WHERE bookmarkAssets.id = bookmarks.id)\r\n       THEN 'asset'\r\nEND;"
  },
  {
    "path": "packages/db/drizzle/0026_silky_imperial_guard.sql",
    "content": "CREATE TABLE `config` (\n\t`key` text PRIMARY KEY NOT NULL,\n\t`value` text NOT NULL\n);\n"
  },
  {
    "path": "packages/db/drizzle/0027_cute_talon.sql",
    "content": "CREATE TABLE `customPrompts` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`text` text NOT NULL,\n\t`enabled` integer NOT NULL,\n\t`attachedBy` text NOT NULL,\n\t`createdAt` integer NOT NULL,\n\t`userId` text NOT NULL,\n\tFOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nCREATE INDEX `customPrompts_userId_idx` ON `customPrompts` (`userId`);"
  },
  {
    "path": "packages/db/drizzle/0028_melodic_norrin_radd.sql",
    "content": "ALTER TABLE `bookmarkTexts` ADD `sourceUrl` text;"
  },
  {
    "path": "packages/db/drizzle/0029_short_gunslinger.sql",
    "content": "CREATE TABLE `assets_new` (\n        `id` text PRIMARY KEY NOT NULL,\n        `assetType` text NOT NULL,\n        `bookmarkId` text,\n\t\t`userId` text NOT NULL,\n        FOREIGN KEY (`bookmarkId`) REFERENCES `bookmarks`(`id`) ON UPDATE no action ON DELETE cascade\n        FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade\n);--> statement-breakpoint\nINSERT INTO `assets_new` (`id`, `assetType`, `bookmarkId`, `userId`) SELECT `id`, `assetType`, `bookmarkId`, (select `bookmarks`.`userId` from `bookmarks` where `bookmarks`.`id` = `assets`.`bookmarkId`) FROM `assets`;--> statement-breakpoint\nDROP TABLE `assets`;--> statement-breakpoint\nALTER TABLE `assets_new` RENAME TO `assets`;--> statement-breakpoint\nCREATE INDEX `assets_bookmarkId_idx` ON `assets` (`bookmarkId`);--> statement-breakpoint\nCREATE INDEX `assets_assetType_idx` ON `assets` (`assetType`);--> statement-breakpoint\nCREATE INDEX `assets_userId_idx` ON `assets` (`userId`);\n"
  },
  {
    "path": "packages/db/drizzle/0030_blue_synch.sql",
    "content": "ALTER TABLE `assets` ADD `size` integer DEFAULT 0 NOT NULL;--> statement-breakpoint\nALTER TABLE `assets` ADD `contentType` text;--> statement-breakpoint\nALTER TABLE `assets` ADD `fileName` text;--> statement-breakpoint\nINSERT INTO `assets` (`id`, `assetType`, `bookmarkId`, `userId`, `fileName`)\n\tSELECT\n\t\t`bookmarkAssets`.`assetId`,\n\t\t'bookmarkAsset',\n\t\t`bookmarkAssets`.`id`,\n\t\t(SELECT `bookmarks`.`userId` FROM `bookmarks` WHERE `bookmarks`.`id` = `bookmarkAssets`.`id`),\n\t\t`bookmarkAssets`.`fileName`\n\tFROM `bookmarkAssets`;\n\n"
  },
  {
    "path": "packages/db/drizzle/0031_yummy_famine.sql",
    "content": "ALTER TABLE `bookmarks` ADD `summary` text;"
  },
  {
    "path": "packages/db/drizzle/0032_futuristic_shiva.sql",
    "content": "CREATE TABLE `rssFeedImports` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`createdAt` integer NOT NULL,\n\t`entryId` text NOT NULL,\n\t`rssFeedId` text NOT NULL,\n\t`bookmarkId` text,\n\tFOREIGN KEY (`rssFeedId`) REFERENCES `rssFeeds`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`bookmarkId`) REFERENCES `bookmarks`(`id`) ON UPDATE no action ON DELETE set null\n);\n--> statement-breakpoint\nCREATE TABLE `rssFeeds` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`name` text NOT NULL,\n\t`url` text NOT NULL,\n\t`createdAt` integer NOT NULL,\n\t`lastFetchedAt` integer,\n\t`lastFetchedStatus` text DEFAULT 'pending',\n\t`userId` text NOT NULL,\n\tFOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nCREATE INDEX `rssFeedImports_feedIdIdx_idx` ON `rssFeedImports` (`rssFeedId`);--> statement-breakpoint\nCREATE INDEX `rssFeedImports_entryIdIdx_idx` ON `rssFeedImports` (`entryId`);--> statement-breakpoint\nCREATE UNIQUE INDEX `rssFeedImports_rssFeedId_entryId_unique` ON `rssFeedImports` (`rssFeedId`,`entryId`);--> statement-breakpoint\nCREATE INDEX `rssFeeds_userId_idx` ON `rssFeeds` (`userId`);"
  },
  {
    "path": "packages/db/drizzle/0033_nappy_molten_man.sql",
    "content": "DROP INDEX IF EXISTS `tagsOnBookmarks_tagId_idx`;--> statement-breakpoint\nCREATE INDEX `tagsOnBookmarks_tagId_idx` ON `tagsOnBookmarks` (`tagId`);"
  },
  {
    "path": "packages/db/drizzle/0034_wet_the_stranger.sql",
    "content": "ALTER TABLE `bookmarkLinks` ADD `crawlStatusCode` integer DEFAULT 200;"
  },
  {
    "path": "packages/db/drizzle/0035_gorgeous_may_parker.sql",
    "content": "CREATE TABLE `highlights` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`bookmarkId` text NOT NULL,\n\t`userId` text NOT NULL,\n\t`startOffset` integer NOT NULL,\n\t`endOffset` integer NOT NULL,\n\t`color` text DEFAULT 'yellow' NOT NULL,\n\t`note` text,\n\t`createdAt` integer NOT NULL,\n\tFOREIGN KEY (`bookmarkId`) REFERENCES `bookmarks`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nCREATE INDEX `highlights_bookmarkId_idx` ON `highlights` (`bookmarkId`);--> statement-breakpoint\nCREATE INDEX `highlights_userId_idx` ON `highlights` (`userId`);"
  },
  {
    "path": "packages/db/drizzle/0036_luxuriant_white_queen.sql",
    "content": "ALTER TABLE `highlights` ADD `text` text;"
  },
  {
    "path": "packages/db/drizzle/0037_daily_smiling_tiger.sql",
    "content": "ALTER TABLE `bookmarkLists` ADD `type` text NOT NULL DEFAULT \"manual\";--> statement-breakpoint\nALTER TABLE `bookmarkLists` ADD `query` text;\n"
  },
  {
    "path": "packages/db/drizzle/0038_calm_clint_barton.sql",
    "content": "UPDATE `customPrompts` SET `attachedBy` = 'all_tagging' WHERE `attachedBy` = 'all';--> statement-breakpoint\nALTER TABLE `customPrompts` RENAME COLUMN `attachedBy` TO `appliesTo`;\n"
  },
  {
    "path": "packages/db/drizzle/0039_purple_albert_cleary.sql",
    "content": "CREATE TABLE `webhooks` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`createdAt` integer NOT NULL,\n\t`url` text NOT NULL,\n\t`userId` text NOT NULL,\n\t`events` text NOT NULL,\n\tFOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nCREATE INDEX `webhooks_userId_idx` ON `webhooks` (`userId`);\n"
  },
  {
    "path": "packages/db/drizzle/0040_long_mindworm.sql",
    "content": "ALTER TABLE `webhooks` ADD `token` text;"
  },
  {
    "path": "packages/db/drizzle/0041_fat_bloodstrike.sql",
    "content": "ALTER TABLE `bookmarks` ADD `modifiedAt` integer;\n--> statement-breakpoint\nUPDATE `bookmarks` SET `modifiedAt` = `createdAt`;\n"
  },
  {
    "path": "packages/db/drizzle/0042_square_gamma_corps.sql",
    "content": "ALTER TABLE `bookmarkLinks` ADD `author` text;--> statement-breakpoint\nALTER TABLE `bookmarkLinks` ADD `publisher` text;--> statement-breakpoint\nALTER TABLE `bookmarkLinks` ADD `datePublished` integer;--> statement-breakpoint\nALTER TABLE `bookmarkLinks` ADD `dateModified` integer;"
  },
  {
    "path": "packages/db/drizzle/0043_puzzling_blonde_phantom.sql",
    "content": "ALTER TABLE `bookmarkLists` ADD `description` text;"
  },
  {
    "path": "packages/db/drizzle/0044_add_password_salt.sql",
    "content": "ALTER TABLE `user` ADD `salt` text DEFAULT '' NOT NULL;"
  },
  {
    "path": "packages/db/drizzle/0045_add_rule_engine.sql",
    "content": "CREATE TABLE `ruleEngineActions` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`userId` text NOT NULL,\n\t`ruleId` text NOT NULL,\n\t`action` text NOT NULL,\n\t`listId` text,\n\t`tagId` text,\n\tFOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`ruleId`) REFERENCES `ruleEngineRules`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`userId`,`tagId`) REFERENCES `bookmarkTags`(`userId`,`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`userId`,`listId`) REFERENCES `bookmarkLists`(`userId`,`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nCREATE INDEX `ruleEngineActions_userId_idx` ON `ruleEngineActions` (`userId`);--> statement-breakpoint\nCREATE INDEX `ruleEngineActions_ruleId_idx` ON `ruleEngineActions` (`ruleId`);--> statement-breakpoint\nCREATE TABLE `ruleEngineRules` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`enabled` integer DEFAULT true NOT NULL,\n\t`name` text NOT NULL,\n\t`description` text,\n\t`event` text NOT NULL,\n\t`condition` text NOT NULL,\n\t`userId` text NOT NULL,\n\t`listId` text,\n\t`tagId` text,\n\tFOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`userId`,`tagId`) REFERENCES `bookmarkTags`(`userId`,`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`userId`,`listId`) REFERENCES `bookmarkLists`(`userId`,`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nCREATE INDEX `ruleEngine_userId_idx` ON `ruleEngineRules` (`userId`);--> statement-breakpoint\nCREATE UNIQUE INDEX `bookmarkLists_userId_id_idx` ON `bookmarkLists` (`userId`,`id`);--> statement-breakpoint\nCREATE UNIQUE INDEX `bookmarkTags_userId_id_idx` ON `bookmarkTags` (`userId`,`id`);"
  },
  {
    "path": "packages/db/drizzle/0046_add_rss_feed_enabled_col.sql",
    "content": "ALTER TABLE `rssFeeds` ADD `enabled` integer DEFAULT true NOT NULL;"
  },
  {
    "path": "packages/db/drizzle/0047_add_summarization_status.sql",
    "content": "ALTER TABLE `bookmarks` ADD `summarizationStatus` text DEFAULT 'pending';"
  },
  {
    "path": "packages/db/drizzle/0048_add_user_settings.sql",
    "content": "CREATE TABLE `userSettings` (\n\t`userId` text PRIMARY KEY NOT NULL,\n\t`bookmarkClickAction` text DEFAULT 'open_original_link' NOT NULL,\n\tFOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade\n);--> statement-breakpoint\nINSERT INTO `userSettings` (`userId`, `bookmarkClickAction`) SELECT `id`, 'open_original_link' FROM `user`;\n"
  },
  {
    "path": "packages/db/drizzle/0049_add_rss_token.sql",
    "content": "ALTER TABLE `bookmarkLists` ADD `rssToken` text;"
  },
  {
    "path": "packages/db/drizzle/0050_add_user_settings_archive_display_behaviour.sql",
    "content": "ALTER TABLE `userSettings` ADD `archiveDisplayBehaviour` text DEFAULT 'show' NOT NULL;\n"
  },
  {
    "path": "packages/db/drizzle/0051_public_lists.sql",
    "content": "ALTER TABLE `bookmarkLists` ADD `public` integer DEFAULT false NOT NULL;"
  },
  {
    "path": "packages/db/drizzle/0052_add_bookmark_quota.sql",
    "content": "ALTER TABLE `user` ADD `bookmarkQuota` integer;\n"
  },
  {
    "path": "packages/db/drizzle/0053_storage_quota.sql",
    "content": "ALTER TABLE `user` ADD `storageQuota` integer;"
  },
  {
    "path": "packages/db/drizzle/0054_add_timezone.sql",
    "content": "ALTER TABLE `userSettings` ADD `timezone` text DEFAULT 'UTC';"
  },
  {
    "path": "packages/db/drizzle/0055_content_asset_id.sql",
    "content": "ALTER TABLE `bookmarkLinks` ADD `contentAssetId` text;--> statement-breakpoint\nALTER TABLE `bookmarkLinks` DROP COLUMN `content`;"
  },
  {
    "path": "packages/db/drizzle/0056_user_invites.sql",
    "content": "CREATE TABLE `invites` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`email` text NOT NULL,\n\t`token` text NOT NULL,\n\t`createdAt` integer NOT NULL,\n\t`expiresAt` integer NOT NULL,\n\t`usedAt` integer,\n\t`invitedBy` text NOT NULL,\n\tFOREIGN KEY (`invitedBy`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nCREATE UNIQUE INDEX `invites_token_unique` ON `invites` (`token`);"
  },
  {
    "path": "packages/db/drizzle/0057_salty_carmella_unuscione.sql",
    "content": "CREATE TABLE `passwordResetToken` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`userId` text NOT NULL,\n\t`token` text NOT NULL,\n\t`expires` integer NOT NULL,\n\t`createdAt` integer NOT NULL,\n\tFOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nCREATE UNIQUE INDEX `passwordResetToken_token_unique` ON `passwordResetToken` (`token`);--> statement-breakpoint\nCREATE INDEX `passwordResetTokens_userId_idx` ON `passwordResetToken` (`userId`);"
  },
  {
    "path": "packages/db/drizzle/0058_add_subscription.sql",
    "content": "CREATE TABLE `subscriptions` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`userId` text NOT NULL,\n\t`stripeCustomerId` text NOT NULL,\n\t`stripeSubscriptionId` text,\n\t`status` text NOT NULL,\n\t`tier` text DEFAULT 'free' NOT NULL,\n\t`priceId` text,\n\t`cancelAtPeriodEnd` integer DEFAULT false,\n\t`startDate` integer,\n\t`endDate` integer,\n\t`createdAt` integer NOT NULL,\n\t`modifiedAt` integer,\n\tFOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nCREATE UNIQUE INDEX `subscriptions_userId_unique` ON `subscriptions` (`userId`);--> statement-breakpoint\nCREATE INDEX `subscriptions_userId_idx` ON `subscriptions` (`userId`);--> statement-breakpoint\nCREATE INDEX `subscriptions_stripeCustomerId_idx` ON `subscriptions` (`stripeCustomerId`);"
  },
  {
    "path": "packages/db/drizzle/0059_browserless_user_setting.sql",
    "content": "ALTER TABLE `user` ADD `browserCrawlingEnabled` integer;"
  },
  {
    "path": "packages/db/drizzle/0060_drop_invite_expire_at.sql",
    "content": "ALTER TABLE `invites` DROP COLUMN `expiresAt`;"
  },
  {
    "path": "packages/db/drizzle/0061_merge_user_settings.sql",
    "content": "ALTER TABLE `user` ADD `bookmarkClickAction` text DEFAULT 'open_original_link' NOT NULL;--> statement-breakpoint\nALTER TABLE `user` ADD `archiveDisplayBehaviour` text DEFAULT 'show' NOT NULL;--> statement-breakpoint\nALTER TABLE `user` ADD `timezone` text DEFAULT 'UTC';--> statement-breakpoint\nUPDATE `user` SET\n  `bookmarkClickAction` = coalesce((\n    SELECT `bookmarkClickAction` FROM `userSettings` WHERE `userSettings`.`userId` = `user`.`id`\n  ), 'open_original_link'),\n  `archiveDisplayBehaviour` = coalesce((\n    SELECT `archiveDisplayBehaviour` FROM `userSettings` WHERE `userSettings`.`userId` = `user`.`id`\n  ), 'show'),\n  `timezone` = coalesce((\n    SELECT `timezone` FROM `userSettings` WHERE `userSettings`.`userId` = `user`.`id`\n  ), 'UTC');--> statement-breakpoint\nDROP TABLE `userSettings`;\n"
  },
  {
    "path": "packages/db/drizzle/0062_add_import_session.sql",
    "content": "CREATE TABLE `importSessionBookmarks` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`importSessionId` text NOT NULL,\n\t`bookmarkId` text NOT NULL,\n\t`createdAt` integer NOT NULL,\n\tFOREIGN KEY (`importSessionId`) REFERENCES `importSessions`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`bookmarkId`) REFERENCES `bookmarks`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nCREATE INDEX `importSessionBookmarks_sessionId_idx` ON `importSessionBookmarks` (`importSessionId`);--> statement-breakpoint\nCREATE INDEX `importSessionBookmarks_bookmarkId_idx` ON `importSessionBookmarks` (`bookmarkId`);--> statement-breakpoint\nCREATE UNIQUE INDEX `importSessionBookmarks_importSessionId_bookmarkId_unique` ON `importSessionBookmarks` (`importSessionId`,`bookmarkId`);--> statement-breakpoint\nCREATE TABLE `importSessions` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`name` text NOT NULL,\n\t`userId` text NOT NULL,\n\t`message` text,\n\t`rootListId` text,\n\t`createdAt` integer NOT NULL,\n\t`modifiedAt` integer,\n\tFOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`rootListId`) REFERENCES `bookmarkLists`(`id`) ON UPDATE no action ON DELETE set null\n);\n--> statement-breakpoint\nCREATE INDEX `importSessions_userId_idx` ON `importSessions` (`userId`);"
  },
  {
    "path": "packages/db/drizzle/0063_add_bookmark_source.sql",
    "content": "ALTER TABLE `bookmarks` ADD `source` text;"
  },
  {
    "path": "packages/db/drizzle/0064_add_import_tags_to_feeds.sql",
    "content": "ALTER TABLE `rssFeeds` ADD `importTags` integer DEFAULT false NOT NULL;"
  },
  {
    "path": "packages/db/drizzle/0065_collaborative_lists.sql",
    "content": "CREATE TABLE `listCollaborators` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`listId` text NOT NULL,\n\t`userId` text NOT NULL,\n\t`role` text NOT NULL,\n\t`createdAt` integer NOT NULL,\n\t`addedBy` text,\n\tFOREIGN KEY (`listId`) REFERENCES `bookmarkLists`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`addedBy`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE set null\n);\n--> statement-breakpoint\nCREATE INDEX `listCollaborators_listId_idx` ON `listCollaborators` (`listId`);--> statement-breakpoint\nCREATE INDEX `listCollaborators_userId_idx` ON `listCollaborators` (`userId`);--> statement-breakpoint\nCREATE UNIQUE INDEX `listCollaborators_listId_userId_unique` ON `listCollaborators` (`listId`,`userId`);--> statement-breakpoint\nALTER TABLE `bookmarksInLists` ADD `listMembershipId` text REFERENCES listCollaborators(id) ON UPDATE no action ON DELETE cascade;\n"
  },
  {
    "path": "packages/db/drizzle/0066_collaborative_lists_invites.sql",
    "content": "CREATE TABLE `listInvitations` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`listId` text NOT NULL,\n\t`userId` text NOT NULL,\n\t`role` text NOT NULL,\n\t`status` text DEFAULT 'pending' NOT NULL,\n\t`invitedAt` integer NOT NULL,\n\t`invitedEmail` text,\n\t`invitedBy` text,\n\tFOREIGN KEY (`listId`) REFERENCES `bookmarkLists`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`invitedBy`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE set null\n);\n--> statement-breakpoint\nCREATE INDEX `listInvitations_listId_idx` ON `listInvitations` (`listId`);--> statement-breakpoint\nCREATE INDEX `listInvitations_userId_idx` ON `listInvitations` (`userId`);--> statement-breakpoint\nCREATE INDEX `listInvitations_status_idx` ON `listInvitations` (`status`);--> statement-breakpoint\nCREATE UNIQUE INDEX `listInvitations_listId_userId_unique` ON `listInvitations` (`listId`,`userId`);"
  },
  {
    "path": "packages/db/drizzle/0067_add_backups_table.sql",
    "content": "CREATE TABLE `backups` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`userId` text NOT NULL,\n\t`assetId` text,\n\t`createdAt` integer NOT NULL,\n\t`size` integer NOT NULL,\n\t`bookmarkCount` integer NOT NULL,\n\t`status` text DEFAULT 'pending' NOT NULL,\n\t`errorMessage` text,\n\tFOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`assetId`) REFERENCES `assets`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nCREATE INDEX `backups_userId_idx` ON `backups` (`userId`);--> statement-breakpoint\nCREATE INDEX `backups_createdAt_idx` ON `backups` (`createdAt`);--> statement-breakpoint\nALTER TABLE `user` ADD `backupsEnabled` integer DEFAULT false NOT NULL;--> statement-breakpoint\nALTER TABLE `user` ADD `backupsFrequency` text DEFAULT 'weekly' NOT NULL;--> statement-breakpoint\nALTER TABLE `user` ADD `backupsRetentionDays` integer DEFAULT 30 NOT NULL;"
  },
  {
    "path": "packages/db/drizzle/0068_optimize_bookmark_indicies.sql",
    "content": "DROP INDEX `bookmarks_archived_idx`;--> statement-breakpoint\nDROP INDEX `bookmarks_favourited_idx`;--> statement-breakpoint\nCREATE INDEX `bookmarks_userId_createdAt_id_idx` ON `bookmarks` (`userId`,`createdAt`,`id`);--> statement-breakpoint\nCREATE INDEX `bookmarks_userId_archived_createdAt_id_idx` ON `bookmarks` (`userId`,`archived`,`createdAt`,`id`);--> statement-breakpoint\nCREATE INDEX `bookmarks_userId_favourited_createdAt_id_idx` ON `bookmarks` (`userId`,`favourited`,`createdAt`,`id`);--> statement-breakpoint\nCREATE INDEX `bookmarksInLists_listId_bookmarkId_idx` ON `bookmarksInLists` (`listId`,`bookmarkId`);--> statement-breakpoint\nCREATE INDEX `rssFeedImports_rssFeedId_bookmarkId_idx` ON `rssFeedImports` (`rssFeedId`,`bookmarkId`);--> statement-breakpoint\nCREATE INDEX `tagsOnBookmarks_tagId_bookmarkId_idx` ON `tagsOnBookmarks` (`tagId`,`bookmarkId`);"
  },
  {
    "path": "packages/db/drizzle/0069_fix_pending_summarization.sql",
    "content": "UPDATE bookmarks SET `summarizationStatus` = NULL WHERE `summarizationStatus` = 'pending' and `bookmarks`.`type` != 'link';\n"
  },
  {
    "path": "packages/db/drizzle/0070_add_reader_settings.sql",
    "content": "ALTER TABLE `user` ADD `readerFontSize` integer;--> statement-breakpoint\nALTER TABLE `user` ADD `readerLineHeight` real;--> statement-breakpoint\nALTER TABLE `user` ADD `readerFontFamily` text;"
  },
  {
    "path": "packages/db/drizzle/0071_add_normalized_tag_name.sql",
    "content": "ALTER TABLE `bookmarkTags` ADD `normalizedName` text GENERATED ALWAYS AS (lower(replace(replace(replace(\"name\", ' ', ''), '-', ''), '_', ''))) VIRTUAL;--> statement-breakpoint\nCREATE INDEX `bookmarkTags_normalizedName_idx` ON `bookmarkTags` (`normalizedName`);"
  },
  {
    "path": "packages/db/drizzle/0072_add_user_ai_preferences.sql",
    "content": "ALTER TABLE `user` ADD `autoTaggingEnabled` integer;--> statement-breakpoint\nALTER TABLE `user` ADD `autoSummarizationEnabled` integer;"
  },
  {
    "path": "packages/db/drizzle/0073_ai_tag_style.sql",
    "content": "ALTER TABLE `user` ADD `tagStyle` text DEFAULT 'lowercase-hyphens';--> statement-breakpoint\nALTER TABLE `user` ADD `inferredTagLang` text;--> statement-breakpoint\nUPDATE `user` SET `tagStyle` = 'as-generated';\n"
  },
  {
    "path": "packages/db/drizzle/0074_reset_tagging_summarization.sql",
    "content": "UPDATE bookmarks\nSET\n  taggingStatus = CASE\n    WHEN taggingStatus = 'pending' THEN NULL\n    ELSE taggingStatus\n  END,\n  summarizationStatus = CASE\n    WHEN summarizationStatus = 'pending' THEN NULL\n    ELSE summarizationStatus\n  END\nWHERE id IN (\n  SELECT id\n  FROM bookmarkLinks\n  WHERE crawlStatus = 'failure'\n);\n"
  },
  {
    "path": "packages/db/drizzle/0075_change_default_tag_style.sql",
    "content": "-- Change default for tagStyle from 'lowercase-hyphens' to 'titlecase-spaces'\n-- Using rename/add/drop pattern to avoid DROP TABLE which triggers cascade deletes\nALTER TABLE `user` RENAME COLUMN `tagStyle` TO `tagStyle_old`;--> statement-breakpoint\nALTER TABLE `user` ADD COLUMN `tagStyle` text DEFAULT 'titlecase-spaces';--> statement-breakpoint\nUPDATE `user` SET `tagStyle` = `tagStyle_old`;--> statement-breakpoint\nALTER TABLE `user` DROP COLUMN `tagStyle_old`;\n"
  },
  {
    "path": "packages/db/drizzle/0076_add_api_key_last_used_tracking.sql",
    "content": "ALTER TABLE `apiKey` ADD `lastUsedAt` integer;"
  },
  {
    "path": "packages/db/drizzle/0077_import_listpaths_to_listids.sql",
    "content": "CREATE TABLE `importStagingBookmarks` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`importSessionId` text NOT NULL,\n\t`type` text NOT NULL,\n\t`url` text,\n\t`title` text,\n\t`content` text,\n\t`note` text,\n\t`tags` text,\n\t`listIds` text,\n\t`sourceAddedAt` integer,\n\t`status` text DEFAULT 'pending' NOT NULL,\n\t`processingStartedAt` integer,\n\t`result` text,\n\t`resultReason` text,\n\t`resultBookmarkId` text,\n\t`createdAt` integer NOT NULL,\n\t`completedAt` integer,\n\tFOREIGN KEY (`importSessionId`) REFERENCES `importSessions`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`resultBookmarkId`) REFERENCES `bookmarks`(`id`) ON UPDATE no action ON DELETE set null\n);\n--> statement-breakpoint\nCREATE INDEX `importStaging_session_status_idx` ON `importStagingBookmarks` (`importSessionId`,`status`);--> statement-breakpoint\nCREATE INDEX `importStaging_completedAt_idx` ON `importStagingBookmarks` (`completedAt`);--> statement-breakpoint\nALTER TABLE `importSessions` ADD `status` text DEFAULT 'staging' NOT NULL;--> statement-breakpoint\nALTER TABLE `importSessions` ADD `lastProcessedAt` integer;--> statement-breakpoint\n-- Migrate legacy importSessionBookmarks into importStagingBookmarks.\n-- Reuses the same ID from the old table.\n-- Calculates status based on actual downstream crawl/tagging state.\nINSERT INTO importStagingBookmarks (\n  id, importSessionId, type, url,\n  status, processingStartedAt, result, resultBookmarkId, createdAt, completedAt\n)\nSELECT\n  isb.id,\n  isb.importSessionId,\n  b.type,\n  bl.url,\n  CASE\n    WHEN (bl.crawlStatus IS NULL OR bl.crawlStatus IN ('success', 'failure'))\n     AND (b.taggingStatus IS NULL OR b.taggingStatus IN ('success', 'failure'))\n    THEN 'completed'\n    ELSE 'processing'\n  END,\n  isb.createdAt,\n  'accepted',\n  isb.bookmarkId,\n  isb.createdAt,\n  CASE\n    WHEN (bl.crawlStatus IS NULL OR bl.crawlStatus IN ('success', 'failure'))\n     AND (b.taggingStatus IS NULL OR b.taggingStatus IN ('success', 'failure'))\n    THEN isb.createdAt\n    ELSE NULL\n  END\nFROM importSessionBookmarks isb\nJOIN bookmarks b ON b.id = isb.bookmarkId\nLEFT JOIN bookmarkLinks bl ON bl.id = isb.bookmarkId\nWHERE NOT EXISTS (\n  SELECT 1 FROM importStagingBookmarks stg\n  WHERE stg.importSessionId = isb.importSessionId\n);\n--> statement-breakpoint\n-- Move legacy sessions out of staging:\n-- - Running if any items are still processing downstream\n-- - Completed otherwise (including sessions with no remaining items)\nUPDATE importSessions\nSET status = CASE\n  WHEN EXISTS (\n    SELECT 1\n    FROM importStagingBookmarks stg\n    WHERE stg.importSessionId = importSessions.id\n      AND stg.status = 'processing'\n  )\n  THEN 'running'\n  ELSE 'completed'\nEND\nWHERE status = 'staging';\n"
  },
  {
    "path": "packages/db/drizzle/0078_add_import_session_indexes.sql",
    "content": "CREATE INDEX `importSessions_status_idx` ON `importSessions` (`status`);--> statement-breakpoint\nCREATE INDEX `importStaging_status_idx` ON `importStagingBookmarks` (`status`);--> statement-breakpoint\nCREATE INDEX `importStaging_status_processingStartedAt_idx` ON `importStagingBookmarks` (`status`,`processingStartedAt`);"
  },
  {
    "path": "packages/db/drizzle/0079_add_tag_granularity_settings.sql",
    "content": "ALTER TABLE `user` ADD `curatedTagIds` text;"
  },
  {
    "path": "packages/db/drizzle/0080_user_reading_progress.sql",
    "content": "CREATE TABLE `userReadingProgress` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`bookmarkId` text NOT NULL,\n\t`userId` text NOT NULL,\n\t`readingProgressOffset` integer NOT NULL,\n\t`readingProgressAnchor` text,\n\t`readingProgressPercent` integer,\n\t`modifiedAt` integer,\n\tFOREIGN KEY (`bookmarkId`) REFERENCES `bookmarks`(`id`) ON UPDATE no action ON DELETE cascade,\n\tFOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade\n);\n--> statement-breakpoint\nCREATE INDEX `userReadingProgress_bookmarkId_idx` ON `userReadingProgress` (`bookmarkId`);--> statement-breakpoint\nCREATE INDEX `userReadingProgress_userId_idx` ON `userReadingProgress` (`userId`);--> statement-breakpoint\nCREATE UNIQUE INDEX `userReadingProgress_bookmarkId_userId_unique` ON `userReadingProgress` (`bookmarkId`,`userId`);"
  },
  {
    "path": "packages/db/drizzle/meta/0000_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_name_unique\": {\n          \"name\": \"apiKey_name_unique\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"926e135f-db63-4273-b193-4eb2b5c1784d\",\n  \"prevId\": \"00000000-0000-0000-0000-000000000000\"\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0001_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_name_unique\": {\n          \"name\": \"apiKey_name_unique\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"30b63cde-0b6e-42fd-8755-edb258b04dd0\",\n  \"prevId\": \"926e135f-db63-4273-b193-4eb2b5c1784d\"\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0002_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_name_unique\": {\n          \"name\": \"apiKey_name_unique\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_name_userId_unique\": {\n          \"name\": \"bookmarkLists_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"e0b3dd84-57d4-4adb-97da-98a6ff5d7d34\",\n  \"prevId\": \"30b63cde-0b6e-42fd-8755-edb258b04dd0\"\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0003_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_name_unique\": {\n          \"name\": \"apiKey_name_unique\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_name_userId_unique\": {\n          \"name\": \"bookmarkLists_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"28b3c23e-65eb-44b4-bf9c-8573a6ccde8a\",\n  \"prevId\": \"e0b3dd84-57d4-4adb-97da-98a6ff5d7d34\"\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0004_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_name_unique\": {\n          \"name\": \"apiKey_name_unique\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_name_userId_unique\": {\n          \"name\": \"bookmarkLists_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"c8e820c3-3f51-4109-a4b8-b11741e956b4\",\n  \"prevId\": \"28b3c23e-65eb-44b4-bf9c-8573a6ccde8a\"\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0005_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_name_userId_unique\": {\n          \"name\": \"bookmarkLists_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"cf381349-a0ec-420a-8719-fe1d2d8a6882\",\n  \"prevId\": \"c8e820c3-3f51-4109-a4b8-b11741e956b4\"\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0006_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_name_userId_unique\": {\n          \"name\": \"bookmarkLists_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"e07f4785-769a-41bc-b7d1-be6cf996a106\",\n  \"prevId\": \"cf381349-a0ec-420a-8719-fe1d2d8a6882\"\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0007_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_name_userId_unique\": {\n          \"name\": \"bookmarkLists_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"a8e7f612-a65b-4988-90fb-175723d8aaec\",\n  \"prevId\": \"e07f4785-769a-41bc-b7d1-be6cf996a106\"\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0008_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_name_userId_unique\": {\n          \"name\": \"bookmarkLists_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"0ec154ac-9cca-4621-b8d8-215c6ce22154\",\n  \"prevId\": \"a8e7f612-a65b-4988-90fb-175723d8aaec\"\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0009_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_name_userId_unique\": {\n          \"name\": \"bookmarkLists_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"11bbea06-c80c-4030-9a54-7dd9f1d13235\",\n  \"prevId\": \"0ec154ac-9cca-4621-b8d8-215c6ce22154\"\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0010_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"encoding\": {\n          \"name\": \"encoding\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"blob\": {\n          \"name\": \"blob\",\n          \"type\": \"blob\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_name_userId_unique\": {\n          \"name\": \"bookmarkLists_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"3c99cddb-9a13-42a5-a5e3-3b42cc33bad5\",\n  \"prevId\": \"11bbea06-c80c-4030-9a54-7dd9f1d13235\"\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0011_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"encoding\": {\n          \"name\": \"encoding\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"blob\": {\n          \"name\": \"blob\",\n          \"type\": \"blob\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarkAssets_assetId_assets_id_fk\": {\n          \"name\": \"bookmarkAssets_assetId_assets_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"columnsFrom\": [\n            \"assetId\"\n          ],\n          \"tableTo\": \"assets\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_name_userId_unique\": {\n          \"name\": \"bookmarkLists_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"a5520e12-3a85-4783-af68-fc4c14ab5485\",\n  \"prevId\": \"3c99cddb-9a13-42a5-a5e3-3b42cc33bad5\"\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0012_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkAssets2\": {\n      \"name\": \"bookmarkAssets2\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets2_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets2_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets2\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_name_userId_unique\": {\n          \"name\": \"bookmarkLists_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"1bfe76b3-a1df-4d6b-aed3-fa27a33eceed\",\n  \"prevId\": \"a5520e12-3a85-4783-af68-fc4c14ab5485\"\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0013_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_name_userId_unique\": {\n          \"name\": \"bookmarkLists_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {\n      \"\\\"bookmarkAssets2\\\"\": \"\\\"bookmarkAssets\\\"\"\n    },\n    \"columns\": {}\n  },\n  \"id\": \"fe8beb72-9585-4552-b636-a06f08c63cf8\",\n  \"prevId\": \"1bfe76b3-a1df-4d6b-aed3-fa27a33eceed\"\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0014_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_name_userId_unique\": {\n          \"name\": \"bookmarkLists_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"55a06f75-6b46-41d9-9f39-d842f065551d\",\n  \"prevId\": \"fe8beb72-9585-4552-b636-a06f08c63cf8\"\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0015_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"info\": {\n          \"name\": \"info\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_name_userId_unique\": {\n          \"name\": \"bookmarkLists_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"72cbe030-48d3-4cd6-ae90-8897291cd711\",\n  \"prevId\": \"55a06f75-6b46-41d9-9f39-d842f065551d\"\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0016_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_name_userId_unique\": {\n          \"name\": \"bookmarkLists_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"72b4a92d-dcf7-4a16-9f58-14dca905cbd4\",\n  \"prevId\": \"72cbe030-48d3-4cd6-ae90-8897291cd711\"\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0017_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_name_userId_unique\": {\n          \"name\": \"bookmarkLists_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"58699146-ac61-4f68-94d3-645566b26b52\",\n  \"prevId\": \"72b4a92d-dcf7-4a16-9f58-14dca905cbd4\"\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0018_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_name_userId_unique\": {\n          \"name\": \"bookmarkLists_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"ca4ff2bd-aed3-474d-92a1-68e3a8349e01\",\n  \"prevId\": \"58699146-ac61-4f68-94d3-645566b26b52\"\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0019_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"set null\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"3e975d6c-1289-487f-8e78-6f4eda5243d2\",\n  \"prevId\": \"ca4ff2bd-aed3-474d-92a1-68e3a8349e01\"\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0020_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"screenshotAssetId\": {\n          \"name\": \"screenshotAssetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageAssetId\": {\n          \"name\": \"imageAssetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"set null\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"cdb3562b-0d7a-4f1d-bbc9-71b1119d8d88\",\n  \"prevId\": \"3e975d6c-1289-487f-8e78-6f4eda5243d2\"\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0021_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"screenshotAssetId\": {\n          \"name\": \"screenshotAssetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageAssetId\": {\n          \"name\": \"imageAssetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"set null\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"35f45396-0455-4384-b42c-e9eb62cb3128\",\n  \"prevId\": \"cdb3562b-0d7a-4f1d-bbc9-71b1119d8d88\"\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0022_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"screenshotAssetId\": {\n          \"name\": \"screenshotAssetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fullPageArchiveAssetId\": {\n          \"name\": \"fullPageArchiveAssetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageAssetId\": {\n          \"name\": \"imageAssetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"set null\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"f2897961-faba-4fc4-9d82-85e7cf316218\",\n  \"prevId\": \"35f45396-0455-4384-b42c-e9eb62cb3128\"\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0023_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"screenshotAssetId\": {\n          \"name\": \"screenshotAssetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fullPageArchiveAssetId\": {\n          \"name\": \"fullPageArchiveAssetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageAssetId\": {\n          \"name\": \"imageAssetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"set null\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"d33de747-6acb-4160-a5ec-a4a7adee3023\",\n  \"prevId\": \"f2897961-faba-4fc4-9d82-85e7cf316218\"\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0024_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"set null\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"8a080f45-d358-465e-80b7-ae0c557e3872\",\n  \"prevId\": \"d33de747-6acb-4160-a5ec-a4a7adee3023\"\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0025_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"set null\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"e3541a64-2e1d-407a-a6b0-7384f402da4d\",\n  \"prevId\": \"8a080f45-d358-465e-80b7-ae0c557e3872\"\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0026_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"set null\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"id\": \"472a0256-4c40-464f-a2ff-851cdebb63c9\",\n  \"prevId\": \"e3541a64-2e1d-407a-a6b0-7384f402da4d\"\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0027_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"486882c9-79bd-4665-948b-831834e3509c\",\n  \"prevId\": \"472a0256-4c40-464f-a2ff-851cdebb63c9\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0028_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"007b1fea-900a-41e4-8684-ca35c279d78f\",\n  \"prevId\": \"486882c9-79bd-4665-948b-831834e3509c\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0029_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"e64dea2c-6fd3-4d93-8258-b1f6babe4d07\",\n  \"prevId\": \"007b1fea-900a-41e4-8684-ca35c279d78f\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0030_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"3188359f-c324-4fca-be40-4903de779a2d\",\n  \"prevId\": \"e64dea2c-6fd3-4d93-8258-b1f6babe4d07\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0031_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"b79989eb-b62a-4ea7-aa4b-f3de839cb15e\",\n  \"prevId\": \"3188359f-c324-4fca-be40-4903de779a2d\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0032_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"f91f9a0f-8d99-4dc8-86db-58be9ede8982\",\n  \"prevId\": \"b79989eb-b62a-4ea7-aa4b-f3de839cb15e\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0033_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"d78bb41e-9b28-4782-9372-ab7a4553e1b8\",\n  \"prevId\": \"f91f9a0f-8d99-4dc8-86db-58be9ede8982\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0034_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"32bc9d76-23e2-4c45-b9f1-53a9ed3ffdc5\",\n  \"prevId\": \"d78bb41e-9b28-4782-9372-ab7a4553e1b8\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0035_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"493565c7-b82b-48b8-aae4-78f83ad62cb3\",\n  \"prevId\": \"32bc9d76-23e2-4c45-b9f1-53a9ed3ffdc5\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0036_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"7c51445d-0b54-4a42-aad3-630b85601478\",\n  \"prevId\": \"493565c7-b82b-48b8-aae4-78f83ad62cb3\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {}\n    }\n  },\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0037_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"21860136-dd1f-4d1e-b157-3c99132e2036\",\n  \"prevId\": \"7c51445d-0b54-4a42-aad3-630b85601478\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0038_snapshot.json",
    "content": "{\n  \"id\": \"6a9c6037-7670-47d3-97c4-a1a3bbb5078a\",\n  \"prevId\": \"21860136-dd1f-4d1e-b157-3c99132e2036\",\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"set null\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"tableTo\": \"rssFeeds\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"set null\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"columns\": {},\n    \"schemas\": {},\n    \"tables\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0039_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"29f70abf-f39f-4743-a063-3db6da5c46a7\",\n  \"prevId\": \"6a9c6037-7670-47d3-97c4-a1a3bbb5078a\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {\n      \"\\\"customPrompts\\\".\\\"attachedBy\\\"\": \"\\\"customPrompts\\\".\\\"appliesTo\\\"\"\n    }\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0040_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"943b28e9-4b89-48c9-9e00-65314a09bd92\",\n  \"prevId\": \"29f70abf-f39f-4743-a063-3db6da5c46a7\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0041_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"4219851a-7eed-421f-b52e-5d9cd045ff85\",\n  \"prevId\": \"943b28e9-4b89-48c9-9e00-65314a09bd92\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0042_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"76f512f9-3530-4241-8cd9-ae7ec9e4a751\",\n  \"prevId\": \"4219851a-7eed-421f-b52e-5d9cd045ff85\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"author\": {\n          \"name\": \"author\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"publisher\": {\n          \"name\": \"publisher\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"datePublished\": {\n          \"name\": \"datePublished\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dateModified\": {\n          \"name\": \"dateModified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0043_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"4ab541c8-c3f7-45fe-814a-17eec69c6351\",\n  \"prevId\": \"76f512f9-3530-4241-8cd9-ae7ec9e4a751\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"author\": {\n          \"name\": \"author\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"publisher\": {\n          \"name\": \"publisher\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"datePublished\": {\n          \"name\": \"datePublished\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dateModified\": {\n          \"name\": \"dateModified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0044_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"1592d5ec-1cbd-45f5-9061-fbaece6eb91e\",\n  \"prevId\": \"4ab541c8-c3f7-45fe-814a-17eec69c6351\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"author\": {\n          \"name\": \"author\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"publisher\": {\n          \"name\": \"publisher\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"datePublished\": {\n          \"name\": \"datePublished\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dateModified\": {\n          \"name\": \"dateModified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"salt\": {\n          \"name\": \"salt\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0045_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"dce61b2b-d896-425a-a9cd-6a79f596d7b2\",\n  \"prevId\": \"1592d5ec-1cbd-45f5-9061-fbaece6eb91e\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"author\": {\n          \"name\": \"author\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"publisher\": {\n          \"name\": \"publisher\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"datePublished\": {\n          \"name\": \"datePublished\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dateModified\": {\n          \"name\": \"dateModified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_userId_id_idx\": {\n          \"name\": \"bookmarkLists_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"bookmarkTags_userId_id_idx\": {\n          \"name\": \"bookmarkTags_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineActions\": {\n      \"name\": \"ruleEngineActions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"ruleId\": {\n          \"name\": \"ruleId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"action\": {\n          \"name\": \"action\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngineActions_userId_idx\": {\n          \"name\": \"ruleEngineActions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"ruleEngineActions_ruleId_idx\": {\n          \"name\": \"ruleEngineActions_ruleId_idx\",\n          \"columns\": [\n            \"ruleId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineActions_userId_user_id_fk\": {\n          \"name\": \"ruleEngineActions_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\": {\n          \"name\": \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"ruleId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_tagId_fk\": {\n          \"name\": \"ruleEngineActions_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_listId_fk\": {\n          \"name\": \"ruleEngineActions_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineRules\": {\n      \"name\": \"ruleEngineRules\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"event\": {\n          \"name\": \"event\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"condition\": {\n          \"name\": \"condition\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngine_userId_idx\": {\n          \"name\": \"ruleEngine_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineRules_userId_user_id_fk\": {\n          \"name\": \"ruleEngineRules_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_tagId_fk\": {\n          \"name\": \"ruleEngineRules_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_listId_fk\": {\n          \"name\": \"ruleEngineRules_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"salt\": {\n          \"name\": \"salt\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0046_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"7a950001-03e7-43f2-8ef5-272dfe82d223\",\n  \"prevId\": \"dce61b2b-d896-425a-a9cd-6a79f596d7b2\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"author\": {\n          \"name\": \"author\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"publisher\": {\n          \"name\": \"publisher\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"datePublished\": {\n          \"name\": \"datePublished\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dateModified\": {\n          \"name\": \"dateModified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_userId_id_idx\": {\n          \"name\": \"bookmarkLists_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"bookmarkTags_userId_id_idx\": {\n          \"name\": \"bookmarkTags_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineActions\": {\n      \"name\": \"ruleEngineActions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"ruleId\": {\n          \"name\": \"ruleId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"action\": {\n          \"name\": \"action\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngineActions_userId_idx\": {\n          \"name\": \"ruleEngineActions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"ruleEngineActions_ruleId_idx\": {\n          \"name\": \"ruleEngineActions_ruleId_idx\",\n          \"columns\": [\n            \"ruleId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineActions_userId_user_id_fk\": {\n          \"name\": \"ruleEngineActions_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\": {\n          \"name\": \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"ruleId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_tagId_fk\": {\n          \"name\": \"ruleEngineActions_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_listId_fk\": {\n          \"name\": \"ruleEngineActions_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineRules\": {\n      \"name\": \"ruleEngineRules\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"event\": {\n          \"name\": \"event\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"condition\": {\n          \"name\": \"condition\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngine_userId_idx\": {\n          \"name\": \"ruleEngine_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineRules_userId_user_id_fk\": {\n          \"name\": \"ruleEngineRules_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_tagId_fk\": {\n          \"name\": \"ruleEngineRules_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_listId_fk\": {\n          \"name\": \"ruleEngineRules_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"salt\": {\n          \"name\": \"salt\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0047_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"cd8ff1a8-c7bb-4576-9bec-2644785c894e\",\n  \"prevId\": \"7a950001-03e7-43f2-8ef5-272dfe82d223\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"author\": {\n          \"name\": \"author\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"publisher\": {\n          \"name\": \"publisher\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"datePublished\": {\n          \"name\": \"datePublished\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dateModified\": {\n          \"name\": \"dateModified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_userId_id_idx\": {\n          \"name\": \"bookmarkLists_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"bookmarkTags_userId_id_idx\": {\n          \"name\": \"bookmarkTags_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summarizationStatus\": {\n          \"name\": \"summarizationStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineActions\": {\n      \"name\": \"ruleEngineActions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"ruleId\": {\n          \"name\": \"ruleId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"action\": {\n          \"name\": \"action\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngineActions_userId_idx\": {\n          \"name\": \"ruleEngineActions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"ruleEngineActions_ruleId_idx\": {\n          \"name\": \"ruleEngineActions_ruleId_idx\",\n          \"columns\": [\n            \"ruleId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineActions_userId_user_id_fk\": {\n          \"name\": \"ruleEngineActions_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\": {\n          \"name\": \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"ruleId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_tagId_fk\": {\n          \"name\": \"ruleEngineActions_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_listId_fk\": {\n          \"name\": \"ruleEngineActions_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineRules\": {\n      \"name\": \"ruleEngineRules\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"event\": {\n          \"name\": \"event\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"condition\": {\n          \"name\": \"condition\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngine_userId_idx\": {\n          \"name\": \"ruleEngine_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineRules_userId_user_id_fk\": {\n          \"name\": \"ruleEngineRules_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_tagId_fk\": {\n          \"name\": \"ruleEngineRules_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_listId_fk\": {\n          \"name\": \"ruleEngineRules_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"salt\": {\n          \"name\": \"salt\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0048_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"d3ffc2ee-399e-4652-813d-652b56d649f6\",\n  \"prevId\": \"cd8ff1a8-c7bb-4576-9bec-2644785c894e\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"author\": {\n          \"name\": \"author\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"publisher\": {\n          \"name\": \"publisher\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"datePublished\": {\n          \"name\": \"datePublished\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dateModified\": {\n          \"name\": \"dateModified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_userId_id_idx\": {\n          \"name\": \"bookmarkLists_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"bookmarkTags_userId_id_idx\": {\n          \"name\": \"bookmarkTags_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summarizationStatus\": {\n          \"name\": \"summarizationStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineActions\": {\n      \"name\": \"ruleEngineActions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"ruleId\": {\n          \"name\": \"ruleId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"action\": {\n          \"name\": \"action\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngineActions_userId_idx\": {\n          \"name\": \"ruleEngineActions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"ruleEngineActions_ruleId_idx\": {\n          \"name\": \"ruleEngineActions_ruleId_idx\",\n          \"columns\": [\n            \"ruleId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineActions_userId_user_id_fk\": {\n          \"name\": \"ruleEngineActions_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\": {\n          \"name\": \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"ruleId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_tagId_fk\": {\n          \"name\": \"ruleEngineActions_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_listId_fk\": {\n          \"name\": \"ruleEngineActions_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineRules\": {\n      \"name\": \"ruleEngineRules\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"event\": {\n          \"name\": \"event\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"condition\": {\n          \"name\": \"condition\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngine_userId_idx\": {\n          \"name\": \"ruleEngine_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineRules_userId_user_id_fk\": {\n          \"name\": \"ruleEngineRules_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_tagId_fk\": {\n          \"name\": \"ruleEngineRules_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_listId_fk\": {\n          \"name\": \"ruleEngineRules_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"userSettings\": {\n      \"name\": \"userSettings\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkClickAction\": {\n          \"name\": \"bookmarkClickAction\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'open_original_link'\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"userSettings_userId_user_id_fk\": {\n          \"name\": \"userSettings_userId_user_id_fk\",\n          \"tableFrom\": \"userSettings\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"salt\": {\n          \"name\": \"salt\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0049_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"98a6c743-e257-4126-a95f-40850e03ee4e\",\n  \"prevId\": \"d3ffc2ee-399e-4652-813d-652b56d649f6\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"author\": {\n          \"name\": \"author\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"publisher\": {\n          \"name\": \"publisher\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"datePublished\": {\n          \"name\": \"datePublished\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dateModified\": {\n          \"name\": \"dateModified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rssToken\": {\n          \"name\": \"rssToken\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_userId_id_idx\": {\n          \"name\": \"bookmarkLists_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"bookmarkTags_userId_id_idx\": {\n          \"name\": \"bookmarkTags_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summarizationStatus\": {\n          \"name\": \"summarizationStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineActions\": {\n      \"name\": \"ruleEngineActions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"ruleId\": {\n          \"name\": \"ruleId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"action\": {\n          \"name\": \"action\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngineActions_userId_idx\": {\n          \"name\": \"ruleEngineActions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"ruleEngineActions_ruleId_idx\": {\n          \"name\": \"ruleEngineActions_ruleId_idx\",\n          \"columns\": [\n            \"ruleId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineActions_userId_user_id_fk\": {\n          \"name\": \"ruleEngineActions_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\": {\n          \"name\": \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"ruleId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_tagId_fk\": {\n          \"name\": \"ruleEngineActions_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_listId_fk\": {\n          \"name\": \"ruleEngineActions_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineRules\": {\n      \"name\": \"ruleEngineRules\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"event\": {\n          \"name\": \"event\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"condition\": {\n          \"name\": \"condition\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngine_userId_idx\": {\n          \"name\": \"ruleEngine_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineRules_userId_user_id_fk\": {\n          \"name\": \"ruleEngineRules_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_tagId_fk\": {\n          \"name\": \"ruleEngineRules_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_listId_fk\": {\n          \"name\": \"ruleEngineRules_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"userSettings\": {\n      \"name\": \"userSettings\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkClickAction\": {\n          \"name\": \"bookmarkClickAction\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'open_original_link'\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"userSettings_userId_user_id_fk\": {\n          \"name\": \"userSettings_userId_user_id_fk\",\n          \"tableFrom\": \"userSettings\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"salt\": {\n          \"name\": \"salt\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0050_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"a92cdc19-e420-4f05-bfac-9fbbb2f5b8a3\",\n  \"prevId\": \"98a6c743-e257-4126-a95f-40850e03ee4e\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"author\": {\n          \"name\": \"author\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"publisher\": {\n          \"name\": \"publisher\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"datePublished\": {\n          \"name\": \"datePublished\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dateModified\": {\n          \"name\": \"dateModified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rssToken\": {\n          \"name\": \"rssToken\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_userId_id_idx\": {\n          \"name\": \"bookmarkLists_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"bookmarkTags_userId_id_idx\": {\n          \"name\": \"bookmarkTags_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summarizationStatus\": {\n          \"name\": \"summarizationStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineActions\": {\n      \"name\": \"ruleEngineActions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"ruleId\": {\n          \"name\": \"ruleId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"action\": {\n          \"name\": \"action\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngineActions_userId_idx\": {\n          \"name\": \"ruleEngineActions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"ruleEngineActions_ruleId_idx\": {\n          \"name\": \"ruleEngineActions_ruleId_idx\",\n          \"columns\": [\n            \"ruleId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineActions_userId_user_id_fk\": {\n          \"name\": \"ruleEngineActions_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\": {\n          \"name\": \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"ruleId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_tagId_fk\": {\n          \"name\": \"ruleEngineActions_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_listId_fk\": {\n          \"name\": \"ruleEngineActions_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineRules\": {\n      \"name\": \"ruleEngineRules\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"event\": {\n          \"name\": \"event\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"condition\": {\n          \"name\": \"condition\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngine_userId_idx\": {\n          \"name\": \"ruleEngine_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineRules_userId_user_id_fk\": {\n          \"name\": \"ruleEngineRules_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_tagId_fk\": {\n          \"name\": \"ruleEngineRules_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_listId_fk\": {\n          \"name\": \"ruleEngineRules_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"userSettings\": {\n      \"name\": \"userSettings\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkClickAction\": {\n          \"name\": \"bookmarkClickAction\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'open_original_link'\"\n        },\n        \"archiveDisplayBehaviour\": {\n          \"name\": \"archiveDisplayBehaviour\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'show'\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"userSettings_userId_user_id_fk\": {\n          \"name\": \"userSettings_userId_user_id_fk\",\n          \"tableFrom\": \"userSettings\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"salt\": {\n          \"name\": \"salt\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0051_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"5a549719-8f7d-49ff-91cc-5ad0f3b5c4ef\",\n  \"prevId\": \"a92cdc19-e420-4f05-bfac-9fbbb2f5b8a3\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"author\": {\n          \"name\": \"author\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"publisher\": {\n          \"name\": \"publisher\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"datePublished\": {\n          \"name\": \"datePublished\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dateModified\": {\n          \"name\": \"dateModified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rssToken\": {\n          \"name\": \"rssToken\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_userId_id_idx\": {\n          \"name\": \"bookmarkLists_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"bookmarkTags_userId_id_idx\": {\n          \"name\": \"bookmarkTags_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summarizationStatus\": {\n          \"name\": \"summarizationStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineActions\": {\n      \"name\": \"ruleEngineActions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"ruleId\": {\n          \"name\": \"ruleId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"action\": {\n          \"name\": \"action\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngineActions_userId_idx\": {\n          \"name\": \"ruleEngineActions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"ruleEngineActions_ruleId_idx\": {\n          \"name\": \"ruleEngineActions_ruleId_idx\",\n          \"columns\": [\n            \"ruleId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineActions_userId_user_id_fk\": {\n          \"name\": \"ruleEngineActions_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\": {\n          \"name\": \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"ruleId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_tagId_fk\": {\n          \"name\": \"ruleEngineActions_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_listId_fk\": {\n          \"name\": \"ruleEngineActions_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineRules\": {\n      \"name\": \"ruleEngineRules\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"event\": {\n          \"name\": \"event\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"condition\": {\n          \"name\": \"condition\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngine_userId_idx\": {\n          \"name\": \"ruleEngine_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineRules_userId_user_id_fk\": {\n          \"name\": \"ruleEngineRules_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_tagId_fk\": {\n          \"name\": \"ruleEngineRules_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_listId_fk\": {\n          \"name\": \"ruleEngineRules_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"userSettings\": {\n      \"name\": \"userSettings\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkClickAction\": {\n          \"name\": \"bookmarkClickAction\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'open_original_link'\"\n        },\n        \"archiveDisplayBehaviour\": {\n          \"name\": \"archiveDisplayBehaviour\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'show'\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"userSettings_userId_user_id_fk\": {\n          \"name\": \"userSettings_userId_user_id_fk\",\n          \"tableFrom\": \"userSettings\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"salt\": {\n          \"name\": \"salt\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0052_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"786712cf-334d-43ab-bf05-8f926364b3e2\",\n  \"prevId\": \"5a549719-8f7d-49ff-91cc-5ad0f3b5c4ef\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"author\": {\n          \"name\": \"author\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"publisher\": {\n          \"name\": \"publisher\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"datePublished\": {\n          \"name\": \"datePublished\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dateModified\": {\n          \"name\": \"dateModified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rssToken\": {\n          \"name\": \"rssToken\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_userId_id_idx\": {\n          \"name\": \"bookmarkLists_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"bookmarkTags_userId_id_idx\": {\n          \"name\": \"bookmarkTags_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summarizationStatus\": {\n          \"name\": \"summarizationStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineActions\": {\n      \"name\": \"ruleEngineActions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"ruleId\": {\n          \"name\": \"ruleId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"action\": {\n          \"name\": \"action\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngineActions_userId_idx\": {\n          \"name\": \"ruleEngineActions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"ruleEngineActions_ruleId_idx\": {\n          \"name\": \"ruleEngineActions_ruleId_idx\",\n          \"columns\": [\n            \"ruleId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineActions_userId_user_id_fk\": {\n          \"name\": \"ruleEngineActions_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\": {\n          \"name\": \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"ruleId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_tagId_fk\": {\n          \"name\": \"ruleEngineActions_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_listId_fk\": {\n          \"name\": \"ruleEngineActions_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineRules\": {\n      \"name\": \"ruleEngineRules\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"event\": {\n          \"name\": \"event\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"condition\": {\n          \"name\": \"condition\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngine_userId_idx\": {\n          \"name\": \"ruleEngine_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineRules_userId_user_id_fk\": {\n          \"name\": \"ruleEngineRules_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_tagId_fk\": {\n          \"name\": \"ruleEngineRules_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_listId_fk\": {\n          \"name\": \"ruleEngineRules_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"userSettings\": {\n      \"name\": \"userSettings\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkClickAction\": {\n          \"name\": \"bookmarkClickAction\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'open_original_link'\"\n        },\n        \"archiveDisplayBehaviour\": {\n          \"name\": \"archiveDisplayBehaviour\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'show'\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"userSettings_userId_user_id_fk\": {\n          \"name\": \"userSettings_userId_user_id_fk\",\n          \"tableFrom\": \"userSettings\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"salt\": {\n          \"name\": \"salt\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        },\n        \"bookmarkQuota\": {\n          \"name\": \"bookmarkQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0053_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"6e851770-74a1-4985-886c-42c1183c14c7\",\n  \"prevId\": \"786712cf-334d-43ab-bf05-8f926364b3e2\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"author\": {\n          \"name\": \"author\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"publisher\": {\n          \"name\": \"publisher\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"datePublished\": {\n          \"name\": \"datePublished\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dateModified\": {\n          \"name\": \"dateModified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rssToken\": {\n          \"name\": \"rssToken\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_userId_id_idx\": {\n          \"name\": \"bookmarkLists_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"bookmarkTags_userId_id_idx\": {\n          \"name\": \"bookmarkTags_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summarizationStatus\": {\n          \"name\": \"summarizationStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineActions\": {\n      \"name\": \"ruleEngineActions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"ruleId\": {\n          \"name\": \"ruleId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"action\": {\n          \"name\": \"action\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngineActions_userId_idx\": {\n          \"name\": \"ruleEngineActions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"ruleEngineActions_ruleId_idx\": {\n          \"name\": \"ruleEngineActions_ruleId_idx\",\n          \"columns\": [\n            \"ruleId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineActions_userId_user_id_fk\": {\n          \"name\": \"ruleEngineActions_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\": {\n          \"name\": \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"ruleId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_tagId_fk\": {\n          \"name\": \"ruleEngineActions_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_listId_fk\": {\n          \"name\": \"ruleEngineActions_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineRules\": {\n      \"name\": \"ruleEngineRules\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"event\": {\n          \"name\": \"event\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"condition\": {\n          \"name\": \"condition\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngine_userId_idx\": {\n          \"name\": \"ruleEngine_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineRules_userId_user_id_fk\": {\n          \"name\": \"ruleEngineRules_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_tagId_fk\": {\n          \"name\": \"ruleEngineRules_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_listId_fk\": {\n          \"name\": \"ruleEngineRules_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"userSettings\": {\n      \"name\": \"userSettings\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkClickAction\": {\n          \"name\": \"bookmarkClickAction\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'open_original_link'\"\n        },\n        \"archiveDisplayBehaviour\": {\n          \"name\": \"archiveDisplayBehaviour\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'show'\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"userSettings_userId_user_id_fk\": {\n          \"name\": \"userSettings_userId_user_id_fk\",\n          \"tableFrom\": \"userSettings\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"salt\": {\n          \"name\": \"salt\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        },\n        \"bookmarkQuota\": {\n          \"name\": \"bookmarkQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"storageQuota\": {\n          \"name\": \"storageQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0054_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"ac5b67e5-b9a8-413b-bba7-80280d8ebfc8\",\n  \"prevId\": \"6e851770-74a1-4985-886c-42c1183c14c7\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"author\": {\n          \"name\": \"author\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"publisher\": {\n          \"name\": \"publisher\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"datePublished\": {\n          \"name\": \"datePublished\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dateModified\": {\n          \"name\": \"dateModified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rssToken\": {\n          \"name\": \"rssToken\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_userId_id_idx\": {\n          \"name\": \"bookmarkLists_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"bookmarkTags_userId_id_idx\": {\n          \"name\": \"bookmarkTags_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summarizationStatus\": {\n          \"name\": \"summarizationStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineActions\": {\n      \"name\": \"ruleEngineActions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"ruleId\": {\n          \"name\": \"ruleId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"action\": {\n          \"name\": \"action\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngineActions_userId_idx\": {\n          \"name\": \"ruleEngineActions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"ruleEngineActions_ruleId_idx\": {\n          \"name\": \"ruleEngineActions_ruleId_idx\",\n          \"columns\": [\n            \"ruleId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineActions_userId_user_id_fk\": {\n          \"name\": \"ruleEngineActions_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\": {\n          \"name\": \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"ruleId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_tagId_fk\": {\n          \"name\": \"ruleEngineActions_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_listId_fk\": {\n          \"name\": \"ruleEngineActions_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineRules\": {\n      \"name\": \"ruleEngineRules\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"event\": {\n          \"name\": \"event\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"condition\": {\n          \"name\": \"condition\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngine_userId_idx\": {\n          \"name\": \"ruleEngine_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineRules_userId_user_id_fk\": {\n          \"name\": \"ruleEngineRules_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_tagId_fk\": {\n          \"name\": \"ruleEngineRules_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_listId_fk\": {\n          \"name\": \"ruleEngineRules_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"userSettings\": {\n      \"name\": \"userSettings\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkClickAction\": {\n          \"name\": \"bookmarkClickAction\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'open_original_link'\"\n        },\n        \"archiveDisplayBehaviour\": {\n          \"name\": \"archiveDisplayBehaviour\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'show'\"\n        },\n        \"timezone\": {\n          \"name\": \"timezone\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'UTC'\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"userSettings_userId_user_id_fk\": {\n          \"name\": \"userSettings_userId_user_id_fk\",\n          \"tableFrom\": \"userSettings\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"salt\": {\n          \"name\": \"salt\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        },\n        \"bookmarkQuota\": {\n          \"name\": \"bookmarkQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"storageQuota\": {\n          \"name\": \"storageQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0055_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"a7674152-1484-4144-9faa-2f4597ba619e\",\n  \"prevId\": \"ac5b67e5-b9a8-413b-bba7-80280d8ebfc8\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"author\": {\n          \"name\": \"author\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"publisher\": {\n          \"name\": \"publisher\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"datePublished\": {\n          \"name\": \"datePublished\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dateModified\": {\n          \"name\": \"dateModified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"contentAssetId\": {\n          \"name\": \"contentAssetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rssToken\": {\n          \"name\": \"rssToken\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_userId_id_idx\": {\n          \"name\": \"bookmarkLists_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"bookmarkTags_userId_id_idx\": {\n          \"name\": \"bookmarkTags_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summarizationStatus\": {\n          \"name\": \"summarizationStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineActions\": {\n      \"name\": \"ruleEngineActions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"ruleId\": {\n          \"name\": \"ruleId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"action\": {\n          \"name\": \"action\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngineActions_userId_idx\": {\n          \"name\": \"ruleEngineActions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"ruleEngineActions_ruleId_idx\": {\n          \"name\": \"ruleEngineActions_ruleId_idx\",\n          \"columns\": [\n            \"ruleId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineActions_userId_user_id_fk\": {\n          \"name\": \"ruleEngineActions_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\": {\n          \"name\": \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"ruleId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_tagId_fk\": {\n          \"name\": \"ruleEngineActions_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_listId_fk\": {\n          \"name\": \"ruleEngineActions_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineRules\": {\n      \"name\": \"ruleEngineRules\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"event\": {\n          \"name\": \"event\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"condition\": {\n          \"name\": \"condition\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngine_userId_idx\": {\n          \"name\": \"ruleEngine_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineRules_userId_user_id_fk\": {\n          \"name\": \"ruleEngineRules_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_tagId_fk\": {\n          \"name\": \"ruleEngineRules_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_listId_fk\": {\n          \"name\": \"ruleEngineRules_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"userSettings\": {\n      \"name\": \"userSettings\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkClickAction\": {\n          \"name\": \"bookmarkClickAction\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'open_original_link'\"\n        },\n        \"archiveDisplayBehaviour\": {\n          \"name\": \"archiveDisplayBehaviour\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'show'\"\n        },\n        \"timezone\": {\n          \"name\": \"timezone\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'UTC'\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"userSettings_userId_user_id_fk\": {\n          \"name\": \"userSettings_userId_user_id_fk\",\n          \"tableFrom\": \"userSettings\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"salt\": {\n          \"name\": \"salt\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        },\n        \"bookmarkQuota\": {\n          \"name\": \"bookmarkQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"storageQuota\": {\n          \"name\": \"storageQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0056_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"c39611a8-5fb3-4fd2-8643-64e5ed826095\",\n  \"prevId\": \"a7674152-1484-4144-9faa-2f4597ba619e\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"author\": {\n          \"name\": \"author\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"publisher\": {\n          \"name\": \"publisher\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"datePublished\": {\n          \"name\": \"datePublished\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dateModified\": {\n          \"name\": \"dateModified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"contentAssetId\": {\n          \"name\": \"contentAssetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rssToken\": {\n          \"name\": \"rssToken\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_userId_id_idx\": {\n          \"name\": \"bookmarkLists_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"bookmarkTags_userId_id_idx\": {\n          \"name\": \"bookmarkTags_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summarizationStatus\": {\n          \"name\": \"summarizationStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invites\": {\n      \"name\": \"invites\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expiresAt\": {\n          \"name\": \"expiresAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"usedAt\": {\n          \"name\": \"usedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"invites_token_unique\": {\n          \"name\": \"invites_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"invites_invitedBy_user_id_fk\": {\n          \"name\": \"invites_invitedBy_user_id_fk\",\n          \"tableFrom\": \"invites\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineActions\": {\n      \"name\": \"ruleEngineActions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"ruleId\": {\n          \"name\": \"ruleId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"action\": {\n          \"name\": \"action\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngineActions_userId_idx\": {\n          \"name\": \"ruleEngineActions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"ruleEngineActions_ruleId_idx\": {\n          \"name\": \"ruleEngineActions_ruleId_idx\",\n          \"columns\": [\n            \"ruleId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineActions_userId_user_id_fk\": {\n          \"name\": \"ruleEngineActions_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\": {\n          \"name\": \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"ruleId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_tagId_fk\": {\n          \"name\": \"ruleEngineActions_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_listId_fk\": {\n          \"name\": \"ruleEngineActions_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineRules\": {\n      \"name\": \"ruleEngineRules\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"event\": {\n          \"name\": \"event\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"condition\": {\n          \"name\": \"condition\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngine_userId_idx\": {\n          \"name\": \"ruleEngine_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineRules_userId_user_id_fk\": {\n          \"name\": \"ruleEngineRules_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_tagId_fk\": {\n          \"name\": \"ruleEngineRules_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_listId_fk\": {\n          \"name\": \"ruleEngineRules_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"userSettings\": {\n      \"name\": \"userSettings\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkClickAction\": {\n          \"name\": \"bookmarkClickAction\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'open_original_link'\"\n        },\n        \"archiveDisplayBehaviour\": {\n          \"name\": \"archiveDisplayBehaviour\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'show'\"\n        },\n        \"timezone\": {\n          \"name\": \"timezone\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'UTC'\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"userSettings_userId_user_id_fk\": {\n          \"name\": \"userSettings_userId_user_id_fk\",\n          \"tableFrom\": \"userSettings\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"salt\": {\n          \"name\": \"salt\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        },\n        \"bookmarkQuota\": {\n          \"name\": \"bookmarkQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"storageQuota\": {\n          \"name\": \"storageQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0057_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"b5e79604-adc2-4ad2-b2e2-96a871ec8f01\",\n  \"prevId\": \"c39611a8-5fb3-4fd2-8643-64e5ed826095\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"author\": {\n          \"name\": \"author\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"publisher\": {\n          \"name\": \"publisher\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"datePublished\": {\n          \"name\": \"datePublished\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dateModified\": {\n          \"name\": \"dateModified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"contentAssetId\": {\n          \"name\": \"contentAssetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rssToken\": {\n          \"name\": \"rssToken\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_userId_id_idx\": {\n          \"name\": \"bookmarkLists_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"bookmarkTags_userId_id_idx\": {\n          \"name\": \"bookmarkTags_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summarizationStatus\": {\n          \"name\": \"summarizationStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invites\": {\n      \"name\": \"invites\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expiresAt\": {\n          \"name\": \"expiresAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"usedAt\": {\n          \"name\": \"usedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"invites_token_unique\": {\n          \"name\": \"invites_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"invites_invitedBy_user_id_fk\": {\n          \"name\": \"invites_invitedBy_user_id_fk\",\n          \"tableFrom\": \"invites\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"passwordResetToken\": {\n      \"name\": \"passwordResetToken\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"passwordResetToken_token_unique\": {\n          \"name\": \"passwordResetToken_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        },\n        \"passwordResetTokens_userId_idx\": {\n          \"name\": \"passwordResetTokens_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"passwordResetToken_userId_user_id_fk\": {\n          \"name\": \"passwordResetToken_userId_user_id_fk\",\n          \"tableFrom\": \"passwordResetToken\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineActions\": {\n      \"name\": \"ruleEngineActions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"ruleId\": {\n          \"name\": \"ruleId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"action\": {\n          \"name\": \"action\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngineActions_userId_idx\": {\n          \"name\": \"ruleEngineActions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"ruleEngineActions_ruleId_idx\": {\n          \"name\": \"ruleEngineActions_ruleId_idx\",\n          \"columns\": [\n            \"ruleId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineActions_userId_user_id_fk\": {\n          \"name\": \"ruleEngineActions_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\": {\n          \"name\": \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"ruleId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_tagId_fk\": {\n          \"name\": \"ruleEngineActions_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_listId_fk\": {\n          \"name\": \"ruleEngineActions_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineRules\": {\n      \"name\": \"ruleEngineRules\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"event\": {\n          \"name\": \"event\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"condition\": {\n          \"name\": \"condition\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngine_userId_idx\": {\n          \"name\": \"ruleEngine_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineRules_userId_user_id_fk\": {\n          \"name\": \"ruleEngineRules_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_tagId_fk\": {\n          \"name\": \"ruleEngineRules_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_listId_fk\": {\n          \"name\": \"ruleEngineRules_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"userSettings\": {\n      \"name\": \"userSettings\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkClickAction\": {\n          \"name\": \"bookmarkClickAction\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'open_original_link'\"\n        },\n        \"archiveDisplayBehaviour\": {\n          \"name\": \"archiveDisplayBehaviour\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'show'\"\n        },\n        \"timezone\": {\n          \"name\": \"timezone\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'UTC'\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"userSettings_userId_user_id_fk\": {\n          \"name\": \"userSettings_userId_user_id_fk\",\n          \"tableFrom\": \"userSettings\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"salt\": {\n          \"name\": \"salt\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        },\n        \"bookmarkQuota\": {\n          \"name\": \"bookmarkQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"storageQuota\": {\n          \"name\": \"storageQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0058_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"67200406-29ea-4aed-9d67-87b4f3d308f7\",\n  \"prevId\": \"b5e79604-adc2-4ad2-b2e2-96a871ec8f01\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"author\": {\n          \"name\": \"author\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"publisher\": {\n          \"name\": \"publisher\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"datePublished\": {\n          \"name\": \"datePublished\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dateModified\": {\n          \"name\": \"dateModified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"contentAssetId\": {\n          \"name\": \"contentAssetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rssToken\": {\n          \"name\": \"rssToken\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_userId_id_idx\": {\n          \"name\": \"bookmarkLists_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"bookmarkTags_userId_id_idx\": {\n          \"name\": \"bookmarkTags_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summarizationStatus\": {\n          \"name\": \"summarizationStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invites\": {\n      \"name\": \"invites\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expiresAt\": {\n          \"name\": \"expiresAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"usedAt\": {\n          \"name\": \"usedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"invites_token_unique\": {\n          \"name\": \"invites_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"invites_invitedBy_user_id_fk\": {\n          \"name\": \"invites_invitedBy_user_id_fk\",\n          \"tableFrom\": \"invites\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"passwordResetToken\": {\n      \"name\": \"passwordResetToken\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"passwordResetToken_token_unique\": {\n          \"name\": \"passwordResetToken_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        },\n        \"passwordResetTokens_userId_idx\": {\n          \"name\": \"passwordResetTokens_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"passwordResetToken_userId_user_id_fk\": {\n          \"name\": \"passwordResetToken_userId_user_id_fk\",\n          \"tableFrom\": \"passwordResetToken\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineActions\": {\n      \"name\": \"ruleEngineActions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"ruleId\": {\n          \"name\": \"ruleId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"action\": {\n          \"name\": \"action\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngineActions_userId_idx\": {\n          \"name\": \"ruleEngineActions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"ruleEngineActions_ruleId_idx\": {\n          \"name\": \"ruleEngineActions_ruleId_idx\",\n          \"columns\": [\n            \"ruleId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineActions_userId_user_id_fk\": {\n          \"name\": \"ruleEngineActions_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\": {\n          \"name\": \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"ruleId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_tagId_fk\": {\n          \"name\": \"ruleEngineActions_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_listId_fk\": {\n          \"name\": \"ruleEngineActions_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineRules\": {\n      \"name\": \"ruleEngineRules\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"event\": {\n          \"name\": \"event\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"condition\": {\n          \"name\": \"condition\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngine_userId_idx\": {\n          \"name\": \"ruleEngine_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineRules_userId_user_id_fk\": {\n          \"name\": \"ruleEngineRules_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_tagId_fk\": {\n          \"name\": \"ruleEngineRules_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_listId_fk\": {\n          \"name\": \"ruleEngineRules_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"subscriptions\": {\n      \"name\": \"subscriptions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeCustomerId\": {\n          \"name\": \"stripeCustomerId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeSubscriptionId\": {\n          \"name\": \"stripeSubscriptionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tier\": {\n          \"name\": \"tier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'free'\"\n        },\n        \"priceId\": {\n          \"name\": \"priceId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cancelAtPeriodEnd\": {\n          \"name\": \"cancelAtPeriodEnd\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"startDate\": {\n          \"name\": \"startDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"endDate\": {\n          \"name\": \"endDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"subscriptions_userId_unique\": {\n          \"name\": \"subscriptions_userId_unique\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": true\n        },\n        \"subscriptions_userId_idx\": {\n          \"name\": \"subscriptions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"subscriptions_stripeCustomerId_idx\": {\n          \"name\": \"subscriptions_stripeCustomerId_idx\",\n          \"columns\": [\n            \"stripeCustomerId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"subscriptions_userId_user_id_fk\": {\n          \"name\": \"subscriptions_userId_user_id_fk\",\n          \"tableFrom\": \"subscriptions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"userSettings\": {\n      \"name\": \"userSettings\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkClickAction\": {\n          \"name\": \"bookmarkClickAction\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'open_original_link'\"\n        },\n        \"archiveDisplayBehaviour\": {\n          \"name\": \"archiveDisplayBehaviour\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'show'\"\n        },\n        \"timezone\": {\n          \"name\": \"timezone\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'UTC'\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"userSettings_userId_user_id_fk\": {\n          \"name\": \"userSettings_userId_user_id_fk\",\n          \"tableFrom\": \"userSettings\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"salt\": {\n          \"name\": \"salt\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        },\n        \"bookmarkQuota\": {\n          \"name\": \"bookmarkQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"storageQuota\": {\n          \"name\": \"storageQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0059_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"b57ec855-4e87-4b69-85ae-75f36c0cdb85\",\n  \"prevId\": \"67200406-29ea-4aed-9d67-87b4f3d308f7\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"author\": {\n          \"name\": \"author\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"publisher\": {\n          \"name\": \"publisher\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"datePublished\": {\n          \"name\": \"datePublished\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dateModified\": {\n          \"name\": \"dateModified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"contentAssetId\": {\n          \"name\": \"contentAssetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rssToken\": {\n          \"name\": \"rssToken\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_userId_id_idx\": {\n          \"name\": \"bookmarkLists_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"bookmarkTags_userId_id_idx\": {\n          \"name\": \"bookmarkTags_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summarizationStatus\": {\n          \"name\": \"summarizationStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invites\": {\n      \"name\": \"invites\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expiresAt\": {\n          \"name\": \"expiresAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"usedAt\": {\n          \"name\": \"usedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"invites_token_unique\": {\n          \"name\": \"invites_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"invites_invitedBy_user_id_fk\": {\n          \"name\": \"invites_invitedBy_user_id_fk\",\n          \"tableFrom\": \"invites\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"passwordResetToken\": {\n      \"name\": \"passwordResetToken\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"passwordResetToken_token_unique\": {\n          \"name\": \"passwordResetToken_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        },\n        \"passwordResetTokens_userId_idx\": {\n          \"name\": \"passwordResetTokens_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"passwordResetToken_userId_user_id_fk\": {\n          \"name\": \"passwordResetToken_userId_user_id_fk\",\n          \"tableFrom\": \"passwordResetToken\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineActions\": {\n      \"name\": \"ruleEngineActions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"ruleId\": {\n          \"name\": \"ruleId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"action\": {\n          \"name\": \"action\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngineActions_userId_idx\": {\n          \"name\": \"ruleEngineActions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"ruleEngineActions_ruleId_idx\": {\n          \"name\": \"ruleEngineActions_ruleId_idx\",\n          \"columns\": [\n            \"ruleId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineActions_userId_user_id_fk\": {\n          \"name\": \"ruleEngineActions_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\": {\n          \"name\": \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"ruleId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_tagId_fk\": {\n          \"name\": \"ruleEngineActions_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_listId_fk\": {\n          \"name\": \"ruleEngineActions_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineRules\": {\n      \"name\": \"ruleEngineRules\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"event\": {\n          \"name\": \"event\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"condition\": {\n          \"name\": \"condition\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngine_userId_idx\": {\n          \"name\": \"ruleEngine_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineRules_userId_user_id_fk\": {\n          \"name\": \"ruleEngineRules_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_tagId_fk\": {\n          \"name\": \"ruleEngineRules_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_listId_fk\": {\n          \"name\": \"ruleEngineRules_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"subscriptions\": {\n      \"name\": \"subscriptions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeCustomerId\": {\n          \"name\": \"stripeCustomerId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeSubscriptionId\": {\n          \"name\": \"stripeSubscriptionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tier\": {\n          \"name\": \"tier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'free'\"\n        },\n        \"priceId\": {\n          \"name\": \"priceId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cancelAtPeriodEnd\": {\n          \"name\": \"cancelAtPeriodEnd\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"startDate\": {\n          \"name\": \"startDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"endDate\": {\n          \"name\": \"endDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"subscriptions_userId_unique\": {\n          \"name\": \"subscriptions_userId_unique\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": true\n        },\n        \"subscriptions_userId_idx\": {\n          \"name\": \"subscriptions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"subscriptions_stripeCustomerId_idx\": {\n          \"name\": \"subscriptions_stripeCustomerId_idx\",\n          \"columns\": [\n            \"stripeCustomerId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"subscriptions_userId_user_id_fk\": {\n          \"name\": \"subscriptions_userId_user_id_fk\",\n          \"tableFrom\": \"subscriptions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"userSettings\": {\n      \"name\": \"userSettings\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkClickAction\": {\n          \"name\": \"bookmarkClickAction\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'open_original_link'\"\n        },\n        \"archiveDisplayBehaviour\": {\n          \"name\": \"archiveDisplayBehaviour\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'show'\"\n        },\n        \"timezone\": {\n          \"name\": \"timezone\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'UTC'\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"userSettings_userId_user_id_fk\": {\n          \"name\": \"userSettings_userId_user_id_fk\",\n          \"tableFrom\": \"userSettings\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"salt\": {\n          \"name\": \"salt\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        },\n        \"bookmarkQuota\": {\n          \"name\": \"bookmarkQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"storageQuota\": {\n          \"name\": \"storageQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"browserCrawlingEnabled\": {\n          \"name\": \"browserCrawlingEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0060_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"2de13cbc-aa24-4a0c-8262-b5be21e2bbd3\",\n  \"prevId\": \"b57ec855-4e87-4b69-85ae-75f36c0cdb85\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"author\": {\n          \"name\": \"author\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"publisher\": {\n          \"name\": \"publisher\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"datePublished\": {\n          \"name\": \"datePublished\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dateModified\": {\n          \"name\": \"dateModified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"contentAssetId\": {\n          \"name\": \"contentAssetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rssToken\": {\n          \"name\": \"rssToken\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_userId_id_idx\": {\n          \"name\": \"bookmarkLists_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"bookmarkTags_userId_id_idx\": {\n          \"name\": \"bookmarkTags_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summarizationStatus\": {\n          \"name\": \"summarizationStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invites\": {\n      \"name\": \"invites\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"usedAt\": {\n          \"name\": \"usedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"invites_token_unique\": {\n          \"name\": \"invites_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"invites_invitedBy_user_id_fk\": {\n          \"name\": \"invites_invitedBy_user_id_fk\",\n          \"tableFrom\": \"invites\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"passwordResetToken\": {\n      \"name\": \"passwordResetToken\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"passwordResetToken_token_unique\": {\n          \"name\": \"passwordResetToken_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        },\n        \"passwordResetTokens_userId_idx\": {\n          \"name\": \"passwordResetTokens_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"passwordResetToken_userId_user_id_fk\": {\n          \"name\": \"passwordResetToken_userId_user_id_fk\",\n          \"tableFrom\": \"passwordResetToken\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineActions\": {\n      \"name\": \"ruleEngineActions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"ruleId\": {\n          \"name\": \"ruleId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"action\": {\n          \"name\": \"action\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngineActions_userId_idx\": {\n          \"name\": \"ruleEngineActions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"ruleEngineActions_ruleId_idx\": {\n          \"name\": \"ruleEngineActions_ruleId_idx\",\n          \"columns\": [\n            \"ruleId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineActions_userId_user_id_fk\": {\n          \"name\": \"ruleEngineActions_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\": {\n          \"name\": \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"ruleId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_tagId_fk\": {\n          \"name\": \"ruleEngineActions_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_listId_fk\": {\n          \"name\": \"ruleEngineActions_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineRules\": {\n      \"name\": \"ruleEngineRules\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"event\": {\n          \"name\": \"event\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"condition\": {\n          \"name\": \"condition\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngine_userId_idx\": {\n          \"name\": \"ruleEngine_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineRules_userId_user_id_fk\": {\n          \"name\": \"ruleEngineRules_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_tagId_fk\": {\n          \"name\": \"ruleEngineRules_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_listId_fk\": {\n          \"name\": \"ruleEngineRules_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"subscriptions\": {\n      \"name\": \"subscriptions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeCustomerId\": {\n          \"name\": \"stripeCustomerId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeSubscriptionId\": {\n          \"name\": \"stripeSubscriptionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tier\": {\n          \"name\": \"tier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'free'\"\n        },\n        \"priceId\": {\n          \"name\": \"priceId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cancelAtPeriodEnd\": {\n          \"name\": \"cancelAtPeriodEnd\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"startDate\": {\n          \"name\": \"startDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"endDate\": {\n          \"name\": \"endDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"subscriptions_userId_unique\": {\n          \"name\": \"subscriptions_userId_unique\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": true\n        },\n        \"subscriptions_userId_idx\": {\n          \"name\": \"subscriptions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"subscriptions_stripeCustomerId_idx\": {\n          \"name\": \"subscriptions_stripeCustomerId_idx\",\n          \"columns\": [\n            \"stripeCustomerId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"subscriptions_userId_user_id_fk\": {\n          \"name\": \"subscriptions_userId_user_id_fk\",\n          \"tableFrom\": \"subscriptions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"userSettings\": {\n      \"name\": \"userSettings\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkClickAction\": {\n          \"name\": \"bookmarkClickAction\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'open_original_link'\"\n        },\n        \"archiveDisplayBehaviour\": {\n          \"name\": \"archiveDisplayBehaviour\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'show'\"\n        },\n        \"timezone\": {\n          \"name\": \"timezone\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'UTC'\"\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"userSettings_userId_user_id_fk\": {\n          \"name\": \"userSettings_userId_user_id_fk\",\n          \"tableFrom\": \"userSettings\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"salt\": {\n          \"name\": \"salt\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        },\n        \"bookmarkQuota\": {\n          \"name\": \"bookmarkQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"storageQuota\": {\n          \"name\": \"storageQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"browserCrawlingEnabled\": {\n          \"name\": \"browserCrawlingEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0061_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"e868f89a-f304-428c-8e96-705453facb18\",\n  \"prevId\": \"2de13cbc-aa24-4a0c-8262-b5be21e2bbd3\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"author\": {\n          \"name\": \"author\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"publisher\": {\n          \"name\": \"publisher\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"datePublished\": {\n          \"name\": \"datePublished\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dateModified\": {\n          \"name\": \"dateModified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"contentAssetId\": {\n          \"name\": \"contentAssetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rssToken\": {\n          \"name\": \"rssToken\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_userId_id_idx\": {\n          \"name\": \"bookmarkLists_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"bookmarkTags_userId_id_idx\": {\n          \"name\": \"bookmarkTags_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summarizationStatus\": {\n          \"name\": \"summarizationStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invites\": {\n      \"name\": \"invites\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"usedAt\": {\n          \"name\": \"usedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"invites_token_unique\": {\n          \"name\": \"invites_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"invites_invitedBy_user_id_fk\": {\n          \"name\": \"invites_invitedBy_user_id_fk\",\n          \"tableFrom\": \"invites\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"passwordResetToken\": {\n      \"name\": \"passwordResetToken\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"passwordResetToken_token_unique\": {\n          \"name\": \"passwordResetToken_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        },\n        \"passwordResetTokens_userId_idx\": {\n          \"name\": \"passwordResetTokens_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"passwordResetToken_userId_user_id_fk\": {\n          \"name\": \"passwordResetToken_userId_user_id_fk\",\n          \"tableFrom\": \"passwordResetToken\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineActions\": {\n      \"name\": \"ruleEngineActions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"ruleId\": {\n          \"name\": \"ruleId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"action\": {\n          \"name\": \"action\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngineActions_userId_idx\": {\n          \"name\": \"ruleEngineActions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"ruleEngineActions_ruleId_idx\": {\n          \"name\": \"ruleEngineActions_ruleId_idx\",\n          \"columns\": [\n            \"ruleId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineActions_userId_user_id_fk\": {\n          \"name\": \"ruleEngineActions_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\": {\n          \"name\": \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"ruleId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_tagId_fk\": {\n          \"name\": \"ruleEngineActions_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_listId_fk\": {\n          \"name\": \"ruleEngineActions_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineRules\": {\n      \"name\": \"ruleEngineRules\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"event\": {\n          \"name\": \"event\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"condition\": {\n          \"name\": \"condition\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngine_userId_idx\": {\n          \"name\": \"ruleEngine_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineRules_userId_user_id_fk\": {\n          \"name\": \"ruleEngineRules_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_tagId_fk\": {\n          \"name\": \"ruleEngineRules_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_listId_fk\": {\n          \"name\": \"ruleEngineRules_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"subscriptions\": {\n      \"name\": \"subscriptions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeCustomerId\": {\n          \"name\": \"stripeCustomerId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeSubscriptionId\": {\n          \"name\": \"stripeSubscriptionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tier\": {\n          \"name\": \"tier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'free'\"\n        },\n        \"priceId\": {\n          \"name\": \"priceId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cancelAtPeriodEnd\": {\n          \"name\": \"cancelAtPeriodEnd\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"startDate\": {\n          \"name\": \"startDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"endDate\": {\n          \"name\": \"endDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"subscriptions_userId_unique\": {\n          \"name\": \"subscriptions_userId_unique\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": true\n        },\n        \"subscriptions_userId_idx\": {\n          \"name\": \"subscriptions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"subscriptions_stripeCustomerId_idx\": {\n          \"name\": \"subscriptions_stripeCustomerId_idx\",\n          \"columns\": [\n            \"stripeCustomerId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"subscriptions_userId_user_id_fk\": {\n          \"name\": \"subscriptions_userId_user_id_fk\",\n          \"tableFrom\": \"subscriptions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"salt\": {\n          \"name\": \"salt\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        },\n        \"bookmarkQuota\": {\n          \"name\": \"bookmarkQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"storageQuota\": {\n          \"name\": \"storageQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"browserCrawlingEnabled\": {\n          \"name\": \"browserCrawlingEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkClickAction\": {\n          \"name\": \"bookmarkClickAction\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'open_original_link'\"\n        },\n        \"archiveDisplayBehaviour\": {\n          \"name\": \"archiveDisplayBehaviour\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'show'\"\n        },\n        \"timezone\": {\n          \"name\": \"timezone\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'UTC'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0062_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"85dc1c66-ccf6-4db8-b722-c5cbaf18f806\",\n  \"prevId\": \"e868f89a-f304-428c-8e96-705453facb18\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"author\": {\n          \"name\": \"author\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"publisher\": {\n          \"name\": \"publisher\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"datePublished\": {\n          \"name\": \"datePublished\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dateModified\": {\n          \"name\": \"dateModified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"contentAssetId\": {\n          \"name\": \"contentAssetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rssToken\": {\n          \"name\": \"rssToken\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_userId_id_idx\": {\n          \"name\": \"bookmarkLists_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"bookmarkTags_userId_id_idx\": {\n          \"name\": \"bookmarkTags_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summarizationStatus\": {\n          \"name\": \"summarizationStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importSessionBookmarks\": {\n      \"name\": \"importSessionBookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"importSessionId\": {\n          \"name\": \"importSessionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importSessionBookmarks_sessionId_idx\": {\n          \"name\": \"importSessionBookmarks_sessionId_idx\",\n          \"columns\": [\n            \"importSessionId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessionBookmarks_bookmarkId_idx\": {\n          \"name\": \"importSessionBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessionBookmarks_importSessionId_bookmarkId_unique\": {\n          \"name\": \"importSessionBookmarks_importSessionId_bookmarkId_unique\",\n          \"columns\": [\n            \"importSessionId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"importSessionBookmarks_importSessionId_importSessions_id_fk\": {\n          \"name\": \"importSessionBookmarks_importSessionId_importSessions_id_fk\",\n          \"tableFrom\": \"importSessionBookmarks\",\n          \"tableTo\": \"importSessions\",\n          \"columnsFrom\": [\n            \"importSessionId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"importSessionBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"importSessionBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"importSessionBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importSessions\": {\n      \"name\": \"importSessions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rootListId\": {\n          \"name\": \"rootListId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importSessions_userId_idx\": {\n          \"name\": \"importSessions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"importSessions_userId_user_id_fk\": {\n          \"name\": \"importSessions_userId_user_id_fk\",\n          \"tableFrom\": \"importSessions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"importSessions_rootListId_bookmarkLists_id_fk\": {\n          \"name\": \"importSessions_rootListId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"importSessions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"rootListId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invites\": {\n      \"name\": \"invites\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"usedAt\": {\n          \"name\": \"usedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"invites_token_unique\": {\n          \"name\": \"invites_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"invites_invitedBy_user_id_fk\": {\n          \"name\": \"invites_invitedBy_user_id_fk\",\n          \"tableFrom\": \"invites\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"passwordResetToken\": {\n      \"name\": \"passwordResetToken\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"passwordResetToken_token_unique\": {\n          \"name\": \"passwordResetToken_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        },\n        \"passwordResetTokens_userId_idx\": {\n          \"name\": \"passwordResetTokens_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"passwordResetToken_userId_user_id_fk\": {\n          \"name\": \"passwordResetToken_userId_user_id_fk\",\n          \"tableFrom\": \"passwordResetToken\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineActions\": {\n      \"name\": \"ruleEngineActions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"ruleId\": {\n          \"name\": \"ruleId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"action\": {\n          \"name\": \"action\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngineActions_userId_idx\": {\n          \"name\": \"ruleEngineActions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"ruleEngineActions_ruleId_idx\": {\n          \"name\": \"ruleEngineActions_ruleId_idx\",\n          \"columns\": [\n            \"ruleId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineActions_userId_user_id_fk\": {\n          \"name\": \"ruleEngineActions_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\": {\n          \"name\": \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"ruleId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_tagId_fk\": {\n          \"name\": \"ruleEngineActions_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_listId_fk\": {\n          \"name\": \"ruleEngineActions_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineRules\": {\n      \"name\": \"ruleEngineRules\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"event\": {\n          \"name\": \"event\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"condition\": {\n          \"name\": \"condition\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngine_userId_idx\": {\n          \"name\": \"ruleEngine_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineRules_userId_user_id_fk\": {\n          \"name\": \"ruleEngineRules_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_tagId_fk\": {\n          \"name\": \"ruleEngineRules_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_listId_fk\": {\n          \"name\": \"ruleEngineRules_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"subscriptions\": {\n      \"name\": \"subscriptions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeCustomerId\": {\n          \"name\": \"stripeCustomerId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeSubscriptionId\": {\n          \"name\": \"stripeSubscriptionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tier\": {\n          \"name\": \"tier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'free'\"\n        },\n        \"priceId\": {\n          \"name\": \"priceId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cancelAtPeriodEnd\": {\n          \"name\": \"cancelAtPeriodEnd\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"startDate\": {\n          \"name\": \"startDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"endDate\": {\n          \"name\": \"endDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"subscriptions_userId_unique\": {\n          \"name\": \"subscriptions_userId_unique\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": true\n        },\n        \"subscriptions_userId_idx\": {\n          \"name\": \"subscriptions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"subscriptions_stripeCustomerId_idx\": {\n          \"name\": \"subscriptions_stripeCustomerId_idx\",\n          \"columns\": [\n            \"stripeCustomerId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"subscriptions_userId_user_id_fk\": {\n          \"name\": \"subscriptions_userId_user_id_fk\",\n          \"tableFrom\": \"subscriptions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"salt\": {\n          \"name\": \"salt\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        },\n        \"bookmarkQuota\": {\n          \"name\": \"bookmarkQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"storageQuota\": {\n          \"name\": \"storageQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"browserCrawlingEnabled\": {\n          \"name\": \"browserCrawlingEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkClickAction\": {\n          \"name\": \"bookmarkClickAction\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'open_original_link'\"\n        },\n        \"archiveDisplayBehaviour\": {\n          \"name\": \"archiveDisplayBehaviour\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'show'\"\n        },\n        \"timezone\": {\n          \"name\": \"timezone\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'UTC'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0063_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"c54b95c2-56b5-418d-be0a-ee75748067e9\",\n  \"prevId\": \"85dc1c66-ccf6-4db8-b722-c5cbaf18f806\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"author\": {\n          \"name\": \"author\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"publisher\": {\n          \"name\": \"publisher\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"datePublished\": {\n          \"name\": \"datePublished\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dateModified\": {\n          \"name\": \"dateModified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"contentAssetId\": {\n          \"name\": \"contentAssetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rssToken\": {\n          \"name\": \"rssToken\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_userId_id_idx\": {\n          \"name\": \"bookmarkLists_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"bookmarkTags_userId_id_idx\": {\n          \"name\": \"bookmarkTags_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summarizationStatus\": {\n          \"name\": \"summarizationStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"source\": {\n          \"name\": \"source\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importSessionBookmarks\": {\n      \"name\": \"importSessionBookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"importSessionId\": {\n          \"name\": \"importSessionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importSessionBookmarks_sessionId_idx\": {\n          \"name\": \"importSessionBookmarks_sessionId_idx\",\n          \"columns\": [\n            \"importSessionId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessionBookmarks_bookmarkId_idx\": {\n          \"name\": \"importSessionBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessionBookmarks_importSessionId_bookmarkId_unique\": {\n          \"name\": \"importSessionBookmarks_importSessionId_bookmarkId_unique\",\n          \"columns\": [\n            \"importSessionId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"importSessionBookmarks_importSessionId_importSessions_id_fk\": {\n          \"name\": \"importSessionBookmarks_importSessionId_importSessions_id_fk\",\n          \"tableFrom\": \"importSessionBookmarks\",\n          \"tableTo\": \"importSessions\",\n          \"columnsFrom\": [\n            \"importSessionId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"importSessionBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"importSessionBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"importSessionBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importSessions\": {\n      \"name\": \"importSessions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rootListId\": {\n          \"name\": \"rootListId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importSessions_userId_idx\": {\n          \"name\": \"importSessions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"importSessions_userId_user_id_fk\": {\n          \"name\": \"importSessions_userId_user_id_fk\",\n          \"tableFrom\": \"importSessions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"importSessions_rootListId_bookmarkLists_id_fk\": {\n          \"name\": \"importSessions_rootListId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"importSessions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"rootListId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invites\": {\n      \"name\": \"invites\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"usedAt\": {\n          \"name\": \"usedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"invites_token_unique\": {\n          \"name\": \"invites_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"invites_invitedBy_user_id_fk\": {\n          \"name\": \"invites_invitedBy_user_id_fk\",\n          \"tableFrom\": \"invites\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"passwordResetToken\": {\n      \"name\": \"passwordResetToken\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"passwordResetToken_token_unique\": {\n          \"name\": \"passwordResetToken_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        },\n        \"passwordResetTokens_userId_idx\": {\n          \"name\": \"passwordResetTokens_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"passwordResetToken_userId_user_id_fk\": {\n          \"name\": \"passwordResetToken_userId_user_id_fk\",\n          \"tableFrom\": \"passwordResetToken\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineActions\": {\n      \"name\": \"ruleEngineActions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"ruleId\": {\n          \"name\": \"ruleId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"action\": {\n          \"name\": \"action\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngineActions_userId_idx\": {\n          \"name\": \"ruleEngineActions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"ruleEngineActions_ruleId_idx\": {\n          \"name\": \"ruleEngineActions_ruleId_idx\",\n          \"columns\": [\n            \"ruleId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineActions_userId_user_id_fk\": {\n          \"name\": \"ruleEngineActions_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\": {\n          \"name\": \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"ruleId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_tagId_fk\": {\n          \"name\": \"ruleEngineActions_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_listId_fk\": {\n          \"name\": \"ruleEngineActions_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineRules\": {\n      \"name\": \"ruleEngineRules\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"event\": {\n          \"name\": \"event\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"condition\": {\n          \"name\": \"condition\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngine_userId_idx\": {\n          \"name\": \"ruleEngine_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineRules_userId_user_id_fk\": {\n          \"name\": \"ruleEngineRules_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_tagId_fk\": {\n          \"name\": \"ruleEngineRules_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_listId_fk\": {\n          \"name\": \"ruleEngineRules_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"subscriptions\": {\n      \"name\": \"subscriptions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeCustomerId\": {\n          \"name\": \"stripeCustomerId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeSubscriptionId\": {\n          \"name\": \"stripeSubscriptionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tier\": {\n          \"name\": \"tier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'free'\"\n        },\n        \"priceId\": {\n          \"name\": \"priceId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cancelAtPeriodEnd\": {\n          \"name\": \"cancelAtPeriodEnd\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"startDate\": {\n          \"name\": \"startDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"endDate\": {\n          \"name\": \"endDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"subscriptions_userId_unique\": {\n          \"name\": \"subscriptions_userId_unique\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": true\n        },\n        \"subscriptions_userId_idx\": {\n          \"name\": \"subscriptions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"subscriptions_stripeCustomerId_idx\": {\n          \"name\": \"subscriptions_stripeCustomerId_idx\",\n          \"columns\": [\n            \"stripeCustomerId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"subscriptions_userId_user_id_fk\": {\n          \"name\": \"subscriptions_userId_user_id_fk\",\n          \"tableFrom\": \"subscriptions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"salt\": {\n          \"name\": \"salt\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        },\n        \"bookmarkQuota\": {\n          \"name\": \"bookmarkQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"storageQuota\": {\n          \"name\": \"storageQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"browserCrawlingEnabled\": {\n          \"name\": \"browserCrawlingEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkClickAction\": {\n          \"name\": \"bookmarkClickAction\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'open_original_link'\"\n        },\n        \"archiveDisplayBehaviour\": {\n          \"name\": \"archiveDisplayBehaviour\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'show'\"\n        },\n        \"timezone\": {\n          \"name\": \"timezone\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'UTC'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0064_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"faf078cf-fa55-46e3-8404-bf74628a25e0\",\n  \"prevId\": \"c54b95c2-56b5-418d-be0a-ee75748067e9\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"author\": {\n          \"name\": \"author\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"publisher\": {\n          \"name\": \"publisher\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"datePublished\": {\n          \"name\": \"datePublished\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dateModified\": {\n          \"name\": \"dateModified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"contentAssetId\": {\n          \"name\": \"contentAssetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rssToken\": {\n          \"name\": \"rssToken\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_userId_id_idx\": {\n          \"name\": \"bookmarkLists_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"bookmarkTags_userId_id_idx\": {\n          \"name\": \"bookmarkTags_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summarizationStatus\": {\n          \"name\": \"summarizationStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"source\": {\n          \"name\": \"source\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importSessionBookmarks\": {\n      \"name\": \"importSessionBookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"importSessionId\": {\n          \"name\": \"importSessionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importSessionBookmarks_sessionId_idx\": {\n          \"name\": \"importSessionBookmarks_sessionId_idx\",\n          \"columns\": [\n            \"importSessionId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessionBookmarks_bookmarkId_idx\": {\n          \"name\": \"importSessionBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessionBookmarks_importSessionId_bookmarkId_unique\": {\n          \"name\": \"importSessionBookmarks_importSessionId_bookmarkId_unique\",\n          \"columns\": [\n            \"importSessionId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"importSessionBookmarks_importSessionId_importSessions_id_fk\": {\n          \"name\": \"importSessionBookmarks_importSessionId_importSessions_id_fk\",\n          \"tableFrom\": \"importSessionBookmarks\",\n          \"tableTo\": \"importSessions\",\n          \"columnsFrom\": [\n            \"importSessionId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"importSessionBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"importSessionBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"importSessionBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importSessions\": {\n      \"name\": \"importSessions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rootListId\": {\n          \"name\": \"rootListId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importSessions_userId_idx\": {\n          \"name\": \"importSessions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"importSessions_userId_user_id_fk\": {\n          \"name\": \"importSessions_userId_user_id_fk\",\n          \"tableFrom\": \"importSessions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"importSessions_rootListId_bookmarkLists_id_fk\": {\n          \"name\": \"importSessions_rootListId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"importSessions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"rootListId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invites\": {\n      \"name\": \"invites\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"usedAt\": {\n          \"name\": \"usedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"invites_token_unique\": {\n          \"name\": \"invites_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"invites_invitedBy_user_id_fk\": {\n          \"name\": \"invites_invitedBy_user_id_fk\",\n          \"tableFrom\": \"invites\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"passwordResetToken\": {\n      \"name\": \"passwordResetToken\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"passwordResetToken_token_unique\": {\n          \"name\": \"passwordResetToken_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        },\n        \"passwordResetTokens_userId_idx\": {\n          \"name\": \"passwordResetTokens_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"passwordResetToken_userId_user_id_fk\": {\n          \"name\": \"passwordResetToken_userId_user_id_fk\",\n          \"tableFrom\": \"passwordResetToken\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"importTags\": {\n          \"name\": \"importTags\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineActions\": {\n      \"name\": \"ruleEngineActions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"ruleId\": {\n          \"name\": \"ruleId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"action\": {\n          \"name\": \"action\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngineActions_userId_idx\": {\n          \"name\": \"ruleEngineActions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"ruleEngineActions_ruleId_idx\": {\n          \"name\": \"ruleEngineActions_ruleId_idx\",\n          \"columns\": [\n            \"ruleId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineActions_userId_user_id_fk\": {\n          \"name\": \"ruleEngineActions_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\": {\n          \"name\": \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"ruleId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_tagId_fk\": {\n          \"name\": \"ruleEngineActions_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_listId_fk\": {\n          \"name\": \"ruleEngineActions_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineRules\": {\n      \"name\": \"ruleEngineRules\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"event\": {\n          \"name\": \"event\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"condition\": {\n          \"name\": \"condition\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngine_userId_idx\": {\n          \"name\": \"ruleEngine_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineRules_userId_user_id_fk\": {\n          \"name\": \"ruleEngineRules_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_tagId_fk\": {\n          \"name\": \"ruleEngineRules_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_listId_fk\": {\n          \"name\": \"ruleEngineRules_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"subscriptions\": {\n      \"name\": \"subscriptions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeCustomerId\": {\n          \"name\": \"stripeCustomerId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeSubscriptionId\": {\n          \"name\": \"stripeSubscriptionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tier\": {\n          \"name\": \"tier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'free'\"\n        },\n        \"priceId\": {\n          \"name\": \"priceId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cancelAtPeriodEnd\": {\n          \"name\": \"cancelAtPeriodEnd\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"startDate\": {\n          \"name\": \"startDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"endDate\": {\n          \"name\": \"endDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"subscriptions_userId_unique\": {\n          \"name\": \"subscriptions_userId_unique\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": true\n        },\n        \"subscriptions_userId_idx\": {\n          \"name\": \"subscriptions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"subscriptions_stripeCustomerId_idx\": {\n          \"name\": \"subscriptions_stripeCustomerId_idx\",\n          \"columns\": [\n            \"stripeCustomerId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"subscriptions_userId_user_id_fk\": {\n          \"name\": \"subscriptions_userId_user_id_fk\",\n          \"tableFrom\": \"subscriptions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"salt\": {\n          \"name\": \"salt\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        },\n        \"bookmarkQuota\": {\n          \"name\": \"bookmarkQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"storageQuota\": {\n          \"name\": \"storageQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"browserCrawlingEnabled\": {\n          \"name\": \"browserCrawlingEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkClickAction\": {\n          \"name\": \"bookmarkClickAction\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'open_original_link'\"\n        },\n        \"archiveDisplayBehaviour\": {\n          \"name\": \"archiveDisplayBehaviour\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'show'\"\n        },\n        \"timezone\": {\n          \"name\": \"timezone\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'UTC'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0065_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"f14d2087-e465-4cb5-81bc-accff017ee02\",\n  \"prevId\": \"faf078cf-fa55-46e3-8404-bf74628a25e0\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"author\": {\n          \"name\": \"author\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"publisher\": {\n          \"name\": \"publisher\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"datePublished\": {\n          \"name\": \"datePublished\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dateModified\": {\n          \"name\": \"dateModified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"contentAssetId\": {\n          \"name\": \"contentAssetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rssToken\": {\n          \"name\": \"rssToken\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_userId_id_idx\": {\n          \"name\": \"bookmarkLists_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"bookmarkTags_userId_id_idx\": {\n          \"name\": \"bookmarkTags_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summarizationStatus\": {\n          \"name\": \"summarizationStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"source\": {\n          \"name\": \"source\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"listMembershipId\": {\n          \"name\": \"listMembershipId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listMembershipId_listCollaborators_id_fk\": {\n          \"name\": \"bookmarksInLists_listMembershipId_listCollaborators_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"listCollaborators\",\n          \"columnsFrom\": [\n            \"listMembershipId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importSessionBookmarks\": {\n      \"name\": \"importSessionBookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"importSessionId\": {\n          \"name\": \"importSessionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importSessionBookmarks_sessionId_idx\": {\n          \"name\": \"importSessionBookmarks_sessionId_idx\",\n          \"columns\": [\n            \"importSessionId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessionBookmarks_bookmarkId_idx\": {\n          \"name\": \"importSessionBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessionBookmarks_importSessionId_bookmarkId_unique\": {\n          \"name\": \"importSessionBookmarks_importSessionId_bookmarkId_unique\",\n          \"columns\": [\n            \"importSessionId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"importSessionBookmarks_importSessionId_importSessions_id_fk\": {\n          \"name\": \"importSessionBookmarks_importSessionId_importSessions_id_fk\",\n          \"tableFrom\": \"importSessionBookmarks\",\n          \"tableTo\": \"importSessions\",\n          \"columnsFrom\": [\n            \"importSessionId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"importSessionBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"importSessionBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"importSessionBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importSessions\": {\n      \"name\": \"importSessions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rootListId\": {\n          \"name\": \"rootListId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importSessions_userId_idx\": {\n          \"name\": \"importSessions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"importSessions_userId_user_id_fk\": {\n          \"name\": \"importSessions_userId_user_id_fk\",\n          \"tableFrom\": \"importSessions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"importSessions_rootListId_bookmarkLists_id_fk\": {\n          \"name\": \"importSessions_rootListId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"importSessions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"rootListId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invites\": {\n      \"name\": \"invites\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"usedAt\": {\n          \"name\": \"usedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"invites_token_unique\": {\n          \"name\": \"invites_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"invites_invitedBy_user_id_fk\": {\n          \"name\": \"invites_invitedBy_user_id_fk\",\n          \"tableFrom\": \"invites\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"listCollaborators\": {\n      \"name\": \"listCollaborators\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedBy\": {\n          \"name\": \"addedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"listCollaborators_listId_idx\": {\n          \"name\": \"listCollaborators_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listCollaborators_userId_idx\": {\n          \"name\": \"listCollaborators_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listCollaborators_listId_userId_unique\": {\n          \"name\": \"listCollaborators_listId_userId_unique\",\n          \"columns\": [\n            \"listId\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"listCollaborators_listId_bookmarkLists_id_fk\": {\n          \"name\": \"listCollaborators_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listCollaborators_userId_user_id_fk\": {\n          \"name\": \"listCollaborators_userId_user_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listCollaborators_addedBy_user_id_fk\": {\n          \"name\": \"listCollaborators_addedBy_user_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"addedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"passwordResetToken\": {\n      \"name\": \"passwordResetToken\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"passwordResetToken_token_unique\": {\n          \"name\": \"passwordResetToken_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        },\n        \"passwordResetTokens_userId_idx\": {\n          \"name\": \"passwordResetTokens_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"passwordResetToken_userId_user_id_fk\": {\n          \"name\": \"passwordResetToken_userId_user_id_fk\",\n          \"tableFrom\": \"passwordResetToken\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"importTags\": {\n          \"name\": \"importTags\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineActions\": {\n      \"name\": \"ruleEngineActions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"ruleId\": {\n          \"name\": \"ruleId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"action\": {\n          \"name\": \"action\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngineActions_userId_idx\": {\n          \"name\": \"ruleEngineActions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"ruleEngineActions_ruleId_idx\": {\n          \"name\": \"ruleEngineActions_ruleId_idx\",\n          \"columns\": [\n            \"ruleId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineActions_userId_user_id_fk\": {\n          \"name\": \"ruleEngineActions_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\": {\n          \"name\": \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"ruleId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_tagId_fk\": {\n          \"name\": \"ruleEngineActions_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_listId_fk\": {\n          \"name\": \"ruleEngineActions_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineRules\": {\n      \"name\": \"ruleEngineRules\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"event\": {\n          \"name\": \"event\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"condition\": {\n          \"name\": \"condition\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngine_userId_idx\": {\n          \"name\": \"ruleEngine_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineRules_userId_user_id_fk\": {\n          \"name\": \"ruleEngineRules_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_tagId_fk\": {\n          \"name\": \"ruleEngineRules_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_listId_fk\": {\n          \"name\": \"ruleEngineRules_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"subscriptions\": {\n      \"name\": \"subscriptions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeCustomerId\": {\n          \"name\": \"stripeCustomerId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeSubscriptionId\": {\n          \"name\": \"stripeSubscriptionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tier\": {\n          \"name\": \"tier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'free'\"\n        },\n        \"priceId\": {\n          \"name\": \"priceId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cancelAtPeriodEnd\": {\n          \"name\": \"cancelAtPeriodEnd\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"startDate\": {\n          \"name\": \"startDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"endDate\": {\n          \"name\": \"endDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"subscriptions_userId_unique\": {\n          \"name\": \"subscriptions_userId_unique\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": true\n        },\n        \"subscriptions_userId_idx\": {\n          \"name\": \"subscriptions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"subscriptions_stripeCustomerId_idx\": {\n          \"name\": \"subscriptions_stripeCustomerId_idx\",\n          \"columns\": [\n            \"stripeCustomerId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"subscriptions_userId_user_id_fk\": {\n          \"name\": \"subscriptions_userId_user_id_fk\",\n          \"tableFrom\": \"subscriptions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"salt\": {\n          \"name\": \"salt\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        },\n        \"bookmarkQuota\": {\n          \"name\": \"bookmarkQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"storageQuota\": {\n          \"name\": \"storageQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"browserCrawlingEnabled\": {\n          \"name\": \"browserCrawlingEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkClickAction\": {\n          \"name\": \"bookmarkClickAction\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'open_original_link'\"\n        },\n        \"archiveDisplayBehaviour\": {\n          \"name\": \"archiveDisplayBehaviour\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'show'\"\n        },\n        \"timezone\": {\n          \"name\": \"timezone\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'UTC'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0066_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"f5049a1f-7de4-4107-9b8c-c13118699381\",\n  \"prevId\": \"f14d2087-e465-4cb5-81bc-accff017ee02\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"author\": {\n          \"name\": \"author\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"publisher\": {\n          \"name\": \"publisher\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"datePublished\": {\n          \"name\": \"datePublished\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dateModified\": {\n          \"name\": \"dateModified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"contentAssetId\": {\n          \"name\": \"contentAssetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rssToken\": {\n          \"name\": \"rssToken\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_userId_id_idx\": {\n          \"name\": \"bookmarkLists_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"bookmarkTags_userId_id_idx\": {\n          \"name\": \"bookmarkTags_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summarizationStatus\": {\n          \"name\": \"summarizationStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"source\": {\n          \"name\": \"source\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"listMembershipId\": {\n          \"name\": \"listMembershipId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listMembershipId_listCollaborators_id_fk\": {\n          \"name\": \"bookmarksInLists_listMembershipId_listCollaborators_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"listCollaborators\",\n          \"columnsFrom\": [\n            \"listMembershipId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importSessionBookmarks\": {\n      \"name\": \"importSessionBookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"importSessionId\": {\n          \"name\": \"importSessionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importSessionBookmarks_sessionId_idx\": {\n          \"name\": \"importSessionBookmarks_sessionId_idx\",\n          \"columns\": [\n            \"importSessionId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessionBookmarks_bookmarkId_idx\": {\n          \"name\": \"importSessionBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessionBookmarks_importSessionId_bookmarkId_unique\": {\n          \"name\": \"importSessionBookmarks_importSessionId_bookmarkId_unique\",\n          \"columns\": [\n            \"importSessionId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"importSessionBookmarks_importSessionId_importSessions_id_fk\": {\n          \"name\": \"importSessionBookmarks_importSessionId_importSessions_id_fk\",\n          \"tableFrom\": \"importSessionBookmarks\",\n          \"tableTo\": \"importSessions\",\n          \"columnsFrom\": [\n            \"importSessionId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"importSessionBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"importSessionBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"importSessionBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importSessions\": {\n      \"name\": \"importSessions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rootListId\": {\n          \"name\": \"rootListId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importSessions_userId_idx\": {\n          \"name\": \"importSessions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"importSessions_userId_user_id_fk\": {\n          \"name\": \"importSessions_userId_user_id_fk\",\n          \"tableFrom\": \"importSessions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"importSessions_rootListId_bookmarkLists_id_fk\": {\n          \"name\": \"importSessions_rootListId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"importSessions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"rootListId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invites\": {\n      \"name\": \"invites\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"usedAt\": {\n          \"name\": \"usedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"invites_token_unique\": {\n          \"name\": \"invites_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"invites_invitedBy_user_id_fk\": {\n          \"name\": \"invites_invitedBy_user_id_fk\",\n          \"tableFrom\": \"invites\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"listCollaborators\": {\n      \"name\": \"listCollaborators\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedBy\": {\n          \"name\": \"addedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"listCollaborators_listId_idx\": {\n          \"name\": \"listCollaborators_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listCollaborators_userId_idx\": {\n          \"name\": \"listCollaborators_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listCollaborators_listId_userId_unique\": {\n          \"name\": \"listCollaborators_listId_userId_unique\",\n          \"columns\": [\n            \"listId\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"listCollaborators_listId_bookmarkLists_id_fk\": {\n          \"name\": \"listCollaborators_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listCollaborators_userId_user_id_fk\": {\n          \"name\": \"listCollaborators_userId_user_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listCollaborators_addedBy_user_id_fk\": {\n          \"name\": \"listCollaborators_addedBy_user_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"addedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"listInvitations\": {\n      \"name\": \"listInvitations\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"invitedAt\": {\n          \"name\": \"invitedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"invitedEmail\": {\n          \"name\": \"invitedEmail\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"listInvitations_listId_idx\": {\n          \"name\": \"listInvitations_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_userId_idx\": {\n          \"name\": \"listInvitations_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_status_idx\": {\n          \"name\": \"listInvitations_status_idx\",\n          \"columns\": [\n            \"status\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_listId_userId_unique\": {\n          \"name\": \"listInvitations_listId_userId_unique\",\n          \"columns\": [\n            \"listId\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"listInvitations_listId_bookmarkLists_id_fk\": {\n          \"name\": \"listInvitations_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listInvitations_userId_user_id_fk\": {\n          \"name\": \"listInvitations_userId_user_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listInvitations_invitedBy_user_id_fk\": {\n          \"name\": \"listInvitations_invitedBy_user_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"passwordResetToken\": {\n      \"name\": \"passwordResetToken\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"passwordResetToken_token_unique\": {\n          \"name\": \"passwordResetToken_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        },\n        \"passwordResetTokens_userId_idx\": {\n          \"name\": \"passwordResetTokens_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"passwordResetToken_userId_user_id_fk\": {\n          \"name\": \"passwordResetToken_userId_user_id_fk\",\n          \"tableFrom\": \"passwordResetToken\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"importTags\": {\n          \"name\": \"importTags\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineActions\": {\n      \"name\": \"ruleEngineActions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"ruleId\": {\n          \"name\": \"ruleId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"action\": {\n          \"name\": \"action\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngineActions_userId_idx\": {\n          \"name\": \"ruleEngineActions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"ruleEngineActions_ruleId_idx\": {\n          \"name\": \"ruleEngineActions_ruleId_idx\",\n          \"columns\": [\n            \"ruleId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineActions_userId_user_id_fk\": {\n          \"name\": \"ruleEngineActions_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\": {\n          \"name\": \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"ruleId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_tagId_fk\": {\n          \"name\": \"ruleEngineActions_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_listId_fk\": {\n          \"name\": \"ruleEngineActions_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineRules\": {\n      \"name\": \"ruleEngineRules\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"event\": {\n          \"name\": \"event\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"condition\": {\n          \"name\": \"condition\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngine_userId_idx\": {\n          \"name\": \"ruleEngine_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineRules_userId_user_id_fk\": {\n          \"name\": \"ruleEngineRules_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_tagId_fk\": {\n          \"name\": \"ruleEngineRules_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_listId_fk\": {\n          \"name\": \"ruleEngineRules_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"subscriptions\": {\n      \"name\": \"subscriptions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeCustomerId\": {\n          \"name\": \"stripeCustomerId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeSubscriptionId\": {\n          \"name\": \"stripeSubscriptionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tier\": {\n          \"name\": \"tier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'free'\"\n        },\n        \"priceId\": {\n          \"name\": \"priceId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cancelAtPeriodEnd\": {\n          \"name\": \"cancelAtPeriodEnd\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"startDate\": {\n          \"name\": \"startDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"endDate\": {\n          \"name\": \"endDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"subscriptions_userId_unique\": {\n          \"name\": \"subscriptions_userId_unique\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": true\n        },\n        \"subscriptions_userId_idx\": {\n          \"name\": \"subscriptions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"subscriptions_stripeCustomerId_idx\": {\n          \"name\": \"subscriptions_stripeCustomerId_idx\",\n          \"columns\": [\n            \"stripeCustomerId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"subscriptions_userId_user_id_fk\": {\n          \"name\": \"subscriptions_userId_user_id_fk\",\n          \"tableFrom\": \"subscriptions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"salt\": {\n          \"name\": \"salt\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        },\n        \"bookmarkQuota\": {\n          \"name\": \"bookmarkQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"storageQuota\": {\n          \"name\": \"storageQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"browserCrawlingEnabled\": {\n          \"name\": \"browserCrawlingEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkClickAction\": {\n          \"name\": \"bookmarkClickAction\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'open_original_link'\"\n        },\n        \"archiveDisplayBehaviour\": {\n          \"name\": \"archiveDisplayBehaviour\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'show'\"\n        },\n        \"timezone\": {\n          \"name\": \"timezone\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'UTC'\"\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0067_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"6efcc681-eaac-44a6-a224-f97a7df01ff3\",\n  \"prevId\": \"f5049a1f-7de4-4107-9b8c-c13118699381\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"backups\": {\n      \"name\": \"backups\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkCount\": {\n          \"name\": \"bookmarkCount\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"errorMessage\": {\n          \"name\": \"errorMessage\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"backups_userId_idx\": {\n          \"name\": \"backups_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"backups_createdAt_idx\": {\n          \"name\": \"backups_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"backups_userId_user_id_fk\": {\n          \"name\": \"backups_userId_user_id_fk\",\n          \"tableFrom\": \"backups\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"backups_assetId_assets_id_fk\": {\n          \"name\": \"backups_assetId_assets_id_fk\",\n          \"tableFrom\": \"backups\",\n          \"tableTo\": \"assets\",\n          \"columnsFrom\": [\n            \"assetId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"author\": {\n          \"name\": \"author\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"publisher\": {\n          \"name\": \"publisher\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"datePublished\": {\n          \"name\": \"datePublished\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dateModified\": {\n          \"name\": \"dateModified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"contentAssetId\": {\n          \"name\": \"contentAssetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rssToken\": {\n          \"name\": \"rssToken\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_userId_id_idx\": {\n          \"name\": \"bookmarkLists_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"bookmarkTags_userId_id_idx\": {\n          \"name\": \"bookmarkTags_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summarizationStatus\": {\n          \"name\": \"summarizationStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"source\": {\n          \"name\": \"source\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_archived_idx\": {\n          \"name\": \"bookmarks_archived_idx\",\n          \"columns\": [\n            \"archived\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_favourited_idx\": {\n          \"name\": \"bookmarks_favourited_idx\",\n          \"columns\": [\n            \"favourited\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"listMembershipId\": {\n          \"name\": \"listMembershipId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listMembershipId_listCollaborators_id_fk\": {\n          \"name\": \"bookmarksInLists_listMembershipId_listCollaborators_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"listCollaborators\",\n          \"columnsFrom\": [\n            \"listMembershipId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importSessionBookmarks\": {\n      \"name\": \"importSessionBookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"importSessionId\": {\n          \"name\": \"importSessionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importSessionBookmarks_sessionId_idx\": {\n          \"name\": \"importSessionBookmarks_sessionId_idx\",\n          \"columns\": [\n            \"importSessionId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessionBookmarks_bookmarkId_idx\": {\n          \"name\": \"importSessionBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessionBookmarks_importSessionId_bookmarkId_unique\": {\n          \"name\": \"importSessionBookmarks_importSessionId_bookmarkId_unique\",\n          \"columns\": [\n            \"importSessionId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"importSessionBookmarks_importSessionId_importSessions_id_fk\": {\n          \"name\": \"importSessionBookmarks_importSessionId_importSessions_id_fk\",\n          \"tableFrom\": \"importSessionBookmarks\",\n          \"tableTo\": \"importSessions\",\n          \"columnsFrom\": [\n            \"importSessionId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"importSessionBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"importSessionBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"importSessionBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importSessions\": {\n      \"name\": \"importSessions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rootListId\": {\n          \"name\": \"rootListId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importSessions_userId_idx\": {\n          \"name\": \"importSessions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"importSessions_userId_user_id_fk\": {\n          \"name\": \"importSessions_userId_user_id_fk\",\n          \"tableFrom\": \"importSessions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"importSessions_rootListId_bookmarkLists_id_fk\": {\n          \"name\": \"importSessions_rootListId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"importSessions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"rootListId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invites\": {\n      \"name\": \"invites\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"usedAt\": {\n          \"name\": \"usedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"invites_token_unique\": {\n          \"name\": \"invites_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"invites_invitedBy_user_id_fk\": {\n          \"name\": \"invites_invitedBy_user_id_fk\",\n          \"tableFrom\": \"invites\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"listCollaborators\": {\n      \"name\": \"listCollaborators\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedBy\": {\n          \"name\": \"addedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"listCollaborators_listId_idx\": {\n          \"name\": \"listCollaborators_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listCollaborators_userId_idx\": {\n          \"name\": \"listCollaborators_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listCollaborators_listId_userId_unique\": {\n          \"name\": \"listCollaborators_listId_userId_unique\",\n          \"columns\": [\n            \"listId\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"listCollaborators_listId_bookmarkLists_id_fk\": {\n          \"name\": \"listCollaborators_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listCollaborators_userId_user_id_fk\": {\n          \"name\": \"listCollaborators_userId_user_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listCollaborators_addedBy_user_id_fk\": {\n          \"name\": \"listCollaborators_addedBy_user_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"addedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"listInvitations\": {\n      \"name\": \"listInvitations\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"invitedAt\": {\n          \"name\": \"invitedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"invitedEmail\": {\n          \"name\": \"invitedEmail\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"listInvitations_listId_idx\": {\n          \"name\": \"listInvitations_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_userId_idx\": {\n          \"name\": \"listInvitations_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_status_idx\": {\n          \"name\": \"listInvitations_status_idx\",\n          \"columns\": [\n            \"status\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_listId_userId_unique\": {\n          \"name\": \"listInvitations_listId_userId_unique\",\n          \"columns\": [\n            \"listId\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"listInvitations_listId_bookmarkLists_id_fk\": {\n          \"name\": \"listInvitations_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listInvitations_userId_user_id_fk\": {\n          \"name\": \"listInvitations_userId_user_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listInvitations_invitedBy_user_id_fk\": {\n          \"name\": \"listInvitations_invitedBy_user_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"passwordResetToken\": {\n      \"name\": \"passwordResetToken\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"passwordResetToken_token_unique\": {\n          \"name\": \"passwordResetToken_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        },\n        \"passwordResetTokens_userId_idx\": {\n          \"name\": \"passwordResetTokens_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"passwordResetToken_userId_user_id_fk\": {\n          \"name\": \"passwordResetToken_userId_user_id_fk\",\n          \"tableFrom\": \"passwordResetToken\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"importTags\": {\n          \"name\": \"importTags\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineActions\": {\n      \"name\": \"ruleEngineActions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"ruleId\": {\n          \"name\": \"ruleId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"action\": {\n          \"name\": \"action\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngineActions_userId_idx\": {\n          \"name\": \"ruleEngineActions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"ruleEngineActions_ruleId_idx\": {\n          \"name\": \"ruleEngineActions_ruleId_idx\",\n          \"columns\": [\n            \"ruleId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineActions_userId_user_id_fk\": {\n          \"name\": \"ruleEngineActions_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\": {\n          \"name\": \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"ruleId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_tagId_fk\": {\n          \"name\": \"ruleEngineActions_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_listId_fk\": {\n          \"name\": \"ruleEngineActions_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineRules\": {\n      \"name\": \"ruleEngineRules\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"event\": {\n          \"name\": \"event\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"condition\": {\n          \"name\": \"condition\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngine_userId_idx\": {\n          \"name\": \"ruleEngine_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineRules_userId_user_id_fk\": {\n          \"name\": \"ruleEngineRules_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_tagId_fk\": {\n          \"name\": \"ruleEngineRules_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_listId_fk\": {\n          \"name\": \"ruleEngineRules_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"subscriptions\": {\n      \"name\": \"subscriptions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeCustomerId\": {\n          \"name\": \"stripeCustomerId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeSubscriptionId\": {\n          \"name\": \"stripeSubscriptionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tier\": {\n          \"name\": \"tier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'free'\"\n        },\n        \"priceId\": {\n          \"name\": \"priceId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cancelAtPeriodEnd\": {\n          \"name\": \"cancelAtPeriodEnd\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"startDate\": {\n          \"name\": \"startDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"endDate\": {\n          \"name\": \"endDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"subscriptions_userId_unique\": {\n          \"name\": \"subscriptions_userId_unique\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": true\n        },\n        \"subscriptions_userId_idx\": {\n          \"name\": \"subscriptions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"subscriptions_stripeCustomerId_idx\": {\n          \"name\": \"subscriptions_stripeCustomerId_idx\",\n          \"columns\": [\n            \"stripeCustomerId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"subscriptions_userId_user_id_fk\": {\n          \"name\": \"subscriptions_userId_user_id_fk\",\n          \"tableFrom\": \"subscriptions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"salt\": {\n          \"name\": \"salt\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        },\n        \"bookmarkQuota\": {\n          \"name\": \"bookmarkQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"storageQuota\": {\n          \"name\": \"storageQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"browserCrawlingEnabled\": {\n          \"name\": \"browserCrawlingEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkClickAction\": {\n          \"name\": \"bookmarkClickAction\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'open_original_link'\"\n        },\n        \"archiveDisplayBehaviour\": {\n          \"name\": \"archiveDisplayBehaviour\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'show'\"\n        },\n        \"timezone\": {\n          \"name\": \"timezone\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'UTC'\"\n        },\n        \"backupsEnabled\": {\n          \"name\": \"backupsEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"backupsFrequency\": {\n          \"name\": \"backupsFrequency\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'weekly'\"\n        },\n        \"backupsRetentionDays\": {\n          \"name\": \"backupsRetentionDays\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 30\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0068_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"4f54e033-103d-44c0-b25e-09c6b6312d90\",\n  \"prevId\": \"6efcc681-eaac-44a6-a224-f97a7df01ff3\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"backups\": {\n      \"name\": \"backups\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkCount\": {\n          \"name\": \"bookmarkCount\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"errorMessage\": {\n          \"name\": \"errorMessage\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"backups_userId_idx\": {\n          \"name\": \"backups_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"backups_createdAt_idx\": {\n          \"name\": \"backups_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"backups_userId_user_id_fk\": {\n          \"name\": \"backups_userId_user_id_fk\",\n          \"tableFrom\": \"backups\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"backups_assetId_assets_id_fk\": {\n          \"name\": \"backups_assetId_assets_id_fk\",\n          \"tableFrom\": \"backups\",\n          \"tableTo\": \"assets\",\n          \"columnsFrom\": [\n            \"assetId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"author\": {\n          \"name\": \"author\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"publisher\": {\n          \"name\": \"publisher\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"datePublished\": {\n          \"name\": \"datePublished\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dateModified\": {\n          \"name\": \"dateModified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"contentAssetId\": {\n          \"name\": \"contentAssetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rssToken\": {\n          \"name\": \"rssToken\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_userId_id_idx\": {\n          \"name\": \"bookmarkLists_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"bookmarkTags_userId_id_idx\": {\n          \"name\": \"bookmarkTags_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summarizationStatus\": {\n          \"name\": \"summarizationStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"source\": {\n          \"name\": \"source\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_userId_createdAt_id_idx\": {\n          \"name\": \"bookmarks_userId_createdAt_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"createdAt\",\n            \"id\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_userId_archived_createdAt_id_idx\": {\n          \"name\": \"bookmarks_userId_archived_createdAt_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"archived\",\n            \"createdAt\",\n            \"id\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_userId_favourited_createdAt_id_idx\": {\n          \"name\": \"bookmarks_userId_favourited_createdAt_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"favourited\",\n            \"createdAt\",\n            \"id\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"listMembershipId\": {\n          \"name\": \"listMembershipId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkId_idx\",\n          \"columns\": [\n            \"listId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listMembershipId_listCollaborators_id_fk\": {\n          \"name\": \"bookmarksInLists_listMembershipId_listCollaborators_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"listCollaborators\",\n          \"columnsFrom\": [\n            \"listMembershipId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importSessionBookmarks\": {\n      \"name\": \"importSessionBookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"importSessionId\": {\n          \"name\": \"importSessionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importSessionBookmarks_sessionId_idx\": {\n          \"name\": \"importSessionBookmarks_sessionId_idx\",\n          \"columns\": [\n            \"importSessionId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessionBookmarks_bookmarkId_idx\": {\n          \"name\": \"importSessionBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessionBookmarks_importSessionId_bookmarkId_unique\": {\n          \"name\": \"importSessionBookmarks_importSessionId_bookmarkId_unique\",\n          \"columns\": [\n            \"importSessionId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"importSessionBookmarks_importSessionId_importSessions_id_fk\": {\n          \"name\": \"importSessionBookmarks_importSessionId_importSessions_id_fk\",\n          \"tableFrom\": \"importSessionBookmarks\",\n          \"tableTo\": \"importSessions\",\n          \"columnsFrom\": [\n            \"importSessionId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"importSessionBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"importSessionBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"importSessionBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importSessions\": {\n      \"name\": \"importSessions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rootListId\": {\n          \"name\": \"rootListId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importSessions_userId_idx\": {\n          \"name\": \"importSessions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"importSessions_userId_user_id_fk\": {\n          \"name\": \"importSessions_userId_user_id_fk\",\n          \"tableFrom\": \"importSessions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"importSessions_rootListId_bookmarkLists_id_fk\": {\n          \"name\": \"importSessions_rootListId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"importSessions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"rootListId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invites\": {\n      \"name\": \"invites\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"usedAt\": {\n          \"name\": \"usedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"invites_token_unique\": {\n          \"name\": \"invites_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"invites_invitedBy_user_id_fk\": {\n          \"name\": \"invites_invitedBy_user_id_fk\",\n          \"tableFrom\": \"invites\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"listCollaborators\": {\n      \"name\": \"listCollaborators\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedBy\": {\n          \"name\": \"addedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"listCollaborators_listId_idx\": {\n          \"name\": \"listCollaborators_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listCollaborators_userId_idx\": {\n          \"name\": \"listCollaborators_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listCollaborators_listId_userId_unique\": {\n          \"name\": \"listCollaborators_listId_userId_unique\",\n          \"columns\": [\n            \"listId\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"listCollaborators_listId_bookmarkLists_id_fk\": {\n          \"name\": \"listCollaborators_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listCollaborators_userId_user_id_fk\": {\n          \"name\": \"listCollaborators_userId_user_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listCollaborators_addedBy_user_id_fk\": {\n          \"name\": \"listCollaborators_addedBy_user_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"addedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"listInvitations\": {\n      \"name\": \"listInvitations\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"invitedAt\": {\n          \"name\": \"invitedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"invitedEmail\": {\n          \"name\": \"invitedEmail\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"listInvitations_listId_idx\": {\n          \"name\": \"listInvitations_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_userId_idx\": {\n          \"name\": \"listInvitations_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_status_idx\": {\n          \"name\": \"listInvitations_status_idx\",\n          \"columns\": [\n            \"status\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_listId_userId_unique\": {\n          \"name\": \"listInvitations_listId_userId_unique\",\n          \"columns\": [\n            \"listId\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"listInvitations_listId_bookmarkLists_id_fk\": {\n          \"name\": \"listInvitations_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listInvitations_userId_user_id_fk\": {\n          \"name\": \"listInvitations_userId_user_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listInvitations_invitedBy_user_id_fk\": {\n          \"name\": \"listInvitations_invitedBy_user_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"passwordResetToken\": {\n      \"name\": \"passwordResetToken\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"passwordResetToken_token_unique\": {\n          \"name\": \"passwordResetToken_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        },\n        \"passwordResetTokens_userId_idx\": {\n          \"name\": \"passwordResetTokens_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"passwordResetToken_userId_user_id_fk\": {\n          \"name\": \"passwordResetToken_userId_user_id_fk\",\n          \"tableFrom\": \"passwordResetToken\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_bookmarkId_idx\": {\n          \"name\": \"rssFeedImports_rssFeedId_bookmarkId_idx\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"importTags\": {\n          \"name\": \"importTags\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineActions\": {\n      \"name\": \"ruleEngineActions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"ruleId\": {\n          \"name\": \"ruleId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"action\": {\n          \"name\": \"action\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngineActions_userId_idx\": {\n          \"name\": \"ruleEngineActions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"ruleEngineActions_ruleId_idx\": {\n          \"name\": \"ruleEngineActions_ruleId_idx\",\n          \"columns\": [\n            \"ruleId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineActions_userId_user_id_fk\": {\n          \"name\": \"ruleEngineActions_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\": {\n          \"name\": \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"ruleId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_tagId_fk\": {\n          \"name\": \"ruleEngineActions_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_listId_fk\": {\n          \"name\": \"ruleEngineActions_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineRules\": {\n      \"name\": \"ruleEngineRules\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"event\": {\n          \"name\": \"event\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"condition\": {\n          \"name\": \"condition\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngine_userId_idx\": {\n          \"name\": \"ruleEngine_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineRules_userId_user_id_fk\": {\n          \"name\": \"ruleEngineRules_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_tagId_fk\": {\n          \"name\": \"ruleEngineRules_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_listId_fk\": {\n          \"name\": \"ruleEngineRules_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"subscriptions\": {\n      \"name\": \"subscriptions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeCustomerId\": {\n          \"name\": \"stripeCustomerId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeSubscriptionId\": {\n          \"name\": \"stripeSubscriptionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tier\": {\n          \"name\": \"tier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'free'\"\n        },\n        \"priceId\": {\n          \"name\": \"priceId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cancelAtPeriodEnd\": {\n          \"name\": \"cancelAtPeriodEnd\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"startDate\": {\n          \"name\": \"startDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"endDate\": {\n          \"name\": \"endDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"subscriptions_userId_unique\": {\n          \"name\": \"subscriptions_userId_unique\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": true\n        },\n        \"subscriptions_userId_idx\": {\n          \"name\": \"subscriptions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"subscriptions_stripeCustomerId_idx\": {\n          \"name\": \"subscriptions_stripeCustomerId_idx\",\n          \"columns\": [\n            \"stripeCustomerId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"subscriptions_userId_user_id_fk\": {\n          \"name\": \"subscriptions_userId_user_id_fk\",\n          \"tableFrom\": \"subscriptions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_tagId_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkId_idx\",\n          \"columns\": [\n            \"tagId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"salt\": {\n          \"name\": \"salt\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        },\n        \"bookmarkQuota\": {\n          \"name\": \"bookmarkQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"storageQuota\": {\n          \"name\": \"storageQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"browserCrawlingEnabled\": {\n          \"name\": \"browserCrawlingEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkClickAction\": {\n          \"name\": \"bookmarkClickAction\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'open_original_link'\"\n        },\n        \"archiveDisplayBehaviour\": {\n          \"name\": \"archiveDisplayBehaviour\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'show'\"\n        },\n        \"timezone\": {\n          \"name\": \"timezone\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'UTC'\"\n        },\n        \"backupsEnabled\": {\n          \"name\": \"backupsEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"backupsFrequency\": {\n          \"name\": \"backupsFrequency\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'weekly'\"\n        },\n        \"backupsRetentionDays\": {\n          \"name\": \"backupsRetentionDays\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 30\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0069_snapshot.json",
    "content": "{\n  \"id\": \"d8334431-a9bc-4397-8bc8-21b09d7cd4b2\",\n  \"prevId\": \"4f54e033-103d-44c0-b25e-09c6b6312d90\",\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"backups\": {\n      \"name\": \"backups\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkCount\": {\n          \"name\": \"bookmarkCount\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"errorMessage\": {\n          \"name\": \"errorMessage\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"backups_userId_idx\": {\n          \"name\": \"backups_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"backups_createdAt_idx\": {\n          \"name\": \"backups_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"backups_userId_user_id_fk\": {\n          \"name\": \"backups_userId_user_id_fk\",\n          \"tableFrom\": \"backups\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"backups_assetId_assets_id_fk\": {\n          \"name\": \"backups_assetId_assets_id_fk\",\n          \"tableFrom\": \"backups\",\n          \"columnsFrom\": [\n            \"assetId\"\n          ],\n          \"tableTo\": \"assets\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"author\": {\n          \"name\": \"author\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"publisher\": {\n          \"name\": \"publisher\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"datePublished\": {\n          \"name\": \"datePublished\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dateModified\": {\n          \"name\": \"dateModified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"contentAssetId\": {\n          \"name\": \"contentAssetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rssToken\": {\n          \"name\": \"rssToken\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_userId_id_idx\": {\n          \"name\": \"bookmarkLists_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"set null\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"bookmarkTags_userId_id_idx\": {\n          \"name\": \"bookmarkTags_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summarizationStatus\": {\n          \"name\": \"summarizationStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"source\": {\n          \"name\": \"source\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_userId_createdAt_id_idx\": {\n          \"name\": \"bookmarks_userId_createdAt_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"createdAt\",\n            \"id\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_userId_archived_createdAt_id_idx\": {\n          \"name\": \"bookmarks_userId_archived_createdAt_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"archived\",\n            \"createdAt\",\n            \"id\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_userId_favourited_createdAt_id_idx\": {\n          \"name\": \"bookmarks_userId_favourited_createdAt_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"favourited\",\n            \"createdAt\",\n            \"id\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"listMembershipId\": {\n          \"name\": \"listMembershipId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkId_idx\",\n          \"columns\": [\n            \"listId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarksInLists_listMembershipId_listCollaborators_id_fk\": {\n          \"name\": \"bookmarksInLists_listMembershipId_listCollaborators_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"listMembershipId\"\n          ],\n          \"tableTo\": \"listCollaborators\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importSessionBookmarks\": {\n      \"name\": \"importSessionBookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"importSessionId\": {\n          \"name\": \"importSessionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importSessionBookmarks_sessionId_idx\": {\n          \"name\": \"importSessionBookmarks_sessionId_idx\",\n          \"columns\": [\n            \"importSessionId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessionBookmarks_bookmarkId_idx\": {\n          \"name\": \"importSessionBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessionBookmarks_importSessionId_bookmarkId_unique\": {\n          \"name\": \"importSessionBookmarks_importSessionId_bookmarkId_unique\",\n          \"columns\": [\n            \"importSessionId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"importSessionBookmarks_importSessionId_importSessions_id_fk\": {\n          \"name\": \"importSessionBookmarks_importSessionId_importSessions_id_fk\",\n          \"tableFrom\": \"importSessionBookmarks\",\n          \"columnsFrom\": [\n            \"importSessionId\"\n          ],\n          \"tableTo\": \"importSessions\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"importSessionBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"importSessionBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"importSessionBookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importSessions\": {\n      \"name\": \"importSessions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rootListId\": {\n          \"name\": \"rootListId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importSessions_userId_idx\": {\n          \"name\": \"importSessions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"importSessions_userId_user_id_fk\": {\n          \"name\": \"importSessions_userId_user_id_fk\",\n          \"tableFrom\": \"importSessions\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"importSessions_rootListId_bookmarkLists_id_fk\": {\n          \"name\": \"importSessions_rootListId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"importSessions\",\n          \"columnsFrom\": [\n            \"rootListId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"set null\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invites\": {\n      \"name\": \"invites\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"usedAt\": {\n          \"name\": \"usedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"invites_token_unique\": {\n          \"name\": \"invites_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"invites_invitedBy_user_id_fk\": {\n          \"name\": \"invites_invitedBy_user_id_fk\",\n          \"tableFrom\": \"invites\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"listCollaborators\": {\n      \"name\": \"listCollaborators\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedBy\": {\n          \"name\": \"addedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"listCollaborators_listId_idx\": {\n          \"name\": \"listCollaborators_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listCollaborators_userId_idx\": {\n          \"name\": \"listCollaborators_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listCollaborators_listId_userId_unique\": {\n          \"name\": \"listCollaborators_listId_userId_unique\",\n          \"columns\": [\n            \"listId\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"listCollaborators_listId_bookmarkLists_id_fk\": {\n          \"name\": \"listCollaborators_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"listCollaborators_userId_user_id_fk\": {\n          \"name\": \"listCollaborators_userId_user_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"listCollaborators_addedBy_user_id_fk\": {\n          \"name\": \"listCollaborators_addedBy_user_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"columnsFrom\": [\n            \"addedBy\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"set null\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"listInvitations\": {\n      \"name\": \"listInvitations\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"invitedAt\": {\n          \"name\": \"invitedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"invitedEmail\": {\n          \"name\": \"invitedEmail\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"listInvitations_listId_idx\": {\n          \"name\": \"listInvitations_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_userId_idx\": {\n          \"name\": \"listInvitations_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_status_idx\": {\n          \"name\": \"listInvitations_status_idx\",\n          \"columns\": [\n            \"status\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_listId_userId_unique\": {\n          \"name\": \"listInvitations_listId_userId_unique\",\n          \"columns\": [\n            \"listId\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"listInvitations_listId_bookmarkLists_id_fk\": {\n          \"name\": \"listInvitations_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"listInvitations_userId_user_id_fk\": {\n          \"name\": \"listInvitations_userId_user_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"listInvitations_invitedBy_user_id_fk\": {\n          \"name\": \"listInvitations_invitedBy_user_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"set null\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"passwordResetToken\": {\n      \"name\": \"passwordResetToken\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"passwordResetToken_token_unique\": {\n          \"name\": \"passwordResetToken_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        },\n        \"passwordResetTokens_userId_idx\": {\n          \"name\": \"passwordResetTokens_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"passwordResetToken_userId_user_id_fk\": {\n          \"name\": \"passwordResetToken_userId_user_id_fk\",\n          \"tableFrom\": \"passwordResetToken\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_bookmarkId_idx\": {\n          \"name\": \"rssFeedImports_rssFeedId_bookmarkId_idx\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"tableTo\": \"rssFeeds\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"set null\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"importTags\": {\n          \"name\": \"importTags\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineActions\": {\n      \"name\": \"ruleEngineActions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"ruleId\": {\n          \"name\": \"ruleId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"action\": {\n          \"name\": \"action\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngineActions_userId_idx\": {\n          \"name\": \"ruleEngineActions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"ruleEngineActions_ruleId_idx\": {\n          \"name\": \"ruleEngineActions_ruleId_idx\",\n          \"columns\": [\n            \"ruleId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineActions_userId_user_id_fk\": {\n          \"name\": \"ruleEngineActions_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\": {\n          \"name\": \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"columnsFrom\": [\n            \"ruleId\"\n          ],\n          \"tableTo\": \"ruleEngineRules\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"ruleEngineActions_userId_tagId_fk\": {\n          \"name\": \"ruleEngineActions_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"ruleEngineActions_userId_listId_fk\": {\n          \"name\": \"ruleEngineActions_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineRules\": {\n      \"name\": \"ruleEngineRules\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"event\": {\n          \"name\": \"event\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"condition\": {\n          \"name\": \"condition\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngine_userId_idx\": {\n          \"name\": \"ruleEngine_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineRules_userId_user_id_fk\": {\n          \"name\": \"ruleEngineRules_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"ruleEngineRules_userId_tagId_fk\": {\n          \"name\": \"ruleEngineRules_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"ruleEngineRules_userId_listId_fk\": {\n          \"name\": \"ruleEngineRules_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"subscriptions\": {\n      \"name\": \"subscriptions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeCustomerId\": {\n          \"name\": \"stripeCustomerId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeSubscriptionId\": {\n          \"name\": \"stripeSubscriptionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tier\": {\n          \"name\": \"tier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'free'\"\n        },\n        \"priceId\": {\n          \"name\": \"priceId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cancelAtPeriodEnd\": {\n          \"name\": \"cancelAtPeriodEnd\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"startDate\": {\n          \"name\": \"startDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"endDate\": {\n          \"name\": \"endDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"subscriptions_userId_unique\": {\n          \"name\": \"subscriptions_userId_unique\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": true\n        },\n        \"subscriptions_userId_idx\": {\n          \"name\": \"subscriptions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"subscriptions_stripeCustomerId_idx\": {\n          \"name\": \"subscriptions_stripeCustomerId_idx\",\n          \"columns\": [\n            \"stripeCustomerId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"subscriptions_userId_user_id_fk\": {\n          \"name\": \"subscriptions_userId_user_id_fk\",\n          \"tableFrom\": \"subscriptions\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_tagId_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkId_idx\",\n          \"columns\": [\n            \"tagId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"salt\": {\n          \"name\": \"salt\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        },\n        \"bookmarkQuota\": {\n          \"name\": \"bookmarkQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"storageQuota\": {\n          \"name\": \"storageQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"browserCrawlingEnabled\": {\n          \"name\": \"browserCrawlingEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkClickAction\": {\n          \"name\": \"bookmarkClickAction\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'open_original_link'\"\n        },\n        \"archiveDisplayBehaviour\": {\n          \"name\": \"archiveDisplayBehaviour\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'show'\"\n        },\n        \"timezone\": {\n          \"name\": \"timezone\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'UTC'\"\n        },\n        \"backupsEnabled\": {\n          \"name\": \"backupsEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"backupsFrequency\": {\n          \"name\": \"backupsFrequency\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'weekly'\"\n        },\n        \"backupsRetentionDays\": {\n          \"name\": \"backupsRetentionDays\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 30\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"columns\": {},\n    \"schemas\": {},\n    \"tables\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0070_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"ff48e807-eda8-4a9f-ab94-afd693f415eb\",\n  \"prevId\": \"d8334431-a9bc-4397-8bc8-21b09d7cd4b2\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"backups\": {\n      \"name\": \"backups\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkCount\": {\n          \"name\": \"bookmarkCount\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"errorMessage\": {\n          \"name\": \"errorMessage\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"backups_userId_idx\": {\n          \"name\": \"backups_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"backups_createdAt_idx\": {\n          \"name\": \"backups_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"backups_userId_user_id_fk\": {\n          \"name\": \"backups_userId_user_id_fk\",\n          \"tableFrom\": \"backups\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"backups_assetId_assets_id_fk\": {\n          \"name\": \"backups_assetId_assets_id_fk\",\n          \"tableFrom\": \"backups\",\n          \"tableTo\": \"assets\",\n          \"columnsFrom\": [\n            \"assetId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"author\": {\n          \"name\": \"author\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"publisher\": {\n          \"name\": \"publisher\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"datePublished\": {\n          \"name\": \"datePublished\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dateModified\": {\n          \"name\": \"dateModified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"contentAssetId\": {\n          \"name\": \"contentAssetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rssToken\": {\n          \"name\": \"rssToken\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_userId_id_idx\": {\n          \"name\": \"bookmarkLists_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"bookmarkTags_userId_id_idx\": {\n          \"name\": \"bookmarkTags_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summarizationStatus\": {\n          \"name\": \"summarizationStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"source\": {\n          \"name\": \"source\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_userId_createdAt_id_idx\": {\n          \"name\": \"bookmarks_userId_createdAt_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"createdAt\",\n            \"id\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_userId_archived_createdAt_id_idx\": {\n          \"name\": \"bookmarks_userId_archived_createdAt_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"archived\",\n            \"createdAt\",\n            \"id\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_userId_favourited_createdAt_id_idx\": {\n          \"name\": \"bookmarks_userId_favourited_createdAt_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"favourited\",\n            \"createdAt\",\n            \"id\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"listMembershipId\": {\n          \"name\": \"listMembershipId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkId_idx\",\n          \"columns\": [\n            \"listId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listMembershipId_listCollaborators_id_fk\": {\n          \"name\": \"bookmarksInLists_listMembershipId_listCollaborators_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"listCollaborators\",\n          \"columnsFrom\": [\n            \"listMembershipId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importSessionBookmarks\": {\n      \"name\": \"importSessionBookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"importSessionId\": {\n          \"name\": \"importSessionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importSessionBookmarks_sessionId_idx\": {\n          \"name\": \"importSessionBookmarks_sessionId_idx\",\n          \"columns\": [\n            \"importSessionId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessionBookmarks_bookmarkId_idx\": {\n          \"name\": \"importSessionBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessionBookmarks_importSessionId_bookmarkId_unique\": {\n          \"name\": \"importSessionBookmarks_importSessionId_bookmarkId_unique\",\n          \"columns\": [\n            \"importSessionId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"importSessionBookmarks_importSessionId_importSessions_id_fk\": {\n          \"name\": \"importSessionBookmarks_importSessionId_importSessions_id_fk\",\n          \"tableFrom\": \"importSessionBookmarks\",\n          \"tableTo\": \"importSessions\",\n          \"columnsFrom\": [\n            \"importSessionId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"importSessionBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"importSessionBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"importSessionBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importSessions\": {\n      \"name\": \"importSessions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rootListId\": {\n          \"name\": \"rootListId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importSessions_userId_idx\": {\n          \"name\": \"importSessions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"importSessions_userId_user_id_fk\": {\n          \"name\": \"importSessions_userId_user_id_fk\",\n          \"tableFrom\": \"importSessions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"importSessions_rootListId_bookmarkLists_id_fk\": {\n          \"name\": \"importSessions_rootListId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"importSessions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"rootListId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invites\": {\n      \"name\": \"invites\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"usedAt\": {\n          \"name\": \"usedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"invites_token_unique\": {\n          \"name\": \"invites_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"invites_invitedBy_user_id_fk\": {\n          \"name\": \"invites_invitedBy_user_id_fk\",\n          \"tableFrom\": \"invites\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"listCollaborators\": {\n      \"name\": \"listCollaborators\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedBy\": {\n          \"name\": \"addedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"listCollaborators_listId_idx\": {\n          \"name\": \"listCollaborators_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listCollaborators_userId_idx\": {\n          \"name\": \"listCollaborators_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listCollaborators_listId_userId_unique\": {\n          \"name\": \"listCollaborators_listId_userId_unique\",\n          \"columns\": [\n            \"listId\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"listCollaborators_listId_bookmarkLists_id_fk\": {\n          \"name\": \"listCollaborators_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listCollaborators_userId_user_id_fk\": {\n          \"name\": \"listCollaborators_userId_user_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listCollaborators_addedBy_user_id_fk\": {\n          \"name\": \"listCollaborators_addedBy_user_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"addedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"listInvitations\": {\n      \"name\": \"listInvitations\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"invitedAt\": {\n          \"name\": \"invitedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"invitedEmail\": {\n          \"name\": \"invitedEmail\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"listInvitations_listId_idx\": {\n          \"name\": \"listInvitations_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_userId_idx\": {\n          \"name\": \"listInvitations_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_status_idx\": {\n          \"name\": \"listInvitations_status_idx\",\n          \"columns\": [\n            \"status\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_listId_userId_unique\": {\n          \"name\": \"listInvitations_listId_userId_unique\",\n          \"columns\": [\n            \"listId\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"listInvitations_listId_bookmarkLists_id_fk\": {\n          \"name\": \"listInvitations_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listInvitations_userId_user_id_fk\": {\n          \"name\": \"listInvitations_userId_user_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listInvitations_invitedBy_user_id_fk\": {\n          \"name\": \"listInvitations_invitedBy_user_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"passwordResetToken\": {\n      \"name\": \"passwordResetToken\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"passwordResetToken_token_unique\": {\n          \"name\": \"passwordResetToken_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        },\n        \"passwordResetTokens_userId_idx\": {\n          \"name\": \"passwordResetTokens_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"passwordResetToken_userId_user_id_fk\": {\n          \"name\": \"passwordResetToken_userId_user_id_fk\",\n          \"tableFrom\": \"passwordResetToken\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_bookmarkId_idx\": {\n          \"name\": \"rssFeedImports_rssFeedId_bookmarkId_idx\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"importTags\": {\n          \"name\": \"importTags\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineActions\": {\n      \"name\": \"ruleEngineActions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"ruleId\": {\n          \"name\": \"ruleId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"action\": {\n          \"name\": \"action\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngineActions_userId_idx\": {\n          \"name\": \"ruleEngineActions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"ruleEngineActions_ruleId_idx\": {\n          \"name\": \"ruleEngineActions_ruleId_idx\",\n          \"columns\": [\n            \"ruleId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineActions_userId_user_id_fk\": {\n          \"name\": \"ruleEngineActions_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\": {\n          \"name\": \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"ruleId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_tagId_fk\": {\n          \"name\": \"ruleEngineActions_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_listId_fk\": {\n          \"name\": \"ruleEngineActions_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineRules\": {\n      \"name\": \"ruleEngineRules\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"event\": {\n          \"name\": \"event\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"condition\": {\n          \"name\": \"condition\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngine_userId_idx\": {\n          \"name\": \"ruleEngine_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineRules_userId_user_id_fk\": {\n          \"name\": \"ruleEngineRules_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_tagId_fk\": {\n          \"name\": \"ruleEngineRules_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_listId_fk\": {\n          \"name\": \"ruleEngineRules_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"subscriptions\": {\n      \"name\": \"subscriptions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeCustomerId\": {\n          \"name\": \"stripeCustomerId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeSubscriptionId\": {\n          \"name\": \"stripeSubscriptionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tier\": {\n          \"name\": \"tier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'free'\"\n        },\n        \"priceId\": {\n          \"name\": \"priceId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cancelAtPeriodEnd\": {\n          \"name\": \"cancelAtPeriodEnd\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"startDate\": {\n          \"name\": \"startDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"endDate\": {\n          \"name\": \"endDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"subscriptions_userId_unique\": {\n          \"name\": \"subscriptions_userId_unique\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": true\n        },\n        \"subscriptions_userId_idx\": {\n          \"name\": \"subscriptions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"subscriptions_stripeCustomerId_idx\": {\n          \"name\": \"subscriptions_stripeCustomerId_idx\",\n          \"columns\": [\n            \"stripeCustomerId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"subscriptions_userId_user_id_fk\": {\n          \"name\": \"subscriptions_userId_user_id_fk\",\n          \"tableFrom\": \"subscriptions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_tagId_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkId_idx\",\n          \"columns\": [\n            \"tagId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"salt\": {\n          \"name\": \"salt\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        },\n        \"bookmarkQuota\": {\n          \"name\": \"bookmarkQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"storageQuota\": {\n          \"name\": \"storageQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"browserCrawlingEnabled\": {\n          \"name\": \"browserCrawlingEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkClickAction\": {\n          \"name\": \"bookmarkClickAction\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'open_original_link'\"\n        },\n        \"archiveDisplayBehaviour\": {\n          \"name\": \"archiveDisplayBehaviour\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'show'\"\n        },\n        \"timezone\": {\n          \"name\": \"timezone\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'UTC'\"\n        },\n        \"backupsEnabled\": {\n          \"name\": \"backupsEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"backupsFrequency\": {\n          \"name\": \"backupsFrequency\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'weekly'\"\n        },\n        \"backupsRetentionDays\": {\n          \"name\": \"backupsRetentionDays\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 30\n        },\n        \"readerFontSize\": {\n          \"name\": \"readerFontSize\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"readerLineHeight\": {\n          \"name\": \"readerLineHeight\",\n          \"type\": \"real\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"readerFontFamily\": {\n          \"name\": \"readerFontFamily\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0071_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"74441c0b-d9b5-4154-9ef5-71fff60d52fe\",\n  \"prevId\": \"ff48e807-eda8-4a9f-ab94-afd693f415eb\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"backups\": {\n      \"name\": \"backups\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkCount\": {\n          \"name\": \"bookmarkCount\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"errorMessage\": {\n          \"name\": \"errorMessage\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"backups_userId_idx\": {\n          \"name\": \"backups_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"backups_createdAt_idx\": {\n          \"name\": \"backups_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"backups_userId_user_id_fk\": {\n          \"name\": \"backups_userId_user_id_fk\",\n          \"tableFrom\": \"backups\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"backups_assetId_assets_id_fk\": {\n          \"name\": \"backups_assetId_assets_id_fk\",\n          \"tableFrom\": \"backups\",\n          \"tableTo\": \"assets\",\n          \"columnsFrom\": [\n            \"assetId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"author\": {\n          \"name\": \"author\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"publisher\": {\n          \"name\": \"publisher\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"datePublished\": {\n          \"name\": \"datePublished\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dateModified\": {\n          \"name\": \"dateModified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"contentAssetId\": {\n          \"name\": \"contentAssetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rssToken\": {\n          \"name\": \"rssToken\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_userId_id_idx\": {\n          \"name\": \"bookmarkLists_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"normalizedName\": {\n          \"name\": \"normalizedName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"generated\": {\n            \"as\": \"(lower(replace(replace(replace(\\\"name\\\", ' ', ''), '-', ''), '_', '')))\",\n            \"type\": \"virtual\"\n          }\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_normalizedName_idx\": {\n          \"name\": \"bookmarkTags_normalizedName_idx\",\n          \"columns\": [\n            \"normalizedName\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"bookmarkTags_userId_id_idx\": {\n          \"name\": \"bookmarkTags_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summarizationStatus\": {\n          \"name\": \"summarizationStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"source\": {\n          \"name\": \"source\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_userId_createdAt_id_idx\": {\n          \"name\": \"bookmarks_userId_createdAt_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"createdAt\",\n            \"id\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_userId_archived_createdAt_id_idx\": {\n          \"name\": \"bookmarks_userId_archived_createdAt_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"archived\",\n            \"createdAt\",\n            \"id\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_userId_favourited_createdAt_id_idx\": {\n          \"name\": \"bookmarks_userId_favourited_createdAt_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"favourited\",\n            \"createdAt\",\n            \"id\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"listMembershipId\": {\n          \"name\": \"listMembershipId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkId_idx\",\n          \"columns\": [\n            \"listId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listMembershipId_listCollaborators_id_fk\": {\n          \"name\": \"bookmarksInLists_listMembershipId_listCollaborators_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"listCollaborators\",\n          \"columnsFrom\": [\n            \"listMembershipId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importSessionBookmarks\": {\n      \"name\": \"importSessionBookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"importSessionId\": {\n          \"name\": \"importSessionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importSessionBookmarks_sessionId_idx\": {\n          \"name\": \"importSessionBookmarks_sessionId_idx\",\n          \"columns\": [\n            \"importSessionId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessionBookmarks_bookmarkId_idx\": {\n          \"name\": \"importSessionBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessionBookmarks_importSessionId_bookmarkId_unique\": {\n          \"name\": \"importSessionBookmarks_importSessionId_bookmarkId_unique\",\n          \"columns\": [\n            \"importSessionId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"importSessionBookmarks_importSessionId_importSessions_id_fk\": {\n          \"name\": \"importSessionBookmarks_importSessionId_importSessions_id_fk\",\n          \"tableFrom\": \"importSessionBookmarks\",\n          \"tableTo\": \"importSessions\",\n          \"columnsFrom\": [\n            \"importSessionId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"importSessionBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"importSessionBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"importSessionBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importSessions\": {\n      \"name\": \"importSessions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rootListId\": {\n          \"name\": \"rootListId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importSessions_userId_idx\": {\n          \"name\": \"importSessions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"importSessions_userId_user_id_fk\": {\n          \"name\": \"importSessions_userId_user_id_fk\",\n          \"tableFrom\": \"importSessions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"importSessions_rootListId_bookmarkLists_id_fk\": {\n          \"name\": \"importSessions_rootListId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"importSessions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"rootListId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invites\": {\n      \"name\": \"invites\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"usedAt\": {\n          \"name\": \"usedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"invites_token_unique\": {\n          \"name\": \"invites_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"invites_invitedBy_user_id_fk\": {\n          \"name\": \"invites_invitedBy_user_id_fk\",\n          \"tableFrom\": \"invites\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"listCollaborators\": {\n      \"name\": \"listCollaborators\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedBy\": {\n          \"name\": \"addedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"listCollaborators_listId_idx\": {\n          \"name\": \"listCollaborators_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listCollaborators_userId_idx\": {\n          \"name\": \"listCollaborators_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listCollaborators_listId_userId_unique\": {\n          \"name\": \"listCollaborators_listId_userId_unique\",\n          \"columns\": [\n            \"listId\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"listCollaborators_listId_bookmarkLists_id_fk\": {\n          \"name\": \"listCollaborators_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listCollaborators_userId_user_id_fk\": {\n          \"name\": \"listCollaborators_userId_user_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listCollaborators_addedBy_user_id_fk\": {\n          \"name\": \"listCollaborators_addedBy_user_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"addedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"listInvitations\": {\n      \"name\": \"listInvitations\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"invitedAt\": {\n          \"name\": \"invitedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"invitedEmail\": {\n          \"name\": \"invitedEmail\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"listInvitations_listId_idx\": {\n          \"name\": \"listInvitations_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_userId_idx\": {\n          \"name\": \"listInvitations_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_status_idx\": {\n          \"name\": \"listInvitations_status_idx\",\n          \"columns\": [\n            \"status\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_listId_userId_unique\": {\n          \"name\": \"listInvitations_listId_userId_unique\",\n          \"columns\": [\n            \"listId\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"listInvitations_listId_bookmarkLists_id_fk\": {\n          \"name\": \"listInvitations_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listInvitations_userId_user_id_fk\": {\n          \"name\": \"listInvitations_userId_user_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listInvitations_invitedBy_user_id_fk\": {\n          \"name\": \"listInvitations_invitedBy_user_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"passwordResetToken\": {\n      \"name\": \"passwordResetToken\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"passwordResetToken_token_unique\": {\n          \"name\": \"passwordResetToken_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        },\n        \"passwordResetTokens_userId_idx\": {\n          \"name\": \"passwordResetTokens_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"passwordResetToken_userId_user_id_fk\": {\n          \"name\": \"passwordResetToken_userId_user_id_fk\",\n          \"tableFrom\": \"passwordResetToken\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_bookmarkId_idx\": {\n          \"name\": \"rssFeedImports_rssFeedId_bookmarkId_idx\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"importTags\": {\n          \"name\": \"importTags\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineActions\": {\n      \"name\": \"ruleEngineActions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"ruleId\": {\n          \"name\": \"ruleId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"action\": {\n          \"name\": \"action\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngineActions_userId_idx\": {\n          \"name\": \"ruleEngineActions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"ruleEngineActions_ruleId_idx\": {\n          \"name\": \"ruleEngineActions_ruleId_idx\",\n          \"columns\": [\n            \"ruleId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineActions_userId_user_id_fk\": {\n          \"name\": \"ruleEngineActions_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\": {\n          \"name\": \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"ruleId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_tagId_fk\": {\n          \"name\": \"ruleEngineActions_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_listId_fk\": {\n          \"name\": \"ruleEngineActions_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineRules\": {\n      \"name\": \"ruleEngineRules\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"event\": {\n          \"name\": \"event\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"condition\": {\n          \"name\": \"condition\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngine_userId_idx\": {\n          \"name\": \"ruleEngine_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineRules_userId_user_id_fk\": {\n          \"name\": \"ruleEngineRules_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_tagId_fk\": {\n          \"name\": \"ruleEngineRules_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_listId_fk\": {\n          \"name\": \"ruleEngineRules_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"subscriptions\": {\n      \"name\": \"subscriptions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeCustomerId\": {\n          \"name\": \"stripeCustomerId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeSubscriptionId\": {\n          \"name\": \"stripeSubscriptionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tier\": {\n          \"name\": \"tier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'free'\"\n        },\n        \"priceId\": {\n          \"name\": \"priceId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cancelAtPeriodEnd\": {\n          \"name\": \"cancelAtPeriodEnd\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"startDate\": {\n          \"name\": \"startDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"endDate\": {\n          \"name\": \"endDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"subscriptions_userId_unique\": {\n          \"name\": \"subscriptions_userId_unique\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": true\n        },\n        \"subscriptions_userId_idx\": {\n          \"name\": \"subscriptions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"subscriptions_stripeCustomerId_idx\": {\n          \"name\": \"subscriptions_stripeCustomerId_idx\",\n          \"columns\": [\n            \"stripeCustomerId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"subscriptions_userId_user_id_fk\": {\n          \"name\": \"subscriptions_userId_user_id_fk\",\n          \"tableFrom\": \"subscriptions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_tagId_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkId_idx\",\n          \"columns\": [\n            \"tagId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"salt\": {\n          \"name\": \"salt\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        },\n        \"bookmarkQuota\": {\n          \"name\": \"bookmarkQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"storageQuota\": {\n          \"name\": \"storageQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"browserCrawlingEnabled\": {\n          \"name\": \"browserCrawlingEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkClickAction\": {\n          \"name\": \"bookmarkClickAction\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'open_original_link'\"\n        },\n        \"archiveDisplayBehaviour\": {\n          \"name\": \"archiveDisplayBehaviour\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'show'\"\n        },\n        \"timezone\": {\n          \"name\": \"timezone\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'UTC'\"\n        },\n        \"backupsEnabled\": {\n          \"name\": \"backupsEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"backupsFrequency\": {\n          \"name\": \"backupsFrequency\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'weekly'\"\n        },\n        \"backupsRetentionDays\": {\n          \"name\": \"backupsRetentionDays\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 30\n        },\n        \"readerFontSize\": {\n          \"name\": \"readerFontSize\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"readerLineHeight\": {\n          \"name\": \"readerLineHeight\",\n          \"type\": \"real\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"readerFontFamily\": {\n          \"name\": \"readerFontFamily\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0072_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"83311234-d840-43f7-9e09-a19a401c3ee8\",\n  \"prevId\": \"74441c0b-d9b5-4154-9ef5-71fff60d52fe\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"backups\": {\n      \"name\": \"backups\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkCount\": {\n          \"name\": \"bookmarkCount\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"errorMessage\": {\n          \"name\": \"errorMessage\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"backups_userId_idx\": {\n          \"name\": \"backups_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"backups_createdAt_idx\": {\n          \"name\": \"backups_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"backups_userId_user_id_fk\": {\n          \"name\": \"backups_userId_user_id_fk\",\n          \"tableFrom\": \"backups\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"backups_assetId_assets_id_fk\": {\n          \"name\": \"backups_assetId_assets_id_fk\",\n          \"tableFrom\": \"backups\",\n          \"tableTo\": \"assets\",\n          \"columnsFrom\": [\n            \"assetId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"author\": {\n          \"name\": \"author\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"publisher\": {\n          \"name\": \"publisher\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"datePublished\": {\n          \"name\": \"datePublished\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dateModified\": {\n          \"name\": \"dateModified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"contentAssetId\": {\n          \"name\": \"contentAssetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rssToken\": {\n          \"name\": \"rssToken\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_userId_id_idx\": {\n          \"name\": \"bookmarkLists_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"normalizedName\": {\n          \"name\": \"normalizedName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"generated\": {\n            \"as\": \"(lower(replace(replace(replace(\\\"name\\\", ' ', ''), '-', ''), '_', '')))\",\n            \"type\": \"virtual\"\n          }\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_normalizedName_idx\": {\n          \"name\": \"bookmarkTags_normalizedName_idx\",\n          \"columns\": [\n            \"normalizedName\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"bookmarkTags_userId_id_idx\": {\n          \"name\": \"bookmarkTags_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summarizationStatus\": {\n          \"name\": \"summarizationStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"source\": {\n          \"name\": \"source\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_userId_createdAt_id_idx\": {\n          \"name\": \"bookmarks_userId_createdAt_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"createdAt\",\n            \"id\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_userId_archived_createdAt_id_idx\": {\n          \"name\": \"bookmarks_userId_archived_createdAt_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"archived\",\n            \"createdAt\",\n            \"id\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_userId_favourited_createdAt_id_idx\": {\n          \"name\": \"bookmarks_userId_favourited_createdAt_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"favourited\",\n            \"createdAt\",\n            \"id\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"listMembershipId\": {\n          \"name\": \"listMembershipId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkId_idx\",\n          \"columns\": [\n            \"listId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listMembershipId_listCollaborators_id_fk\": {\n          \"name\": \"bookmarksInLists_listMembershipId_listCollaborators_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"listCollaborators\",\n          \"columnsFrom\": [\n            \"listMembershipId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importSessionBookmarks\": {\n      \"name\": \"importSessionBookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"importSessionId\": {\n          \"name\": \"importSessionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importSessionBookmarks_sessionId_idx\": {\n          \"name\": \"importSessionBookmarks_sessionId_idx\",\n          \"columns\": [\n            \"importSessionId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessionBookmarks_bookmarkId_idx\": {\n          \"name\": \"importSessionBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessionBookmarks_importSessionId_bookmarkId_unique\": {\n          \"name\": \"importSessionBookmarks_importSessionId_bookmarkId_unique\",\n          \"columns\": [\n            \"importSessionId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"importSessionBookmarks_importSessionId_importSessions_id_fk\": {\n          \"name\": \"importSessionBookmarks_importSessionId_importSessions_id_fk\",\n          \"tableFrom\": \"importSessionBookmarks\",\n          \"tableTo\": \"importSessions\",\n          \"columnsFrom\": [\n            \"importSessionId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"importSessionBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"importSessionBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"importSessionBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importSessions\": {\n      \"name\": \"importSessions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rootListId\": {\n          \"name\": \"rootListId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importSessions_userId_idx\": {\n          \"name\": \"importSessions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"importSessions_userId_user_id_fk\": {\n          \"name\": \"importSessions_userId_user_id_fk\",\n          \"tableFrom\": \"importSessions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"importSessions_rootListId_bookmarkLists_id_fk\": {\n          \"name\": \"importSessions_rootListId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"importSessions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"rootListId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invites\": {\n      \"name\": \"invites\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"usedAt\": {\n          \"name\": \"usedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"invites_token_unique\": {\n          \"name\": \"invites_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"invites_invitedBy_user_id_fk\": {\n          \"name\": \"invites_invitedBy_user_id_fk\",\n          \"tableFrom\": \"invites\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"listCollaborators\": {\n      \"name\": \"listCollaborators\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedBy\": {\n          \"name\": \"addedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"listCollaborators_listId_idx\": {\n          \"name\": \"listCollaborators_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listCollaborators_userId_idx\": {\n          \"name\": \"listCollaborators_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listCollaborators_listId_userId_unique\": {\n          \"name\": \"listCollaborators_listId_userId_unique\",\n          \"columns\": [\n            \"listId\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"listCollaborators_listId_bookmarkLists_id_fk\": {\n          \"name\": \"listCollaborators_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listCollaborators_userId_user_id_fk\": {\n          \"name\": \"listCollaborators_userId_user_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listCollaborators_addedBy_user_id_fk\": {\n          \"name\": \"listCollaborators_addedBy_user_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"addedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"listInvitations\": {\n      \"name\": \"listInvitations\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"invitedAt\": {\n          \"name\": \"invitedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"invitedEmail\": {\n          \"name\": \"invitedEmail\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"listInvitations_listId_idx\": {\n          \"name\": \"listInvitations_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_userId_idx\": {\n          \"name\": \"listInvitations_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_status_idx\": {\n          \"name\": \"listInvitations_status_idx\",\n          \"columns\": [\n            \"status\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_listId_userId_unique\": {\n          \"name\": \"listInvitations_listId_userId_unique\",\n          \"columns\": [\n            \"listId\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"listInvitations_listId_bookmarkLists_id_fk\": {\n          \"name\": \"listInvitations_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listInvitations_userId_user_id_fk\": {\n          \"name\": \"listInvitations_userId_user_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listInvitations_invitedBy_user_id_fk\": {\n          \"name\": \"listInvitations_invitedBy_user_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"passwordResetToken\": {\n      \"name\": \"passwordResetToken\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"passwordResetToken_token_unique\": {\n          \"name\": \"passwordResetToken_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        },\n        \"passwordResetTokens_userId_idx\": {\n          \"name\": \"passwordResetTokens_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"passwordResetToken_userId_user_id_fk\": {\n          \"name\": \"passwordResetToken_userId_user_id_fk\",\n          \"tableFrom\": \"passwordResetToken\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_bookmarkId_idx\": {\n          \"name\": \"rssFeedImports_rssFeedId_bookmarkId_idx\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"importTags\": {\n          \"name\": \"importTags\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineActions\": {\n      \"name\": \"ruleEngineActions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"ruleId\": {\n          \"name\": \"ruleId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"action\": {\n          \"name\": \"action\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngineActions_userId_idx\": {\n          \"name\": \"ruleEngineActions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"ruleEngineActions_ruleId_idx\": {\n          \"name\": \"ruleEngineActions_ruleId_idx\",\n          \"columns\": [\n            \"ruleId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineActions_userId_user_id_fk\": {\n          \"name\": \"ruleEngineActions_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\": {\n          \"name\": \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"ruleId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_tagId_fk\": {\n          \"name\": \"ruleEngineActions_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_listId_fk\": {\n          \"name\": \"ruleEngineActions_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineRules\": {\n      \"name\": \"ruleEngineRules\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"event\": {\n          \"name\": \"event\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"condition\": {\n          \"name\": \"condition\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngine_userId_idx\": {\n          \"name\": \"ruleEngine_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineRules_userId_user_id_fk\": {\n          \"name\": \"ruleEngineRules_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_tagId_fk\": {\n          \"name\": \"ruleEngineRules_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_listId_fk\": {\n          \"name\": \"ruleEngineRules_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"subscriptions\": {\n      \"name\": \"subscriptions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeCustomerId\": {\n          \"name\": \"stripeCustomerId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeSubscriptionId\": {\n          \"name\": \"stripeSubscriptionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tier\": {\n          \"name\": \"tier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'free'\"\n        },\n        \"priceId\": {\n          \"name\": \"priceId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cancelAtPeriodEnd\": {\n          \"name\": \"cancelAtPeriodEnd\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"startDate\": {\n          \"name\": \"startDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"endDate\": {\n          \"name\": \"endDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"subscriptions_userId_unique\": {\n          \"name\": \"subscriptions_userId_unique\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": true\n        },\n        \"subscriptions_userId_idx\": {\n          \"name\": \"subscriptions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"subscriptions_stripeCustomerId_idx\": {\n          \"name\": \"subscriptions_stripeCustomerId_idx\",\n          \"columns\": [\n            \"stripeCustomerId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"subscriptions_userId_user_id_fk\": {\n          \"name\": \"subscriptions_userId_user_id_fk\",\n          \"tableFrom\": \"subscriptions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_tagId_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkId_idx\",\n          \"columns\": [\n            \"tagId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"salt\": {\n          \"name\": \"salt\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        },\n        \"bookmarkQuota\": {\n          \"name\": \"bookmarkQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"storageQuota\": {\n          \"name\": \"storageQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"browserCrawlingEnabled\": {\n          \"name\": \"browserCrawlingEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkClickAction\": {\n          \"name\": \"bookmarkClickAction\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'open_original_link'\"\n        },\n        \"archiveDisplayBehaviour\": {\n          \"name\": \"archiveDisplayBehaviour\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'show'\"\n        },\n        \"timezone\": {\n          \"name\": \"timezone\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'UTC'\"\n        },\n        \"backupsEnabled\": {\n          \"name\": \"backupsEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"backupsFrequency\": {\n          \"name\": \"backupsFrequency\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'weekly'\"\n        },\n        \"backupsRetentionDays\": {\n          \"name\": \"backupsRetentionDays\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 30\n        },\n        \"readerFontSize\": {\n          \"name\": \"readerFontSize\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"readerLineHeight\": {\n          \"name\": \"readerLineHeight\",\n          \"type\": \"real\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"readerFontFamily\": {\n          \"name\": \"readerFontFamily\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"autoTaggingEnabled\": {\n          \"name\": \"autoTaggingEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"autoSummarizationEnabled\": {\n          \"name\": \"autoSummarizationEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0073_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"52bf703a-60db-4de2-8f8e-9d4d941517cc\",\n  \"prevId\": \"83311234-d840-43f7-9e09-a19a401c3ee8\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"backups\": {\n      \"name\": \"backups\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkCount\": {\n          \"name\": \"bookmarkCount\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"errorMessage\": {\n          \"name\": \"errorMessage\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"backups_userId_idx\": {\n          \"name\": \"backups_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"backups_createdAt_idx\": {\n          \"name\": \"backups_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"backups_userId_user_id_fk\": {\n          \"name\": \"backups_userId_user_id_fk\",\n          \"tableFrom\": \"backups\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"backups_assetId_assets_id_fk\": {\n          \"name\": \"backups_assetId_assets_id_fk\",\n          \"tableFrom\": \"backups\",\n          \"tableTo\": \"assets\",\n          \"columnsFrom\": [\n            \"assetId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"author\": {\n          \"name\": \"author\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"publisher\": {\n          \"name\": \"publisher\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"datePublished\": {\n          \"name\": \"datePublished\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dateModified\": {\n          \"name\": \"dateModified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"contentAssetId\": {\n          \"name\": \"contentAssetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rssToken\": {\n          \"name\": \"rssToken\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_userId_id_idx\": {\n          \"name\": \"bookmarkLists_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"normalizedName\": {\n          \"name\": \"normalizedName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"generated\": {\n            \"as\": \"(lower(replace(replace(replace(\\\"name\\\", ' ', ''), '-', ''), '_', '')))\",\n            \"type\": \"virtual\"\n          }\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_normalizedName_idx\": {\n          \"name\": \"bookmarkTags_normalizedName_idx\",\n          \"columns\": [\n            \"normalizedName\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"bookmarkTags_userId_id_idx\": {\n          \"name\": \"bookmarkTags_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summarizationStatus\": {\n          \"name\": \"summarizationStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"source\": {\n          \"name\": \"source\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_userId_createdAt_id_idx\": {\n          \"name\": \"bookmarks_userId_createdAt_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"createdAt\",\n            \"id\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_userId_archived_createdAt_id_idx\": {\n          \"name\": \"bookmarks_userId_archived_createdAt_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"archived\",\n            \"createdAt\",\n            \"id\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_userId_favourited_createdAt_id_idx\": {\n          \"name\": \"bookmarks_userId_favourited_createdAt_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"favourited\",\n            \"createdAt\",\n            \"id\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"listMembershipId\": {\n          \"name\": \"listMembershipId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkId_idx\",\n          \"columns\": [\n            \"listId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listMembershipId_listCollaborators_id_fk\": {\n          \"name\": \"bookmarksInLists_listMembershipId_listCollaborators_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"listCollaborators\",\n          \"columnsFrom\": [\n            \"listMembershipId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importSessionBookmarks\": {\n      \"name\": \"importSessionBookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"importSessionId\": {\n          \"name\": \"importSessionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importSessionBookmarks_sessionId_idx\": {\n          \"name\": \"importSessionBookmarks_sessionId_idx\",\n          \"columns\": [\n            \"importSessionId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessionBookmarks_bookmarkId_idx\": {\n          \"name\": \"importSessionBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessionBookmarks_importSessionId_bookmarkId_unique\": {\n          \"name\": \"importSessionBookmarks_importSessionId_bookmarkId_unique\",\n          \"columns\": [\n            \"importSessionId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"importSessionBookmarks_importSessionId_importSessions_id_fk\": {\n          \"name\": \"importSessionBookmarks_importSessionId_importSessions_id_fk\",\n          \"tableFrom\": \"importSessionBookmarks\",\n          \"tableTo\": \"importSessions\",\n          \"columnsFrom\": [\n            \"importSessionId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"importSessionBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"importSessionBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"importSessionBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importSessions\": {\n      \"name\": \"importSessions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rootListId\": {\n          \"name\": \"rootListId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importSessions_userId_idx\": {\n          \"name\": \"importSessions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"importSessions_userId_user_id_fk\": {\n          \"name\": \"importSessions_userId_user_id_fk\",\n          \"tableFrom\": \"importSessions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"importSessions_rootListId_bookmarkLists_id_fk\": {\n          \"name\": \"importSessions_rootListId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"importSessions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"rootListId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invites\": {\n      \"name\": \"invites\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"usedAt\": {\n          \"name\": \"usedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"invites_token_unique\": {\n          \"name\": \"invites_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"invites_invitedBy_user_id_fk\": {\n          \"name\": \"invites_invitedBy_user_id_fk\",\n          \"tableFrom\": \"invites\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"listCollaborators\": {\n      \"name\": \"listCollaborators\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedBy\": {\n          \"name\": \"addedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"listCollaborators_listId_idx\": {\n          \"name\": \"listCollaborators_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listCollaborators_userId_idx\": {\n          \"name\": \"listCollaborators_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listCollaborators_listId_userId_unique\": {\n          \"name\": \"listCollaborators_listId_userId_unique\",\n          \"columns\": [\n            \"listId\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"listCollaborators_listId_bookmarkLists_id_fk\": {\n          \"name\": \"listCollaborators_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listCollaborators_userId_user_id_fk\": {\n          \"name\": \"listCollaborators_userId_user_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listCollaborators_addedBy_user_id_fk\": {\n          \"name\": \"listCollaborators_addedBy_user_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"addedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"listInvitations\": {\n      \"name\": \"listInvitations\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"invitedAt\": {\n          \"name\": \"invitedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"invitedEmail\": {\n          \"name\": \"invitedEmail\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"listInvitations_listId_idx\": {\n          \"name\": \"listInvitations_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_userId_idx\": {\n          \"name\": \"listInvitations_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_status_idx\": {\n          \"name\": \"listInvitations_status_idx\",\n          \"columns\": [\n            \"status\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_listId_userId_unique\": {\n          \"name\": \"listInvitations_listId_userId_unique\",\n          \"columns\": [\n            \"listId\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"listInvitations_listId_bookmarkLists_id_fk\": {\n          \"name\": \"listInvitations_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listInvitations_userId_user_id_fk\": {\n          \"name\": \"listInvitations_userId_user_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listInvitations_invitedBy_user_id_fk\": {\n          \"name\": \"listInvitations_invitedBy_user_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"passwordResetToken\": {\n      \"name\": \"passwordResetToken\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"passwordResetToken_token_unique\": {\n          \"name\": \"passwordResetToken_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        },\n        \"passwordResetTokens_userId_idx\": {\n          \"name\": \"passwordResetTokens_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"passwordResetToken_userId_user_id_fk\": {\n          \"name\": \"passwordResetToken_userId_user_id_fk\",\n          \"tableFrom\": \"passwordResetToken\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_bookmarkId_idx\": {\n          \"name\": \"rssFeedImports_rssFeedId_bookmarkId_idx\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"importTags\": {\n          \"name\": \"importTags\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineActions\": {\n      \"name\": \"ruleEngineActions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"ruleId\": {\n          \"name\": \"ruleId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"action\": {\n          \"name\": \"action\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngineActions_userId_idx\": {\n          \"name\": \"ruleEngineActions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"ruleEngineActions_ruleId_idx\": {\n          \"name\": \"ruleEngineActions_ruleId_idx\",\n          \"columns\": [\n            \"ruleId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineActions_userId_user_id_fk\": {\n          \"name\": \"ruleEngineActions_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\": {\n          \"name\": \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"ruleId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_tagId_fk\": {\n          \"name\": \"ruleEngineActions_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_listId_fk\": {\n          \"name\": \"ruleEngineActions_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineRules\": {\n      \"name\": \"ruleEngineRules\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"event\": {\n          \"name\": \"event\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"condition\": {\n          \"name\": \"condition\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngine_userId_idx\": {\n          \"name\": \"ruleEngine_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineRules_userId_user_id_fk\": {\n          \"name\": \"ruleEngineRules_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_tagId_fk\": {\n          \"name\": \"ruleEngineRules_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_listId_fk\": {\n          \"name\": \"ruleEngineRules_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"subscriptions\": {\n      \"name\": \"subscriptions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeCustomerId\": {\n          \"name\": \"stripeCustomerId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeSubscriptionId\": {\n          \"name\": \"stripeSubscriptionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tier\": {\n          \"name\": \"tier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'free'\"\n        },\n        \"priceId\": {\n          \"name\": \"priceId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cancelAtPeriodEnd\": {\n          \"name\": \"cancelAtPeriodEnd\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"startDate\": {\n          \"name\": \"startDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"endDate\": {\n          \"name\": \"endDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"subscriptions_userId_unique\": {\n          \"name\": \"subscriptions_userId_unique\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": true\n        },\n        \"subscriptions_userId_idx\": {\n          \"name\": \"subscriptions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"subscriptions_stripeCustomerId_idx\": {\n          \"name\": \"subscriptions_stripeCustomerId_idx\",\n          \"columns\": [\n            \"stripeCustomerId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"subscriptions_userId_user_id_fk\": {\n          \"name\": \"subscriptions_userId_user_id_fk\",\n          \"tableFrom\": \"subscriptions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_tagId_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkId_idx\",\n          \"columns\": [\n            \"tagId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"salt\": {\n          \"name\": \"salt\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        },\n        \"bookmarkQuota\": {\n          \"name\": \"bookmarkQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"storageQuota\": {\n          \"name\": \"storageQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"browserCrawlingEnabled\": {\n          \"name\": \"browserCrawlingEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkClickAction\": {\n          \"name\": \"bookmarkClickAction\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'open_original_link'\"\n        },\n        \"archiveDisplayBehaviour\": {\n          \"name\": \"archiveDisplayBehaviour\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'show'\"\n        },\n        \"timezone\": {\n          \"name\": \"timezone\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'UTC'\"\n        },\n        \"backupsEnabled\": {\n          \"name\": \"backupsEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"backupsFrequency\": {\n          \"name\": \"backupsFrequency\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'weekly'\"\n        },\n        \"backupsRetentionDays\": {\n          \"name\": \"backupsRetentionDays\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 30\n        },\n        \"readerFontSize\": {\n          \"name\": \"readerFontSize\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"readerLineHeight\": {\n          \"name\": \"readerLineHeight\",\n          \"type\": \"real\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"readerFontFamily\": {\n          \"name\": \"readerFontFamily\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"autoTaggingEnabled\": {\n          \"name\": \"autoTaggingEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"autoSummarizationEnabled\": {\n          \"name\": \"autoSummarizationEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagStyle\": {\n          \"name\": \"tagStyle\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'lowercase-hyphens'\"\n        },\n        \"inferredTagLang\": {\n          \"name\": \"inferredTagLang\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0074_snapshot.json",
    "content": "{\n  \"id\": \"1c1e77d2-bc04-43e5-b7fc-f728b79bf990\",\n  \"prevId\": \"52bf703a-60db-4de2-8f8e-9d4d941517cc\",\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"backups\": {\n      \"name\": \"backups\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkCount\": {\n          \"name\": \"bookmarkCount\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"errorMessage\": {\n          \"name\": \"errorMessage\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"backups_userId_idx\": {\n          \"name\": \"backups_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"backups_createdAt_idx\": {\n          \"name\": \"backups_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"backups_userId_user_id_fk\": {\n          \"name\": \"backups_userId_user_id_fk\",\n          \"tableFrom\": \"backups\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"backups_assetId_assets_id_fk\": {\n          \"name\": \"backups_assetId_assets_id_fk\",\n          \"tableFrom\": \"backups\",\n          \"columnsFrom\": [\n            \"assetId\"\n          ],\n          \"tableTo\": \"assets\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"author\": {\n          \"name\": \"author\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"publisher\": {\n          \"name\": \"publisher\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"datePublished\": {\n          \"name\": \"datePublished\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dateModified\": {\n          \"name\": \"dateModified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"contentAssetId\": {\n          \"name\": \"contentAssetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rssToken\": {\n          \"name\": \"rssToken\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_userId_id_idx\": {\n          \"name\": \"bookmarkLists_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"set null\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"normalizedName\": {\n          \"name\": \"normalizedName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"generated\": {\n            \"type\": \"virtual\",\n            \"as\": \"(lower(replace(replace(replace(\\\"name\\\", ' ', ''), '-', ''), '_', '')))\"\n          }\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_normalizedName_idx\": {\n          \"name\": \"bookmarkTags_normalizedName_idx\",\n          \"columns\": [\n            \"normalizedName\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"bookmarkTags_userId_id_idx\": {\n          \"name\": \"bookmarkTags_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summarizationStatus\": {\n          \"name\": \"summarizationStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"source\": {\n          \"name\": \"source\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_userId_createdAt_id_idx\": {\n          \"name\": \"bookmarks_userId_createdAt_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"createdAt\",\n            \"id\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_userId_archived_createdAt_id_idx\": {\n          \"name\": \"bookmarks_userId_archived_createdAt_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"archived\",\n            \"createdAt\",\n            \"id\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_userId_favourited_createdAt_id_idx\": {\n          \"name\": \"bookmarks_userId_favourited_createdAt_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"favourited\",\n            \"createdAt\",\n            \"id\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"listMembershipId\": {\n          \"name\": \"listMembershipId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkId_idx\",\n          \"columns\": [\n            \"listId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"bookmarksInLists_listMembershipId_listCollaborators_id_fk\": {\n          \"name\": \"bookmarksInLists_listMembershipId_listCollaborators_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"columnsFrom\": [\n            \"listMembershipId\"\n          ],\n          \"tableTo\": \"listCollaborators\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importSessionBookmarks\": {\n      \"name\": \"importSessionBookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"importSessionId\": {\n          \"name\": \"importSessionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importSessionBookmarks_sessionId_idx\": {\n          \"name\": \"importSessionBookmarks_sessionId_idx\",\n          \"columns\": [\n            \"importSessionId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessionBookmarks_bookmarkId_idx\": {\n          \"name\": \"importSessionBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessionBookmarks_importSessionId_bookmarkId_unique\": {\n          \"name\": \"importSessionBookmarks_importSessionId_bookmarkId_unique\",\n          \"columns\": [\n            \"importSessionId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"importSessionBookmarks_importSessionId_importSessions_id_fk\": {\n          \"name\": \"importSessionBookmarks_importSessionId_importSessions_id_fk\",\n          \"tableFrom\": \"importSessionBookmarks\",\n          \"columnsFrom\": [\n            \"importSessionId\"\n          ],\n          \"tableTo\": \"importSessions\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"importSessionBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"importSessionBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"importSessionBookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importSessions\": {\n      \"name\": \"importSessions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rootListId\": {\n          \"name\": \"rootListId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importSessions_userId_idx\": {\n          \"name\": \"importSessions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"importSessions_userId_user_id_fk\": {\n          \"name\": \"importSessions_userId_user_id_fk\",\n          \"tableFrom\": \"importSessions\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"importSessions_rootListId_bookmarkLists_id_fk\": {\n          \"name\": \"importSessions_rootListId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"importSessions\",\n          \"columnsFrom\": [\n            \"rootListId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"set null\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invites\": {\n      \"name\": \"invites\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"usedAt\": {\n          \"name\": \"usedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"invites_token_unique\": {\n          \"name\": \"invites_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"invites_invitedBy_user_id_fk\": {\n          \"name\": \"invites_invitedBy_user_id_fk\",\n          \"tableFrom\": \"invites\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"listCollaborators\": {\n      \"name\": \"listCollaborators\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedBy\": {\n          \"name\": \"addedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"listCollaborators_listId_idx\": {\n          \"name\": \"listCollaborators_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listCollaborators_userId_idx\": {\n          \"name\": \"listCollaborators_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listCollaborators_listId_userId_unique\": {\n          \"name\": \"listCollaborators_listId_userId_unique\",\n          \"columns\": [\n            \"listId\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"listCollaborators_listId_bookmarkLists_id_fk\": {\n          \"name\": \"listCollaborators_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"listCollaborators_userId_user_id_fk\": {\n          \"name\": \"listCollaborators_userId_user_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"listCollaborators_addedBy_user_id_fk\": {\n          \"name\": \"listCollaborators_addedBy_user_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"columnsFrom\": [\n            \"addedBy\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"set null\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"listInvitations\": {\n      \"name\": \"listInvitations\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"invitedAt\": {\n          \"name\": \"invitedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"invitedEmail\": {\n          \"name\": \"invitedEmail\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"listInvitations_listId_idx\": {\n          \"name\": \"listInvitations_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_userId_idx\": {\n          \"name\": \"listInvitations_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_status_idx\": {\n          \"name\": \"listInvitations_status_idx\",\n          \"columns\": [\n            \"status\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_listId_userId_unique\": {\n          \"name\": \"listInvitations_listId_userId_unique\",\n          \"columns\": [\n            \"listId\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"listInvitations_listId_bookmarkLists_id_fk\": {\n          \"name\": \"listInvitations_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"listInvitations_userId_user_id_fk\": {\n          \"name\": \"listInvitations_userId_user_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"listInvitations_invitedBy_user_id_fk\": {\n          \"name\": \"listInvitations_invitedBy_user_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"set null\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"passwordResetToken\": {\n      \"name\": \"passwordResetToken\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"passwordResetToken_token_unique\": {\n          \"name\": \"passwordResetToken_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        },\n        \"passwordResetTokens_userId_idx\": {\n          \"name\": \"passwordResetTokens_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"passwordResetToken_userId_user_id_fk\": {\n          \"name\": \"passwordResetToken_userId_user_id_fk\",\n          \"tableFrom\": \"passwordResetToken\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_bookmarkId_idx\": {\n          \"name\": \"rssFeedImports_rssFeedId_bookmarkId_idx\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"tableTo\": \"rssFeeds\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"set null\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"importTags\": {\n          \"name\": \"importTags\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineActions\": {\n      \"name\": \"ruleEngineActions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"ruleId\": {\n          \"name\": \"ruleId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"action\": {\n          \"name\": \"action\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngineActions_userId_idx\": {\n          \"name\": \"ruleEngineActions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"ruleEngineActions_ruleId_idx\": {\n          \"name\": \"ruleEngineActions_ruleId_idx\",\n          \"columns\": [\n            \"ruleId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineActions_userId_user_id_fk\": {\n          \"name\": \"ruleEngineActions_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\": {\n          \"name\": \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"columnsFrom\": [\n            \"ruleId\"\n          ],\n          \"tableTo\": \"ruleEngineRules\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"ruleEngineActions_userId_tagId_fk\": {\n          \"name\": \"ruleEngineActions_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"ruleEngineActions_userId_listId_fk\": {\n          \"name\": \"ruleEngineActions_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineRules\": {\n      \"name\": \"ruleEngineRules\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"event\": {\n          \"name\": \"event\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"condition\": {\n          \"name\": \"condition\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngine_userId_idx\": {\n          \"name\": \"ruleEngine_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineRules_userId_user_id_fk\": {\n          \"name\": \"ruleEngineRules_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"ruleEngineRules_userId_tagId_fk\": {\n          \"name\": \"ruleEngineRules_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"ruleEngineRules_userId_listId_fk\": {\n          \"name\": \"ruleEngineRules_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"subscriptions\": {\n      \"name\": \"subscriptions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeCustomerId\": {\n          \"name\": \"stripeCustomerId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeSubscriptionId\": {\n          \"name\": \"stripeSubscriptionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tier\": {\n          \"name\": \"tier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'free'\"\n        },\n        \"priceId\": {\n          \"name\": \"priceId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cancelAtPeriodEnd\": {\n          \"name\": \"cancelAtPeriodEnd\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"startDate\": {\n          \"name\": \"startDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"endDate\": {\n          \"name\": \"endDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"subscriptions_userId_unique\": {\n          \"name\": \"subscriptions_userId_unique\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": true\n        },\n        \"subscriptions_userId_idx\": {\n          \"name\": \"subscriptions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"subscriptions_stripeCustomerId_idx\": {\n          \"name\": \"subscriptions_stripeCustomerId_idx\",\n          \"columns\": [\n            \"stripeCustomerId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"subscriptions_userId_user_id_fk\": {\n          \"name\": \"subscriptions_userId_user_id_fk\",\n          \"tableFrom\": \"subscriptions\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_tagId_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkId_idx\",\n          \"columns\": [\n            \"tagId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"tableTo\": \"bookmarks\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"salt\": {\n          \"name\": \"salt\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        },\n        \"bookmarkQuota\": {\n          \"name\": \"bookmarkQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"storageQuota\": {\n          \"name\": \"storageQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"browserCrawlingEnabled\": {\n          \"name\": \"browserCrawlingEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkClickAction\": {\n          \"name\": \"bookmarkClickAction\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'open_original_link'\"\n        },\n        \"archiveDisplayBehaviour\": {\n          \"name\": \"archiveDisplayBehaviour\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'show'\"\n        },\n        \"timezone\": {\n          \"name\": \"timezone\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'UTC'\"\n        },\n        \"backupsEnabled\": {\n          \"name\": \"backupsEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"backupsFrequency\": {\n          \"name\": \"backupsFrequency\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'weekly'\"\n        },\n        \"backupsRetentionDays\": {\n          \"name\": \"backupsRetentionDays\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 30\n        },\n        \"readerFontSize\": {\n          \"name\": \"readerFontSize\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"readerLineHeight\": {\n          \"name\": \"readerLineHeight\",\n          \"type\": \"real\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"readerFontFamily\": {\n          \"name\": \"readerFontFamily\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"autoTaggingEnabled\": {\n          \"name\": \"autoTaggingEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"autoSummarizationEnabled\": {\n          \"name\": \"autoSummarizationEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagStyle\": {\n          \"name\": \"tagStyle\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'lowercase-hyphens'\"\n        },\n        \"inferredTagLang\": {\n          \"name\": \"inferredTagLang\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"tableTo\": \"user\",\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onUpdate\": \"no action\",\n          \"onDelete\": \"cascade\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"columns\": {},\n    \"schemas\": {},\n    \"tables\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0075_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"ee577510-4b62-4675-be2c-777446e86a6d\",\n  \"prevId\": \"1c1e77d2-bc04-43e5-b7fc-f728b79bf990\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"backups\": {\n      \"name\": \"backups\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkCount\": {\n          \"name\": \"bookmarkCount\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"errorMessage\": {\n          \"name\": \"errorMessage\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"backups_userId_idx\": {\n          \"name\": \"backups_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"backups_createdAt_idx\": {\n          \"name\": \"backups_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"backups_userId_user_id_fk\": {\n          \"name\": \"backups_userId_user_id_fk\",\n          \"tableFrom\": \"backups\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"backups_assetId_assets_id_fk\": {\n          \"name\": \"backups_assetId_assets_id_fk\",\n          \"tableFrom\": \"backups\",\n          \"tableTo\": \"assets\",\n          \"columnsFrom\": [\n            \"assetId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"author\": {\n          \"name\": \"author\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"publisher\": {\n          \"name\": \"publisher\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"datePublished\": {\n          \"name\": \"datePublished\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dateModified\": {\n          \"name\": \"dateModified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"contentAssetId\": {\n          \"name\": \"contentAssetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rssToken\": {\n          \"name\": \"rssToken\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_userId_id_idx\": {\n          \"name\": \"bookmarkLists_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"normalizedName\": {\n          \"name\": \"normalizedName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"generated\": {\n            \"as\": \"(lower(replace(replace(replace(\\\"name\\\", ' ', ''), '-', ''), '_', '')))\",\n            \"type\": \"virtual\"\n          }\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_normalizedName_idx\": {\n          \"name\": \"bookmarkTags_normalizedName_idx\",\n          \"columns\": [\n            \"normalizedName\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"bookmarkTags_userId_id_idx\": {\n          \"name\": \"bookmarkTags_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summarizationStatus\": {\n          \"name\": \"summarizationStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"source\": {\n          \"name\": \"source\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_userId_createdAt_id_idx\": {\n          \"name\": \"bookmarks_userId_createdAt_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"createdAt\",\n            \"id\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_userId_archived_createdAt_id_idx\": {\n          \"name\": \"bookmarks_userId_archived_createdAt_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"archived\",\n            \"createdAt\",\n            \"id\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_userId_favourited_createdAt_id_idx\": {\n          \"name\": \"bookmarks_userId_favourited_createdAt_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"favourited\",\n            \"createdAt\",\n            \"id\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"listMembershipId\": {\n          \"name\": \"listMembershipId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkId_idx\",\n          \"columns\": [\n            \"listId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listMembershipId_listCollaborators_id_fk\": {\n          \"name\": \"bookmarksInLists_listMembershipId_listCollaborators_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"listCollaborators\",\n          \"columnsFrom\": [\n            \"listMembershipId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importSessionBookmarks\": {\n      \"name\": \"importSessionBookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"importSessionId\": {\n          \"name\": \"importSessionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importSessionBookmarks_sessionId_idx\": {\n          \"name\": \"importSessionBookmarks_sessionId_idx\",\n          \"columns\": [\n            \"importSessionId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessionBookmarks_bookmarkId_idx\": {\n          \"name\": \"importSessionBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessionBookmarks_importSessionId_bookmarkId_unique\": {\n          \"name\": \"importSessionBookmarks_importSessionId_bookmarkId_unique\",\n          \"columns\": [\n            \"importSessionId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"importSessionBookmarks_importSessionId_importSessions_id_fk\": {\n          \"name\": \"importSessionBookmarks_importSessionId_importSessions_id_fk\",\n          \"tableFrom\": \"importSessionBookmarks\",\n          \"tableTo\": \"importSessions\",\n          \"columnsFrom\": [\n            \"importSessionId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"importSessionBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"importSessionBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"importSessionBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importSessions\": {\n      \"name\": \"importSessions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rootListId\": {\n          \"name\": \"rootListId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importSessions_userId_idx\": {\n          \"name\": \"importSessions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"importSessions_userId_user_id_fk\": {\n          \"name\": \"importSessions_userId_user_id_fk\",\n          \"tableFrom\": \"importSessions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"importSessions_rootListId_bookmarkLists_id_fk\": {\n          \"name\": \"importSessions_rootListId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"importSessions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"rootListId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invites\": {\n      \"name\": \"invites\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"usedAt\": {\n          \"name\": \"usedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"invites_token_unique\": {\n          \"name\": \"invites_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"invites_invitedBy_user_id_fk\": {\n          \"name\": \"invites_invitedBy_user_id_fk\",\n          \"tableFrom\": \"invites\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"listCollaborators\": {\n      \"name\": \"listCollaborators\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedBy\": {\n          \"name\": \"addedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"listCollaborators_listId_idx\": {\n          \"name\": \"listCollaborators_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listCollaborators_userId_idx\": {\n          \"name\": \"listCollaborators_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listCollaborators_listId_userId_unique\": {\n          \"name\": \"listCollaborators_listId_userId_unique\",\n          \"columns\": [\n            \"listId\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"listCollaborators_listId_bookmarkLists_id_fk\": {\n          \"name\": \"listCollaborators_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listCollaborators_userId_user_id_fk\": {\n          \"name\": \"listCollaborators_userId_user_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listCollaborators_addedBy_user_id_fk\": {\n          \"name\": \"listCollaborators_addedBy_user_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"addedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"listInvitations\": {\n      \"name\": \"listInvitations\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"invitedAt\": {\n          \"name\": \"invitedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"invitedEmail\": {\n          \"name\": \"invitedEmail\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"listInvitations_listId_idx\": {\n          \"name\": \"listInvitations_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_userId_idx\": {\n          \"name\": \"listInvitations_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_status_idx\": {\n          \"name\": \"listInvitations_status_idx\",\n          \"columns\": [\n            \"status\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_listId_userId_unique\": {\n          \"name\": \"listInvitations_listId_userId_unique\",\n          \"columns\": [\n            \"listId\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"listInvitations_listId_bookmarkLists_id_fk\": {\n          \"name\": \"listInvitations_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listInvitations_userId_user_id_fk\": {\n          \"name\": \"listInvitations_userId_user_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listInvitations_invitedBy_user_id_fk\": {\n          \"name\": \"listInvitations_invitedBy_user_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"passwordResetToken\": {\n      \"name\": \"passwordResetToken\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"passwordResetToken_token_unique\": {\n          \"name\": \"passwordResetToken_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        },\n        \"passwordResetTokens_userId_idx\": {\n          \"name\": \"passwordResetTokens_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"passwordResetToken_userId_user_id_fk\": {\n          \"name\": \"passwordResetToken_userId_user_id_fk\",\n          \"tableFrom\": \"passwordResetToken\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_bookmarkId_idx\": {\n          \"name\": \"rssFeedImports_rssFeedId_bookmarkId_idx\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"importTags\": {\n          \"name\": \"importTags\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineActions\": {\n      \"name\": \"ruleEngineActions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"ruleId\": {\n          \"name\": \"ruleId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"action\": {\n          \"name\": \"action\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngineActions_userId_idx\": {\n          \"name\": \"ruleEngineActions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"ruleEngineActions_ruleId_idx\": {\n          \"name\": \"ruleEngineActions_ruleId_idx\",\n          \"columns\": [\n            \"ruleId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineActions_userId_user_id_fk\": {\n          \"name\": \"ruleEngineActions_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\": {\n          \"name\": \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"ruleId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_tagId_fk\": {\n          \"name\": \"ruleEngineActions_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_listId_fk\": {\n          \"name\": \"ruleEngineActions_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineRules\": {\n      \"name\": \"ruleEngineRules\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"event\": {\n          \"name\": \"event\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"condition\": {\n          \"name\": \"condition\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngine_userId_idx\": {\n          \"name\": \"ruleEngine_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineRules_userId_user_id_fk\": {\n          \"name\": \"ruleEngineRules_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_tagId_fk\": {\n          \"name\": \"ruleEngineRules_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_listId_fk\": {\n          \"name\": \"ruleEngineRules_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"subscriptions\": {\n      \"name\": \"subscriptions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeCustomerId\": {\n          \"name\": \"stripeCustomerId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeSubscriptionId\": {\n          \"name\": \"stripeSubscriptionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tier\": {\n          \"name\": \"tier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'free'\"\n        },\n        \"priceId\": {\n          \"name\": \"priceId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cancelAtPeriodEnd\": {\n          \"name\": \"cancelAtPeriodEnd\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"startDate\": {\n          \"name\": \"startDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"endDate\": {\n          \"name\": \"endDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"subscriptions_userId_unique\": {\n          \"name\": \"subscriptions_userId_unique\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": true\n        },\n        \"subscriptions_userId_idx\": {\n          \"name\": \"subscriptions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"subscriptions_stripeCustomerId_idx\": {\n          \"name\": \"subscriptions_stripeCustomerId_idx\",\n          \"columns\": [\n            \"stripeCustomerId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"subscriptions_userId_user_id_fk\": {\n          \"name\": \"subscriptions_userId_user_id_fk\",\n          \"tableFrom\": \"subscriptions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_tagId_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkId_idx\",\n          \"columns\": [\n            \"tagId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"salt\": {\n          \"name\": \"salt\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        },\n        \"bookmarkQuota\": {\n          \"name\": \"bookmarkQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"storageQuota\": {\n          \"name\": \"storageQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"browserCrawlingEnabled\": {\n          \"name\": \"browserCrawlingEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkClickAction\": {\n          \"name\": \"bookmarkClickAction\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'open_original_link'\"\n        },\n        \"archiveDisplayBehaviour\": {\n          \"name\": \"archiveDisplayBehaviour\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'show'\"\n        },\n        \"timezone\": {\n          \"name\": \"timezone\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'UTC'\"\n        },\n        \"backupsEnabled\": {\n          \"name\": \"backupsEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"backupsFrequency\": {\n          \"name\": \"backupsFrequency\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'weekly'\"\n        },\n        \"backupsRetentionDays\": {\n          \"name\": \"backupsRetentionDays\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 30\n        },\n        \"readerFontSize\": {\n          \"name\": \"readerFontSize\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"readerLineHeight\": {\n          \"name\": \"readerLineHeight\",\n          \"type\": \"real\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"readerFontFamily\": {\n          \"name\": \"readerFontFamily\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"autoTaggingEnabled\": {\n          \"name\": \"autoTaggingEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"autoSummarizationEnabled\": {\n          \"name\": \"autoSummarizationEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagStyle\": {\n          \"name\": \"tagStyle\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'titlecase-spaces'\"\n        },\n        \"inferredTagLang\": {\n          \"name\": \"inferredTagLang\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0076_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"609c4402-c36e-4efc-9fd0-4f9038f8a164\",\n  \"prevId\": \"ee577510-4b62-4675-be2c-777446e86a6d\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastUsedAt\": {\n          \"name\": \"lastUsedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"backups\": {\n      \"name\": \"backups\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkCount\": {\n          \"name\": \"bookmarkCount\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"errorMessage\": {\n          \"name\": \"errorMessage\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"backups_userId_idx\": {\n          \"name\": \"backups_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"backups_createdAt_idx\": {\n          \"name\": \"backups_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"backups_userId_user_id_fk\": {\n          \"name\": \"backups_userId_user_id_fk\",\n          \"tableFrom\": \"backups\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"backups_assetId_assets_id_fk\": {\n          \"name\": \"backups_assetId_assets_id_fk\",\n          \"tableFrom\": \"backups\",\n          \"tableTo\": \"assets\",\n          \"columnsFrom\": [\n            \"assetId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"author\": {\n          \"name\": \"author\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"publisher\": {\n          \"name\": \"publisher\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"datePublished\": {\n          \"name\": \"datePublished\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dateModified\": {\n          \"name\": \"dateModified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"contentAssetId\": {\n          \"name\": \"contentAssetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rssToken\": {\n          \"name\": \"rssToken\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_userId_id_idx\": {\n          \"name\": \"bookmarkLists_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"normalizedName\": {\n          \"name\": \"normalizedName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"generated\": {\n            \"as\": \"(lower(replace(replace(replace(\\\"name\\\", ' ', ''), '-', ''), '_', '')))\",\n            \"type\": \"virtual\"\n          }\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_normalizedName_idx\": {\n          \"name\": \"bookmarkTags_normalizedName_idx\",\n          \"columns\": [\n            \"normalizedName\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"bookmarkTags_userId_id_idx\": {\n          \"name\": \"bookmarkTags_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summarizationStatus\": {\n          \"name\": \"summarizationStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"source\": {\n          \"name\": \"source\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_userId_createdAt_id_idx\": {\n          \"name\": \"bookmarks_userId_createdAt_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"createdAt\",\n            \"id\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_userId_archived_createdAt_id_idx\": {\n          \"name\": \"bookmarks_userId_archived_createdAt_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"archived\",\n            \"createdAt\",\n            \"id\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_userId_favourited_createdAt_id_idx\": {\n          \"name\": \"bookmarks_userId_favourited_createdAt_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"favourited\",\n            \"createdAt\",\n            \"id\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"listMembershipId\": {\n          \"name\": \"listMembershipId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkId_idx\",\n          \"columns\": [\n            \"listId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listMembershipId_listCollaborators_id_fk\": {\n          \"name\": \"bookmarksInLists_listMembershipId_listCollaborators_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"listCollaborators\",\n          \"columnsFrom\": [\n            \"listMembershipId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importSessionBookmarks\": {\n      \"name\": \"importSessionBookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"importSessionId\": {\n          \"name\": \"importSessionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importSessionBookmarks_sessionId_idx\": {\n          \"name\": \"importSessionBookmarks_sessionId_idx\",\n          \"columns\": [\n            \"importSessionId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessionBookmarks_bookmarkId_idx\": {\n          \"name\": \"importSessionBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessionBookmarks_importSessionId_bookmarkId_unique\": {\n          \"name\": \"importSessionBookmarks_importSessionId_bookmarkId_unique\",\n          \"columns\": [\n            \"importSessionId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"importSessionBookmarks_importSessionId_importSessions_id_fk\": {\n          \"name\": \"importSessionBookmarks_importSessionId_importSessions_id_fk\",\n          \"tableFrom\": \"importSessionBookmarks\",\n          \"tableTo\": \"importSessions\",\n          \"columnsFrom\": [\n            \"importSessionId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"importSessionBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"importSessionBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"importSessionBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importSessions\": {\n      \"name\": \"importSessions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rootListId\": {\n          \"name\": \"rootListId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importSessions_userId_idx\": {\n          \"name\": \"importSessions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"importSessions_userId_user_id_fk\": {\n          \"name\": \"importSessions_userId_user_id_fk\",\n          \"tableFrom\": \"importSessions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"importSessions_rootListId_bookmarkLists_id_fk\": {\n          \"name\": \"importSessions_rootListId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"importSessions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"rootListId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invites\": {\n      \"name\": \"invites\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"usedAt\": {\n          \"name\": \"usedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"invites_token_unique\": {\n          \"name\": \"invites_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"invites_invitedBy_user_id_fk\": {\n          \"name\": \"invites_invitedBy_user_id_fk\",\n          \"tableFrom\": \"invites\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"listCollaborators\": {\n      \"name\": \"listCollaborators\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedBy\": {\n          \"name\": \"addedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"listCollaborators_listId_idx\": {\n          \"name\": \"listCollaborators_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listCollaborators_userId_idx\": {\n          \"name\": \"listCollaborators_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listCollaborators_listId_userId_unique\": {\n          \"name\": \"listCollaborators_listId_userId_unique\",\n          \"columns\": [\n            \"listId\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"listCollaborators_listId_bookmarkLists_id_fk\": {\n          \"name\": \"listCollaborators_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listCollaborators_userId_user_id_fk\": {\n          \"name\": \"listCollaborators_userId_user_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listCollaborators_addedBy_user_id_fk\": {\n          \"name\": \"listCollaborators_addedBy_user_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"addedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"listInvitations\": {\n      \"name\": \"listInvitations\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"invitedAt\": {\n          \"name\": \"invitedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"invitedEmail\": {\n          \"name\": \"invitedEmail\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"listInvitations_listId_idx\": {\n          \"name\": \"listInvitations_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_userId_idx\": {\n          \"name\": \"listInvitations_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_status_idx\": {\n          \"name\": \"listInvitations_status_idx\",\n          \"columns\": [\n            \"status\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_listId_userId_unique\": {\n          \"name\": \"listInvitations_listId_userId_unique\",\n          \"columns\": [\n            \"listId\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"listInvitations_listId_bookmarkLists_id_fk\": {\n          \"name\": \"listInvitations_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listInvitations_userId_user_id_fk\": {\n          \"name\": \"listInvitations_userId_user_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listInvitations_invitedBy_user_id_fk\": {\n          \"name\": \"listInvitations_invitedBy_user_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"passwordResetToken\": {\n      \"name\": \"passwordResetToken\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"passwordResetToken_token_unique\": {\n          \"name\": \"passwordResetToken_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        },\n        \"passwordResetTokens_userId_idx\": {\n          \"name\": \"passwordResetTokens_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"passwordResetToken_userId_user_id_fk\": {\n          \"name\": \"passwordResetToken_userId_user_id_fk\",\n          \"tableFrom\": \"passwordResetToken\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_bookmarkId_idx\": {\n          \"name\": \"rssFeedImports_rssFeedId_bookmarkId_idx\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"importTags\": {\n          \"name\": \"importTags\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineActions\": {\n      \"name\": \"ruleEngineActions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"ruleId\": {\n          \"name\": \"ruleId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"action\": {\n          \"name\": \"action\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngineActions_userId_idx\": {\n          \"name\": \"ruleEngineActions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"ruleEngineActions_ruleId_idx\": {\n          \"name\": \"ruleEngineActions_ruleId_idx\",\n          \"columns\": [\n            \"ruleId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineActions_userId_user_id_fk\": {\n          \"name\": \"ruleEngineActions_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\": {\n          \"name\": \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"ruleId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_tagId_fk\": {\n          \"name\": \"ruleEngineActions_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_listId_fk\": {\n          \"name\": \"ruleEngineActions_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineRules\": {\n      \"name\": \"ruleEngineRules\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"event\": {\n          \"name\": \"event\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"condition\": {\n          \"name\": \"condition\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngine_userId_idx\": {\n          \"name\": \"ruleEngine_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineRules_userId_user_id_fk\": {\n          \"name\": \"ruleEngineRules_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_tagId_fk\": {\n          \"name\": \"ruleEngineRules_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_listId_fk\": {\n          \"name\": \"ruleEngineRules_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"subscriptions\": {\n      \"name\": \"subscriptions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeCustomerId\": {\n          \"name\": \"stripeCustomerId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeSubscriptionId\": {\n          \"name\": \"stripeSubscriptionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tier\": {\n          \"name\": \"tier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'free'\"\n        },\n        \"priceId\": {\n          \"name\": \"priceId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cancelAtPeriodEnd\": {\n          \"name\": \"cancelAtPeriodEnd\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"startDate\": {\n          \"name\": \"startDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"endDate\": {\n          \"name\": \"endDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"subscriptions_userId_unique\": {\n          \"name\": \"subscriptions_userId_unique\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": true\n        },\n        \"subscriptions_userId_idx\": {\n          \"name\": \"subscriptions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"subscriptions_stripeCustomerId_idx\": {\n          \"name\": \"subscriptions_stripeCustomerId_idx\",\n          \"columns\": [\n            \"stripeCustomerId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"subscriptions_userId_user_id_fk\": {\n          \"name\": \"subscriptions_userId_user_id_fk\",\n          \"tableFrom\": \"subscriptions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_tagId_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkId_idx\",\n          \"columns\": [\n            \"tagId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"salt\": {\n          \"name\": \"salt\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        },\n        \"bookmarkQuota\": {\n          \"name\": \"bookmarkQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"storageQuota\": {\n          \"name\": \"storageQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"browserCrawlingEnabled\": {\n          \"name\": \"browserCrawlingEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkClickAction\": {\n          \"name\": \"bookmarkClickAction\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'open_original_link'\"\n        },\n        \"archiveDisplayBehaviour\": {\n          \"name\": \"archiveDisplayBehaviour\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'show'\"\n        },\n        \"timezone\": {\n          \"name\": \"timezone\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'UTC'\"\n        },\n        \"backupsEnabled\": {\n          \"name\": \"backupsEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"backupsFrequency\": {\n          \"name\": \"backupsFrequency\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'weekly'\"\n        },\n        \"backupsRetentionDays\": {\n          \"name\": \"backupsRetentionDays\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 30\n        },\n        \"readerFontSize\": {\n          \"name\": \"readerFontSize\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"readerLineHeight\": {\n          \"name\": \"readerLineHeight\",\n          \"type\": \"real\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"readerFontFamily\": {\n          \"name\": \"readerFontFamily\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"autoTaggingEnabled\": {\n          \"name\": \"autoTaggingEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"autoSummarizationEnabled\": {\n          \"name\": \"autoSummarizationEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagStyle\": {\n          \"name\": \"tagStyle\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'titlecase-spaces'\"\n        },\n        \"inferredTagLang\": {\n          \"name\": \"inferredTagLang\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0077_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"49a682e5-a215-43da-ac0f-dd1a2ccea538\",\n  \"prevId\": \"609c4402-c36e-4efc-9fd0-4f9038f8a164\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastUsedAt\": {\n          \"name\": \"lastUsedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"backups\": {\n      \"name\": \"backups\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkCount\": {\n          \"name\": \"bookmarkCount\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"errorMessage\": {\n          \"name\": \"errorMessage\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"backups_userId_idx\": {\n          \"name\": \"backups_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"backups_createdAt_idx\": {\n          \"name\": \"backups_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"backups_userId_user_id_fk\": {\n          \"name\": \"backups_userId_user_id_fk\",\n          \"tableFrom\": \"backups\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"backups_assetId_assets_id_fk\": {\n          \"name\": \"backups_assetId_assets_id_fk\",\n          \"tableFrom\": \"backups\",\n          \"tableTo\": \"assets\",\n          \"columnsFrom\": [\n            \"assetId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"author\": {\n          \"name\": \"author\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"publisher\": {\n          \"name\": \"publisher\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"datePublished\": {\n          \"name\": \"datePublished\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dateModified\": {\n          \"name\": \"dateModified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"contentAssetId\": {\n          \"name\": \"contentAssetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rssToken\": {\n          \"name\": \"rssToken\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_userId_id_idx\": {\n          \"name\": \"bookmarkLists_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"normalizedName\": {\n          \"name\": \"normalizedName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"generated\": {\n            \"as\": \"(lower(replace(replace(replace(\\\"name\\\", ' ', ''), '-', ''), '_', '')))\",\n            \"type\": \"virtual\"\n          }\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_normalizedName_idx\": {\n          \"name\": \"bookmarkTags_normalizedName_idx\",\n          \"columns\": [\n            \"normalizedName\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"bookmarkTags_userId_id_idx\": {\n          \"name\": \"bookmarkTags_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summarizationStatus\": {\n          \"name\": \"summarizationStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"source\": {\n          \"name\": \"source\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_userId_createdAt_id_idx\": {\n          \"name\": \"bookmarks_userId_createdAt_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"createdAt\",\n            \"id\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_userId_archived_createdAt_id_idx\": {\n          \"name\": \"bookmarks_userId_archived_createdAt_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"archived\",\n            \"createdAt\",\n            \"id\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_userId_favourited_createdAt_id_idx\": {\n          \"name\": \"bookmarks_userId_favourited_createdAt_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"favourited\",\n            \"createdAt\",\n            \"id\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"listMembershipId\": {\n          \"name\": \"listMembershipId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkId_idx\",\n          \"columns\": [\n            \"listId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listMembershipId_listCollaborators_id_fk\": {\n          \"name\": \"bookmarksInLists_listMembershipId_listCollaborators_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"listCollaborators\",\n          \"columnsFrom\": [\n            \"listMembershipId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importSessionBookmarks\": {\n      \"name\": \"importSessionBookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"importSessionId\": {\n          \"name\": \"importSessionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importSessionBookmarks_sessionId_idx\": {\n          \"name\": \"importSessionBookmarks_sessionId_idx\",\n          \"columns\": [\n            \"importSessionId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessionBookmarks_bookmarkId_idx\": {\n          \"name\": \"importSessionBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessionBookmarks_importSessionId_bookmarkId_unique\": {\n          \"name\": \"importSessionBookmarks_importSessionId_bookmarkId_unique\",\n          \"columns\": [\n            \"importSessionId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"importSessionBookmarks_importSessionId_importSessions_id_fk\": {\n          \"name\": \"importSessionBookmarks_importSessionId_importSessions_id_fk\",\n          \"tableFrom\": \"importSessionBookmarks\",\n          \"tableTo\": \"importSessions\",\n          \"columnsFrom\": [\n            \"importSessionId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"importSessionBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"importSessionBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"importSessionBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importSessions\": {\n      \"name\": \"importSessions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rootListId\": {\n          \"name\": \"rootListId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'staging'\"\n        },\n        \"lastProcessedAt\": {\n          \"name\": \"lastProcessedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importSessions_userId_idx\": {\n          \"name\": \"importSessions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"importSessions_userId_user_id_fk\": {\n          \"name\": \"importSessions_userId_user_id_fk\",\n          \"tableFrom\": \"importSessions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"importSessions_rootListId_bookmarkLists_id_fk\": {\n          \"name\": \"importSessions_rootListId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"importSessions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"rootListId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importStagingBookmarks\": {\n      \"name\": \"importStagingBookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"importSessionId\": {\n          \"name\": \"importSessionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tags\": {\n          \"name\": \"tags\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"listIds\": {\n          \"name\": \"listIds\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceAddedAt\": {\n          \"name\": \"sourceAddedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"processingStartedAt\": {\n          \"name\": \"processingStartedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"result\": {\n          \"name\": \"result\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resultReason\": {\n          \"name\": \"resultReason\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resultBookmarkId\": {\n          \"name\": \"resultBookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"completedAt\": {\n          \"name\": \"completedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importStaging_session_status_idx\": {\n          \"name\": \"importStaging_session_status_idx\",\n          \"columns\": [\n            \"importSessionId\",\n            \"status\"\n          ],\n          \"isUnique\": false\n        },\n        \"importStaging_completedAt_idx\": {\n          \"name\": \"importStaging_completedAt_idx\",\n          \"columns\": [\n            \"completedAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"importStagingBookmarks_importSessionId_importSessions_id_fk\": {\n          \"name\": \"importStagingBookmarks_importSessionId_importSessions_id_fk\",\n          \"tableFrom\": \"importStagingBookmarks\",\n          \"tableTo\": \"importSessions\",\n          \"columnsFrom\": [\n            \"importSessionId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"importStagingBookmarks_resultBookmarkId_bookmarks_id_fk\": {\n          \"name\": \"importStagingBookmarks_resultBookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"importStagingBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"resultBookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invites\": {\n      \"name\": \"invites\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"usedAt\": {\n          \"name\": \"usedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"invites_token_unique\": {\n          \"name\": \"invites_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"invites_invitedBy_user_id_fk\": {\n          \"name\": \"invites_invitedBy_user_id_fk\",\n          \"tableFrom\": \"invites\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"listCollaborators\": {\n      \"name\": \"listCollaborators\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedBy\": {\n          \"name\": \"addedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"listCollaborators_listId_idx\": {\n          \"name\": \"listCollaborators_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listCollaborators_userId_idx\": {\n          \"name\": \"listCollaborators_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listCollaborators_listId_userId_unique\": {\n          \"name\": \"listCollaborators_listId_userId_unique\",\n          \"columns\": [\n            \"listId\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"listCollaborators_listId_bookmarkLists_id_fk\": {\n          \"name\": \"listCollaborators_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listCollaborators_userId_user_id_fk\": {\n          \"name\": \"listCollaborators_userId_user_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listCollaborators_addedBy_user_id_fk\": {\n          \"name\": \"listCollaborators_addedBy_user_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"addedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"listInvitations\": {\n      \"name\": \"listInvitations\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"invitedAt\": {\n          \"name\": \"invitedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"invitedEmail\": {\n          \"name\": \"invitedEmail\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"listInvitations_listId_idx\": {\n          \"name\": \"listInvitations_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_userId_idx\": {\n          \"name\": \"listInvitations_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_status_idx\": {\n          \"name\": \"listInvitations_status_idx\",\n          \"columns\": [\n            \"status\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_listId_userId_unique\": {\n          \"name\": \"listInvitations_listId_userId_unique\",\n          \"columns\": [\n            \"listId\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"listInvitations_listId_bookmarkLists_id_fk\": {\n          \"name\": \"listInvitations_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listInvitations_userId_user_id_fk\": {\n          \"name\": \"listInvitations_userId_user_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listInvitations_invitedBy_user_id_fk\": {\n          \"name\": \"listInvitations_invitedBy_user_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"passwordResetToken\": {\n      \"name\": \"passwordResetToken\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"passwordResetToken_token_unique\": {\n          \"name\": \"passwordResetToken_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        },\n        \"passwordResetTokens_userId_idx\": {\n          \"name\": \"passwordResetTokens_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"passwordResetToken_userId_user_id_fk\": {\n          \"name\": \"passwordResetToken_userId_user_id_fk\",\n          \"tableFrom\": \"passwordResetToken\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_bookmarkId_idx\": {\n          \"name\": \"rssFeedImports_rssFeedId_bookmarkId_idx\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"importTags\": {\n          \"name\": \"importTags\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineActions\": {\n      \"name\": \"ruleEngineActions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"ruleId\": {\n          \"name\": \"ruleId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"action\": {\n          \"name\": \"action\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngineActions_userId_idx\": {\n          \"name\": \"ruleEngineActions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"ruleEngineActions_ruleId_idx\": {\n          \"name\": \"ruleEngineActions_ruleId_idx\",\n          \"columns\": [\n            \"ruleId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineActions_userId_user_id_fk\": {\n          \"name\": \"ruleEngineActions_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\": {\n          \"name\": \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"ruleId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_tagId_fk\": {\n          \"name\": \"ruleEngineActions_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_listId_fk\": {\n          \"name\": \"ruleEngineActions_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineRules\": {\n      \"name\": \"ruleEngineRules\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"event\": {\n          \"name\": \"event\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"condition\": {\n          \"name\": \"condition\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngine_userId_idx\": {\n          \"name\": \"ruleEngine_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineRules_userId_user_id_fk\": {\n          \"name\": \"ruleEngineRules_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_tagId_fk\": {\n          \"name\": \"ruleEngineRules_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_listId_fk\": {\n          \"name\": \"ruleEngineRules_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"subscriptions\": {\n      \"name\": \"subscriptions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeCustomerId\": {\n          \"name\": \"stripeCustomerId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeSubscriptionId\": {\n          \"name\": \"stripeSubscriptionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tier\": {\n          \"name\": \"tier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'free'\"\n        },\n        \"priceId\": {\n          \"name\": \"priceId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cancelAtPeriodEnd\": {\n          \"name\": \"cancelAtPeriodEnd\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"startDate\": {\n          \"name\": \"startDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"endDate\": {\n          \"name\": \"endDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"subscriptions_userId_unique\": {\n          \"name\": \"subscriptions_userId_unique\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": true\n        },\n        \"subscriptions_userId_idx\": {\n          \"name\": \"subscriptions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"subscriptions_stripeCustomerId_idx\": {\n          \"name\": \"subscriptions_stripeCustomerId_idx\",\n          \"columns\": [\n            \"stripeCustomerId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"subscriptions_userId_user_id_fk\": {\n          \"name\": \"subscriptions_userId_user_id_fk\",\n          \"tableFrom\": \"subscriptions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_tagId_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkId_idx\",\n          \"columns\": [\n            \"tagId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"salt\": {\n          \"name\": \"salt\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        },\n        \"bookmarkQuota\": {\n          \"name\": \"bookmarkQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"storageQuota\": {\n          \"name\": \"storageQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"browserCrawlingEnabled\": {\n          \"name\": \"browserCrawlingEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkClickAction\": {\n          \"name\": \"bookmarkClickAction\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'open_original_link'\"\n        },\n        \"archiveDisplayBehaviour\": {\n          \"name\": \"archiveDisplayBehaviour\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'show'\"\n        },\n        \"timezone\": {\n          \"name\": \"timezone\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'UTC'\"\n        },\n        \"backupsEnabled\": {\n          \"name\": \"backupsEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"backupsFrequency\": {\n          \"name\": \"backupsFrequency\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'weekly'\"\n        },\n        \"backupsRetentionDays\": {\n          \"name\": \"backupsRetentionDays\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 30\n        },\n        \"readerFontSize\": {\n          \"name\": \"readerFontSize\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"readerLineHeight\": {\n          \"name\": \"readerLineHeight\",\n          \"type\": \"real\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"readerFontFamily\": {\n          \"name\": \"readerFontFamily\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"autoTaggingEnabled\": {\n          \"name\": \"autoTaggingEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"autoSummarizationEnabled\": {\n          \"name\": \"autoSummarizationEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagStyle\": {\n          \"name\": \"tagStyle\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'titlecase-spaces'\"\n        },\n        \"inferredTagLang\": {\n          \"name\": \"inferredTagLang\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0078_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"4606d370-ba98-401d-af38-646707d73764\",\n  \"prevId\": \"49a682e5-a215-43da-ac0f-dd1a2ccea538\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastUsedAt\": {\n          \"name\": \"lastUsedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"backups\": {\n      \"name\": \"backups\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkCount\": {\n          \"name\": \"bookmarkCount\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"errorMessage\": {\n          \"name\": \"errorMessage\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"backups_userId_idx\": {\n          \"name\": \"backups_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"backups_createdAt_idx\": {\n          \"name\": \"backups_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"backups_userId_user_id_fk\": {\n          \"name\": \"backups_userId_user_id_fk\",\n          \"tableFrom\": \"backups\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"backups_assetId_assets_id_fk\": {\n          \"name\": \"backups_assetId_assets_id_fk\",\n          \"tableFrom\": \"backups\",\n          \"tableTo\": \"assets\",\n          \"columnsFrom\": [\n            \"assetId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"author\": {\n          \"name\": \"author\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"publisher\": {\n          \"name\": \"publisher\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"datePublished\": {\n          \"name\": \"datePublished\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dateModified\": {\n          \"name\": \"dateModified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"contentAssetId\": {\n          \"name\": \"contentAssetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rssToken\": {\n          \"name\": \"rssToken\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_userId_id_idx\": {\n          \"name\": \"bookmarkLists_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"normalizedName\": {\n          \"name\": \"normalizedName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"generated\": {\n            \"as\": \"(lower(replace(replace(replace(\\\"name\\\", ' ', ''), '-', ''), '_', '')))\",\n            \"type\": \"virtual\"\n          }\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_normalizedName_idx\": {\n          \"name\": \"bookmarkTags_normalizedName_idx\",\n          \"columns\": [\n            \"normalizedName\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"bookmarkTags_userId_id_idx\": {\n          \"name\": \"bookmarkTags_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summarizationStatus\": {\n          \"name\": \"summarizationStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"source\": {\n          \"name\": \"source\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_userId_createdAt_id_idx\": {\n          \"name\": \"bookmarks_userId_createdAt_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"createdAt\",\n            \"id\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_userId_archived_createdAt_id_idx\": {\n          \"name\": \"bookmarks_userId_archived_createdAt_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"archived\",\n            \"createdAt\",\n            \"id\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_userId_favourited_createdAt_id_idx\": {\n          \"name\": \"bookmarks_userId_favourited_createdAt_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"favourited\",\n            \"createdAt\",\n            \"id\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"listMembershipId\": {\n          \"name\": \"listMembershipId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkId_idx\",\n          \"columns\": [\n            \"listId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listMembershipId_listCollaborators_id_fk\": {\n          \"name\": \"bookmarksInLists_listMembershipId_listCollaborators_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"listCollaborators\",\n          \"columnsFrom\": [\n            \"listMembershipId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importSessionBookmarks\": {\n      \"name\": \"importSessionBookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"importSessionId\": {\n          \"name\": \"importSessionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importSessionBookmarks_sessionId_idx\": {\n          \"name\": \"importSessionBookmarks_sessionId_idx\",\n          \"columns\": [\n            \"importSessionId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessionBookmarks_bookmarkId_idx\": {\n          \"name\": \"importSessionBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessionBookmarks_importSessionId_bookmarkId_unique\": {\n          \"name\": \"importSessionBookmarks_importSessionId_bookmarkId_unique\",\n          \"columns\": [\n            \"importSessionId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"importSessionBookmarks_importSessionId_importSessions_id_fk\": {\n          \"name\": \"importSessionBookmarks_importSessionId_importSessions_id_fk\",\n          \"tableFrom\": \"importSessionBookmarks\",\n          \"tableTo\": \"importSessions\",\n          \"columnsFrom\": [\n            \"importSessionId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"importSessionBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"importSessionBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"importSessionBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importSessions\": {\n      \"name\": \"importSessions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rootListId\": {\n          \"name\": \"rootListId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'staging'\"\n        },\n        \"lastProcessedAt\": {\n          \"name\": \"lastProcessedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importSessions_userId_idx\": {\n          \"name\": \"importSessions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessions_status_idx\": {\n          \"name\": \"importSessions_status_idx\",\n          \"columns\": [\n            \"status\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"importSessions_userId_user_id_fk\": {\n          \"name\": \"importSessions_userId_user_id_fk\",\n          \"tableFrom\": \"importSessions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"importSessions_rootListId_bookmarkLists_id_fk\": {\n          \"name\": \"importSessions_rootListId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"importSessions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"rootListId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importStagingBookmarks\": {\n      \"name\": \"importStagingBookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"importSessionId\": {\n          \"name\": \"importSessionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tags\": {\n          \"name\": \"tags\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"listIds\": {\n          \"name\": \"listIds\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceAddedAt\": {\n          \"name\": \"sourceAddedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"processingStartedAt\": {\n          \"name\": \"processingStartedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"result\": {\n          \"name\": \"result\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resultReason\": {\n          \"name\": \"resultReason\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resultBookmarkId\": {\n          \"name\": \"resultBookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"completedAt\": {\n          \"name\": \"completedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importStaging_session_status_idx\": {\n          \"name\": \"importStaging_session_status_idx\",\n          \"columns\": [\n            \"importSessionId\",\n            \"status\"\n          ],\n          \"isUnique\": false\n        },\n        \"importStaging_completedAt_idx\": {\n          \"name\": \"importStaging_completedAt_idx\",\n          \"columns\": [\n            \"completedAt\"\n          ],\n          \"isUnique\": false\n        },\n        \"importStaging_status_idx\": {\n          \"name\": \"importStaging_status_idx\",\n          \"columns\": [\n            \"status\"\n          ],\n          \"isUnique\": false\n        },\n        \"importStaging_status_processingStartedAt_idx\": {\n          \"name\": \"importStaging_status_processingStartedAt_idx\",\n          \"columns\": [\n            \"status\",\n            \"processingStartedAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"importStagingBookmarks_importSessionId_importSessions_id_fk\": {\n          \"name\": \"importStagingBookmarks_importSessionId_importSessions_id_fk\",\n          \"tableFrom\": \"importStagingBookmarks\",\n          \"tableTo\": \"importSessions\",\n          \"columnsFrom\": [\n            \"importSessionId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"importStagingBookmarks_resultBookmarkId_bookmarks_id_fk\": {\n          \"name\": \"importStagingBookmarks_resultBookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"importStagingBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"resultBookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invites\": {\n      \"name\": \"invites\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"usedAt\": {\n          \"name\": \"usedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"invites_token_unique\": {\n          \"name\": \"invites_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"invites_invitedBy_user_id_fk\": {\n          \"name\": \"invites_invitedBy_user_id_fk\",\n          \"tableFrom\": \"invites\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"listCollaborators\": {\n      \"name\": \"listCollaborators\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedBy\": {\n          \"name\": \"addedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"listCollaborators_listId_idx\": {\n          \"name\": \"listCollaborators_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listCollaborators_userId_idx\": {\n          \"name\": \"listCollaborators_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listCollaborators_listId_userId_unique\": {\n          \"name\": \"listCollaborators_listId_userId_unique\",\n          \"columns\": [\n            \"listId\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"listCollaborators_listId_bookmarkLists_id_fk\": {\n          \"name\": \"listCollaborators_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listCollaborators_userId_user_id_fk\": {\n          \"name\": \"listCollaborators_userId_user_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listCollaborators_addedBy_user_id_fk\": {\n          \"name\": \"listCollaborators_addedBy_user_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"addedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"listInvitations\": {\n      \"name\": \"listInvitations\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"invitedAt\": {\n          \"name\": \"invitedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"invitedEmail\": {\n          \"name\": \"invitedEmail\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"listInvitations_listId_idx\": {\n          \"name\": \"listInvitations_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_userId_idx\": {\n          \"name\": \"listInvitations_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_status_idx\": {\n          \"name\": \"listInvitations_status_idx\",\n          \"columns\": [\n            \"status\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_listId_userId_unique\": {\n          \"name\": \"listInvitations_listId_userId_unique\",\n          \"columns\": [\n            \"listId\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"listInvitations_listId_bookmarkLists_id_fk\": {\n          \"name\": \"listInvitations_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listInvitations_userId_user_id_fk\": {\n          \"name\": \"listInvitations_userId_user_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listInvitations_invitedBy_user_id_fk\": {\n          \"name\": \"listInvitations_invitedBy_user_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"passwordResetToken\": {\n      \"name\": \"passwordResetToken\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"passwordResetToken_token_unique\": {\n          \"name\": \"passwordResetToken_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        },\n        \"passwordResetTokens_userId_idx\": {\n          \"name\": \"passwordResetTokens_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"passwordResetToken_userId_user_id_fk\": {\n          \"name\": \"passwordResetToken_userId_user_id_fk\",\n          \"tableFrom\": \"passwordResetToken\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_bookmarkId_idx\": {\n          \"name\": \"rssFeedImports_rssFeedId_bookmarkId_idx\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"importTags\": {\n          \"name\": \"importTags\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineActions\": {\n      \"name\": \"ruleEngineActions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"ruleId\": {\n          \"name\": \"ruleId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"action\": {\n          \"name\": \"action\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngineActions_userId_idx\": {\n          \"name\": \"ruleEngineActions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"ruleEngineActions_ruleId_idx\": {\n          \"name\": \"ruleEngineActions_ruleId_idx\",\n          \"columns\": [\n            \"ruleId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineActions_userId_user_id_fk\": {\n          \"name\": \"ruleEngineActions_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\": {\n          \"name\": \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"ruleId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_tagId_fk\": {\n          \"name\": \"ruleEngineActions_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_listId_fk\": {\n          \"name\": \"ruleEngineActions_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineRules\": {\n      \"name\": \"ruleEngineRules\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"event\": {\n          \"name\": \"event\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"condition\": {\n          \"name\": \"condition\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngine_userId_idx\": {\n          \"name\": \"ruleEngine_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineRules_userId_user_id_fk\": {\n          \"name\": \"ruleEngineRules_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_tagId_fk\": {\n          \"name\": \"ruleEngineRules_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_listId_fk\": {\n          \"name\": \"ruleEngineRules_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"subscriptions\": {\n      \"name\": \"subscriptions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeCustomerId\": {\n          \"name\": \"stripeCustomerId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeSubscriptionId\": {\n          \"name\": \"stripeSubscriptionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tier\": {\n          \"name\": \"tier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'free'\"\n        },\n        \"priceId\": {\n          \"name\": \"priceId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cancelAtPeriodEnd\": {\n          \"name\": \"cancelAtPeriodEnd\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"startDate\": {\n          \"name\": \"startDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"endDate\": {\n          \"name\": \"endDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"subscriptions_userId_unique\": {\n          \"name\": \"subscriptions_userId_unique\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": true\n        },\n        \"subscriptions_userId_idx\": {\n          \"name\": \"subscriptions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"subscriptions_stripeCustomerId_idx\": {\n          \"name\": \"subscriptions_stripeCustomerId_idx\",\n          \"columns\": [\n            \"stripeCustomerId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"subscriptions_userId_user_id_fk\": {\n          \"name\": \"subscriptions_userId_user_id_fk\",\n          \"tableFrom\": \"subscriptions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_tagId_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkId_idx\",\n          \"columns\": [\n            \"tagId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"salt\": {\n          \"name\": \"salt\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        },\n        \"bookmarkQuota\": {\n          \"name\": \"bookmarkQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"storageQuota\": {\n          \"name\": \"storageQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"browserCrawlingEnabled\": {\n          \"name\": \"browserCrawlingEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkClickAction\": {\n          \"name\": \"bookmarkClickAction\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'open_original_link'\"\n        },\n        \"archiveDisplayBehaviour\": {\n          \"name\": \"archiveDisplayBehaviour\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'show'\"\n        },\n        \"timezone\": {\n          \"name\": \"timezone\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'UTC'\"\n        },\n        \"backupsEnabled\": {\n          \"name\": \"backupsEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"backupsFrequency\": {\n          \"name\": \"backupsFrequency\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'weekly'\"\n        },\n        \"backupsRetentionDays\": {\n          \"name\": \"backupsRetentionDays\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 30\n        },\n        \"readerFontSize\": {\n          \"name\": \"readerFontSize\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"readerLineHeight\": {\n          \"name\": \"readerLineHeight\",\n          \"type\": \"real\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"readerFontFamily\": {\n          \"name\": \"readerFontFamily\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"autoTaggingEnabled\": {\n          \"name\": \"autoTaggingEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"autoSummarizationEnabled\": {\n          \"name\": \"autoSummarizationEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagStyle\": {\n          \"name\": \"tagStyle\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'titlecase-spaces'\"\n        },\n        \"inferredTagLang\": {\n          \"name\": \"inferredTagLang\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0079_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"eefce4a3-19af-48d5-a588-e8f2a538fa39\",\n  \"prevId\": \"4606d370-ba98-401d-af38-646707d73764\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastUsedAt\": {\n          \"name\": \"lastUsedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"backups\": {\n      \"name\": \"backups\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkCount\": {\n          \"name\": \"bookmarkCount\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"errorMessage\": {\n          \"name\": \"errorMessage\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"backups_userId_idx\": {\n          \"name\": \"backups_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"backups_createdAt_idx\": {\n          \"name\": \"backups_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"backups_userId_user_id_fk\": {\n          \"name\": \"backups_userId_user_id_fk\",\n          \"tableFrom\": \"backups\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"backups_assetId_assets_id_fk\": {\n          \"name\": \"backups_assetId_assets_id_fk\",\n          \"tableFrom\": \"backups\",\n          \"tableTo\": \"assets\",\n          \"columnsFrom\": [\n            \"assetId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"author\": {\n          \"name\": \"author\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"publisher\": {\n          \"name\": \"publisher\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"datePublished\": {\n          \"name\": \"datePublished\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dateModified\": {\n          \"name\": \"dateModified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"contentAssetId\": {\n          \"name\": \"contentAssetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rssToken\": {\n          \"name\": \"rssToken\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_userId_id_idx\": {\n          \"name\": \"bookmarkLists_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"normalizedName\": {\n          \"name\": \"normalizedName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"generated\": {\n            \"as\": \"(lower(replace(replace(replace(\\\"name\\\", ' ', ''), '-', ''), '_', '')))\",\n            \"type\": \"virtual\"\n          }\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_normalizedName_idx\": {\n          \"name\": \"bookmarkTags_normalizedName_idx\",\n          \"columns\": [\n            \"normalizedName\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"bookmarkTags_userId_id_idx\": {\n          \"name\": \"bookmarkTags_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summarizationStatus\": {\n          \"name\": \"summarizationStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"source\": {\n          \"name\": \"source\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_userId_createdAt_id_idx\": {\n          \"name\": \"bookmarks_userId_createdAt_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"createdAt\",\n            \"id\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_userId_archived_createdAt_id_idx\": {\n          \"name\": \"bookmarks_userId_archived_createdAt_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"archived\",\n            \"createdAt\",\n            \"id\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_userId_favourited_createdAt_id_idx\": {\n          \"name\": \"bookmarks_userId_favourited_createdAt_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"favourited\",\n            \"createdAt\",\n            \"id\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"listMembershipId\": {\n          \"name\": \"listMembershipId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkId_idx\",\n          \"columns\": [\n            \"listId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listMembershipId_listCollaborators_id_fk\": {\n          \"name\": \"bookmarksInLists_listMembershipId_listCollaborators_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"listCollaborators\",\n          \"columnsFrom\": [\n            \"listMembershipId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importSessionBookmarks\": {\n      \"name\": \"importSessionBookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"importSessionId\": {\n          \"name\": \"importSessionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importSessionBookmarks_sessionId_idx\": {\n          \"name\": \"importSessionBookmarks_sessionId_idx\",\n          \"columns\": [\n            \"importSessionId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessionBookmarks_bookmarkId_idx\": {\n          \"name\": \"importSessionBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessionBookmarks_importSessionId_bookmarkId_unique\": {\n          \"name\": \"importSessionBookmarks_importSessionId_bookmarkId_unique\",\n          \"columns\": [\n            \"importSessionId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"importSessionBookmarks_importSessionId_importSessions_id_fk\": {\n          \"name\": \"importSessionBookmarks_importSessionId_importSessions_id_fk\",\n          \"tableFrom\": \"importSessionBookmarks\",\n          \"tableTo\": \"importSessions\",\n          \"columnsFrom\": [\n            \"importSessionId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"importSessionBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"importSessionBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"importSessionBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importSessions\": {\n      \"name\": \"importSessions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rootListId\": {\n          \"name\": \"rootListId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'staging'\"\n        },\n        \"lastProcessedAt\": {\n          \"name\": \"lastProcessedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importSessions_userId_idx\": {\n          \"name\": \"importSessions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessions_status_idx\": {\n          \"name\": \"importSessions_status_idx\",\n          \"columns\": [\n            \"status\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"importSessions_userId_user_id_fk\": {\n          \"name\": \"importSessions_userId_user_id_fk\",\n          \"tableFrom\": \"importSessions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"importSessions_rootListId_bookmarkLists_id_fk\": {\n          \"name\": \"importSessions_rootListId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"importSessions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"rootListId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importStagingBookmarks\": {\n      \"name\": \"importStagingBookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"importSessionId\": {\n          \"name\": \"importSessionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tags\": {\n          \"name\": \"tags\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"listIds\": {\n          \"name\": \"listIds\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceAddedAt\": {\n          \"name\": \"sourceAddedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"processingStartedAt\": {\n          \"name\": \"processingStartedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"result\": {\n          \"name\": \"result\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resultReason\": {\n          \"name\": \"resultReason\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resultBookmarkId\": {\n          \"name\": \"resultBookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"completedAt\": {\n          \"name\": \"completedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importStaging_session_status_idx\": {\n          \"name\": \"importStaging_session_status_idx\",\n          \"columns\": [\n            \"importSessionId\",\n            \"status\"\n          ],\n          \"isUnique\": false\n        },\n        \"importStaging_completedAt_idx\": {\n          \"name\": \"importStaging_completedAt_idx\",\n          \"columns\": [\n            \"completedAt\"\n          ],\n          \"isUnique\": false\n        },\n        \"importStaging_status_idx\": {\n          \"name\": \"importStaging_status_idx\",\n          \"columns\": [\n            \"status\"\n          ],\n          \"isUnique\": false\n        },\n        \"importStaging_status_processingStartedAt_idx\": {\n          \"name\": \"importStaging_status_processingStartedAt_idx\",\n          \"columns\": [\n            \"status\",\n            \"processingStartedAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"importStagingBookmarks_importSessionId_importSessions_id_fk\": {\n          \"name\": \"importStagingBookmarks_importSessionId_importSessions_id_fk\",\n          \"tableFrom\": \"importStagingBookmarks\",\n          \"tableTo\": \"importSessions\",\n          \"columnsFrom\": [\n            \"importSessionId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"importStagingBookmarks_resultBookmarkId_bookmarks_id_fk\": {\n          \"name\": \"importStagingBookmarks_resultBookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"importStagingBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"resultBookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invites\": {\n      \"name\": \"invites\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"usedAt\": {\n          \"name\": \"usedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"invites_token_unique\": {\n          \"name\": \"invites_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"invites_invitedBy_user_id_fk\": {\n          \"name\": \"invites_invitedBy_user_id_fk\",\n          \"tableFrom\": \"invites\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"listCollaborators\": {\n      \"name\": \"listCollaborators\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedBy\": {\n          \"name\": \"addedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"listCollaborators_listId_idx\": {\n          \"name\": \"listCollaborators_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listCollaborators_userId_idx\": {\n          \"name\": \"listCollaborators_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listCollaborators_listId_userId_unique\": {\n          \"name\": \"listCollaborators_listId_userId_unique\",\n          \"columns\": [\n            \"listId\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"listCollaborators_listId_bookmarkLists_id_fk\": {\n          \"name\": \"listCollaborators_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listCollaborators_userId_user_id_fk\": {\n          \"name\": \"listCollaborators_userId_user_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listCollaborators_addedBy_user_id_fk\": {\n          \"name\": \"listCollaborators_addedBy_user_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"addedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"listInvitations\": {\n      \"name\": \"listInvitations\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"invitedAt\": {\n          \"name\": \"invitedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"invitedEmail\": {\n          \"name\": \"invitedEmail\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"listInvitations_listId_idx\": {\n          \"name\": \"listInvitations_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_userId_idx\": {\n          \"name\": \"listInvitations_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_status_idx\": {\n          \"name\": \"listInvitations_status_idx\",\n          \"columns\": [\n            \"status\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_listId_userId_unique\": {\n          \"name\": \"listInvitations_listId_userId_unique\",\n          \"columns\": [\n            \"listId\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"listInvitations_listId_bookmarkLists_id_fk\": {\n          \"name\": \"listInvitations_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listInvitations_userId_user_id_fk\": {\n          \"name\": \"listInvitations_userId_user_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listInvitations_invitedBy_user_id_fk\": {\n          \"name\": \"listInvitations_invitedBy_user_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"passwordResetToken\": {\n      \"name\": \"passwordResetToken\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"passwordResetToken_token_unique\": {\n          \"name\": \"passwordResetToken_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        },\n        \"passwordResetTokens_userId_idx\": {\n          \"name\": \"passwordResetTokens_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"passwordResetToken_userId_user_id_fk\": {\n          \"name\": \"passwordResetToken_userId_user_id_fk\",\n          \"tableFrom\": \"passwordResetToken\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_bookmarkId_idx\": {\n          \"name\": \"rssFeedImports_rssFeedId_bookmarkId_idx\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"importTags\": {\n          \"name\": \"importTags\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineActions\": {\n      \"name\": \"ruleEngineActions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"ruleId\": {\n          \"name\": \"ruleId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"action\": {\n          \"name\": \"action\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngineActions_userId_idx\": {\n          \"name\": \"ruleEngineActions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"ruleEngineActions_ruleId_idx\": {\n          \"name\": \"ruleEngineActions_ruleId_idx\",\n          \"columns\": [\n            \"ruleId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineActions_userId_user_id_fk\": {\n          \"name\": \"ruleEngineActions_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\": {\n          \"name\": \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"ruleId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_tagId_fk\": {\n          \"name\": \"ruleEngineActions_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_listId_fk\": {\n          \"name\": \"ruleEngineActions_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineRules\": {\n      \"name\": \"ruleEngineRules\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"event\": {\n          \"name\": \"event\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"condition\": {\n          \"name\": \"condition\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngine_userId_idx\": {\n          \"name\": \"ruleEngine_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineRules_userId_user_id_fk\": {\n          \"name\": \"ruleEngineRules_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_tagId_fk\": {\n          \"name\": \"ruleEngineRules_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_listId_fk\": {\n          \"name\": \"ruleEngineRules_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"subscriptions\": {\n      \"name\": \"subscriptions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeCustomerId\": {\n          \"name\": \"stripeCustomerId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeSubscriptionId\": {\n          \"name\": \"stripeSubscriptionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tier\": {\n          \"name\": \"tier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'free'\"\n        },\n        \"priceId\": {\n          \"name\": \"priceId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cancelAtPeriodEnd\": {\n          \"name\": \"cancelAtPeriodEnd\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"startDate\": {\n          \"name\": \"startDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"endDate\": {\n          \"name\": \"endDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"subscriptions_userId_unique\": {\n          \"name\": \"subscriptions_userId_unique\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": true\n        },\n        \"subscriptions_userId_idx\": {\n          \"name\": \"subscriptions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"subscriptions_stripeCustomerId_idx\": {\n          \"name\": \"subscriptions_stripeCustomerId_idx\",\n          \"columns\": [\n            \"stripeCustomerId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"subscriptions_userId_user_id_fk\": {\n          \"name\": \"subscriptions_userId_user_id_fk\",\n          \"tableFrom\": \"subscriptions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_tagId_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkId_idx\",\n          \"columns\": [\n            \"tagId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"salt\": {\n          \"name\": \"salt\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        },\n        \"bookmarkQuota\": {\n          \"name\": \"bookmarkQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"storageQuota\": {\n          \"name\": \"storageQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"browserCrawlingEnabled\": {\n          \"name\": \"browserCrawlingEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkClickAction\": {\n          \"name\": \"bookmarkClickAction\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'open_original_link'\"\n        },\n        \"archiveDisplayBehaviour\": {\n          \"name\": \"archiveDisplayBehaviour\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'show'\"\n        },\n        \"timezone\": {\n          \"name\": \"timezone\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'UTC'\"\n        },\n        \"backupsEnabled\": {\n          \"name\": \"backupsEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"backupsFrequency\": {\n          \"name\": \"backupsFrequency\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'weekly'\"\n        },\n        \"backupsRetentionDays\": {\n          \"name\": \"backupsRetentionDays\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 30\n        },\n        \"readerFontSize\": {\n          \"name\": \"readerFontSize\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"readerLineHeight\": {\n          \"name\": \"readerLineHeight\",\n          \"type\": \"real\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"readerFontFamily\": {\n          \"name\": \"readerFontFamily\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"autoTaggingEnabled\": {\n          \"name\": \"autoTaggingEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"autoSummarizationEnabled\": {\n          \"name\": \"autoSummarizationEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagStyle\": {\n          \"name\": \"tagStyle\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'titlecase-spaces'\"\n        },\n        \"curatedTagIds\": {\n          \"name\": \"curatedTagIds\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"inferredTagLang\": {\n          \"name\": \"inferredTagLang\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/0080_snapshot.json",
    "content": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"cda8a968-ca40-4ea7-bd65-9a004ca16ae5\",\n  \"prevId\": \"eefce4a3-19af-48d5-a588-e8f2a538fa39\",\n  \"tables\": {\n    \"account\": {\n      \"name\": \"account\",\n      \"columns\": {\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"provider\": {\n          \"name\": \"provider\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"providerAccountId\": {\n          \"name\": \"providerAccountId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"refresh_token\": {\n          \"name\": \"refresh_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"access_token\": {\n          \"name\": \"access_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"expires_at\": {\n          \"name\": \"expires_at\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"token_type\": {\n          \"name\": \"token_type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"scope\": {\n          \"name\": \"scope\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"id_token\": {\n          \"name\": \"id_token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"session_state\": {\n          \"name\": \"session_state\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"account_userId_user_id_fk\": {\n          \"name\": \"account_userId_user_id_fk\",\n          \"tableFrom\": \"account\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"account_provider_providerAccountId_pk\": {\n          \"columns\": [\n            \"provider\",\n            \"providerAccountId\"\n          ],\n          \"name\": \"account_provider_providerAccountId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"apiKey\": {\n      \"name\": \"apiKey\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastUsedAt\": {\n          \"name\": \"lastUsedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"keyId\": {\n          \"name\": \"keyId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"keyHash\": {\n          \"name\": \"keyHash\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"apiKey_keyId_unique\": {\n          \"name\": \"apiKey_keyId_unique\",\n          \"columns\": [\n            \"keyId\"\n          ],\n          \"isUnique\": true\n        },\n        \"apiKey_name_userId_unique\": {\n          \"name\": \"apiKey_name_userId_unique\",\n          \"columns\": [\n            \"name\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"apiKey_userId_user_id_fk\": {\n          \"name\": \"apiKey_userId_user_id_fk\",\n          \"tableFrom\": \"apiKey\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"assets\": {\n      \"name\": \"assets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 0\n        },\n        \"contentType\": {\n          \"name\": \"contentType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"assets_bookmarkId_idx\": {\n          \"name\": \"assets_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_assetType_idx\": {\n          \"name\": \"assets_assetType_idx\",\n          \"columns\": [\n            \"assetType\"\n          ],\n          \"isUnique\": false\n        },\n        \"assets_userId_idx\": {\n          \"name\": \"assets_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"assets_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"assets_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"assets_userId_user_id_fk\": {\n          \"name\": \"assets_userId_user_id_fk\",\n          \"tableFrom\": \"assets\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"backups\": {\n      \"name\": \"backups\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"size\": {\n          \"name\": \"size\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkCount\": {\n          \"name\": \"bookmarkCount\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"errorMessage\": {\n          \"name\": \"errorMessage\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"backups_userId_idx\": {\n          \"name\": \"backups_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"backups_createdAt_idx\": {\n          \"name\": \"backups_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"backups_userId_user_id_fk\": {\n          \"name\": \"backups_userId_user_id_fk\",\n          \"tableFrom\": \"backups\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"backups_assetId_assets_id_fk\": {\n          \"name\": \"backups_assetId_assets_id_fk\",\n          \"tableFrom\": \"backups\",\n          \"tableTo\": \"assets\",\n          \"columnsFrom\": [\n            \"assetId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkAssets\": {\n      \"name\": \"bookmarkAssets\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetType\": {\n          \"name\": \"assetType\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"assetId\": {\n          \"name\": \"assetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"metadata\": {\n          \"name\": \"metadata\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"fileName\": {\n          \"name\": \"fileName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkAssets_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkAssets_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkAssets\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLinks\": {\n      \"name\": \"bookmarkLinks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"author\": {\n          \"name\": \"author\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"publisher\": {\n          \"name\": \"publisher\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"datePublished\": {\n          \"name\": \"datePublished\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"dateModified\": {\n          \"name\": \"dateModified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"imageUrl\": {\n          \"name\": \"imageUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"favicon\": {\n          \"name\": \"favicon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"htmlContent\": {\n          \"name\": \"htmlContent\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"contentAssetId\": {\n          \"name\": \"contentAssetId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawledAt\": {\n          \"name\": \"crawledAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"crawlStatus\": {\n          \"name\": \"crawlStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"crawlStatusCode\": {\n          \"name\": \"crawlStatusCode\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": 200\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLinks_url_idx\": {\n          \"name\": \"bookmarkLinks_url_idx\",\n          \"columns\": [\n            \"url\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLinks_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkLinks_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkLinks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkLists\": {\n      \"name\": \"bookmarkLists\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"icon\": {\n          \"name\": \"icon\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"query\": {\n          \"name\": \"query\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"parentId\": {\n          \"name\": \"parentId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rssToken\": {\n          \"name\": \"rssToken\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"public\": {\n          \"name\": \"public\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkLists_userId_idx\": {\n          \"name\": \"bookmarkLists_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkLists_userId_id_idx\": {\n          \"name\": \"bookmarkLists_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkLists_userId_user_id_fk\": {\n          \"name\": \"bookmarkLists_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarkLists_parentId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarkLists_parentId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarkLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"parentId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTags\": {\n      \"name\": \"bookmarkTags\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"normalizedName\": {\n          \"name\": \"normalizedName\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"generated\": {\n            \"as\": \"(lower(replace(replace(replace(\\\"name\\\", ' ', ''), '-', ''), '_', '')))\",\n            \"type\": \"virtual\"\n          }\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarkTags_name_idx\": {\n          \"name\": \"bookmarkTags_name_idx\",\n          \"columns\": [\n            \"name\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_idx\": {\n          \"name\": \"bookmarkTags_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_normalizedName_idx\": {\n          \"name\": \"bookmarkTags_normalizedName_idx\",\n          \"columns\": [\n            \"normalizedName\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarkTags_userId_name_unique\": {\n          \"name\": \"bookmarkTags_userId_name_unique\",\n          \"columns\": [\n            \"userId\",\n            \"name\"\n          ],\n          \"isUnique\": true\n        },\n        \"bookmarkTags_userId_id_idx\": {\n          \"name\": \"bookmarkTags_userId_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarkTags_userId_user_id_fk\": {\n          \"name\": \"bookmarkTags_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarkTags\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarkTexts\": {\n      \"name\": \"bookmarkTexts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceUrl\": {\n          \"name\": \"sourceUrl\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"bookmarkTexts_id_bookmarks_id_fk\": {\n          \"name\": \"bookmarkTexts_id_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarkTexts\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"id\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarks\": {\n      \"name\": \"bookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"archived\": {\n          \"name\": \"archived\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"favourited\": {\n          \"name\": \"favourited\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"taggingStatus\": {\n          \"name\": \"taggingStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summarizationStatus\": {\n          \"name\": \"summarizationStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"summary\": {\n          \"name\": \"summary\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"source\": {\n          \"name\": \"source\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarks_userId_idx\": {\n          \"name\": \"bookmarks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_createdAt_idx\": {\n          \"name\": \"bookmarks_createdAt_idx\",\n          \"columns\": [\n            \"createdAt\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_userId_createdAt_id_idx\": {\n          \"name\": \"bookmarks_userId_createdAt_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"createdAt\",\n            \"id\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_userId_archived_createdAt_id_idx\": {\n          \"name\": \"bookmarks_userId_archived_createdAt_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"archived\",\n            \"createdAt\",\n            \"id\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarks_userId_favourited_createdAt_id_idx\": {\n          \"name\": \"bookmarks_userId_favourited_createdAt_id_idx\",\n          \"columns\": [\n            \"userId\",\n            \"favourited\",\n            \"createdAt\",\n            \"id\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarks_userId_user_id_fk\": {\n          \"name\": \"bookmarks_userId_user_id_fk\",\n          \"tableFrom\": \"bookmarks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"bookmarksInLists\": {\n      \"name\": \"bookmarksInLists\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedAt\": {\n          \"name\": \"addedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"listMembershipId\": {\n          \"name\": \"listMembershipId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"bookmarksInLists_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_idx\": {\n          \"name\": \"bookmarksInLists_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"bookmarksInLists_listId_bookmarkId_idx\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkId_idx\",\n          \"columns\": [\n            \"listId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"bookmarksInLists_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"bookmarksInLists_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listId_bookmarkLists_id_fk\": {\n          \"name\": \"bookmarksInLists_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"bookmarksInLists_listMembershipId_listCollaborators_id_fk\": {\n          \"name\": \"bookmarksInLists_listMembershipId_listCollaborators_id_fk\",\n          \"tableFrom\": \"bookmarksInLists\",\n          \"tableTo\": \"listCollaborators\",\n          \"columnsFrom\": [\n            \"listMembershipId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"bookmarksInLists_bookmarkId_listId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"listId\"\n          ],\n          \"name\": \"bookmarksInLists_bookmarkId_listId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"config\": {\n      \"name\": \"config\",\n      \"columns\": {\n        \"key\": {\n          \"name\": \"key\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"value\": {\n          \"name\": \"value\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"customPrompts\": {\n      \"name\": \"customPrompts\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"appliesTo\": {\n          \"name\": \"appliesTo\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"customPrompts_userId_idx\": {\n          \"name\": \"customPrompts_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"customPrompts_userId_user_id_fk\": {\n          \"name\": \"customPrompts_userId_user_id_fk\",\n          \"tableFrom\": \"customPrompts\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"highlights\": {\n      \"name\": \"highlights\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"startOffset\": {\n          \"name\": \"startOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"endOffset\": {\n          \"name\": \"endOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"color\": {\n          \"name\": \"color\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'yellow'\"\n        },\n        \"text\": {\n          \"name\": \"text\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"highlights_bookmarkId_idx\": {\n          \"name\": \"highlights_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"highlights_userId_idx\": {\n          \"name\": \"highlights_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"highlights_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"highlights_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"highlights_userId_user_id_fk\": {\n          \"name\": \"highlights_userId_user_id_fk\",\n          \"tableFrom\": \"highlights\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importSessionBookmarks\": {\n      \"name\": \"importSessionBookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"importSessionId\": {\n          \"name\": \"importSessionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importSessionBookmarks_sessionId_idx\": {\n          \"name\": \"importSessionBookmarks_sessionId_idx\",\n          \"columns\": [\n            \"importSessionId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessionBookmarks_bookmarkId_idx\": {\n          \"name\": \"importSessionBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessionBookmarks_importSessionId_bookmarkId_unique\": {\n          \"name\": \"importSessionBookmarks_importSessionId_bookmarkId_unique\",\n          \"columns\": [\n            \"importSessionId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"importSessionBookmarks_importSessionId_importSessions_id_fk\": {\n          \"name\": \"importSessionBookmarks_importSessionId_importSessions_id_fk\",\n          \"tableFrom\": \"importSessionBookmarks\",\n          \"tableTo\": \"importSessions\",\n          \"columnsFrom\": [\n            \"importSessionId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"importSessionBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"importSessionBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"importSessionBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importSessions\": {\n      \"name\": \"importSessions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"message\": {\n          \"name\": \"message\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"rootListId\": {\n          \"name\": \"rootListId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'staging'\"\n        },\n        \"lastProcessedAt\": {\n          \"name\": \"lastProcessedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importSessions_userId_idx\": {\n          \"name\": \"importSessions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"importSessions_status_idx\": {\n          \"name\": \"importSessions_status_idx\",\n          \"columns\": [\n            \"status\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"importSessions_userId_user_id_fk\": {\n          \"name\": \"importSessions_userId_user_id_fk\",\n          \"tableFrom\": \"importSessions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"importSessions_rootListId_bookmarkLists_id_fk\": {\n          \"name\": \"importSessions_rootListId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"importSessions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"rootListId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"importStagingBookmarks\": {\n      \"name\": \"importStagingBookmarks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"importSessionId\": {\n          \"name\": \"importSessionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"type\": {\n          \"name\": \"type\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"title\": {\n          \"name\": \"title\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"content\": {\n          \"name\": \"content\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"note\": {\n          \"name\": \"note\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tags\": {\n          \"name\": \"tags\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"listIds\": {\n          \"name\": \"listIds\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"sourceAddedAt\": {\n          \"name\": \"sourceAddedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"processingStartedAt\": {\n          \"name\": \"processingStartedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"result\": {\n          \"name\": \"result\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resultReason\": {\n          \"name\": \"resultReason\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"resultBookmarkId\": {\n          \"name\": \"resultBookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"completedAt\": {\n          \"name\": \"completedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"importStaging_session_status_idx\": {\n          \"name\": \"importStaging_session_status_idx\",\n          \"columns\": [\n            \"importSessionId\",\n            \"status\"\n          ],\n          \"isUnique\": false\n        },\n        \"importStaging_completedAt_idx\": {\n          \"name\": \"importStaging_completedAt_idx\",\n          \"columns\": [\n            \"completedAt\"\n          ],\n          \"isUnique\": false\n        },\n        \"importStaging_status_idx\": {\n          \"name\": \"importStaging_status_idx\",\n          \"columns\": [\n            \"status\"\n          ],\n          \"isUnique\": false\n        },\n        \"importStaging_status_processingStartedAt_idx\": {\n          \"name\": \"importStaging_status_processingStartedAt_idx\",\n          \"columns\": [\n            \"status\",\n            \"processingStartedAt\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"importStagingBookmarks_importSessionId_importSessions_id_fk\": {\n          \"name\": \"importStagingBookmarks_importSessionId_importSessions_id_fk\",\n          \"tableFrom\": \"importStagingBookmarks\",\n          \"tableTo\": \"importSessions\",\n          \"columnsFrom\": [\n            \"importSessionId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"importStagingBookmarks_resultBookmarkId_bookmarks_id_fk\": {\n          \"name\": \"importStagingBookmarks_resultBookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"importStagingBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"resultBookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"invites\": {\n      \"name\": \"invites\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"usedAt\": {\n          \"name\": \"usedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"invites_token_unique\": {\n          \"name\": \"invites_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"invites_invitedBy_user_id_fk\": {\n          \"name\": \"invites_invitedBy_user_id_fk\",\n          \"tableFrom\": \"invites\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"listCollaborators\": {\n      \"name\": \"listCollaborators\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"addedBy\": {\n          \"name\": \"addedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"listCollaborators_listId_idx\": {\n          \"name\": \"listCollaborators_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listCollaborators_userId_idx\": {\n          \"name\": \"listCollaborators_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listCollaborators_listId_userId_unique\": {\n          \"name\": \"listCollaborators_listId_userId_unique\",\n          \"columns\": [\n            \"listId\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"listCollaborators_listId_bookmarkLists_id_fk\": {\n          \"name\": \"listCollaborators_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listCollaborators_userId_user_id_fk\": {\n          \"name\": \"listCollaborators_userId_user_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listCollaborators_addedBy_user_id_fk\": {\n          \"name\": \"listCollaborators_addedBy_user_id_fk\",\n          \"tableFrom\": \"listCollaborators\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"addedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"listInvitations\": {\n      \"name\": \"listInvitations\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"invitedAt\": {\n          \"name\": \"invitedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"invitedEmail\": {\n          \"name\": \"invitedEmail\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"invitedBy\": {\n          \"name\": \"invitedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"listInvitations_listId_idx\": {\n          \"name\": \"listInvitations_listId_idx\",\n          \"columns\": [\n            \"listId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_userId_idx\": {\n          \"name\": \"listInvitations_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_status_idx\": {\n          \"name\": \"listInvitations_status_idx\",\n          \"columns\": [\n            \"status\"\n          ],\n          \"isUnique\": false\n        },\n        \"listInvitations_listId_userId_unique\": {\n          \"name\": \"listInvitations_listId_userId_unique\",\n          \"columns\": [\n            \"listId\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"listInvitations_listId_bookmarkLists_id_fk\": {\n          \"name\": \"listInvitations_listId_bookmarkLists_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listInvitations_userId_user_id_fk\": {\n          \"name\": \"listInvitations_userId_user_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"listInvitations_invitedBy_user_id_fk\": {\n          \"name\": \"listInvitations_invitedBy_user_id_fk\",\n          \"tableFrom\": \"listInvitations\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"invitedBy\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"passwordResetToken\": {\n      \"name\": \"passwordResetToken\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"passwordResetToken_token_unique\": {\n          \"name\": \"passwordResetToken_token_unique\",\n          \"columns\": [\n            \"token\"\n          ],\n          \"isUnique\": true\n        },\n        \"passwordResetTokens_userId_idx\": {\n          \"name\": \"passwordResetTokens_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"passwordResetToken_userId_user_id_fk\": {\n          \"name\": \"passwordResetToken_userId_user_id_fk\",\n          \"tableFrom\": \"passwordResetToken\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeedImports\": {\n      \"name\": \"rssFeedImports\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"entryId\": {\n          \"name\": \"entryId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"rssFeedId\": {\n          \"name\": \"rssFeedId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeedImports_feedIdIdx_idx\": {\n          \"name\": \"rssFeedImports_feedIdIdx_idx\",\n          \"columns\": [\n            \"rssFeedId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_entryIdIdx_idx\": {\n          \"name\": \"rssFeedImports_entryIdIdx_idx\",\n          \"columns\": [\n            \"entryId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_bookmarkId_idx\": {\n          \"name\": \"rssFeedImports_rssFeedId_bookmarkId_idx\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"rssFeedImports_rssFeedId_entryId_unique\": {\n          \"name\": \"rssFeedImports_rssFeedId_entryId_unique\",\n          \"columns\": [\n            \"rssFeedId\",\n            \"entryId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeedImports_rssFeedId_rssFeeds_id_fk\": {\n          \"name\": \"rssFeedImports_rssFeedId_rssFeeds_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"rssFeeds\",\n          \"columnsFrom\": [\n            \"rssFeedId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"rssFeedImports_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"rssFeedImports_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"rssFeedImports\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"set null\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"rssFeeds\": {\n      \"name\": \"rssFeeds\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"importTags\": {\n          \"name\": \"importTags\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"lastFetchedAt\": {\n          \"name\": \"lastFetchedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"lastFetchedStatus\": {\n          \"name\": \"lastFetchedStatus\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'pending'\"\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"rssFeeds_userId_idx\": {\n          \"name\": \"rssFeeds_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"rssFeeds_userId_user_id_fk\": {\n          \"name\": \"rssFeeds_userId_user_id_fk\",\n          \"tableFrom\": \"rssFeeds\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineActions\": {\n      \"name\": \"ruleEngineActions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"ruleId\": {\n          \"name\": \"ruleId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"action\": {\n          \"name\": \"action\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngineActions_userId_idx\": {\n          \"name\": \"ruleEngineActions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"ruleEngineActions_ruleId_idx\": {\n          \"name\": \"ruleEngineActions_ruleId_idx\",\n          \"columns\": [\n            \"ruleId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineActions_userId_user_id_fk\": {\n          \"name\": \"ruleEngineActions_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\": {\n          \"name\": \"ruleEngineActions_ruleId_ruleEngineRules_id_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"ruleEngineRules\",\n          \"columnsFrom\": [\n            \"ruleId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_tagId_fk\": {\n          \"name\": \"ruleEngineActions_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineActions_userId_listId_fk\": {\n          \"name\": \"ruleEngineActions_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineActions\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"ruleEngineRules\": {\n      \"name\": \"ruleEngineRules\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"enabled\": {\n          \"name\": \"enabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": true\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"description\": {\n          \"name\": \"description\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"event\": {\n          \"name\": \"event\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"condition\": {\n          \"name\": \"condition\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"listId\": {\n          \"name\": \"listId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"ruleEngine_userId_idx\": {\n          \"name\": \"ruleEngine_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"ruleEngineRules_userId_user_id_fk\": {\n          \"name\": \"ruleEngineRules_userId_user_id_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_tagId_fk\": {\n          \"name\": \"ruleEngineRules_userId_tagId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"ruleEngineRules_userId_listId_fk\": {\n          \"name\": \"ruleEngineRules_userId_listId_fk\",\n          \"tableFrom\": \"ruleEngineRules\",\n          \"tableTo\": \"bookmarkLists\",\n          \"columnsFrom\": [\n            \"userId\",\n            \"listId\"\n          ],\n          \"columnsTo\": [\n            \"userId\",\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"session\": {\n      \"name\": \"session\",\n      \"columns\": {\n        \"sessionToken\": {\n          \"name\": \"sessionToken\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {\n        \"session_userId_user_id_fk\": {\n          \"name\": \"session_userId_user_id_fk\",\n          \"tableFrom\": \"session\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"subscriptions\": {\n      \"name\": \"subscriptions\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeCustomerId\": {\n          \"name\": \"stripeCustomerId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"stripeSubscriptionId\": {\n          \"name\": \"stripeSubscriptionId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"status\": {\n          \"name\": \"status\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tier\": {\n          \"name\": \"tier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'free'\"\n        },\n        \"priceId\": {\n          \"name\": \"priceId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"cancelAtPeriodEnd\": {\n          \"name\": \"cancelAtPeriodEnd\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"startDate\": {\n          \"name\": \"startDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"endDate\": {\n          \"name\": \"endDate\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"subscriptions_userId_unique\": {\n          \"name\": \"subscriptions_userId_unique\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": true\n        },\n        \"subscriptions_userId_idx\": {\n          \"name\": \"subscriptions_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"subscriptions_stripeCustomerId_idx\": {\n          \"name\": \"subscriptions_stripeCustomerId_idx\",\n          \"columns\": [\n            \"stripeCustomerId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"subscriptions_userId_user_id_fk\": {\n          \"name\": \"subscriptions_userId_user_id_fk\",\n          \"tableFrom\": \"subscriptions\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"tagsOnBookmarks\": {\n      \"name\": \"tagsOnBookmarks\",\n      \"columns\": {\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"tagId\": {\n          \"name\": \"tagId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"attachedAt\": {\n          \"name\": \"attachedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"attachedBy\": {\n          \"name\": \"attachedBy\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"tagsOnBookmarks_tagId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_idx\",\n          \"columns\": [\n            \"tagId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"tagsOnBookmarks_tagId_bookmarkId_idx\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkId_idx\",\n          \"columns\": [\n            \"tagId\",\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"tagsOnBookmarks_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\": {\n          \"name\": \"tagsOnBookmarks_tagId_bookmarkTags_id_fk\",\n          \"tableFrom\": \"tagsOnBookmarks\",\n          \"tableTo\": \"bookmarkTags\",\n          \"columnsFrom\": [\n            \"tagId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {\n        \"tagsOnBookmarks_bookmarkId_tagId_pk\": {\n          \"columns\": [\n            \"bookmarkId\",\n            \"tagId\"\n          ],\n          \"name\": \"tagsOnBookmarks_bookmarkId_tagId_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"userReadingProgress\": {\n      \"name\": \"userReadingProgress\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"bookmarkId\": {\n          \"name\": \"bookmarkId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"readingProgressOffset\": {\n          \"name\": \"readingProgressOffset\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"readingProgressAnchor\": {\n          \"name\": \"readingProgressAnchor\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"readingProgressPercent\": {\n          \"name\": \"readingProgressPercent\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"modifiedAt\": {\n          \"name\": \"modifiedAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"userReadingProgress_bookmarkId_idx\": {\n          \"name\": \"userReadingProgress_bookmarkId_idx\",\n          \"columns\": [\n            \"bookmarkId\"\n          ],\n          \"isUnique\": false\n        },\n        \"userReadingProgress_userId_idx\": {\n          \"name\": \"userReadingProgress_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        },\n        \"userReadingProgress_bookmarkId_userId_unique\": {\n          \"name\": \"userReadingProgress_bookmarkId_userId_unique\",\n          \"columns\": [\n            \"bookmarkId\",\n            \"userId\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {\n        \"userReadingProgress_bookmarkId_bookmarks_id_fk\": {\n          \"name\": \"userReadingProgress_bookmarkId_bookmarks_id_fk\",\n          \"tableFrom\": \"userReadingProgress\",\n          \"tableTo\": \"bookmarks\",\n          \"columnsFrom\": [\n            \"bookmarkId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        },\n        \"userReadingProgress_userId_user_id_fk\": {\n          \"name\": \"userReadingProgress_userId_user_id_fk\",\n          \"tableFrom\": \"userReadingProgress\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"user\": {\n      \"name\": \"user\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"name\": {\n          \"name\": \"name\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"email\": {\n          \"name\": \"email\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"emailVerified\": {\n          \"name\": \"emailVerified\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"image\": {\n          \"name\": \"image\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"password\": {\n          \"name\": \"password\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"salt\": {\n          \"name\": \"salt\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"''\"\n        },\n        \"role\": {\n          \"name\": \"role\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'user'\"\n        },\n        \"bookmarkQuota\": {\n          \"name\": \"bookmarkQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"storageQuota\": {\n          \"name\": \"storageQuota\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"browserCrawlingEnabled\": {\n          \"name\": \"browserCrawlingEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"bookmarkClickAction\": {\n          \"name\": \"bookmarkClickAction\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'open_original_link'\"\n        },\n        \"archiveDisplayBehaviour\": {\n          \"name\": \"archiveDisplayBehaviour\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'show'\"\n        },\n        \"timezone\": {\n          \"name\": \"timezone\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'UTC'\"\n        },\n        \"backupsEnabled\": {\n          \"name\": \"backupsEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": false\n        },\n        \"backupsFrequency\": {\n          \"name\": \"backupsFrequency\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": \"'weekly'\"\n        },\n        \"backupsRetentionDays\": {\n          \"name\": \"backupsRetentionDays\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false,\n          \"default\": 30\n        },\n        \"readerFontSize\": {\n          \"name\": \"readerFontSize\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"readerLineHeight\": {\n          \"name\": \"readerLineHeight\",\n          \"type\": \"real\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"readerFontFamily\": {\n          \"name\": \"readerFontFamily\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"autoTaggingEnabled\": {\n          \"name\": \"autoTaggingEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"autoSummarizationEnabled\": {\n          \"name\": \"autoSummarizationEnabled\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"tagStyle\": {\n          \"name\": \"tagStyle\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false,\n          \"default\": \"'titlecase-spaces'\"\n        },\n        \"curatedTagIds\": {\n          \"name\": \"curatedTagIds\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        },\n        \"inferredTagLang\": {\n          \"name\": \"inferredTagLang\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"user_email_unique\": {\n          \"name\": \"user_email_unique\",\n          \"columns\": [\n            \"email\"\n          ],\n          \"isUnique\": true\n        }\n      },\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"verificationToken\": {\n      \"name\": \"verificationToken\",\n      \"columns\": {\n        \"identifier\": {\n          \"name\": \"identifier\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"expires\": {\n          \"name\": \"expires\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {},\n      \"foreignKeys\": {},\n      \"compositePrimaryKeys\": {\n        \"verificationToken_identifier_token_pk\": {\n          \"columns\": [\n            \"identifier\",\n            \"token\"\n          ],\n          \"name\": \"verificationToken_identifier_token_pk\"\n        }\n      },\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    },\n    \"webhooks\": {\n      \"name\": \"webhooks\",\n      \"columns\": {\n        \"id\": {\n          \"name\": \"id\",\n          \"type\": \"text\",\n          \"primaryKey\": true,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"createdAt\": {\n          \"name\": \"createdAt\",\n          \"type\": \"integer\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"url\": {\n          \"name\": \"url\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"userId\": {\n          \"name\": \"userId\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"events\": {\n          \"name\": \"events\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": true,\n          \"autoincrement\": false\n        },\n        \"token\": {\n          \"name\": \"token\",\n          \"type\": \"text\",\n          \"primaryKey\": false,\n          \"notNull\": false,\n          \"autoincrement\": false\n        }\n      },\n      \"indexes\": {\n        \"webhooks_userId_idx\": {\n          \"name\": \"webhooks_userId_idx\",\n          \"columns\": [\n            \"userId\"\n          ],\n          \"isUnique\": false\n        }\n      },\n      \"foreignKeys\": {\n        \"webhooks_userId_user_id_fk\": {\n          \"name\": \"webhooks_userId_user_id_fk\",\n          \"tableFrom\": \"webhooks\",\n          \"tableTo\": \"user\",\n          \"columnsFrom\": [\n            \"userId\"\n          ],\n          \"columnsTo\": [\n            \"id\"\n          ],\n          \"onDelete\": \"cascade\",\n          \"onUpdate\": \"no action\"\n        }\n      },\n      \"compositePrimaryKeys\": {},\n      \"uniqueConstraints\": {},\n      \"checkConstraints\": {}\n    }\n  },\n  \"views\": {},\n  \"enums\": {},\n  \"_meta\": {\n    \"schemas\": {},\n    \"tables\": {},\n    \"columns\": {}\n  },\n  \"internal\": {\n    \"indexes\": {}\n  }\n}"
  },
  {
    "path": "packages/db/drizzle/meta/_journal.json",
    "content": "{\n  \"version\": \"5\",\n  \"dialect\": \"sqlite\",\n  \"entries\": [\n    {\n      \"idx\": 0,\n      \"version\": \"5\",\n      \"when\": 1708710681721,\n      \"tag\": \"0000_luxuriant_johnny_blaze\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 1,\n      \"version\": \"5\",\n      \"when\": 1709144284383,\n      \"tag\": \"0001_dapper_trauma\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 2,\n      \"version\": \"5\",\n      \"when\": 1709293861959,\n      \"tag\": \"0002_worried_beyonder\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 3,\n      \"version\": \"5\",\n      \"when\": 1709331420929,\n      \"tag\": \"0003_parallel_supernaut\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 4,\n      \"version\": \"5\",\n      \"when\": 1709339866036,\n      \"tag\": \"0004_skinny_vengeance\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 5,\n      \"version\": \"5\",\n      \"when\": 1709341990430,\n      \"tag\": \"0005_quiet_gunslinger\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 6,\n      \"version\": \"5\",\n      \"when\": 1709376352390,\n      \"tag\": \"0006_funny_mac_gargan\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 7,\n      \"version\": \"5\",\n      \"when\": 1709599077219,\n      \"tag\": \"0007_messy_raza\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 8,\n      \"version\": \"5\",\n      \"when\": 1710550864756,\n      \"tag\": \"0008_cloudy_skin\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 9,\n      \"version\": \"5\",\n      \"when\": 1710681166089,\n      \"tag\": \"0009_cuddly_cammi\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 10,\n      \"version\": \"5\",\n      \"when\": 1710770092205,\n      \"tag\": \"0010_curved_sharon_ventura\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 11,\n      \"version\": \"5\",\n      \"when\": 1710778903315,\n      \"tag\": \"0011_ordinary_phalanx\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 12,\n      \"version\": \"5\",\n      \"when\": 1710812490438,\n      \"tag\": \"0012_noisy_grim_reaper\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 13,\n      \"version\": \"5\",\n      \"when\": 1710813047585,\n      \"tag\": \"0013_square_lady_ursula\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 14,\n      \"version\": \"5\",\n      \"when\": 1711767601057,\n      \"tag\": \"0014_lonely_thaddeus_ross\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 15,\n      \"version\": \"5\",\n      \"when\": 1712584035880,\n      \"tag\": \"0015_first_reavers\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 16,\n      \"version\": \"5\",\n      \"when\": 1712610210210,\n      \"tag\": \"0016_shallow_rawhide_kid\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 17,\n      \"version\": \"5\",\n      \"when\": 1712837113359,\n      \"tag\": \"0017_slippery_senator_kelly\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 18,\n      \"version\": \"5\",\n      \"when\": 1713183014188,\n      \"tag\": \"0018_bright_infant_terrible\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 19,\n      \"version\": \"5\",\n      \"when\": 1713432890859,\n      \"tag\": \"0019_many_vertigo\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 20,\n      \"version\": \"5\",\n      \"when\": 1713539346326,\n      \"tag\": \"0020_sudden_dagger\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 21,\n      \"version\": \"5\",\n      \"when\": 1716031428677,\n      \"tag\": \"0021_magical_firebrand\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 22,\n      \"version\": \"5\",\n      \"when\": 1716679762529,\n      \"tag\": \"0022_tough_nextwave\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 23,\n      \"version\": \"5\",\n      \"when\": 1717960986361,\n      \"tag\": \"0023_late_night_nurse\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 24,\n      \"version\": \"5\",\n      \"when\": 1719135100480,\n      \"tag\": \"0024_premium_hammerhead\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 25,\n      \"version\": \"5\",\n      \"when\": 1719251349398,\n      \"tag\": \"0025_aspiring_skaar\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 26,\n      \"version\": \"5\",\n      \"when\": 1720334457344,\n      \"tag\": \"0026_silky_imperial_guard\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 27,\n      \"version\": \"6\",\n      \"when\": 1727572281889,\n      \"tag\": \"0027_cute_talon\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 28,\n      \"version\": \"6\",\n      \"when\": 1728149644203,\n      \"tag\": \"0028_melodic_norrin_radd\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 29,\n      \"version\": \"6\",\n      \"when\": 1728214930701,\n      \"tag\": \"0029_short_gunslinger\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 30,\n      \"version\": \"6\",\n      \"when\": 1728220453621,\n      \"tag\": \"0030_blue_synch\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 31,\n      \"version\": \"6\",\n      \"when\": 1729980727614,\n      \"tag\": \"0031_yummy_famine\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 32,\n      \"version\": \"6\",\n      \"when\": 1730653452808,\n      \"tag\": \"0032_futuristic_shiva\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 33,\n      \"version\": \"6\",\n      \"when\": 1731106561236,\n      \"tag\": \"0033_nappy_molten_man\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 34,\n      \"version\": \"6\",\n      \"when\": 1732990622928,\n      \"tag\": \"0034_wet_the_stranger\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 35,\n      \"version\": \"6\",\n      \"when\": 1735291137509,\n      \"tag\": \"0035_gorgeous_may_parker\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 36,\n      \"version\": \"6\",\n      \"when\": 1735308236125,\n      \"tag\": \"0036_luxuriant_white_queen\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 37,\n      \"version\": \"6\",\n      \"when\": 1735750275339,\n      \"tag\": \"0037_daily_smiling_tiger\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 38,\n      \"version\": \"6\",\n      \"when\": 1736695194056,\n      \"tag\": \"0038_calm_clint_barton\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 39,\n      \"version\": \"6\",\n      \"when\": 1737293459640,\n      \"tag\": \"0039_purple_albert_cleary\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 40,\n      \"version\": \"6\",\n      \"when\": 1737310389771,\n      \"tag\": \"0040_long_mindworm\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 41,\n      \"version\": \"6\",\n      \"when\": 1738424745186,\n      \"tag\": \"0041_fat_bloodstrike\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 42,\n      \"version\": \"6\",\n      \"when\": 1742655644239,\n      \"tag\": \"0042_square_gamma_corps\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 43,\n      \"version\": \"6\",\n      \"when\": 1744157597541,\n      \"tag\": \"0043_puzzling_blonde_phantom\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 44,\n      \"version\": \"6\",\n      \"when\": 1744744684677,\n      \"tag\": \"0044_add_password_salt\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 45,\n      \"version\": \"6\",\n      \"when\": 1745705657846,\n      \"tag\": \"0045_add_rule_engine\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 46,\n      \"version\": \"6\",\n      \"when\": 1746902541511,\n      \"tag\": \"0046_add_rss_feed_enabled_col\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 47,\n      \"version\": \"6\",\n      \"when\": 1747598543992,\n      \"tag\": \"0047_add_summarization_status\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 48,\n      \"version\": \"6\",\n      \"when\": 1748086734370,\n      \"tag\": \"0048_add_user_settings\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 49,\n      \"version\": \"6\",\n      \"when\": 1748699971545,\n      \"tag\": \"0049_add_rss_token\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 50,\n      \"version\": \"6\",\n      \"when\": 1748795265779,\n      \"tag\": \"0050_add_user_settings_archive_display_behaviour\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 51,\n      \"version\": \"6\",\n      \"when\": 1748804695561,\n      \"tag\": \"0051_public_lists\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 52,\n      \"version\": \"6\",\n      \"when\": 1751409503089,\n      \"tag\": \"0052_add_bookmark_quota\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 53,\n      \"version\": \"6\",\n      \"when\": 1751816757805,\n      \"tag\": \"0053_storage_quota\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 54,\n      \"version\": \"6\",\n      \"when\": 1751826417328,\n      \"tag\": \"0054_add_timezone\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 55,\n      \"version\": \"6\",\n      \"when\": 1751839469055,\n      \"tag\": \"0055_content_asset_id\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 56,\n      \"version\": \"6\",\n      \"when\": 1752180326709,\n      \"tag\": \"0056_user_invites\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 57,\n      \"version\": \"6\",\n      \"when\": 1752314617600,\n      \"tag\": \"0057_salty_carmella_unuscione\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 58,\n      \"version\": \"6\",\n      \"when\": 1752436258865,\n      \"tag\": \"0058_add_subscription\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 59,\n      \"version\": \"6\",\n      \"when\": 1752922057728,\n      \"tag\": \"0059_browserless_user_setting\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 60,\n      \"version\": \"6\",\n      \"when\": 1754187929331,\n      \"tag\": \"0060_drop_invite_expire_at\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 61,\n      \"version\": \"6\",\n      \"when\": 1754236017965,\n      \"tag\": \"0061_merge_user_settings\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 62,\n      \"version\": \"6\",\n      \"when\": 1759573697911,\n      \"tag\": \"0062_add_import_session\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 63,\n      \"version\": \"6\",\n      \"when\": 1760302856618,\n      \"tag\": \"0063_add_bookmark_source\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 64,\n      \"version\": \"6\",\n      \"when\": 1762115406895,\n      \"tag\": \"0064_add_import_tags_to_feeds\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 65,\n      \"version\": \"6\",\n      \"when\": 1763335572156,\n      \"tag\": \"0065_collaborative_lists\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 66,\n      \"version\": \"6\",\n      \"when\": 1763854050669,\n      \"tag\": \"0066_collaborative_lists_invites\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 67,\n      \"version\": \"6\",\n      \"when\": 1764418020312,\n      \"tag\": \"0067_add_backups_table\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 68,\n      \"version\": \"6\",\n      \"when\": 1765310170813,\n      \"tag\": \"0068_optimize_bookmark_indicies\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 69,\n      \"version\": \"6\",\n      \"when\": 1765721715670,\n      \"tag\": \"0069_fix_pending_summarization\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 70,\n      \"version\": \"6\",\n      \"when\": 1765744716304,\n      \"tag\": \"0070_add_reader_settings\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 71,\n      \"version\": \"6\",\n      \"when\": 1766393060393,\n      \"tag\": \"0071_add_normalized_tag_name\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 72,\n      \"version\": \"6\",\n      \"when\": 1766414953855,\n      \"tag\": \"0072_add_user_ai_preferences\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 73,\n      \"version\": \"6\",\n      \"when\": 1766843938658,\n      \"tag\": \"0073_ai_tag_style\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 74,\n      \"version\": \"6\",\n      \"when\": 1767006387391,\n      \"tag\": \"0074_reset_tagging_summarization\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 75,\n      \"version\": \"6\",\n      \"when\": 1767052770526,\n      \"tag\": \"0075_change_default_tag_style\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 76,\n      \"version\": \"6\",\n      \"when\": 1768691440519,\n      \"tag\": \"0076_add_api_key_last_used_tracking\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 77,\n      \"version\": \"6\",\n      \"when\": 1770141423845,\n      \"tag\": \"0077_import_listpaths_to_listids\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 78,\n      \"version\": \"6\",\n      \"when\": 1770142086939,\n      \"tag\": \"0078_add_import_session_indexes\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 79,\n      \"version\": \"6\",\n      \"when\": 1770564384451,\n      \"tag\": \"0079_add_tag_granularity_settings\",\n      \"breakpoints\": true\n    },\n    {\n      \"idx\": 80,\n      \"version\": \"6\",\n      \"when\": 1771481519588,\n      \"tag\": \"0080_user_reading_progress\",\n      \"breakpoints\": true\n    }\n  ]\n}"
  },
  {
    "path": "packages/db/drizzle.config.ts",
    "content": "import \"dotenv/config\";\n\nimport type { Config } from \"drizzle-kit\";\n\nimport serverConfig from \"@karakeep/shared/config\";\n\nconst databaseURL = serverConfig.dataDir\n  ? `${serverConfig.dataDir}/db.db`\n  : \"./db.db\";\n\nexport default {\n  dialect: \"sqlite\",\n  schema: \"./schema.ts\",\n  out: \"./drizzle\",\n  dbCredentials: {\n    url: databaseURL,\n  },\n} satisfies Config;\n"
  },
  {
    "path": "packages/db/drizzle.ts",
    "content": "import \"dotenv/config\";\n\nimport path from \"path\";\nimport Database from \"better-sqlite3\";\nimport { drizzle } from \"drizzle-orm/better-sqlite3\";\nimport { migrate } from \"drizzle-orm/better-sqlite3/migrator\";\n\nimport serverConfig from \"@karakeep/shared/config\";\n\nimport dbConfig from \"./drizzle.config\";\nimport { instrumentDatabase } from \"./instrumentation\";\nimport * as schema from \"./schema\";\n\nconst sqlite = new Database(dbConfig.dbCredentials.url);\n\nif (serverConfig.database.walMode) {\n  sqlite.pragma(\"journal_mode = WAL\");\n  sqlite.pragma(\"synchronous = NORMAL\");\n} else {\n  sqlite.pragma(\"journal_mode = DELETE\");\n}\nsqlite.pragma(\"cache_size = -65536\");\nsqlite.pragma(\"foreign_keys = ON\");\nsqlite.pragma(\"temp_store = MEMORY\");\n\ninstrumentDatabase(sqlite);\n\nexport const db = drizzle(sqlite, { schema });\nexport type DB = typeof db;\n\nexport function getInMemoryDB(runMigrations: boolean) {\n  const mem = new Database(\":memory:\");\n  const db = drizzle(mem, { schema, logger: false });\n  if (runMigrations) {\n    migrate(db, { migrationsFolder: path.resolve(__dirname, \"./drizzle\") });\n  }\n  return db;\n}\n"
  },
  {
    "path": "packages/db/index.ts",
    "content": "import Database from \"better-sqlite3\";\nimport { ExtractTablesWithRelations } from \"drizzle-orm\";\nimport { SQLiteTransaction } from \"drizzle-orm/sqlite-core\";\n\nimport * as schema from \"./schema\";\n\nexport { db } from \"./drizzle\";\nexport type { DB } from \"./drizzle\";\nexport * as schema from \"./schema\";\nexport { SqliteError } from \"better-sqlite3\";\n\n// This is exported here to avoid leaking better-sqlite types outside of this package.\nexport type KarakeepDBTransaction = SQLiteTransaction<\n  \"sync\",\n  Database.RunResult,\n  typeof schema,\n  ExtractTablesWithRelations<typeof schema>\n>;\n"
  },
  {
    "path": "packages/db/instrumentation.ts",
    "content": "import type Database from \"better-sqlite3\";\nimport { SpanKind, SpanStatusCode, trace } from \"@opentelemetry/api\";\n\nconst TRACER_NAME = \"@karakeep/db\";\n\nfunction getOperationType(sql: string): string {\n  return sql.trimStart().split(/\\s/, 1)[0]?.toUpperCase() ?? \"UNKNOWN\";\n}\n\n/**\n * Instruments a better-sqlite3 Database instance with OpenTelemetry tracing.\n *\n * Wraps `prepare()` so that every `run()`, `get()`, and `all()` call on\n * the returned Statement produces an OTel span with db.system, db.statement,\n * and db.operation attributes.\n *\n * The instrumentation is a no-op when no OTel TracerProvider is registered\n * (i.e. when tracing is disabled), following standard OTel conventions.\n */\nexport function instrumentDatabase(\n  sqlite: Database.Database,\n): Database.Database {\n  const tracer = trace.getTracer(TRACER_NAME);\n  const origPrepare = sqlite.prepare.bind(sqlite);\n\n  sqlite.prepare = function (sql: string) {\n    const stmt = origPrepare(sql);\n    const operation = getOperationType(sql);\n    const spanName = `db.${operation.toLowerCase()}`;\n\n    for (const method of [\"run\", \"get\", \"all\"] as const) {\n      type QueryFn = (...args: unknown[]) => unknown;\n      const original = (stmt[method] as QueryFn).bind(stmt);\n      (stmt[method] as QueryFn) = function (...args: unknown[]) {\n        const span = tracer.startSpan(spanName, {\n          kind: SpanKind.CLIENT,\n          attributes: {\n            \"db.system\": \"sqlite\",\n            \"db.statement\": sql,\n            \"db.operation\": operation,\n          },\n        });\n\n        try {\n          const result = original(...args);\n          span.setStatus({ code: SpanStatusCode.OK });\n          return result;\n        } catch (error) {\n          span.setStatus({\n            code: SpanStatusCode.ERROR,\n            message: error instanceof Error ? error.message : String(error),\n          });\n          span.recordException(\n            error instanceof Error ? error : new Error(String(error)),\n          );\n          throw error;\n        } finally {\n          span.end();\n        }\n      };\n    }\n\n    return stmt;\n  } as typeof sqlite.prepare;\n\n  return sqlite;\n}\n"
  },
  {
    "path": "packages/db/migrate.ts",
    "content": "import { migrate } from \"drizzle-orm/better-sqlite3/migrator\";\n\nimport { db } from \"./drizzle\";\n\nmigrate(db, { migrationsFolder: \"./drizzle\" });\n"
  },
  {
    "path": "packages/db/package.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/package.json\",\n  \"name\": \"@karakeep/db\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"main\": \"index.ts\",\n  \"scripts\": {\n    \"typecheck\": \"tsc --noEmit\",\n    \"migrate\": \"tsx migrate.ts\",\n    \"generate\": \"drizzle-kit generate\",\n    \"studio\": \"drizzle-kit studio\",\n    \"format\": \"oxfmt --check .\",\n    \"format:fix\": \"oxfmt .\",\n    \"lint\": \"oxlint .\",\n    \"lint:fix\": \"oxlint . --fix\"\n  },\n  \"dependencies\": {\n    \"@auth/core\": \"^0.27.0\",\n    \"@karakeep/shared\": \"workspace:*\",\n    \"@opentelemetry/api\": \"^1.9.0\",\n    \"@paralleldrive/cuid2\": \"^2.2.2\",\n    \"better-sqlite3\": \"^11.3.0\",\n    \"dotenv\": \"^16.4.1\",\n    \"drizzle-kit\": \"^0.31.4\",\n    \"drizzle-orm\": \"^0.44.2\",\n    \"tsx\": \"^4.8.1\"\n  },\n  \"devDependencies\": {\n    \"@karakeep/tsconfig\": \"workspace:^0.1.0\",\n    \"@tsconfig/node22\": \"^22.0.0\",\n    \"@types/better-sqlite3\": \"^7.6.11\",\n    \"@vercel/ncc\": \"^0.38.4\"\n  }\n}\n"
  },
  {
    "path": "packages/db/schema.ts",
    "content": "import type { AdapterAccount } from \"@auth/core/adapters\";\nimport { createId } from \"@paralleldrive/cuid2\";\nimport { relations, sql, SQL } from \"drizzle-orm\";\nimport {\n  AnySQLiteColumn,\n  foreignKey,\n  index,\n  integer,\n  primaryKey,\n  real,\n  sqliteTable,\n  text,\n  unique,\n} from \"drizzle-orm/sqlite-core\";\n\nimport { BookmarkTypes } from \"@karakeep/shared/types/bookmarks\";\n\nfunction createdAtField() {\n  return integer(\"createdAt\", { mode: \"timestamp\" })\n    .notNull()\n    .$defaultFn(() => new Date());\n}\n\nfunction modifiedAtField() {\n  return integer(\"modifiedAt\", { mode: \"timestamp\" })\n    .$defaultFn(() => new Date())\n    .$onUpdate(() => new Date());\n}\n\nexport const users = sqliteTable(\"user\", {\n  id: text(\"id\")\n    .notNull()\n    .primaryKey()\n    .$defaultFn(() => createId()),\n  name: text(\"name\").notNull(),\n  email: text(\"email\").notNull().unique(),\n  emailVerified: integer(\"emailVerified\", { mode: \"timestamp_ms\" }),\n  image: text(\"image\"),\n  password: text(\"password\"),\n  salt: text(\"salt\").notNull().default(\"\"),\n  role: text(\"role\", { enum: [\"admin\", \"user\"] }).default(\"user\"),\n\n  // Admin Only Settings\n  bookmarkQuota: integer(\"bookmarkQuota\"),\n  storageQuota: integer(\"storageQuota\"),\n  browserCrawlingEnabled: integer(\"browserCrawlingEnabled\", {\n    mode: \"boolean\",\n  }),\n\n  // User Settings\n  bookmarkClickAction: text(\"bookmarkClickAction\", {\n    enum: [\"open_original_link\", \"expand_bookmark_preview\"],\n  })\n    .notNull()\n    .default(\"open_original_link\"),\n  archiveDisplayBehaviour: text(\"archiveDisplayBehaviour\", {\n    enum: [\"show\", \"hide\"],\n  })\n    .notNull()\n    .default(\"show\"),\n  timezone: text(\"timezone\").default(\"UTC\"),\n\n  // Backup Settings\n  backupsEnabled: integer(\"backupsEnabled\", { mode: \"boolean\" })\n    .notNull()\n    .default(false),\n  backupsFrequency: text(\"backupsFrequency\", {\n    enum: [\"daily\", \"weekly\"],\n  })\n    .notNull()\n    .default(\"weekly\"),\n  backupsRetentionDays: integer(\"backupsRetentionDays\").notNull().default(30),\n\n  // Reader view settings (nullable = opt-in, null means use client default)\n  readerFontSize: integer(\"readerFontSize\"),\n  readerLineHeight: real(\"readerLineHeight\"),\n  readerFontFamily: text(\"readerFontFamily\", {\n    enum: [\"serif\", \"sans\", \"mono\"],\n  }),\n\n  // AI Settings (nullable = opt-in, null means use server default)\n  autoTaggingEnabled: integer(\"autoTaggingEnabled\", { mode: \"boolean\" }),\n  autoSummarizationEnabled: integer(\"autoSummarizationEnabled\", {\n    mode: \"boolean\",\n  }),\n  tagStyle: text(\"tagStyle\", {\n    enum: [\n      \"lowercase-hyphens\",\n      \"lowercase-spaces\",\n      \"lowercase-underscores\",\n      \"titlecase-spaces\",\n      \"titlecase-hyphens\",\n      \"camelCase\",\n      \"as-generated\",\n    ],\n  }).default(\"titlecase-spaces\"),\n  curatedTagIds: text(\"curatedTagIds\", { mode: \"json\" }).$type<string[]>(),\n  inferredTagLang: text(\"inferredTagLang\"),\n});\n\nexport const accounts = sqliteTable(\n  \"account\",\n  {\n    userId: text(\"userId\")\n      .notNull()\n      .references(() => users.id, { onDelete: \"cascade\" }),\n    type: text(\"type\").$type<AdapterAccount[\"type\"]>().notNull(),\n    provider: text(\"provider\").notNull(),\n    providerAccountId: text(\"providerAccountId\").notNull(),\n    refresh_token: text(\"refresh_token\"),\n    access_token: text(\"access_token\"),\n    expires_at: integer(\"expires_at\"),\n    token_type: text(\"token_type\"),\n    scope: text(\"scope\"),\n    id_token: text(\"id_token\"),\n    session_state: text(\"session_state\"),\n  },\n  (account) => [\n    primaryKey({\n      columns: [account.provider, account.providerAccountId],\n    }),\n  ],\n);\n\nexport const sessions = sqliteTable(\"session\", {\n  sessionToken: text(\"sessionToken\")\n    .notNull()\n    .primaryKey()\n    .$defaultFn(() => createId()),\n  userId: text(\"userId\")\n    .notNull()\n    .references(() => users.id, { onDelete: \"cascade\" }),\n  expires: integer(\"expires\", { mode: \"timestamp_ms\" }).notNull(),\n});\n\nexport const verificationTokens = sqliteTable(\n  \"verificationToken\",\n  {\n    identifier: text(\"identifier\").notNull(),\n    token: text(\"token\").notNull(),\n    expires: integer(\"expires\", { mode: \"timestamp_ms\" }).notNull(),\n  },\n  (vt) => [primaryKey({ columns: [vt.identifier, vt.token] })],\n);\n\nexport const passwordResetTokens = sqliteTable(\n  \"passwordResetToken\",\n  {\n    id: text(\"id\")\n      .notNull()\n      .primaryKey()\n      .$defaultFn(() => createId()),\n    userId: text(\"userId\")\n      .notNull()\n      .references(() => users.id, { onDelete: \"cascade\" }),\n    token: text(\"token\").notNull().unique(),\n    expires: integer(\"expires\", { mode: \"timestamp_ms\" }).notNull(),\n    createdAt: createdAtField(),\n  },\n  (prt) => [index(\"passwordResetTokens_userId_idx\").on(prt.userId)],\n);\n\nexport const apiKeys = sqliteTable(\n  \"apiKey\",\n  {\n    id: text(\"id\")\n      .notNull()\n      .primaryKey()\n      .$defaultFn(() => createId()),\n    name: text(\"name\").notNull(),\n    createdAt: createdAtField(),\n    lastUsedAt: integer(\"lastUsedAt\", { mode: \"timestamp\" }),\n    keyId: text(\"keyId\").notNull().unique(),\n    keyHash: text(\"keyHash\").notNull(),\n    userId: text(\"userId\")\n      .notNull()\n      .references(() => users.id, { onDelete: \"cascade\" }),\n  },\n  (ak) => [unique().on(ak.name, ak.userId)],\n);\n\nexport const bookmarks = sqliteTable(\n  \"bookmarks\",\n  {\n    id: text(\"id\")\n      .notNull()\n      .primaryKey()\n      .$defaultFn(() => createId()),\n    createdAt: createdAtField(),\n    modifiedAt: modifiedAtField(),\n    title: text(\"title\"),\n    archived: integer(\"archived\", { mode: \"boolean\" }).notNull().default(false),\n    favourited: integer(\"favourited\", { mode: \"boolean\" })\n      .notNull()\n      .default(false),\n    userId: text(\"userId\")\n      .notNull()\n      .references(() => users.id, { onDelete: \"cascade\" }),\n    taggingStatus: text(\"taggingStatus\", {\n      enum: [\"pending\", \"failure\", \"success\"],\n    }).default(\"pending\"),\n    summarizationStatus: text(\"summarizationStatus\", {\n      enum: [\"pending\", \"failure\", \"success\"],\n    }).default(\"pending\"),\n    summary: text(\"summary\"),\n    note: text(\"note\"),\n    type: text(\"type\", {\n      enum: [BookmarkTypes.LINK, BookmarkTypes.TEXT, BookmarkTypes.ASSET],\n    }).notNull(),\n    source: text(\"source\", {\n      enum: [\n        \"api\",\n        \"web\",\n        \"extension\",\n        \"cli\",\n        \"mobile\",\n        \"singlefile\",\n        \"rss\",\n        \"import\",\n      ],\n    }),\n  },\n  (b) => [\n    index(\"bookmarks_userId_idx\").on(b.userId),\n    index(\"bookmarks_createdAt_idx\").on(b.createdAt),\n    // Composite indexes for optimized pagination queries\n    index(\"bookmarks_userId_createdAt_id_idx\").on(b.userId, b.createdAt, b.id),\n    index(\"bookmarks_userId_archived_createdAt_id_idx\").on(\n      b.userId,\n      b.archived,\n      b.createdAt,\n      b.id,\n    ),\n    index(\"bookmarks_userId_favourited_createdAt_id_idx\").on(\n      b.userId,\n      b.favourited,\n      b.createdAt,\n      b.id,\n    ),\n  ],\n);\n\nexport const bookmarkLinks = sqliteTable(\n  \"bookmarkLinks\",\n  {\n    id: text(\"id\")\n      .notNull()\n      .primaryKey()\n      .$defaultFn(() => createId())\n      .references(() => bookmarks.id, { onDelete: \"cascade\" }),\n    url: text(\"url\").notNull(),\n\n    // Crawled info\n    title: text(\"title\"),\n    description: text(\"description\"),\n    author: text(\"author\"),\n    publisher: text(\"publisher\"),\n    datePublished: integer(\"datePublished\", { mode: \"timestamp\" }),\n    dateModified: integer(\"dateModified\", { mode: \"timestamp\" }),\n    imageUrl: text(\"imageUrl\"),\n    favicon: text(\"favicon\"),\n    htmlContent: text(\"htmlContent\"),\n    contentAssetId: text(\"contentAssetId\"),\n    crawledAt: integer(\"crawledAt\", { mode: \"timestamp\" }),\n    crawlStatus: text(\"crawlStatus\", {\n      enum: [\"pending\", \"failure\", \"success\"],\n    }).default(\"pending\"),\n    crawlStatusCode: integer(\"crawlStatusCode\").default(200),\n  },\n  (bl) => [index(\"bookmarkLinks_url_idx\").on(bl.url)],\n);\n\nexport const enum AssetTypes {\n  LINK_BANNER_IMAGE = \"linkBannerImage\",\n  LINK_SCREENSHOT = \"linkScreenshot\",\n  LINK_PDF = \"linkPdf\",\n  ASSET_SCREENSHOT = \"assetScreenshot\",\n  LINK_FULL_PAGE_ARCHIVE = \"linkFullPageArchive\",\n  LINK_PRECRAWLED_ARCHIVE = \"linkPrecrawledArchive\",\n  LINK_VIDEO = \"linkVideo\",\n  LINK_HTML_CONTENT = \"linkHtmlContent\",\n  BOOKMARK_ASSET = \"bookmarkAsset\",\n  USER_UPLOADED = \"userUploaded\",\n  AVATAR = \"avatar\",\n  BACKUP = \"backup\",\n  UNKNOWN = \"unknown\",\n}\n\nexport const assets = sqliteTable(\n  \"assets\",\n  {\n    // Asset ids don't have a default function as they are generated by the caller\n    id: text(\"id\").notNull().primaryKey(),\n    assetType: text(\"assetType\", {\n      enum: [\n        AssetTypes.LINK_BANNER_IMAGE,\n        AssetTypes.LINK_SCREENSHOT,\n        AssetTypes.LINK_PDF,\n        AssetTypes.ASSET_SCREENSHOT,\n        AssetTypes.LINK_FULL_PAGE_ARCHIVE,\n        AssetTypes.LINK_PRECRAWLED_ARCHIVE,\n        AssetTypes.LINK_VIDEO,\n        AssetTypes.LINK_HTML_CONTENT,\n        AssetTypes.BOOKMARK_ASSET,\n        AssetTypes.USER_UPLOADED,\n        AssetTypes.AVATAR,\n        AssetTypes.BACKUP,\n        AssetTypes.UNKNOWN,\n      ],\n    }).notNull(),\n    size: integer(\"size\").notNull().default(0),\n    contentType: text(\"contentType\"),\n    fileName: text(\"fileName\"),\n    bookmarkId: text(\"bookmarkId\").references(() => bookmarks.id, {\n      onDelete: \"cascade\",\n    }),\n    userId: text(\"userId\")\n      .notNull()\n      .references(() => users.id, { onDelete: \"cascade\" }),\n  },\n\n  (tb) => [\n    index(\"assets_bookmarkId_idx\").on(tb.bookmarkId),\n    index(\"assets_assetType_idx\").on(tb.assetType),\n    index(\"assets_userId_idx\").on(tb.userId),\n  ],\n);\n\nexport const highlights = sqliteTable(\n  \"highlights\",\n  {\n    id: text(\"id\")\n      .notNull()\n      .primaryKey()\n      .$defaultFn(() => createId()),\n    bookmarkId: text(\"bookmarkId\")\n      .notNull()\n      .references(() => bookmarks.id, {\n        onDelete: \"cascade\",\n      }),\n    userId: text(\"userId\")\n      .notNull()\n      .references(() => users.id, { onDelete: \"cascade\" }),\n    startOffset: integer(\"startOffset\").notNull(),\n    endOffset: integer(\"endOffset\").notNull(),\n    color: text(\"color\", {\n      enum: [\"red\", \"green\", \"blue\", \"yellow\"],\n    })\n      .default(\"yellow\")\n      .notNull(),\n    text: text(\"text\"),\n    note: text(\"note\"),\n    createdAt: createdAtField(),\n  },\n  (tb) => [\n    index(\"highlights_bookmarkId_idx\").on(tb.bookmarkId),\n    index(\"highlights_userId_idx\").on(tb.userId),\n  ],\n);\n\nexport const userReadingProgress = sqliteTable(\n  \"userReadingProgress\",\n  {\n    id: text(\"id\")\n      .notNull()\n      .primaryKey()\n      .$defaultFn(() => createId()),\n    bookmarkId: text(\"bookmarkId\")\n      .notNull()\n      .references(() => bookmarks.id, {\n        onDelete: \"cascade\",\n      }),\n    userId: text(\"userId\")\n      .notNull()\n      .references(() => users.id, { onDelete: \"cascade\" }),\n    readingProgressOffset: integer(\"readingProgressOffset\").notNull(),\n    readingProgressAnchor: text(\"readingProgressAnchor\"),\n    readingProgressPercent: integer(\"readingProgressPercent\"),\n    modifiedAt: modifiedAtField(),\n  },\n  (tb) => [\n    unique().on(tb.bookmarkId, tb.userId),\n    index(\"userReadingProgress_bookmarkId_idx\").on(tb.bookmarkId),\n    index(\"userReadingProgress_userId_idx\").on(tb.userId),\n  ],\n);\n\nexport const bookmarkTexts = sqliteTable(\"bookmarkTexts\", {\n  id: text(\"id\")\n    .notNull()\n    .primaryKey()\n    .$defaultFn(() => createId())\n    .references(() => bookmarks.id, { onDelete: \"cascade\" }),\n  text: text(\"text\"),\n  sourceUrl: text(\"sourceUrl\"),\n});\n\nexport const bookmarkAssets = sqliteTable(\"bookmarkAssets\", {\n  id: text(\"id\")\n    .notNull()\n    .primaryKey()\n    .$defaultFn(() => createId())\n    .references(() => bookmarks.id, { onDelete: \"cascade\" }),\n  assetType: text(\"assetType\", { enum: [\"image\", \"pdf\"] }).notNull(),\n  assetId: text(\"assetId\").notNull(),\n  content: text(\"content\"),\n  metadata: text(\"metadata\"),\n  fileName: text(\"fileName\"),\n  sourceUrl: text(\"sourceUrl\"),\n});\n\nexport const bookmarkTags = sqliteTable(\n  \"bookmarkTags\",\n  {\n    id: text(\"id\")\n      .notNull()\n      .primaryKey()\n      .$defaultFn(() => createId()),\n    name: text(\"name\").notNull(),\n    normalizedName: text(\"normalizedName\").generatedAlwaysAs(\n      (): SQL =>\n        // This function needs to be in sync with the tagNormalizer function in tagging.ts\n        sql`lower(replace(replace(replace(${bookmarkTags.name}, ' ', ''), '-', ''), '_', ''))`,\n      {\n        mode: \"virtual\",\n      },\n    ),\n    createdAt: createdAtField(),\n    userId: text(\"userId\")\n      .notNull()\n      .references(() => users.id, { onDelete: \"cascade\" }),\n  },\n  (bt) => [\n    unique().on(bt.userId, bt.name),\n    unique(\"bookmarkTags_userId_id_idx\").on(bt.userId, bt.id),\n    index(\"bookmarkTags_name_idx\").on(bt.name),\n    index(\"bookmarkTags_userId_idx\").on(bt.userId),\n    index(\"bookmarkTags_normalizedName_idx\").on(bt.normalizedName),\n  ],\n);\n\nexport const tagsOnBookmarks = sqliteTable(\n  \"tagsOnBookmarks\",\n  {\n    bookmarkId: text(\"bookmarkId\")\n      .notNull()\n      .references(() => bookmarks.id, { onDelete: \"cascade\" }),\n    tagId: text(\"tagId\")\n      .notNull()\n      .references(() => bookmarkTags.id, { onDelete: \"cascade\" }),\n\n    attachedAt: integer(\"attachedAt\", { mode: \"timestamp\" }).$defaultFn(\n      () => new Date(),\n    ),\n    attachedBy: text(\"attachedBy\", { enum: [\"ai\", \"human\"] }).notNull(),\n  },\n  (tb) => [\n    primaryKey({ columns: [tb.bookmarkId, tb.tagId] }),\n    index(\"tagsOnBookmarks_tagId_idx\").on(tb.tagId),\n    index(\"tagsOnBookmarks_bookmarkId_idx\").on(tb.bookmarkId),\n    // Composite index for tag-first queries (when filtering by tagId)\n    index(\"tagsOnBookmarks_tagId_bookmarkId_idx\").on(tb.tagId, tb.bookmarkId),\n  ],\n);\n\nexport const bookmarkLists = sqliteTable(\n  \"bookmarkLists\",\n  {\n    id: text(\"id\")\n      .notNull()\n      .primaryKey()\n      .$defaultFn(() => createId()),\n    name: text(\"name\").notNull(),\n    description: text(\"description\"),\n    icon: text(\"icon\").notNull(),\n    createdAt: createdAtField(),\n    userId: text(\"userId\")\n      .notNull()\n      .references(() => users.id, { onDelete: \"cascade\" }),\n    type: text(\"type\", { enum: [\"manual\", \"smart\"] }).notNull(),\n    // Only applicable for smart lists\n    query: text(\"query\"),\n    parentId: text(\"parentId\").references(\n      (): AnySQLiteColumn => bookmarkLists.id,\n      { onDelete: \"set null\" },\n    ),\n    // Whoever have access to this token can read the content of this list\n    rssToken: text(\"rssToken\"),\n    public: integer(\"public\", { mode: \"boolean\" }).notNull().default(false),\n  },\n  (bl) => [\n    index(\"bookmarkLists_userId_idx\").on(bl.userId),\n    unique(\"bookmarkLists_userId_id_idx\").on(bl.userId, bl.id),\n  ],\n);\n\nexport const bookmarksInLists = sqliteTable(\n  \"bookmarksInLists\",\n  {\n    bookmarkId: text(\"bookmarkId\")\n      .notNull()\n      .references(() => bookmarks.id, { onDelete: \"cascade\" }),\n    listId: text(\"listId\")\n      .notNull()\n      .references(() => bookmarkLists.id, { onDelete: \"cascade\" }),\n    addedAt: integer(\"addedAt\", { mode: \"timestamp\" }).$defaultFn(\n      () => new Date(),\n    ),\n    // Tie the list's existence to the user's membership\n    // of this list.\n    listMembershipId: text(\"listMembershipId\").references(\n      () => listCollaborators.id,\n      {\n        onDelete: \"cascade\",\n      },\n    ),\n  },\n  (tb) => [\n    primaryKey({ columns: [tb.bookmarkId, tb.listId] }),\n    index(\"bookmarksInLists_bookmarkId_idx\").on(tb.bookmarkId),\n    index(\"bookmarksInLists_listId_idx\").on(tb.listId),\n    // Composite index for list-first queries (when filtering by listId)\n    index(\"bookmarksInLists_listId_bookmarkId_idx\").on(\n      tb.listId,\n      tb.bookmarkId,\n    ),\n  ],\n);\n\nexport const listCollaborators = sqliteTable(\n  \"listCollaborators\",\n  {\n    id: text(\"id\")\n      .notNull()\n      .primaryKey()\n      .$defaultFn(() => createId()),\n    listId: text(\"listId\")\n      .notNull()\n      .references(() => bookmarkLists.id, { onDelete: \"cascade\" }),\n    userId: text(\"userId\")\n      .notNull()\n      .references(() => users.id, { onDelete: \"cascade\" }),\n    role: text(\"role\", { enum: [\"viewer\", \"editor\"] }).notNull(),\n    addedAt: createdAtField(),\n    addedBy: text(\"addedBy\").references(() => users.id, {\n      onDelete: \"set null\",\n    }),\n  },\n  (lc) => [\n    unique().on(lc.listId, lc.userId),\n    index(\"listCollaborators_listId_idx\").on(lc.listId),\n    index(\"listCollaborators_userId_idx\").on(lc.userId),\n  ],\n);\n\nexport const listInvitations = sqliteTable(\n  \"listInvitations\",\n  {\n    id: text(\"id\")\n      .notNull()\n      .primaryKey()\n      .$defaultFn(() => createId()),\n    listId: text(\"listId\")\n      .notNull()\n      .references(() => bookmarkLists.id, { onDelete: \"cascade\" }),\n    userId: text(\"userId\")\n      .notNull()\n      .references(() => users.id, { onDelete: \"cascade\" }),\n    role: text(\"role\", { enum: [\"viewer\", \"editor\"] }).notNull(),\n    status: text(\"status\", { enum: [\"pending\", \"declined\"] })\n      .notNull()\n      .default(\"pending\"),\n    invitedAt: integer(\"invitedAt\", { mode: \"timestamp\" })\n      .notNull()\n      .$defaultFn(() => new Date()),\n    invitedEmail: text(\"invitedEmail\"),\n    invitedBy: text(\"invitedBy\").references(() => users.id, {\n      onDelete: \"set null\",\n    }),\n  },\n  (li) => [\n    unique().on(li.listId, li.userId),\n    index(\"listInvitations_listId_idx\").on(li.listId),\n    index(\"listInvitations_userId_idx\").on(li.userId),\n    index(\"listInvitations_status_idx\").on(li.status),\n  ],\n);\n\nexport const customPrompts = sqliteTable(\n  \"customPrompts\",\n  {\n    id: text(\"id\")\n      .notNull()\n      .primaryKey()\n      .$defaultFn(() => createId()),\n    text: text(\"text\").notNull(),\n    enabled: integer(\"enabled\", { mode: \"boolean\" }).notNull(),\n    appliesTo: text(\"appliesTo\", {\n      enum: [\"all_tagging\", \"text\", \"images\", \"summary\"],\n    }).notNull(),\n    createdAt: createdAtField(),\n    userId: text(\"userId\")\n      .notNull()\n      .references(() => users.id, { onDelete: \"cascade\" }),\n  },\n  (bl) => [index(\"customPrompts_userId_idx\").on(bl.userId)],\n);\n\nexport const rssFeedsTable = sqliteTable(\n  \"rssFeeds\",\n  {\n    id: text(\"id\")\n      .notNull()\n      .primaryKey()\n      .$defaultFn(() => createId()),\n    name: text(\"name\").notNull(),\n    url: text(\"url\").notNull(),\n    enabled: integer(\"enabled\", { mode: \"boolean\" }).notNull().default(true),\n    importTags: integer(\"importTags\", { mode: \"boolean\" })\n      .notNull()\n      .default(false),\n    createdAt: createdAtField(),\n    lastFetchedAt: integer(\"lastFetchedAt\", { mode: \"timestamp\" }),\n    lastFetchedStatus: text(\"lastFetchedStatus\", {\n      enum: [\"pending\", \"failure\", \"success\"],\n    }).default(\"pending\"),\n    userId: text(\"userId\")\n      .notNull()\n      .references(() => users.id, { onDelete: \"cascade\" }),\n  },\n  (bl) => [index(\"rssFeeds_userId_idx\").on(bl.userId)],\n);\n\nexport const webhooksTable = sqliteTable(\n  \"webhooks\",\n  {\n    id: text(\"id\")\n      .notNull()\n      .primaryKey()\n      .$defaultFn(() => createId()),\n    createdAt: createdAtField(),\n    url: text(\"url\").notNull(),\n    userId: text(\"userId\")\n      .notNull()\n      .references(() => users.id, { onDelete: \"cascade\" }),\n    events: text(\"events\", { mode: \"json\" })\n      .notNull()\n      .$type<(\"created\" | \"edited\" | \"crawled\" | \"ai tagged\" | \"deleted\")[]>(),\n    token: text(\"token\"),\n  },\n  (bl) => [index(\"webhooks_userId_idx\").on(bl.userId)],\n);\n\nexport const rssFeedImportsTable = sqliteTable(\n  \"rssFeedImports\",\n  {\n    id: text(\"id\")\n      .notNull()\n      .primaryKey()\n      .$defaultFn(() => createId()),\n    createdAt: createdAtField(),\n    entryId: text(\"entryId\").notNull(),\n    rssFeedId: text(\"rssFeedId\")\n      .notNull()\n      .references(() => rssFeedsTable.id, { onDelete: \"cascade\" }),\n    bookmarkId: text(\"bookmarkId\").references(() => bookmarks.id, {\n      onDelete: \"set null\",\n    }),\n  },\n  (bl) => [\n    index(\"rssFeedImports_feedIdIdx_idx\").on(bl.rssFeedId),\n    index(\"rssFeedImports_entryIdIdx_idx\").on(bl.entryId),\n    unique().on(bl.rssFeedId, bl.entryId),\n    // Composite index for RSS feed filter queries (when filtering by rssFeedId)\n    index(\"rssFeedImports_rssFeedId_bookmarkId_idx\").on(\n      bl.rssFeedId,\n      bl.bookmarkId,\n    ),\n  ],\n);\n\nexport const backupsTable = sqliteTable(\n  \"backups\",\n  {\n    id: text(\"id\")\n      .notNull()\n      .primaryKey()\n      .$defaultFn(() => createId()),\n    userId: text(\"userId\")\n      .notNull()\n      .references(() => users.id, { onDelete: \"cascade\" }),\n    assetId: text(\"assetId\").references(() => assets.id, {\n      onDelete: \"cascade\",\n    }),\n    createdAt: createdAtField(),\n    size: integer(\"size\").notNull(),\n    bookmarkCount: integer(\"bookmarkCount\").notNull(),\n    status: text(\"status\", {\n      enum: [\"pending\", \"success\", \"failure\"],\n    })\n      .notNull()\n      .default(\"pending\"),\n    errorMessage: text(\"errorMessage\"),\n  },\n  (b) => [\n    index(\"backups_userId_idx\").on(b.userId),\n    index(\"backups_createdAt_idx\").on(b.createdAt),\n  ],\n);\n\nexport const config = sqliteTable(\"config\", {\n  key: text(\"key\").notNull().primaryKey(),\n  value: text(\"value\").notNull(),\n});\n\nexport const ruleEngineRulesTable = sqliteTable(\n  \"ruleEngineRules\",\n  {\n    id: text(\"id\")\n      .notNull()\n      .primaryKey()\n      .$defaultFn(() => createId()),\n    enabled: integer(\"enabled\", { mode: \"boolean\" }).notNull().default(true),\n    name: text(\"name\").notNull(),\n    description: text(\"description\"),\n    event: text(\"event\").notNull(),\n    condition: text(\"condition\").notNull(),\n\n    // References\n    userId: text(\"userId\")\n      .notNull()\n      .references(() => users.id, { onDelete: \"cascade\" }),\n\n    listId: text(\"listId\"),\n    tagId: text(\"tagId\"),\n  },\n  (rl) => [\n    index(\"ruleEngine_userId_idx\").on(rl.userId),\n\n    // Ensures correct ownership\n    foreignKey({\n      columns: [rl.userId, rl.tagId],\n      foreignColumns: [bookmarkTags.userId, bookmarkTags.id],\n      name: \"ruleEngineRules_userId_tagId_fk\",\n    }).onDelete(\"cascade\"),\n    foreignKey({\n      columns: [rl.userId, rl.listId],\n      foreignColumns: [bookmarkLists.userId, bookmarkLists.id],\n      name: \"ruleEngineRules_userId_listId_fk\",\n    }).onDelete(\"cascade\"),\n  ],\n);\n\nexport const ruleEngineActionsTable = sqliteTable(\n  \"ruleEngineActions\",\n  {\n    id: text(\"id\")\n      .notNull()\n      .primaryKey()\n      .$defaultFn(() => createId()),\n    userId: text(\"userId\")\n      .notNull()\n      .references(() => users.id, { onDelete: \"cascade\" }),\n    ruleId: text(\"ruleId\")\n      .notNull()\n      .references(() => ruleEngineRulesTable.id, { onDelete: \"cascade\" }),\n    action: text(\"action\").notNull(),\n\n    // References\n    listId: text(\"listId\"),\n    tagId: text(\"tagId\"),\n  },\n  (rl) => [\n    index(\"ruleEngineActions_userId_idx\").on(rl.userId),\n    index(\"ruleEngineActions_ruleId_idx\").on(rl.ruleId),\n    // Ensures correct ownership\n    foreignKey({\n      columns: [rl.userId, rl.tagId],\n      foreignColumns: [bookmarkTags.userId, bookmarkTags.id],\n      name: \"ruleEngineActions_userId_tagId_fk\",\n    }).onDelete(\"cascade\"),\n    foreignKey({\n      columns: [rl.userId, rl.listId],\n      foreignColumns: [bookmarkLists.userId, bookmarkLists.id],\n      name: \"ruleEngineActions_userId_listId_fk\",\n    }).onDelete(\"cascade\"),\n  ],\n);\n\nexport const invites = sqliteTable(\"invites\", {\n  id: text(\"id\")\n    .notNull()\n    .primaryKey()\n    .$defaultFn(() => createId()),\n  email: text(\"email\").notNull(),\n  token: text(\"token\").notNull().unique(),\n  createdAt: createdAtField(),\n  usedAt: integer(\"usedAt\", { mode: \"timestamp\" }),\n  invitedBy: text(\"invitedBy\")\n    .notNull()\n    .references(() => users.id, { onDelete: \"cascade\" }),\n});\n\nexport const subscriptions = sqliteTable(\n  \"subscriptions\",\n  {\n    id: text(\"id\")\n      .notNull()\n      .primaryKey()\n      .$defaultFn(() => createId()),\n    userId: text(\"userId\")\n      .notNull()\n      .references(() => users.id, { onDelete: \"cascade\" })\n      .unique(),\n    stripeCustomerId: text(\"stripeCustomerId\").notNull(),\n    stripeSubscriptionId: text(\"stripeSubscriptionId\"),\n    status: text(\"status\", {\n      enum: [\n        \"active\",\n        \"canceled\",\n        \"past_due\",\n        \"unpaid\",\n        \"incomplete\",\n        \"trialing\",\n        \"incomplete_expired\",\n        \"paused\",\n      ],\n    }).notNull(),\n    tier: text(\"tier\", {\n      enum: [\"free\", \"paid\"],\n    })\n      .notNull()\n      .default(\"free\"),\n    priceId: text(\"priceId\"),\n    cancelAtPeriodEnd: integer(\"cancelAtPeriodEnd\", {\n      mode: \"boolean\",\n    }).default(false),\n    startDate: integer(\"startDate\", { mode: \"timestamp\" }),\n    endDate: integer(\"endDate\", { mode: \"timestamp\" }),\n    createdAt: createdAtField(),\n    modifiedAt: modifiedAtField(),\n  },\n  (s) => [\n    index(\"subscriptions_userId_idx\").on(s.userId),\n    index(\"subscriptions_stripeCustomerId_idx\").on(s.stripeCustomerId),\n  ],\n);\n\nexport const importSessions = sqliteTable(\n  \"importSessions\",\n  {\n    id: text(\"id\")\n      .notNull()\n      .primaryKey()\n      .$defaultFn(() => createId()),\n    name: text(\"name\").notNull(),\n    userId: text(\"userId\")\n      .notNull()\n      .references(() => users.id, { onDelete: \"cascade\" }),\n    message: text(\"message\"),\n    rootListId: text(\"rootListId\").references(() => bookmarkLists.id, {\n      onDelete: \"set null\",\n    }),\n    status: text(\"status\", {\n      enum: [\"staging\", \"pending\", \"running\", \"paused\", \"completed\", \"failed\"],\n    })\n      .notNull()\n      .default(\"staging\"),\n    lastProcessedAt: integer(\"lastProcessedAt\", { mode: \"timestamp\" }),\n    createdAt: createdAtField(),\n    modifiedAt: modifiedAtField(),\n  },\n  (is) => [\n    index(\"importSessions_userId_idx\").on(is.userId),\n    index(\"importSessions_status_idx\").on(is.status),\n  ],\n);\n\nexport const importSessionBookmarks = sqliteTable(\n  \"importSessionBookmarks\",\n  {\n    id: text(\"id\")\n      .notNull()\n      .primaryKey()\n      .$defaultFn(() => createId()),\n    importSessionId: text(\"importSessionId\")\n      .notNull()\n      .references(() => importSessions.id, { onDelete: \"cascade\" }),\n    bookmarkId: text(\"bookmarkId\")\n      .notNull()\n      .references(() => bookmarks.id, { onDelete: \"cascade\" }),\n    createdAt: createdAtField(),\n  },\n  (isb) => [\n    index(\"importSessionBookmarks_sessionId_idx\").on(isb.importSessionId),\n    index(\"importSessionBookmarks_bookmarkId_idx\").on(isb.bookmarkId),\n    unique().on(isb.importSessionId, isb.bookmarkId),\n  ],\n);\n\nexport const importStagingBookmarks = sqliteTable(\n  \"importStagingBookmarks\",\n  {\n    id: text(\"id\")\n      .notNull()\n      .primaryKey()\n      .$defaultFn(() => createId()),\n    importSessionId: text(\"importSessionId\")\n      .notNull()\n      .references(() => importSessions.id, { onDelete: \"cascade\" }),\n\n    // Bookmark data to create\n    type: text(\"type\", { enum: [\"link\", \"text\", \"asset\"] }).notNull(),\n    url: text(\"url\"),\n    title: text(\"title\"),\n    content: text(\"content\"),\n    note: text(\"note\"),\n    tags: text(\"tags\", { mode: \"json\" }).$type<string[]>(),\n    listIds: text(\"listIds\", { mode: \"json\" }).$type<string[]>(),\n    sourceAddedAt: integer(\"sourceAddedAt\", { mode: \"timestamp\" }),\n\n    // Processing state\n    status: text(\"status\", {\n      enum: [\"pending\", \"processing\", \"completed\", \"failed\"],\n    })\n      .notNull()\n      .default(\"pending\"),\n    processingStartedAt: integer(\"processingStartedAt\", {\n      mode: \"timestamp\",\n    }),\n\n    // Result (for observability)\n    result: text(\"result\", {\n      enum: [\"accepted\", \"rejected\", \"skipped_duplicate\"],\n    }),\n    resultReason: text(\"resultReason\"),\n    resultBookmarkId: text(\"resultBookmarkId\").references(() => bookmarks.id, {\n      onDelete: \"set null\",\n    }),\n\n    createdAt: createdAtField(),\n    completedAt: integer(\"completedAt\", { mode: \"timestamp\" }),\n  },\n  (isb) => [\n    index(\"importStaging_session_status_idx\").on(\n      isb.importSessionId,\n      isb.status,\n    ),\n    index(\"importStaging_completedAt_idx\").on(isb.completedAt),\n    index(\"importStaging_status_idx\").on(isb.status),\n    index(\"importStaging_status_processingStartedAt_idx\").on(\n      isb.status,\n      isb.processingStartedAt,\n    ),\n  ],\n);\n\n// Relations\n\nexport const userRelations = relations(users, ({ many, one }) => ({\n  tags: many(bookmarkTags),\n  bookmarks: many(bookmarks),\n  webhooks: many(webhooksTable),\n  rules: many(ruleEngineRulesTable),\n  invites: many(invites),\n  subscription: one(subscriptions),\n  importSessions: many(importSessions),\n  listCollaborations: many(listCollaborators),\n  backups: many(backupsTable),\n  listInvitations: many(listInvitations),\n}));\n\nexport const bookmarkRelations = relations(bookmarks, ({ many, one }) => ({\n  user: one(users, {\n    fields: [bookmarks.userId],\n    references: [users.id],\n  }),\n  link: one(bookmarkLinks, {\n    fields: [bookmarks.id],\n    references: [bookmarkLinks.id],\n  }),\n  text: one(bookmarkTexts, {\n    fields: [bookmarks.id],\n    references: [bookmarkTexts.id],\n  }),\n  asset: one(bookmarkAssets, {\n    fields: [bookmarks.id],\n    references: [bookmarkAssets.id],\n  }),\n  tagsOnBookmarks: many(tagsOnBookmarks),\n  bookmarksInLists: many(bookmarksInLists),\n  assets: many(assets),\n  rssFeeds: many(rssFeedImportsTable),\n  importSessionBookmarks: many(importSessionBookmarks),\n}));\n\nexport const assetRelations = relations(assets, ({ one }) => ({\n  bookmark: one(bookmarks, {\n    fields: [assets.bookmarkId],\n    references: [bookmarks.id],\n  }),\n}));\n\nexport const bookmarkTagsRelations = relations(\n  bookmarkTags,\n  ({ many, one }) => ({\n    user: one(users, {\n      fields: [bookmarkTags.userId],\n      references: [users.id],\n    }),\n    tagsOnBookmarks: many(tagsOnBookmarks),\n  }),\n);\n\nexport const tagsOnBookmarksRelations = relations(\n  tagsOnBookmarks,\n  ({ one }) => ({\n    tag: one(bookmarkTags, {\n      fields: [tagsOnBookmarks.tagId],\n      references: [bookmarkTags.id],\n    }),\n    bookmark: one(bookmarks, {\n      fields: [tagsOnBookmarks.bookmarkId],\n      references: [bookmarks.id],\n    }),\n  }),\n);\n\nexport const apiKeyRelations = relations(apiKeys, ({ one }) => ({\n  user: one(users, {\n    fields: [apiKeys.userId],\n    references: [users.id],\n  }),\n}));\n\nexport const bookmarkListsRelations = relations(\n  bookmarkLists,\n  ({ one, many }) => ({\n    bookmarksInLists: many(bookmarksInLists),\n    collaborators: many(listCollaborators),\n    invitations: many(listInvitations),\n    user: one(users, {\n      fields: [bookmarkLists.userId],\n      references: [users.id],\n    }),\n    parent: one(bookmarkLists, {\n      fields: [bookmarkLists.parentId],\n      references: [bookmarkLists.id],\n    }),\n  }),\n);\n\nexport const bookmarksInListsRelations = relations(\n  bookmarksInLists,\n  ({ one }) => ({\n    bookmark: one(bookmarks, {\n      fields: [bookmarksInLists.bookmarkId],\n      references: [bookmarks.id],\n    }),\n    list: one(bookmarkLists, {\n      fields: [bookmarksInLists.listId],\n      references: [bookmarkLists.id],\n    }),\n  }),\n);\n\nexport const listCollaboratorsRelations = relations(\n  listCollaborators,\n  ({ one }) => ({\n    list: one(bookmarkLists, {\n      fields: [listCollaborators.listId],\n      references: [bookmarkLists.id],\n    }),\n    user: one(users, {\n      fields: [listCollaborators.userId],\n      references: [users.id],\n    }),\n    addedByUser: one(users, {\n      fields: [listCollaborators.addedBy],\n      references: [users.id],\n    }),\n  }),\n);\n\nexport const listInvitationsRelations = relations(\n  listInvitations,\n  ({ one }) => ({\n    list: one(bookmarkLists, {\n      fields: [listInvitations.listId],\n      references: [bookmarkLists.id],\n    }),\n    user: one(users, {\n      fields: [listInvitations.userId],\n      references: [users.id],\n    }),\n    invitedByUser: one(users, {\n      fields: [listInvitations.invitedBy],\n      references: [users.id],\n    }),\n  }),\n);\n\nexport const webhooksRelations = relations(webhooksTable, ({ one }) => ({\n  user: one(users, {\n    fields: [webhooksTable.userId],\n    references: [users.id],\n  }),\n}));\n\nexport const ruleEngineRulesRelations = relations(\n  ruleEngineRulesTable,\n  ({ one, many }) => ({\n    user: one(users, {\n      fields: [ruleEngineRulesTable.userId],\n      references: [users.id],\n    }),\n    actions: many(ruleEngineActionsTable),\n  }),\n);\n\nexport const ruleEngineActionsTableRelations = relations(\n  ruleEngineActionsTable,\n  ({ one }) => ({\n    rule: one(ruleEngineRulesTable, {\n      fields: [ruleEngineActionsTable.ruleId],\n      references: [ruleEngineRulesTable.id],\n    }),\n  }),\n);\n\nexport const rssFeedImportsTableRelations = relations(\n  rssFeedImportsTable,\n  ({ one }) => ({\n    rssFeed: one(rssFeedsTable, {\n      fields: [rssFeedImportsTable.rssFeedId],\n      references: [rssFeedsTable.id],\n    }),\n    bookmark: one(bookmarks, {\n      fields: [rssFeedImportsTable.bookmarkId],\n      references: [bookmarks.id],\n    }),\n  }),\n);\n\nexport const invitesRelations = relations(invites, ({ one }) => ({\n  invitedBy: one(users, {\n    fields: [invites.invitedBy],\n    references: [users.id],\n  }),\n}));\n\nexport const subscriptionsRelations = relations(subscriptions, ({ one }) => ({\n  user: one(users, {\n    fields: [subscriptions.userId],\n    references: [users.id],\n  }),\n}));\n\nexport const passwordResetTokensRelations = relations(\n  passwordResetTokens,\n  ({ one }) => ({\n    user: one(users, {\n      fields: [passwordResetTokens.userId],\n      references: [users.id],\n    }),\n  }),\n);\n\nexport const importSessionsRelations = relations(\n  importSessions,\n  ({ one, many }) => ({\n    user: one(users, {\n      fields: [importSessions.userId],\n      references: [users.id],\n    }),\n    bookmarks: many(importSessionBookmarks),\n  }),\n);\n\nexport const importSessionBookmarksRelations = relations(\n  importSessionBookmarks,\n  ({ one }) => ({\n    importSession: one(importSessions, {\n      fields: [importSessionBookmarks.importSessionId],\n      references: [importSessions.id],\n    }),\n    bookmark: one(bookmarks, {\n      fields: [importSessionBookmarks.bookmarkId],\n      references: [bookmarks.id],\n    }),\n  }),\n);\n\nexport const backupsRelations = relations(backupsTable, ({ one }) => ({\n  user: one(users, {\n    fields: [backupsTable.userId],\n    references: [users.id],\n  }),\n  asset: one(assets, {\n    fields: [backupsTable.assetId],\n    references: [assets.id],\n  }),\n}));\n\nexport const userReadingProgressRelations = relations(\n  userReadingProgress,\n  ({ one }) => ({\n    bookmark: one(bookmarks, {\n      fields: [userReadingProgress.bookmarkId],\n      references: [bookmarks.id],\n    }),\n    user: one(users, {\n      fields: [userReadingProgress.userId],\n      references: [users.id],\n    }),\n  }),\n);\n"
  },
  {
    "path": "packages/db/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@karakeep/tsconfig/node.json\",\n  \"include\": [\"**/*.ts\"],\n  \"exclude\": [\"node_modules\"],\n  \"compilerOptions\": {\n    \"baseUrl\" : \".\",\n    \"tsBuildInfoFile\": \"node_modules/.cache/tsbuildinfo.json\"\n  }\n}\n"
  },
  {
    "path": "packages/e2e_tests/.gitignore",
    "content": "# Docker logs captured during test runs\nsetup/docker-logs/\n"
  },
  {
    "path": "packages/e2e_tests/.oxlintrc.json",
    "content": "{\n  \"$schema\": \"../../node_modules/oxlint/configuration_schema.json\",\n  \"extends\": [\n    \"../../tooling/oxlint/oxlint-base.json\"\n  ],\n  \"env\": {\n    \"builtin\": true,\n    \"commonjs\": true\n  },\n  \"ignorePatterns\": [\n    \"**/*.config.js\",\n    \"**/*.config.cjs\",\n    \"**/.eslintrc.cjs\",\n    \"**/.next\",\n    \"**/dist\",\n    \"**/build\",\n    \"**/pnpm-lock.yaml\"\n  ]\n}\n"
  },
  {
    "path": "packages/e2e_tests/docker-compose.yml",
    "content": "services:\n  web:\n    build:\n      dockerfile: docker/Dockerfile\n      context: ../../\n      target: aio\n    restart: unless-stopped\n    ports:\n      - \"${KARAKEEP_PORT:-3000}:3000\"\n    environment:\n      DATA_DIR: /tmp\n      NEXTAUTH_SECRET: secret\n      NEXTAUTH_URL: http://localhost:${KARAKEEP_PORT:-3000}\n      MEILI_MASTER_KEY: dummy\n      MEILI_ADDR: http://meilisearch:7700\n      BROWSER_WEB_URL: http://chrome:9222\n      CRAWLER_NUM_WORKERS: 6\n      CRAWLER_ALLOWED_INTERNAL_HOSTNAMES: nginx\n  meilisearch:\n    image: getmeili/meilisearch:v1.37.0\n    restart: unless-stopped\n    environment:\n      MEILI_NO_ANALYTICS: \"true\"\n      MEILI_MASTER_KEY: dummy\n  chrome:\n    image: gcr.io/zenika-hub/alpine-chrome:124\n    restart: unless-stopped\n    command:\n      - --no-sandbox\n      - --disable-gpu\n      - --disable-dev-shm-usage\n      - --remote-debugging-address=0.0.0.0\n      - --remote-debugging-port=9222\n      - --hide-scrollbars\n  nginx:\n    image: nginx:alpine\n    restart: unless-stopped\n    volumes:\n      - ./setup/html:/usr/share/nginx/html\n  minio:\n    image: minio/minio:latest\n    restart: unless-stopped\n    ports:\n      - \"9000:9000\"\n      - \"9001:9001\"\n    environment:\n      MINIO_ROOT_USER: minioadmin\n      MINIO_ROOT_PASSWORD: minioadmin\n    command: server /data --console-address \":9001\"\n    volumes:\n      - minio_data:/data\n\nvolumes:\n  minio_data:\n"
  },
  {
    "path": "packages/e2e_tests/package.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/package.json\",\n  \"name\": \"@karakeep/e2e_tests\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"typecheck\": \"tsc --noEmit\",\n    \"format\": \"oxfmt --check .\",\n    \"format:fix\": \"oxfmt .\",\n    \"lint\": \"oxlint .\",\n    \"lint:fix\": \"oxlint . --fix\",\n    \"test\": \"vitest run\",\n    \"test:watch\": \"vitest\",\n    \"test:no-build\": \"E2E_TEST_NO_BUILD=1 vitest run\"\n  },\n  \"dependencies\": {\n    \"@aws-sdk/client-s3\": \"^3.842.0\",\n    \"@karakeep/sdk\": \"workspace:*\",\n    \"@karakeep/shared\": \"workspace:^0.1.0\",\n    \"@karakeep/trpc\": \"workspace:^0.1.0\",\n    \"@trpc/client\": \"^11.9.0\",\n    \"superjson\": \"^2.2.1\",\n    \"zod\": \"^3.24.2\"\n  },\n  \"devDependencies\": {\n    \"@karakeep/tsconfig\": \"workspace:^0.1.0\",\n    \"@types/adm-zip\": \"^0.5.7\",\n    \"adm-zip\": \"^0.5.16\",\n    \"vite-tsconfig-paths\": \"^4.3.1\",\n    \"vitest\": \"^3.2.4\"\n  }\n}\n"
  },
  {
    "path": "packages/e2e_tests/setup/html/feed.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<rss version=\"2.0\">\n  <channel>\n    <title>Test RSS Feed</title>\n    <link>http://nginx:80</link>\n    <description>A test RSS feed for e2e testing</description>\n    <item>\n      <title>First Test Article</title>\n      <link>http://nginx:80/hello.html</link>\n      <guid>test-entry-1</guid>\n      <description>This is the first test article</description>\n      <category>tech</category>\n      <category>testing</category>\n    </item>\n    <item>\n      <title>Second Test Article</title>\n      <link>http://nginx:80/hello.html?article=2</link>\n      <guid>test-entry-2</guid>\n      <description>This is the second test article</description>\n      <category>news</category>\n    </item>\n  </channel>\n</rss>\n"
  },
  {
    "path": "packages/e2e_tests/setup/html/hello.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>My test title</title>\n  </head>\n  <body>\n    <h1>Hello World</h1>\n  </body>\n</html>\n"
  },
  {
    "path": "packages/e2e_tests/setup/seed.ts",
    "content": "import { GlobalSetupContext } from \"vitest/node\";\n\nimport { getTrpcClient } from \"../utils/trpc\";\n\nexport async function setup({ provide }: GlobalSetupContext) {\n  const trpc = getTrpcClient();\n  await trpc.users.create.mutate({\n    name: \"Test User\",\n    email: \"admin@example.com\",\n    password: \"test1234\",\n    confirmPassword: \"test1234\",\n  });\n\n  const { key } = await trpc.apiKeys.exchange.mutate({\n    email: \"admin@example.com\",\n    password: \"test1234\",\n    keyName: \"test-key\",\n  });\n  provide(\"adminApiKey\", key);\n  return () => ({});\n}\n\ndeclare module \"vitest\" {\n  export interface ProvidedContext {\n    adminApiKey: string;\n  }\n}\n"
  },
  {
    "path": "packages/e2e_tests/setup/startContainers.ts",
    "content": "import { execSync } from \"child_process\";\nimport net from \"net\";\nimport path from \"path\";\nimport { fileURLToPath } from \"url\";\nimport type { GlobalSetupContext } from \"vitest/node\";\n\nimport { waitUntil } from \"../utils/general\";\n\nasync function getRandomPort(): Promise<number> {\n  const server = net.createServer();\n  return new Promise<number>((resolve, reject) => {\n    server.unref();\n    server.on(\"error\", reject);\n    server.listen(0, () => {\n      const port = (server.address() as net.AddressInfo).port;\n      server.close(() => resolve(port));\n    });\n  });\n}\n\nasync function waitForHealthy(port: number, timeout = 60000): Promise<void> {\n  return waitUntil(\n    async () => {\n      const response = await fetch(`http://localhost:${port}/api/health`);\n      return response.status === 200;\n    },\n    \"Container are healthy\",\n    timeout,\n  );\n}\n\nexport default async function ({ provide }: GlobalSetupContext) {\n  const __dirname = path.dirname(fileURLToPath(import.meta.url));\n  const port = await getRandomPort();\n\n  const buildArg = process.env.E2E_TEST_NO_BUILD ? \"\" : \"--build\";\n\n  console.log(`Starting docker compose on port ${port}...`);\n  execSync(`docker compose up ${buildArg} -d`, {\n    cwd: __dirname,\n    stdio: \"inherit\",\n    env: {\n      ...process.env,\n      KARAKEEP_PORT: port.toString(),\n    },\n  });\n\n  console.log(\"Waiting for service to become healthy...\");\n  await waitForHealthy(port);\n\n  // Wait 5 seconds for the worker to start\n  await new Promise((resolve) => setTimeout(resolve, 5000));\n\n  provide(\"karakeepPort\", port);\n\n  process.env.KARAKEEP_PORT = port.toString();\n\n  return async () => {\n    console.log(\"Capturing docker logs...\");\n    try {\n      const logsDir = path.join(__dirname, \"docker-logs\");\n      execSync(`mkdir -p \"${logsDir}\"`, { cwd: __dirname });\n\n      const services = [\"web\", \"meilisearch\", \"chrome\", \"nginx\", \"minio\"];\n      for (const service of services) {\n        try {\n          execSync(\n            `/bin/sh -c 'docker compose logs ${service} > \"${logsDir}/${service}.log\" 2>&1'`,\n            {\n              cwd: __dirname,\n            },\n          );\n          console.log(`Captured logs for ${service}`);\n        } catch (error) {\n          console.error(`Failed to capture logs for ${service}:`, error);\n        }\n      }\n    } catch (error) {\n      console.error(\"Failed to capture docker logs:\", error);\n    }\n\n    console.log(\"Stopping docker compose...\");\n    execSync(\"docker compose down\", {\n      cwd: __dirname,\n      stdio: \"inherit\",\n    });\n    return Promise.resolve();\n  };\n}\n\ndeclare module \"vitest\" {\n  export interface ProvidedContext {\n    karakeepPort: number;\n  }\n}\n"
  },
  {
    "path": "packages/e2e_tests/tests/api/assets.test.ts",
    "content": "import { assert, beforeEach, describe, expect, inject, it } from \"vitest\";\n\nimport { createKarakeepClient } from \"@karakeep/sdk\";\n\nimport { createTestUser, uploadTestAsset } from \"../../utils/api\";\nimport { createTestPdfFile } from \"../../utils/assets\";\n\ndescribe(\"Assets API\", () => {\n  const port = inject(\"karakeepPort\");\n\n  if (!port) {\n    throw new Error(\"Missing required environment variables\");\n  }\n\n  let client: ReturnType<typeof createKarakeepClient>;\n  let apiKey: string;\n\n  beforeEach(async () => {\n    apiKey = await createTestUser();\n    client = createKarakeepClient({\n      baseUrl: `http://localhost:${port}/api/v1/`,\n      headers: {\n        \"Content-Type\": \"application/json\",\n        authorization: `Bearer ${apiKey}`,\n      },\n    });\n  });\n\n  it(\"should upload and retrieve an asset\", async () => {\n    // Create a test file\n    const file = createTestPdfFile();\n\n    // Upload the asset\n    const uploadResponse = await uploadTestAsset(apiKey, port, file);\n    expect(uploadResponse.assetId).toBeDefined();\n    expect(uploadResponse.contentType).toBe(\"application/pdf\");\n    expect(uploadResponse.fileName).toBe(\"test.pdf\");\n\n    // Retrieve the asset\n    const resp = await fetch(\n      `http://localhost:${port}/api/v1/assets/${uploadResponse.assetId}`,\n      {\n        headers: {\n          authorization: `Bearer ${apiKey}`,\n        },\n      },\n    );\n\n    expect(resp.status).toBe(200);\n  });\n\n  it(\"should attach an asset to a bookmark\", async () => {\n    // Create a test file\n    const file = createTestPdfFile();\n\n    // Upload the asset\n    const uploadResponse = await uploadTestAsset(apiKey, port, file);\n\n    // Create a bookmark\n    const { data: createdBookmark } = await client.POST(\"/bookmarks\", {\n      body: {\n        type: \"asset\",\n        title: \"Test Asset Bookmark\",\n        assetType: \"pdf\",\n        assetId: uploadResponse.assetId,\n      },\n    });\n\n    expect(createdBookmark).toBeDefined();\n    expect(createdBookmark?.id).toBeDefined();\n\n    // Get the bookmark and verify asset\n    const { data: retrievedBookmark } = await client.GET(\n      \"/bookmarks/{bookmarkId}\",\n      {\n        params: {\n          path: {\n            bookmarkId: createdBookmark!.id,\n          },\n        },\n      },\n    );\n\n    expect(retrievedBookmark).toBeDefined();\n    assert(retrievedBookmark!.content.type === \"asset\");\n    expect(retrievedBookmark!.content.assetId).toBe(uploadResponse.assetId);\n  });\n\n  it(\"should delete asset when deleting bookmark\", async () => {\n    // Create a test file\n    const file = createTestPdfFile();\n\n    // Upload the asset\n    const uploadResponse = await uploadTestAsset(apiKey, port, file);\n\n    // Create a bookmark\n    const { data: createdBookmark } = await client.POST(\"/bookmarks\", {\n      body: {\n        type: \"asset\",\n        title: \"Test Asset Bookmark\",\n        assetType: \"pdf\",\n        assetId: uploadResponse.assetId,\n      },\n    });\n\n    // Delete the bookmark\n    const { response: deleteResponse } = await client.DELETE(\n      \"/bookmarks/{bookmarkId}\",\n      {\n        params: {\n          path: {\n            bookmarkId: createdBookmark!.id,\n          },\n        },\n      },\n    );\n    expect(deleteResponse.status).toBe(204);\n\n    // Verify asset is deleted\n    const assetResponse = await fetch(\n      `http://localhost:${port}/api/v1/assets/${uploadResponse.assetId}`,\n      {\n        headers: {\n          authorization: `Bearer ${apiKey}`,\n        },\n      },\n    );\n    expect(assetResponse.status).toBe(404);\n  });\n\n  it(\"should manage assets on a bookmark\", async () => {\n    // Create a new bookmark\n    const { data: createdBookmark, error: createError } = await client.POST(\n      \"/bookmarks\",\n      {\n        body: {\n          type: \"text\",\n          title: \"Test Bookmark\",\n          text: \"This is a test bookmark\",\n        },\n      },\n    );\n\n    if (createError) {\n      console.error(\"Error creating bookmark:\", createError);\n      throw createError;\n    }\n    if (!createdBookmark) {\n      throw new Error(\"Bookmark creation failed\");\n    }\n\n    const file = createTestPdfFile();\n\n    // Upload the asset\n    const uploadResponse1 = await uploadTestAsset(apiKey, port, file);\n    const uploadResponse2 = await uploadTestAsset(apiKey, port, file);\n    const uploadResponse3 = await uploadTestAsset(apiKey, port, file);\n\n    // Attach first asset\n    const { data: firstAsset, response: attachFirstRes } = await client.POST(\n      \"/bookmarks/{bookmarkId}/assets\",\n      {\n        params: {\n          path: {\n            bookmarkId: createdBookmark.id,\n          },\n        },\n        body: {\n          id: uploadResponse1.assetId,\n          assetType: \"bannerImage\",\n        },\n      },\n    );\n\n    expect(attachFirstRes.status).toBe(201);\n    expect(firstAsset).toEqual({\n      id: uploadResponse1.assetId,\n      assetType: \"bannerImage\",\n      fileName: \"test.pdf\",\n    });\n\n    // Attach second asset\n    const { data: secondAsset, response: attachSecondRes } = await client.POST(\n      \"/bookmarks/{bookmarkId}/assets\",\n      {\n        params: {\n          path: {\n            bookmarkId: createdBookmark.id,\n          },\n        },\n        body: {\n          id: uploadResponse2.assetId,\n          assetType: \"bannerImage\",\n        },\n      },\n    );\n\n    expect(attachSecondRes.status).toBe(201);\n    expect(secondAsset).toEqual({\n      id: uploadResponse2.assetId,\n      assetType: \"bannerImage\",\n      fileName: \"test.pdf\",\n    });\n\n    // Get bookmark and verify assets\n    const { data: bookmarkWithAssets } = await client.GET(\n      \"/bookmarks/{bookmarkId}\",\n      {\n        params: {\n          path: {\n            bookmarkId: createdBookmark.id,\n          },\n        },\n      },\n    );\n\n    expect(bookmarkWithAssets?.assets).toEqual(\n      expect.arrayContaining([\n        {\n          id: uploadResponse1.assetId,\n          assetType: \"bannerImage\",\n          fileName: \"test.pdf\",\n        },\n        {\n          id: uploadResponse2.assetId,\n          assetType: \"bannerImage\",\n          fileName: \"test.pdf\",\n        },\n      ]),\n    );\n\n    // Replace first asset\n    const { response: replaceRes } = await client.PUT(\n      \"/bookmarks/{bookmarkId}/assets/{assetId}\",\n      {\n        params: {\n          path: {\n            bookmarkId: createdBookmark.id,\n            assetId: uploadResponse1.assetId,\n          },\n        },\n        body: {\n          assetId: uploadResponse3.assetId,\n        },\n      },\n    );\n\n    expect(replaceRes.status).toBe(204);\n\n    // Verify replacement\n    const { data: bookmarkAfterReplace } = await client.GET(\n      \"/bookmarks/{bookmarkId}\",\n      {\n        params: {\n          path: {\n            bookmarkId: createdBookmark.id,\n          },\n        },\n      },\n    );\n\n    expect(bookmarkAfterReplace?.assets).toEqual(\n      expect.arrayContaining([\n        {\n          id: uploadResponse3.assetId,\n          assetType: \"bannerImage\",\n          fileName: \"test.pdf\",\n        },\n        {\n          id: uploadResponse2.assetId,\n          assetType: \"bannerImage\",\n          fileName: \"test.pdf\",\n        },\n      ]),\n    );\n\n    // Detach second asset\n    const { response: detachRes } = await client.DELETE(\n      \"/bookmarks/{bookmarkId}/assets/{assetId}\",\n      {\n        params: {\n          path: {\n            bookmarkId: createdBookmark.id,\n            assetId: uploadResponse2.assetId,\n          },\n        },\n      },\n    );\n\n    expect(detachRes.status).toBe(204);\n\n    // Verify detachment\n    const { data: bookmarkAfterDetach } = await client.GET(\n      \"/bookmarks/{bookmarkId}\",\n      {\n        params: {\n          path: {\n            bookmarkId: createdBookmark.id,\n          },\n        },\n      },\n    );\n\n    expect(bookmarkAfterDetach?.assets).toEqual([\n      {\n        id: uploadResponse3.assetId,\n        assetType: \"bannerImage\",\n        fileName: \"test.pdf\",\n      },\n    ]);\n  });\n});\n"
  },
  {
    "path": "packages/e2e_tests/tests/api/backups.test.ts",
    "content": "import AdmZip from \"adm-zip\";\nimport { beforeEach, describe, expect, inject, it } from \"vitest\";\n\nimport { createKarakeepClient } from \"@karakeep/sdk\";\n\nimport { createTestUser } from \"../../utils/api\";\n\ndescribe(\"Backups API\", () => {\n  const port = inject(\"karakeepPort\");\n\n  if (!port) {\n    throw new Error(\"Missing required environment variables\");\n  }\n\n  let client: ReturnType<typeof createKarakeepClient>;\n  let apiKey: string;\n\n  beforeEach(async () => {\n    apiKey = await createTestUser();\n    client = createKarakeepClient({\n      baseUrl: `http://localhost:${port}/api/v1/`,\n      headers: {\n        \"Content-Type\": \"application/json\",\n        authorization: `Bearer ${apiKey}`,\n      },\n    });\n  });\n\n  it(\"should list backups\", async () => {\n    const { data: backupsData, response } = await client.GET(\"/backups\");\n\n    expect(response.status).toBe(200);\n    expect(backupsData).toBeDefined();\n    expect(backupsData!.backups).toBeDefined();\n    expect(Array.isArray(backupsData!.backups)).toBe(true);\n  });\n\n  it(\"should trigger a backup and return the backup record\", async () => {\n    const { data: backup, response } = await client.POST(\"/backups\");\n\n    expect(response.status).toBe(201);\n    expect(backup).toBeDefined();\n    expect(backup!.id).toBeDefined();\n    expect(backup!.userId).toBeDefined();\n    expect(backup!.assetId).toBeDefined();\n    expect(backup!.status).toBe(\"pending\");\n    expect(backup!.size).toBe(0);\n    expect(backup!.bookmarkCount).toBe(0);\n\n    // Verify the backup appears in the list\n    const { data: backupsData } = await client.GET(\"/backups\");\n    expect(backupsData).toBeDefined();\n    expect(backupsData!.backups).toBeDefined();\n    expect(backupsData!.backups.some((b) => b.id === backup!.id)).toBe(true);\n  });\n\n  it(\"should get and delete a backup\", async () => {\n    // First trigger a backup\n    const { data: createdBackup } = await client.POST(\"/backups\");\n    expect(createdBackup).toBeDefined();\n\n    const backupId = createdBackup!.id;\n\n    // Get the specific backup\n    const { data: backup, response: getResponse } = await client.GET(\n      \"/backups/{backupId}\",\n      {\n        params: {\n          path: {\n            backupId,\n          },\n        },\n      },\n    );\n\n    expect(getResponse.status).toBe(200);\n    expect(backup).toBeDefined();\n    expect(backup!.id).toBe(backupId);\n    expect(backup!.userId).toBeDefined();\n    expect(backup!.assetId).toBeDefined();\n    expect(backup!.status).toBe(\"pending\");\n\n    // Delete the backup\n    const { response: deleteResponse } = await client.DELETE(\n      \"/backups/{backupId}\",\n      {\n        params: {\n          path: {\n            backupId,\n          },\n        },\n      },\n    );\n\n    expect(deleteResponse.status).toBe(204);\n\n    // Verify it's deleted\n    const { response: getDeletedResponse } = await client.GET(\n      \"/backups/{backupId}\",\n      {\n        params: {\n          path: {\n            backupId,\n          },\n        },\n      },\n    );\n\n    expect(getDeletedResponse.status).toBe(404);\n  });\n\n  it(\"should return 404 for non-existent backup\", async () => {\n    const { response } = await client.GET(\"/backups/{backupId}\", {\n      params: {\n        path: {\n          backupId: \"non-existent-backup-id\",\n        },\n      },\n    });\n\n    expect(response.status).toBe(404);\n  });\n\n  it(\"should return 404 when deleting non-existent backup\", async () => {\n    const { response } = await client.DELETE(\"/backups/{backupId}\", {\n      params: {\n        path: {\n          backupId: \"non-existent-backup-id\",\n        },\n      },\n    });\n\n    expect(response.status).toBe(404);\n  });\n\n  it(\"should handle multiple backups\", async () => {\n    // Trigger multiple backups\n    const { data: backup1 } = await client.POST(\"/backups\");\n    const { data: backup2 } = await client.POST(\"/backups\");\n\n    expect(backup1).toBeDefined();\n    expect(backup2).toBeDefined();\n    expect(backup1!.id).not.toBe(backup2!.id);\n\n    // Get all backups\n    const { data: backupsData, response } = await client.GET(\"/backups\");\n\n    expect(response.status).toBe(200);\n    expect(backupsData).toBeDefined();\n    expect(backupsData!.backups).toBeDefined();\n    expect(Array.isArray(backupsData!.backups)).toBe(true);\n    expect(backupsData!.backups.length).toBeGreaterThanOrEqual(2);\n    expect(backupsData!.backups.some((b) => b.id === backup1!.id)).toBe(true);\n    expect(backupsData!.backups.some((b) => b.id === backup2!.id)).toBe(true);\n  });\n\n  it(\"should validate full backup lifecycle\", async () => {\n    // Step 1: Create some test bookmarks\n    const bookmarks = [];\n    for (let i = 0; i < 3; i++) {\n      const { data: bookmark } = await client.POST(\"/bookmarks\", {\n        body: {\n          type: \"text\",\n          title: `Test Bookmark ${i + 1}`,\n          text: `This is test bookmark number ${i + 1}`,\n        },\n      });\n      expect(bookmark).toBeDefined();\n      bookmarks.push(bookmark!);\n    }\n\n    // Step 1b: Create a list and add first two bookmarks to it\n    const { data: listData } = await client.POST(\"/lists\", {\n      body: {\n        name: \"Backup Test List\",\n        icon: \"📋\",\n      },\n    });\n    expect(listData).toBeDefined();\n    const listId = listData!.id;\n\n    await client.PUT(\"/lists/{listId}/bookmarks/{bookmarkId}\", {\n      params: {\n        path: { listId, bookmarkId: bookmarks[0].id },\n      },\n    });\n    await client.PUT(\"/lists/{listId}/bookmarks/{bookmarkId}\", {\n      params: {\n        path: { listId, bookmarkId: bookmarks[1].id },\n      },\n    });\n\n    // Step 2: Trigger a backup\n    const { data: createdBackup, response: createResponse } =\n      await client.POST(\"/backups\");\n\n    expect(createResponse.status).toBe(201);\n    expect(createdBackup).toBeDefined();\n    expect(createdBackup!.id).toBeDefined();\n    expect(createdBackup!.status).toBe(\"pending\");\n    expect(createdBackup!.bookmarkCount).toBe(0);\n    expect(createdBackup!.size).toBe(0);\n\n    const backupId = createdBackup!.id;\n\n    // Step 3: Poll until backup is completed or failed\n    let backup;\n    let attempts = 0;\n    const maxAttempts = 60; // Wait up to 60 seconds\n    const pollInterval = 1000; // Poll every second\n\n    while (attempts < maxAttempts) {\n      const { data: currentBackup } = await client.GET(\"/backups/{backupId}\", {\n        params: {\n          path: {\n            backupId,\n          },\n        },\n      });\n\n      backup = currentBackup;\n\n      if (backup!.status === \"success\" || backup!.status === \"failure\") {\n        break;\n      }\n\n      await new Promise((resolve) => setTimeout(resolve, pollInterval));\n      attempts++;\n    }\n\n    // Step 4: Verify backup completed successfully\n    expect(backup).toBeDefined();\n    expect(backup!.status).toBe(\"success\");\n    expect(backup!.bookmarkCount).toBeGreaterThanOrEqual(3);\n    expect(backup!.size).toBeGreaterThan(0);\n    expect(backup!.errorMessage).toBeNull();\n\n    // Step 5: Download the backup\n    const downloadResponse = await fetch(\n      `http://localhost:${port}/api/v1/backups/${backupId}/download`,\n      {\n        headers: {\n          authorization: `Bearer ${apiKey}`,\n        },\n      },\n    );\n\n    expect(downloadResponse.status).toBe(200);\n    expect(downloadResponse.headers.get(\"content-type\")).toContain(\n      \"application/zip\",\n    );\n\n    const backupBlob = await downloadResponse.blob();\n    expect(backupBlob.size).toBeGreaterThan(0);\n    expect(backupBlob.size).toBe(backup!.size);\n\n    // Step 6: Unzip and validate the backup contents\n    const arrayBuffer = await backupBlob.arrayBuffer();\n    const buffer = Buffer.from(arrayBuffer);\n\n    // Verify it's a valid ZIP file (starts with PK signature)\n    expect(buffer[0]).toBe(0x50); // 'P'\n    expect(buffer[1]).toBe(0x4b); // 'K'\n\n    // Unzip the backup file\n    const zip = new AdmZip(buffer);\n    const zipEntries = zip.getEntries();\n\n    // Should contain exactly one JSON file\n    expect(zipEntries.length).toBe(1);\n    const jsonEntry = zipEntries[0];\n    expect(jsonEntry.entryName).toMatch(/^karakeep-backup-.*\\.json$/);\n\n    // Extract and parse the JSON content\n    const jsonContent = jsonEntry.getData().toString(\"utf8\");\n    const backupData = JSON.parse(jsonContent);\n\n    // Validate the backup structure\n    expect(backupData).toBeDefined();\n    expect(backupData.bookmarks).toBeDefined();\n    expect(Array.isArray(backupData.bookmarks)).toBe(true);\n    expect(backupData.bookmarks.length).toBeGreaterThanOrEqual(3);\n\n    // Validate that our test bookmarks are in the backup\n    const backupTitles = backupData.bookmarks.map(\n      (b: { title: string }) => b.title,\n    );\n    expect(backupTitles).toContain(\"Test Bookmark 1\");\n    expect(backupTitles).toContain(\"Test Bookmark 2\");\n    expect(backupTitles).toContain(\"Test Bookmark 3\");\n\n    // Validate bookmark structure\n    const firstBookmark = backupData.bookmarks[0];\n    expect(firstBookmark).toHaveProperty(\"content\");\n    expect(firstBookmark.content).toHaveProperty(\"type\");\n\n    // Validate lists in backup\n    expect(backupData.lists).toBeDefined();\n    expect(Array.isArray(backupData.lists)).toBe(true);\n    const exportedList = backupData.lists.find(\n      (l: { name: string }) => l.name === \"Backup Test List\",\n    );\n    expect(exportedList).toBeDefined();\n    expect(exportedList.icon).toBe(\"📋\");\n    expect(exportedList.type).toBe(\"manual\");\n    expect(exportedList.id).toBe(listId);\n\n    // Validate bookmark-to-list memberships\n    const bm1 = backupData.bookmarks.find(\n      (b: { title: string }) => b.title === \"Test Bookmark 1\",\n    );\n    const bm2 = backupData.bookmarks.find(\n      (b: { title: string }) => b.title === \"Test Bookmark 2\",\n    );\n    const bm3 = backupData.bookmarks.find(\n      (b: { title: string }) => b.title === \"Test Bookmark 3\",\n    );\n    expect(bm1.lists).toContain(listId);\n    expect(bm2.lists).toContain(listId);\n    expect(bm3.lists).toEqual([]);\n\n    // Step 7: Verify the backup appears in the list with updated status\n    const { data: backupsData } = await client.GET(\"/backups\");\n    const listedBackup = backupsData!.backups.find((b) => b.id === backupId);\n\n    expect(listedBackup).toBeDefined();\n    expect(listedBackup!.status).toBe(\"success\");\n    expect(listedBackup!.bookmarkCount).toBe(backup!.bookmarkCount);\n    expect(listedBackup!.size).toBe(backup!.size);\n  });\n});\n"
  },
  {
    "path": "packages/e2e_tests/tests/api/bookmarks.test.ts",
    "content": "import { assert, beforeEach, describe, expect, inject, it } from \"vitest\";\n\nimport { createKarakeepClient } from \"@karakeep/sdk\";\n\nimport { createTestUser } from \"../../utils/api\";\nimport { waitUntil } from \"../../utils/general\";\n\ndescribe(\"Bookmarks API\", () => {\n  const port = inject(\"karakeepPort\");\n\n  if (!port) {\n    throw new Error(\"Missing required environment variables\");\n  }\n\n  let client: ReturnType<typeof createKarakeepClient>;\n  let apiKey: string;\n\n  beforeEach(async () => {\n    apiKey = await createTestUser();\n    client = createKarakeepClient({\n      baseUrl: `http://localhost:${port}/api/v1/`,\n      headers: {\n        \"Content-Type\": \"application/json\",\n        authorization: `Bearer ${apiKey}`,\n      },\n    });\n  });\n\n  it(\"should create and retrieve a bookmark\", async () => {\n    // Create a new bookmark\n    const {\n      data: createdBookmark,\n      response: createResponse,\n      error,\n    } = await client.POST(\"/bookmarks\", {\n      body: {\n        type: \"text\",\n        title: \"Test Bookmark\",\n        text: \"This is a test bookmark\",\n      },\n    });\n\n    if (error) {\n      console.error(\"Error creating bookmark:\", error);\n      throw error;\n    }\n\n    expect(createResponse.status).toBe(201);\n    expect(createdBookmark).toBeDefined();\n    expect(createdBookmark?.id).toBeDefined();\n\n    // Get the created bookmark\n    const { data: retrievedBookmark, response: getResponse } = await client.GET(\n      \"/bookmarks/{bookmarkId}\",\n      {\n        params: {\n          path: {\n            bookmarkId: createdBookmark.id,\n          },\n        },\n      },\n    );\n\n    expect(getResponse.status).toBe(200);\n    expect(retrievedBookmark!.id).toBe(createdBookmark.id);\n    expect(retrievedBookmark!.title).toBe(\"Test Bookmark\");\n    assert(retrievedBookmark!.content.type === \"text\");\n    expect(retrievedBookmark!.content.text).toBe(\"This is a test bookmark\");\n  });\n\n  it(\"should update a bookmark\", async () => {\n    // Create a new bookmark\n    const { data: createdBookmark, error: createError } = await client.POST(\n      \"/bookmarks\",\n      {\n        body: {\n          type: \"text\",\n          title: \"Test Bookmark\",\n          text: \"This is a test bookmark\",\n        },\n      },\n    );\n\n    if (createError) {\n      console.error(\"Error creating bookmark:\", createError);\n      throw createError;\n    }\n    if (!createdBookmark) {\n      throw new Error(\"Bookmark creation failed\");\n    }\n\n    // Update the bookmark\n    const { data: updatedBookmark, response: updateResponse } =\n      await client.PATCH(\"/bookmarks/{bookmarkId}\", {\n        params: {\n          path: {\n            bookmarkId: createdBookmark.id,\n          },\n        },\n        body: {\n          title: \"Updated Title\",\n        },\n      });\n\n    expect(updateResponse.status).toBe(200);\n    expect(updatedBookmark!.title).toBe(\"Updated Title\");\n  });\n\n  it(\"should delete a bookmark\", async () => {\n    // Create a new bookmark\n    const { data: createdBookmark, error: createError } = await client.POST(\n      \"/bookmarks\",\n      {\n        body: {\n          type: \"text\",\n          title: \"Test Bookmark\",\n          text: \"This is a test bookmark\",\n        },\n      },\n    );\n\n    if (createError) {\n      console.error(\"Error creating bookmark:\", createError);\n      throw createError;\n    }\n    if (!createdBookmark) {\n      throw new Error(\"Bookmark creation failed\");\n    }\n\n    // Delete the bookmark\n    const { response: deleteResponse } = await client.DELETE(\n      \"/bookmarks/{bookmarkId}\",\n      {\n        params: {\n          path: {\n            bookmarkId: createdBookmark.id,\n          },\n        },\n      },\n    );\n\n    expect(deleteResponse.status).toBe(204);\n\n    // Verify it's deleted\n    const { response: getResponse } = await client.GET(\n      \"/bookmarks/{bookmarkId}\",\n      {\n        params: {\n          path: {\n            bookmarkId: createdBookmark.id,\n          },\n        },\n      },\n    );\n\n    expect(getResponse.status).toBe(404);\n  });\n\n  it(\"should paginate through bookmarks\", async () => {\n    // Create multiple bookmarks\n    const bookmarkPromises = Array.from({ length: 5 }, (_, i) =>\n      client.POST(\"/bookmarks\", {\n        body: {\n          type: \"text\",\n          title: `Test Bookmark ${i}`,\n          text: `This is test bookmark ${i}`,\n        },\n      }),\n    );\n\n    const createdBookmarks = await Promise.all(bookmarkPromises);\n    const bookmarkIds = createdBookmarks.map((b) => b.data!.id);\n\n    // Get first page\n    const { data: firstPage, response: firstResponse } = await client.GET(\n      \"/bookmarks\",\n      {\n        params: {\n          query: {\n            limit: 2,\n          },\n        },\n      },\n    );\n\n    expect(firstResponse.status).toBe(200);\n    expect(firstPage!.bookmarks.length).toBe(2);\n    expect(firstPage!.nextCursor).toBeDefined();\n\n    // Get second page\n    const { data: secondPage, response: secondResponse } = await client.GET(\n      \"/bookmarks\",\n      {\n        params: {\n          query: {\n            limit: 2,\n            cursor: firstPage!.nextCursor!,\n          },\n        },\n      },\n    );\n\n    expect(secondResponse.status).toBe(200);\n    expect(secondPage!.bookmarks.length).toBe(2);\n    expect(secondPage!.nextCursor).toBeDefined();\n\n    // Get final page\n    const { data: finalPage, response: finalResponse } = await client.GET(\n      \"/bookmarks\",\n      {\n        params: {\n          query: {\n            limit: 2,\n            cursor: secondPage!.nextCursor!,\n          },\n        },\n      },\n    );\n\n    expect(finalResponse.status).toBe(200);\n    expect(finalPage!.bookmarks.length).toBe(1);\n    expect(finalPage!.nextCursor).toBeNull();\n\n    // Verify all bookmarks were returned\n    const allBookmarks = [\n      ...firstPage!.bookmarks,\n      ...secondPage!.bookmarks,\n      ...finalPage!.bookmarks,\n    ];\n    expect(allBookmarks.map((b) => b.id)).toEqual(\n      expect.arrayContaining(bookmarkIds),\n    );\n  });\n\n  it(\"should manage tags on a bookmark\", async () => {\n    // Create a new bookmark\n    const { data: createdBookmark, error: createError } = await client.POST(\n      \"/bookmarks\",\n      {\n        body: {\n          type: \"text\",\n          title: \"Test Bookmark\",\n          text: \"This is a test bookmark\",\n        },\n      },\n    );\n\n    if (createError) {\n      console.error(\"Error creating bookmark:\", createError);\n      throw createError;\n    }\n    if (!createdBookmark) {\n      throw new Error(\"Bookmark creation failed\");\n    }\n\n    // Add tags\n    const { data: addTagsResponse, response: addTagsRes } = await client.POST(\n      \"/bookmarks/{bookmarkId}/tags\",\n      {\n        params: {\n          path: {\n            bookmarkId: createdBookmark.id,\n          },\n        },\n        body: {\n          tags: [{ tagName: \"test-tag\" }],\n        },\n      },\n    );\n\n    expect(addTagsRes.status).toBe(200);\n    expect(addTagsResponse!.attached.length).toBe(1);\n\n    // Remove tags\n    const { response: removeTagsRes } = await client.DELETE(\n      \"/bookmarks/{bookmarkId}/tags\",\n      {\n        params: {\n          path: {\n            bookmarkId: createdBookmark.id,\n          },\n        },\n        body: {\n          tags: [{ tagId: addTagsResponse!.attached[0] }],\n        },\n      },\n    );\n\n    expect(removeTagsRes.status).toBe(200);\n  });\n\n  it(\"should manage tags with attachedBy field\", async () => {\n    // Create a new bookmark\n    const { data: createdBookmark, error: createError } = await client.POST(\n      \"/bookmarks\",\n      {\n        body: {\n          type: \"text\",\n          title: \"Test Bookmark for attachedBy\",\n          text: \"Testing attachedBy field\",\n        },\n      },\n    );\n\n    if (createError) {\n      console.error(\"Error creating bookmark:\", createError);\n      throw createError;\n    }\n    if (!createdBookmark) {\n      throw new Error(\"Bookmark creation failed\");\n    }\n\n    // Add tags with different attachedBy values\n    const { data: addTagsResponse, response: addTagsRes } = await client.POST(\n      \"/bookmarks/{bookmarkId}/tags\",\n      {\n        params: {\n          path: {\n            bookmarkId: createdBookmark.id,\n          },\n        },\n        body: {\n          tags: [\n            { tagName: \"ai-tag\", attachedBy: \"ai\" },\n            { tagName: \"human-tag\", attachedBy: \"human\" },\n            { tagName: \"default-tag\" }, // Should default to \"human\"\n          ],\n        },\n      },\n    );\n\n    expect(addTagsRes.status).toBe(200);\n    expect(addTagsResponse!.attached.length).toBe(3);\n\n    // Get the bookmark and verify the attachedBy values\n    const { data: retrievedBookmark } = await client.GET(\n      \"/bookmarks/{bookmarkId}\",\n      {\n        params: {\n          path: {\n            bookmarkId: createdBookmark.id,\n          },\n        },\n      },\n    );\n\n    expect(retrievedBookmark!.tags.length).toBe(3);\n\n    const aiTag = retrievedBookmark!.tags.find((t) => t.name === \"ai-tag\");\n    const humanTag = retrievedBookmark!.tags.find(\n      (t) => t.name === \"human-tag\",\n    );\n    const defaultTag = retrievedBookmark!.tags.find(\n      (t) => t.name === \"default-tag\",\n    );\n\n    expect(aiTag?.attachedBy).toBe(\"ai\");\n    expect(humanTag?.attachedBy).toBe(\"human\");\n    expect(defaultTag?.attachedBy).toBe(\"human\");\n  });\n\n  it(\"should get lists for a bookmark\", async () => {\n    const { data: createdBookmark } = await client.POST(\"/bookmarks\", {\n      body: {\n        type: \"text\",\n        title: \"Test Bookmark\",\n        text: \"This is a test bookmark\",\n      },\n    });\n\n    const { data: createdList } = await client.POST(\"/lists\", {\n      body: {\n        name: \"Test List\",\n        icon: \"📚\",\n      },\n    });\n\n    const { response: addBookmarkResponse } = await client.PUT(\n      \"/lists/{listId}/bookmarks/{bookmarkId}\",\n      {\n        params: {\n          path: {\n            listId: createdList!.id,\n            bookmarkId: createdBookmark!.id,\n          },\n        },\n      },\n    );\n\n    expect(addBookmarkResponse.status).toBe(204);\n\n    const { data: lists, response: getListsResponse } = await client.GET(\n      \"/bookmarks/{bookmarkId}/lists\",\n      {\n        params: {\n          path: {\n            bookmarkId: createdBookmark!.id,\n          },\n        },\n      },\n    );\n\n    expect(getListsResponse.status).toBe(200);\n    expect(lists!.lists.length).toBe(1);\n    expect(lists!.lists[0].id).toBe(createdList!.id);\n    expect(lists!.lists[0].name).toBe(\"Test List\");\n    expect(lists!.lists[0].icon).toBe(\"📚\");\n  });\n\n  it(\"should search bookmarks\", async () => {\n    // Create test bookmarks\n    await client.POST(\"/bookmarks\", {\n      body: {\n        type: \"text\",\n        title: \"Search Test 1\",\n        text: \"This is a test bookmark for search\",\n      },\n    });\n    await client.POST(\"/bookmarks\", {\n      body: {\n        type: \"text\",\n        title: \"Search Test 2\",\n        text: \"Another test bookmark for search\",\n      },\n    });\n\n    await waitUntil(async () => {\n      const { data, response, error } = await client.GET(\"/bookmarks/search\", {\n        params: {\n          query: {\n            q: \"test bookmark\",\n          },\n        },\n      });\n      if (error) {\n        throw error;\n      }\n      if (response.status !== 200) {\n        throw new Error(`Search request failed with status ${response.status}`);\n      }\n      return (data?.bookmarks.length ?? 0) >= 2;\n    }, 'Search index contains the new bookmarks for query \"test bookmark\"');\n\n    // Search for bookmarks\n    const { data: searchResults, response: searchResponse } = await client.GET(\n      \"/bookmarks/search\",\n      {\n        params: {\n          query: {\n            q: \"test bookmark\",\n          },\n        },\n      },\n    );\n\n    expect(searchResponse.status).toBe(200);\n    expect(searchResults!.bookmarks.length).toBeGreaterThanOrEqual(2);\n  });\n\n  it(\"should paginate search results\", async () => {\n    // Create multiple bookmarks\n    const bookmarkPromises = Array.from({ length: 5 }, (_, i) =>\n      client.POST(\"/bookmarks\", {\n        body: {\n          type: \"text\",\n          title: `Search Pagination ${i}`,\n          text: `This is test bookmark ${i} for pagination`,\n        },\n      }),\n    );\n\n    await Promise.all(bookmarkPromises);\n\n    await waitUntil(async () => {\n      const { data, response, error } = await client.GET(\"/bookmarks/search\", {\n        params: {\n          query: {\n            q: \"pagination\",\n            limit: 5,\n          },\n        },\n      });\n      if (error) {\n        throw error;\n      }\n      if (response.status !== 200) {\n        throw new Error(`Search request failed with status ${response.status}`);\n      }\n      return (data?.bookmarks.length ?? 0) >= 5;\n    }, \"Search index contains the pagination test bookmarks\");\n\n    // Get first page\n    const { data: firstPage, response: firstResponse } = await client.GET(\n      \"/bookmarks/search\",\n      {\n        params: {\n          query: {\n            q: \"pagination\",\n            limit: 2,\n          },\n        },\n      },\n    );\n\n    expect(firstResponse.status).toBe(200);\n    expect(firstPage!.bookmarks.length).toBe(2);\n    expect(firstPage!.nextCursor).toBeDefined();\n\n    // Get second page\n    const { data: secondPage, response: secondResponse } = await client.GET(\n      \"/bookmarks/search\",\n      {\n        params: {\n          query: {\n            q: \"pagination\",\n            limit: 2,\n            cursor: firstPage!.nextCursor!,\n          },\n        },\n      },\n    );\n\n    expect(secondResponse.status).toBe(200);\n    expect(secondPage!.bookmarks.length).toBe(2);\n    expect(secondPage!.nextCursor).toBeDefined();\n\n    // Get final page\n    const { data: finalPage, response: finalResponse } = await client.GET(\n      \"/bookmarks/search\",\n      {\n        params: {\n          query: {\n            q: \"pagination\",\n            limit: 2,\n            cursor: secondPage!.nextCursor!,\n          },\n        },\n      },\n    );\n\n    expect(finalResponse.status).toBe(200);\n    expect(finalPage!.bookmarks.length).toBe(1);\n    expect(finalPage!.nextCursor).toBeNull();\n  });\n\n  describe(\"singlefile\", () => {\n    async function uploadSinglefileAsset(ifexists?: string) {\n      const file = new File([\"<html>HELLO WORLD</html>\"], \"test.html\", {\n        type: \"text/html\",\n      });\n\n      const formData = new FormData();\n      formData.append(\"url\", \"https://example.com\");\n      formData.append(\"file\", file);\n\n      const url = new URL(\n        `http://localhost:${port}/api/v1/bookmarks/singlefile`,\n      );\n      if (ifexists) {\n        url.searchParams.append(\"ifexists\", ifexists);\n      }\n\n      const response = await fetch(url.toString(), {\n        method: \"POST\",\n        headers: {\n          authorization: `Bearer ${apiKey}`,\n        },\n        body: formData,\n      });\n\n      if (!response.ok) {\n        return [null, response] as const;\n      }\n\n      const data = (await response.json()) as { id: string };\n      return [data, response] as const;\n    }\n\n    it(\"should support precrawling via singlefile with ifexists=skip\", async () => {\n      // First upload: create a bookmark\n      const [data, response] = await uploadSinglefileAsset();\n      expect(response?.status).toBe(201);\n      const bookmarkId = data?.id;\n      if (!bookmarkId) throw new Error(\"Bookmark ID not found\");\n\n      // Get the bookmark and record the precrawled asset id\n      const { data: bookmark, response: getResponse1 } = await client.GET(\n        \"/bookmarks/{bookmarkId}\",\n        {\n          params: { path: { bookmarkId } },\n        },\n      );\n      expect(getResponse1.status).toBe(200);\n      const assetIds = bookmark!.assets\n        .filter((a) => a.assetType === \"precrawledArchive\")\n        .map((a) => a.id);\n      expect(assetIds.length).toBe(1);\n      const firstAssetId = assetIds[0];\n\n      // Second upload with skip\n      const [data2, response2] = await uploadSinglefileAsset(\"skip\");\n      expect(response2?.status).toBe(200);\n      expect(data2?.id).toBe(bookmarkId);\n\n      // Get the bookmark again\n      const { data: bookmark2, response: getResponse2 } = await client.GET(\n        \"/bookmarks/{bookmarkId}\",\n        {\n          params: { path: { bookmarkId } },\n        },\n      );\n      expect(getResponse2.status).toBe(200);\n      const assetIds2 = bookmark2!.assets\n        .filter((a) => a.assetType === \"precrawledArchive\")\n        .map((a) => a.id);\n      expect(assetIds2).toEqual([firstAssetId]); // same asset\n    });\n\n    it(\"should support precrawling via singlefile with ifexists=overwrite\", async () => {\n      // First upload\n      const [data, response] = await uploadSinglefileAsset(\"overwrite\");\n      expect(response?.status).toBe(201);\n      const bookmarkId = data?.id;\n      if (!bookmarkId) throw new Error(\"Bookmark ID not found\");\n\n      // Record the asset\n      const { data: bookmark, response: getResponse1 } = await client.GET(\n        \"/bookmarks/{bookmarkId}\",\n        {\n          params: { path: { bookmarkId } },\n        },\n      );\n      expect(getResponse1.status).toBe(200);\n      const firstAssetId = bookmark!.assets.find(\n        (a) => a.assetType === \"precrawledArchive\",\n      )?.id;\n      expect(firstAssetId).toBeDefined();\n\n      // Second upload with overwrite\n      const [data2, response2] = await uploadSinglefileAsset(\"overwrite\");\n      expect(response2?.status).toBe(200);\n      expect(data2?.id).toBe(bookmarkId);\n\n      // Get the bookmark again\n      const { data: bookmark2, response: getResponse2 } = await client.GET(\n        \"/bookmarks/{bookmarkId}\",\n        {\n          params: { path: { bookmarkId } },\n        },\n      );\n      expect(getResponse2.status).toBe(200);\n      const secondAssetId = bookmark2!.assets.find(\n        (a) => a.assetType === \"precrawledArchive\",\n      )?.id;\n      expect(secondAssetId).toBeDefined();\n      expect(secondAssetId).not.toBe(firstAssetId);\n      // There should be only one precrawledArchive asset\n      const precrawledAssets = bookmark2!.assets.filter(\n        (a) => a.assetType === \"precrawledArchive\",\n      );\n      expect(precrawledAssets.length).toBe(1);\n    });\n\n    it(\"should support precrawling via singlefile with ifexists=overwrite-recrawl\", async () => {\n      // First upload\n      const [data, response] = await uploadSinglefileAsset(\"overwrite-recrawl\");\n      expect(response?.status).toBe(201);\n      const bookmarkId = data?.id;\n      if (!bookmarkId) throw new Error(\"Bookmark ID not found\");\n\n      // Record the asset\n      const { data: bookmark, response: getResponse1 } = await client.GET(\n        \"/bookmarks/{bookmarkId}\",\n        {\n          params: { path: { bookmarkId } },\n        },\n      );\n      expect(getResponse1.status).toBe(200);\n      const firstAssetId = bookmark!.assets.find(\n        (a) => a.assetType === \"precrawledArchive\",\n      )?.id;\n      expect(firstAssetId).toBeDefined();\n\n      // Second upload with overwrite-recrawl\n      const [data2, response2] =\n        await uploadSinglefileAsset(\"overwrite-recrawl\");\n      expect(response2?.status).toBe(200);\n      expect(data2?.id).toBe(bookmarkId);\n\n      // Get the bookmark again\n      const { data: bookmark2, response: getResponse2 } = await client.GET(\n        \"/bookmarks/{bookmarkId}\",\n        {\n          params: { path: { bookmarkId } },\n        },\n      );\n      expect(getResponse2.status).toBe(200);\n      const secondAssetId = bookmark2!.assets.find(\n        (a) => a.assetType === \"precrawledArchive\",\n      )?.id;\n      expect(secondAssetId).toBeDefined();\n      expect(secondAssetId).not.toBe(firstAssetId);\n      // There should be only one precrawledArchive asset\n      const precrawledAssets = bookmark2!.assets.filter(\n        (a) => a.assetType === \"precrawledArchive\",\n      );\n      expect(precrawledAssets.length).toBe(1);\n    });\n\n    it(\"should support precrawling via singlefile with ifexists=append\", async () => {\n      // First upload\n      const [data, response] = await uploadSinglefileAsset(\"append\");\n      expect(response?.status).toBe(201);\n      const bookmarkId = data?.id;\n      if (!bookmarkId) throw new Error(\"Bookmark ID not found\");\n\n      // Record the first asset\n      const { data: bookmark, response: getResponse1 } = await client.GET(\n        \"/bookmarks/{bookmarkId}\",\n        {\n          params: { path: { bookmarkId } },\n        },\n      );\n      expect(getResponse1.status).toBe(200);\n      const firstAssetId = bookmark!.assets.find(\n        (a) => a.assetType === \"precrawledArchive\",\n      )?.id;\n      expect(firstAssetId).toBeDefined();\n\n      // Second upload with append\n      const [data2, response2] = await uploadSinglefileAsset(\"append\");\n      expect(response2?.status).toBe(200);\n      expect(data2?.id).toBe(bookmarkId);\n\n      // Get the bookmark again\n      const { data: bookmark2, response: getResponse2 } = await client.GET(\n        \"/bookmarks/{bookmarkId}\",\n        {\n          params: { path: { bookmarkId } },\n        },\n      );\n      expect(getResponse2.status).toBe(200);\n      const precrawledAssets = bookmark2!.assets.filter(\n        (a) => a.assetType === \"precrawledArchive\",\n      );\n      expect(precrawledAssets.length).toBe(2);\n      expect(precrawledAssets.map((a) => a.id)).toContain(firstAssetId);\n      // The second asset id should be different\n      const secondAssetId = precrawledAssets.find(\n        (asset) => asset.id !== firstAssetId,\n      )?.id;\n      expect(secondAssetId).toBeDefined();\n    });\n\n    it(\"should support precrawling via singlefile with ifexists=append-recrawl\", async () => {\n      // First upload\n      const [data, response] = await uploadSinglefileAsset(\"append-recrawl\");\n      expect(response?.status).toBe(201);\n      const bookmarkId = data?.id;\n      if (!bookmarkId) throw new Error(\"Bookmark ID not found\");\n\n      // Record the first asset\n      const { data: bookmark, response: getResponse1 } = await client.GET(\n        \"/bookmarks/{bookmarkId}\",\n        {\n          params: { path: { bookmarkId } },\n        },\n      );\n      expect(getResponse1.status).toBe(200);\n      const firstAssetId = bookmark!.assets.find(\n        (a) => a.assetType === \"precrawledArchive\",\n      )?.id;\n      expect(firstAssetId).toBeDefined();\n\n      // Second upload with append-recrawl\n      const [data2, response2] = await uploadSinglefileAsset(\"append-recrawl\");\n      expect(response2?.status).toBe(200);\n      expect(data2?.id).toBe(bookmarkId);\n\n      // Get the bookmark again\n      const { data: bookmark2, response: getResponse2 } = await client.GET(\n        \"/bookmarks/{bookmarkId}\",\n        {\n          params: { path: { bookmarkId } },\n        },\n      );\n      expect(getResponse2.status).toBe(200);\n      const precrawledAssets = bookmark2!.assets.filter(\n        (a) => a.assetType === \"precrawledArchive\",\n      );\n      expect(precrawledAssets.length).toBe(2);\n      expect(precrawledAssets.map((a) => a.id)).toContain(firstAssetId);\n      // The second asset id should be different\n      const secondAssetId = precrawledAssets.find(\n        (asset) => asset.id !== firstAssetId,\n      )?.id;\n      expect(secondAssetId).toBeDefined();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/e2e_tests/tests/api/highlights.test.ts",
    "content": "import { beforeEach, describe, expect, inject, it } from \"vitest\";\n\nimport { createKarakeepClient } from \"@karakeep/sdk\";\n\nimport { createTestUser } from \"../../utils/api\";\n\ndescribe(\"Highlights API\", () => {\n  const port = inject(\"karakeepPort\");\n\n  if (!port) {\n    throw new Error(\"Missing required environment variables\");\n  }\n\n  let client: ReturnType<typeof createKarakeepClient>;\n  let apiKey: string;\n\n  beforeEach(async () => {\n    apiKey = await createTestUser();\n    client = createKarakeepClient({\n      baseUrl: `http://localhost:${port}/api/v1/`,\n      headers: {\n        \"Content-Type\": \"application/json\",\n        authorization: `Bearer ${apiKey}`,\n      },\n    });\n  });\n\n  it(\"should create, get, update and delete a highlight\", async () => {\n    // Create a bookmark first\n    const { data: createdBookmark } = await client.POST(\"/bookmarks\", {\n      body: {\n        type: \"text\",\n        title: \"Test Bookmark\",\n        text: \"This is a test bookmark\",\n      },\n    });\n\n    // Create a new highlight\n    const { data: createdHighlight, response: createResponse } =\n      await client.POST(\"/highlights\", {\n        body: {\n          bookmarkId: createdBookmark!.id,\n          startOffset: 0,\n          endOffset: 5,\n          text: \"This \",\n          note: \"Test note\",\n          color: \"yellow\",\n        },\n      });\n\n    expect(createResponse.status).toBe(201);\n    expect(createdHighlight).toBeDefined();\n    expect(createdHighlight?.id).toBeDefined();\n    expect(createdHighlight?.text).toBe(\"This \");\n    expect(createdHighlight?.note).toBe(\"Test note\");\n\n    // Get the created highlight\n    const { data: retrievedHighlight, response: getResponse } =\n      await client.GET(\"/highlights/{highlightId}\", {\n        params: {\n          path: {\n            highlightId: createdHighlight!.id,\n          },\n        },\n      });\n\n    expect(getResponse.status).toBe(200);\n    expect(retrievedHighlight!.id).toBe(createdHighlight!.id);\n    expect(retrievedHighlight!.text).toBe(\"This \");\n    expect(retrievedHighlight!.note).toBe(\"Test note\");\n\n    // Update the highlight\n    const { data: updatedHighlight, response: updateResponse } =\n      await client.PATCH(\"/highlights/{highlightId}\", {\n        params: {\n          path: {\n            highlightId: createdHighlight!.id,\n          },\n        },\n        body: {\n          color: \"blue\",\n        },\n      });\n\n    expect(updateResponse.status).toBe(200);\n    expect(updatedHighlight!.color).toBe(\"blue\");\n\n    // Delete the highlight\n    const { response: deleteResponse } = await client.DELETE(\n      \"/highlights/{highlightId}\",\n      {\n        params: {\n          path: {\n            highlightId: createdHighlight!.id,\n          },\n        },\n      },\n    );\n\n    expect(deleteResponse.status).toBe(200);\n\n    // Verify it's deleted\n    const { response: getDeletedResponse } = await client.GET(\n      \"/highlights/{highlightId}\",\n      {\n        params: {\n          path: {\n            highlightId: createdHighlight!.id,\n          },\n        },\n      },\n    );\n\n    expect(getDeletedResponse.status).toBe(404);\n  });\n\n  it(\"should paginate through highlights\", async () => {\n    // Create a bookmark first\n    const { data: createdBookmark } = await client.POST(\"/bookmarks\", {\n      body: {\n        type: \"text\",\n        title: \"Test Bookmark\",\n        text: \"This is a test bookmark\",\n      },\n    });\n\n    // Create multiple highlights\n    const highlightPromises = Array.from({ length: 5 }, (_, i) =>\n      client.POST(\"/highlights\", {\n        body: {\n          bookmarkId: createdBookmark!.id,\n          startOffset: i * 5,\n          endOffset: (i + 1) * 5,\n          text: `Highlight ${i}`,\n          note: `Note ${i}`,\n        },\n      }),\n    );\n\n    await Promise.all(highlightPromises);\n\n    // Get first page\n    const { data: firstPage, response: firstResponse } = await client.GET(\n      \"/highlights\",\n      {\n        params: {\n          query: {\n            limit: 2,\n          },\n        },\n      },\n    );\n\n    expect(firstResponse.status).toBe(200);\n    expect(firstPage!.highlights.length).toBe(2);\n    expect(firstPage!.nextCursor).toBeDefined();\n\n    // Get second page\n    const { data: secondPage, response: secondResponse } = await client.GET(\n      \"/highlights\",\n      {\n        params: {\n          query: {\n            limit: 2,\n            cursor: firstPage!.nextCursor!,\n          },\n        },\n      },\n    );\n\n    expect(secondResponse.status).toBe(200);\n    expect(secondPage!.highlights.length).toBe(2);\n    expect(secondPage!.nextCursor).toBeDefined();\n\n    // Get final page\n    const { data: finalPage, response: finalResponse } = await client.GET(\n      \"/highlights\",\n      {\n        params: {\n          query: {\n            limit: 2,\n            cursor: secondPage!.nextCursor!,\n          },\n        },\n      },\n    );\n\n    expect(finalResponse.status).toBe(200);\n    expect(finalPage!.highlights.length).toBe(1);\n    expect(finalPage!.nextCursor).toBeNull();\n  });\n\n  it(\"should get highlights for a bookmark\", async () => {\n    // Create a bookmark first\n    const { data: createdBookmark } = await client.POST(\"/bookmarks\", {\n      body: {\n        type: \"text\",\n        title: \"Test Bookmark\",\n        text: \"This is a test bookmark\",\n      },\n    });\n\n    // Create highlights\n    await client.POST(\"/highlights\", {\n      body: {\n        bookmarkId: createdBookmark!.id,\n        startOffset: 0,\n        endOffset: 5,\n        text: \"This \",\n        note: \"First highlight\",\n        color: \"yellow\",\n      },\n    });\n\n    await client.POST(\"/highlights\", {\n      body: {\n        bookmarkId: createdBookmark!.id,\n        startOffset: 5,\n        endOffset: 10,\n        text: \"is a \",\n        note: \"Second highlight\",\n        color: \"blue\",\n      },\n    });\n\n    // Get highlights for bookmark\n    const { data: highlights, response: getResponse } = await client.GET(\n      \"/bookmarks/{bookmarkId}/highlights\",\n      {\n        params: {\n          path: {\n            bookmarkId: createdBookmark!.id,\n          },\n        },\n      },\n    );\n\n    expect(getResponse.status).toBe(200);\n    expect(highlights!.highlights.length).toBe(2);\n    expect(highlights!.highlights.map((h) => h.text)).toContain(\"This \");\n    expect(highlights!.highlights.map((h) => h.text)).toContain(\"is a \");\n  });\n});\n"
  },
  {
    "path": "packages/e2e_tests/tests/api/lists.test.ts",
    "content": "import { beforeEach, describe, expect, inject, it } from \"vitest\";\n\nimport { createKarakeepClient } from \"@karakeep/sdk\";\n\nimport { createTestUser } from \"../../utils/api\";\n\ndescribe(\"Lists API\", () => {\n  const port = inject(\"karakeepPort\");\n\n  if (!port) {\n    throw new Error(\"Missing required environment variables\");\n  }\n\n  let client: ReturnType<typeof createKarakeepClient>;\n  let apiKey: string;\n\n  beforeEach(async () => {\n    apiKey = await createTestUser();\n    client = createKarakeepClient({\n      baseUrl: `http://localhost:${port}/api/v1/`,\n      headers: {\n        \"Content-Type\": \"application/json\",\n        authorization: `Bearer ${apiKey}`,\n      },\n    });\n  });\n\n  it(\"should list all lists\", async () => {\n    // Create multiple lists\n    const { data: list1 } = await client.POST(\"/lists\", {\n      body: {\n        name: \"First List\",\n        icon: \"🚀\",\n      },\n    });\n\n    const { data: list2 } = await client.POST(\"/lists\", {\n      body: {\n        name: \"Second List\",\n        icon: \"📚\",\n        type: \"smart\",\n        query: \"is:fav\",\n      },\n    });\n\n    // Get all lists\n    const { data: allLists, response: getResponse } = await client.GET(\n      \"/lists\",\n      {\n        params: {},\n      },\n    );\n\n    expect(getResponse.status).toBe(200);\n    expect(allLists).toBeDefined();\n    expect(allLists!.lists.length).toBeGreaterThanOrEqual(2);\n\n    const listIds = allLists!.lists.map((l) => l.id);\n    expect(listIds).toContain(list1!.id);\n    expect(listIds).toContain(list2!.id);\n  });\n\n  it(\"should create, get, update and delete a list\", async () => {\n    // Create a new list\n    const { data: createdList, response: createResponse } = await client.POST(\n      \"/lists\",\n      {\n        body: {\n          name: \"Test List\",\n          icon: \"🚀\",\n        },\n      },\n    );\n\n    expect(createResponse.status).toBe(201);\n    expect(createdList).toBeDefined();\n    expect(createdList?.id).toBeDefined();\n    expect(createdList?.name).toBe(\"Test List\");\n\n    // Get the created list\n    const { data: retrievedList, response: getResponse } = await client.GET(\n      \"/lists/{listId}\",\n      {\n        params: {\n          path: {\n            listId: createdList!.id,\n          },\n        },\n      },\n    );\n\n    expect(getResponse.status).toBe(200);\n    expect(retrievedList!.id).toBe(createdList!.id);\n    expect(retrievedList!.name).toBe(\"Test List\");\n\n    // Update the list\n    const { data: updatedList, response: updateResponse } = await client.PATCH(\n      \"/lists/{listId}\",\n      {\n        params: {\n          path: {\n            listId: createdList!.id,\n          },\n        },\n        body: {\n          name: \"Updated List\",\n        },\n      },\n    );\n\n    expect(updateResponse.status).toBe(200);\n    expect(updatedList!.name).toBe(\"Updated List\");\n\n    // Delete the list\n    const { response: deleteResponse } = await client.DELETE(\n      \"/lists/{listId}\",\n      {\n        params: {\n          path: {\n            listId: createdList!.id,\n          },\n        },\n      },\n    );\n\n    expect(deleteResponse.status).toBe(204);\n\n    // Verify it's deleted\n    const { response: getDeletedResponse } = await client.GET(\n      \"/lists/{listId}\",\n      {\n        params: {\n          path: {\n            listId: createdList!.id,\n          },\n        },\n      },\n    );\n\n    expect(getDeletedResponse.status).toBe(404);\n  });\n\n  it(\"should manage bookmarks in a list\", async () => {\n    // Create a list\n    const { data: createdList } = await client.POST(\"/lists\", {\n      body: {\n        name: \"Test List\",\n        icon: \"🚀\",\n      },\n    });\n\n    // Create a bookmark\n    const { data: createdBookmark } = await client.POST(\"/bookmarks\", {\n      body: {\n        type: \"text\",\n        title: \"Test Bookmark\",\n        text: \"This is a test bookmark\",\n      },\n    });\n\n    // Add bookmark to list\n    const { response: addResponse } = await client.PUT(\n      \"/lists/{listId}/bookmarks/{bookmarkId}\",\n      {\n        params: {\n          path: {\n            listId: createdList!.id,\n            bookmarkId: createdBookmark!.id,\n          },\n        },\n      },\n    );\n\n    expect(addResponse.status).toBe(204);\n\n    // Get bookmarks in list\n    const { data: listBookmarks, response: getResponse } = await client.GET(\n      \"/lists/{listId}/bookmarks\",\n      {\n        params: {\n          path: {\n            listId: createdList!.id,\n          },\n        },\n      },\n    );\n\n    expect(getResponse.status).toBe(200);\n    expect(listBookmarks!.bookmarks.length).toBe(1);\n    expect(listBookmarks!.bookmarks[0].id).toBe(createdBookmark!.id);\n\n    // Remove bookmark from list\n    const { response: removeResponse } = await client.DELETE(\n      \"/lists/{listId}/bookmarks/{bookmarkId}\",\n      {\n        params: {\n          path: {\n            listId: createdList!.id,\n            bookmarkId: createdBookmark!.id,\n          },\n        },\n      },\n    );\n\n    expect(removeResponse.status).toBe(204);\n\n    // Verify bookmark is removed\n    const { data: updatedListBookmarks } = await client.GET(\n      \"/lists/{listId}/bookmarks\",\n      {\n        params: {\n          path: {\n            listId: createdList!.id,\n          },\n        },\n      },\n    );\n\n    expect(updatedListBookmarks!.bookmarks.length).toBe(0);\n  });\n\n  it(\"should support smart lists\", async () => {\n    // Create a bookmark\n    const { data: createdBookmark1 } = await client.POST(\"/bookmarks\", {\n      body: {\n        type: \"text\",\n        title: \"Test Bookmark\",\n        text: \"This is a test bookmark\",\n        favourited: true,\n      },\n    });\n\n    const { data: _ } = await client.POST(\"/bookmarks\", {\n      body: {\n        type: \"text\",\n        title: \"Test Bookmark\",\n        text: \"This is a test bookmark\",\n        favourited: false,\n      },\n    });\n\n    // Create a list\n    const { data: createdList } = await client.POST(\"/lists\", {\n      body: {\n        name: \"Test List\",\n        icon: \"🚀\",\n        type: \"smart\",\n        query: \"is:fav\",\n      },\n    });\n\n    // Get bookmarks in list\n    const { data: listBookmarks, response: getResponse } = await client.GET(\n      \"/lists/{listId}/bookmarks\",\n      {\n        params: {\n          path: {\n            listId: createdList!.id,\n          },\n        },\n      },\n    );\n\n    expect(getResponse.status).toBe(200);\n    expect(listBookmarks!.bookmarks.length).toBe(1);\n    expect(listBookmarks!.bookmarks[0].id).toBe(createdBookmark1!.id);\n  });\n});\n"
  },
  {
    "path": "packages/e2e_tests/tests/api/public.test.ts",
    "content": "import { assert, beforeEach, describe, expect, inject, it } from \"vitest\";\nimport { z } from \"zod\";\n\nimport { createSignedToken } from \"@karakeep/shared/signedTokens\";\nimport { zAssetSignedTokenSchema } from \"@karakeep/shared/types/assets\";\nimport { BookmarkTypes } from \"@karakeep/shared/types/bookmarks\";\n\nimport { createTestUser, uploadTestAsset } from \"../../utils/api\";\nimport { createTestPdfFile } from \"../../utils/assets\";\nimport { waitUntil } from \"../../utils/general\";\nimport { getTrpcClient } from \"../../utils/trpc\";\n\nconst SINGING_SECRET = \"secret\";\n\ndescribe(\"Public API\", () => {\n  const port = inject(\"karakeepPort\");\n\n  if (!port) {\n    throw new Error(\"Missing required environment variables\");\n  }\n\n  let apiKey: string; // For the primary test user\n\n  async function seedDatabase(currentApiKey: string) {\n    const trpcClient = getTrpcClient(currentApiKey);\n\n    // Create two lists\n    const publicList = await trpcClient.lists.create.mutate({\n      name: \"Public List\",\n      icon: \"🚀\",\n      type: \"manual\",\n    });\n\n    await trpcClient.lists.edit.mutate({\n      listId: publicList.id,\n      public: true,\n    });\n\n    // Create two bookmarks\n    const createBookmark1 = await trpcClient.bookmarks.createBookmark.mutate({\n      title: \"Test Bookmark #1\",\n      url: \"http://nginx:80/hello.html\",\n      type: BookmarkTypes.LINK,\n    });\n\n    // Create a second bookmark with an asset\n    const file = createTestPdfFile();\n\n    const uploadResponse = await uploadTestAsset(currentApiKey, port, file);\n    const createBookmark2 = await trpcClient.bookmarks.createBookmark.mutate({\n      title: \"Test Bookmark #2\",\n      type: BookmarkTypes.ASSET,\n      assetType: \"pdf\",\n      assetId: uploadResponse.assetId,\n    });\n\n    await trpcClient.lists.addToList.mutate({\n      listId: publicList.id,\n      bookmarkId: createBookmark1.id,\n    });\n    await trpcClient.lists.addToList.mutate({\n      listId: publicList.id,\n      bookmarkId: createBookmark2.id,\n    });\n\n    return { publicList, createBookmark1, createBookmark2 };\n  }\n\n  beforeEach(async () => {\n    apiKey = await createTestUser();\n  });\n\n  it(\"should get public bookmarks\", async () => {\n    const { publicList } = await seedDatabase(apiKey);\n    const trpcClient = getTrpcClient(apiKey);\n\n    const res = await trpcClient.publicBookmarks.getPublicBookmarksInList.query(\n      {\n        listId: publicList.id,\n      },\n    );\n\n    expect(res.bookmarks.length).toBe(2);\n  });\n\n  it(\"should be able to access the assets of the public bookmarks\", async () => {\n    const { publicList, createBookmark1, createBookmark2 } =\n      await seedDatabase(apiKey);\n\n    const trpcClient = getTrpcClient(apiKey);\n    // Wait for link bookmark to be crawled and have a banner image (screenshot)\n    await waitUntil(async () => {\n      const res = await trpcClient.bookmarks.getBookmark.query({\n        bookmarkId: createBookmark1.id,\n      });\n      assert(res.content.type === BookmarkTypes.LINK);\n      // Check for screenshotAssetId as bannerImageUrl might be derived from it or original imageUrl\n      return !!res.content.screenshotAssetId || !!res.content.imageUrl;\n    }, \"Bookmark is crawled and has banner info\");\n\n    const res = await trpcClient.publicBookmarks.getPublicBookmarksInList.query(\n      {\n        listId: publicList.id,\n      },\n    );\n\n    const b1Resp = res.bookmarks.find((b) => b.id === createBookmark1.id);\n    expect(b1Resp).toBeDefined();\n    const b2Resp = res.bookmarks.find((b) => b.id === createBookmark2.id);\n    expect(b2Resp).toBeDefined();\n\n    assert(b1Resp!.content.type === BookmarkTypes.LINK);\n    assert(b2Resp!.content.type === BookmarkTypes.ASSET);\n\n    {\n      // Banner image fetch for link bookmark\n      assert(\n        b1Resp!.bannerImageUrl,\n        \"Link bookmark should have a bannerImageUrl\",\n      );\n      const assetFetch = await fetch(b1Resp!.bannerImageUrl);\n      expect(assetFetch.status).toBe(200);\n    }\n\n    {\n      // Actual asset fetch for asset bookmark\n      assert(\n        b2Resp!.content.assetUrl,\n        \"Asset bookmark should have an assetUrl\",\n      );\n      const assetFetch = await fetch(b2Resp!.content.assetUrl);\n      expect(assetFetch.status).toBe(200);\n    }\n  });\n\n  it(\"Accessing non public list should fail\", async () => {\n    const trpcClient = getTrpcClient(apiKey);\n    const nonPublicList = await trpcClient.lists.create.mutate({\n      name: \"Non Public List\",\n      icon: \"🚀\",\n      type: \"manual\",\n    });\n\n    await expect(\n      trpcClient.publicBookmarks.getPublicBookmarksInList.query({\n        listId: nonPublicList.id,\n      }),\n    ).rejects.toThrow(/List not found/);\n  });\n\n  describe(\"Public asset token validation\", () => {\n    let userId: string;\n    let assetId: string; // Asset belonging to the primary user (userId)\n\n    beforeEach(async () => {\n      const trpcClient = getTrpcClient(apiKey);\n      const whoami = await trpcClient.users.whoami.query();\n      userId = whoami.id;\n      const assetUpload = await uploadTestAsset(\n        apiKey,\n        port,\n        createTestPdfFile(\"token_test.pdf\"),\n      );\n      assetId = assetUpload.assetId;\n    });\n\n    it(\"should succeed with a valid token\", async () => {\n      const token = createSignedToken(\n        {\n          assetId,\n          userId,\n        } as z.infer<typeof zAssetSignedTokenSchema>,\n        SINGING_SECRET,\n        Date.now() + 60000, // Expires in 60 seconds\n      );\n      const res = await fetch(\n        `http://localhost:${port}/api/public/assets/${assetId}?token=${token}`,\n      );\n      expect(res.status).toBe(200);\n      expect((await res.blob()).type).toBe(\"application/pdf\");\n    });\n\n    it(\"should fail without a token\", async () => {\n      const res = await fetch(\n        `http://localhost:${port}/api/public/assets/${assetId}`,\n      );\n      expect(res.status).toBe(400); // Bad Request due to missing token query param\n    });\n\n    it(\"should fail with a malformed token string (e.g., not base64)\", async () => {\n      const res = await fetch(\n        `http://localhost:${port}/api/public/assets/${assetId}?token=thisIsNotValidBase64!@#`,\n      );\n      expect(res.status).toBe(403);\n      expect(await res.json()).toEqual(\n        expect.objectContaining({ error: \"Invalid or expired token\" }),\n      );\n    });\n\n    it(\"should fail with a token having a structurally invalid inner payload\", async () => {\n      // Payload that doesn't conform to zAssetSignedTokenSchema (e.g. misspelled key)\n      const malformedInnerPayload = {\n        asset_id_mispelled: assetId,\n        userId: userId,\n      };\n      const token = createSignedToken(\n        malformedInnerPayload,\n        SINGING_SECRET,\n        Date.now() + 60000,\n      );\n      const res = await fetch(\n        `http://localhost:${port}/api/public/assets/${assetId}?token=${token}`,\n      );\n      expect(res.status).toBe(403);\n      expect(await res.json()).toEqual(\n        expect.objectContaining({ error: \"Invalid or expired token\" }),\n      );\n    });\n\n    it(\"should fail after token expiry\", async () => {\n      const token = createSignedToken(\n        {\n          assetId,\n          userId,\n        } as z.infer<typeof zAssetSignedTokenSchema>,\n        SINGING_SECRET,\n        Date.now() + 1000, // Expires in 1 second\n      );\n\n      // Wait for more than 1 second to ensure expiry\n      await new Promise((resolve) => setTimeout(resolve, 2000));\n\n      const res = await fetch(\n        `http://localhost:${port}/api/public/assets/${assetId}?token=${token}`,\n      );\n      expect(res.status).toBe(403);\n      expect(await res.json()).toEqual(\n        expect.objectContaining({ error: \"Invalid or expired token\" }),\n      );\n    });\n\n    it(\"should fail when using a valid token for a different asset\", async () => {\n      const anotherAssetUpload = await uploadTestAsset(\n        apiKey, // Same user\n        port,\n        createTestPdfFile(\"other_asset.pdf\"),\n      );\n      const anotherAssetId = anotherAssetUpload.assetId;\n\n      // Token is valid for 'anotherAssetId'\n      const tokenForAnotherAsset = createSignedToken(\n        {\n          assetId: anotherAssetId,\n          userId,\n        } as z.infer<typeof zAssetSignedTokenSchema>,\n        SINGING_SECRET,\n        Date.now() + 60000,\n      );\n\n      // Attempt to use this token to access the original 'assetId'\n      const res = await fetch(\n        `http://localhost:${port}/api/public/assets/${assetId}?token=${tokenForAnotherAsset}`,\n      );\n      expect(res.status).toBe(403);\n      expect(await res.json()).toEqual(\n        expect.objectContaining({ error: \"Invalid or expired token\" }),\n      );\n    });\n\n    it(\"should fail if token's userId does not own the requested assetId (expect 404)\", async () => {\n      // User1 (primary, `apiKey`, `userId`) owns `assetId` (from beforeEach)\n\n      // Create User2 - ensure unique email for user creation\n      const apiKeyUser2 = await createTestUser();\n      const trpcClientUser2 = getTrpcClient(apiKeyUser2);\n      const whoamiUser2 = await trpcClientUser2.users.whoami.query();\n      const userIdUser2 = whoamiUser2.id;\n\n      // Generate a token where the payload claims assetId is being accessed by userIdUser2,\n      // but assetId actually belongs to the original userId.\n      const tokenForUser2AttemptingAsset1 = createSignedToken(\n        {\n          assetId: assetId, // assetId belongs to user1 (userId)\n          userId: userIdUser2, // token claims user2 is accessing it\n        } as z.infer<typeof zAssetSignedTokenSchema>,\n        SINGING_SECRET,\n        Date.now() + 60000,\n      );\n\n      // User2 attempts to access assetId (owned by User1) using a token that has User2's ID in its payload.\n      // The API route will use userIdUser2 from the token to query the DB for assetId.\n      // Since assetId is not owned by userIdUser2, the DB query will find nothing.\n      const res = await fetch(\n        `http://localhost:${port}/api/public/assets/${assetId}?token=${tokenForUser2AttemptingAsset1}`,\n      );\n      expect(res.status).toBe(404);\n      expect(await res.json()).toEqual(\n        expect.objectContaining({ error: \"Asset not found\" }),\n      );\n    });\n\n    it(\"should fail for a token referencing a non-existent assetId (expect 404)\", async () => {\n      const nonExistentAssetId = `nonexistent-asset-${Date.now()}`;\n      const token = createSignedToken(\n        {\n          assetId: nonExistentAssetId,\n          userId, // Valid userId from the primary user\n        } as z.infer<typeof zAssetSignedTokenSchema>,\n        SINGING_SECRET,\n        Date.now() + 60000,\n      );\n\n      const res = await fetch(\n        `http://localhost:${port}/api/public/assets/${nonExistentAssetId}?token=${token}`,\n      );\n      expect(res.status).toBe(404);\n      expect(await res.json()).toEqual(\n        expect.objectContaining({ error: \"Asset not found\" }),\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "packages/e2e_tests/tests/api/rss.test.ts",
    "content": "import { beforeEach, describe, expect, inject, it } from \"vitest\";\n\nimport { BookmarkTypes } from \"@karakeep/shared/types/bookmarks\";\n\nimport { createTestUser } from \"../../utils/api\";\nimport { getTrpcClient } from \"../../utils/trpc\";\n\ndescribe(\"RSS Feed API\", () => {\n  const port = inject(\"karakeepPort\");\n\n  if (!port) {\n    throw new Error(\"Missing required environment variables\");\n  }\n\n  async function fetchRssFeed(listId: string, token: string) {\n    return await fetch(\n      `http://localhost:${port}/api/v1/rss/lists/${listId}?token=${token}`,\n    );\n  }\n\n  async function seedDatabase() {\n    const trpcClient = getTrpcClient(apiKey);\n\n    // Create two lists\n    const manualList = await trpcClient.lists.create.mutate({\n      name: \"Test List #1\",\n      icon: \"🚀\",\n      type: \"manual\",\n    });\n\n    const smartList = await trpcClient.lists.create.mutate({\n      name: \"Test List #2\",\n      icon: \"🚀\",\n      type: \"smart\",\n      query: \"is:fav\",\n    });\n\n    // Create two bookmarks\n    const createBookmark1 = await trpcClient.bookmarks.createBookmark.mutate({\n      title: \"Test Bookmark #1\",\n      url: \"https://example.com\",\n      type: BookmarkTypes.LINK,\n    });\n\n    const createBookmark2 = await trpcClient.bookmarks.createBookmark.mutate({\n      title: \"Test Bookmark #2\",\n      url: \"https://example.com/2\",\n      type: BookmarkTypes.LINK,\n      favourited: true,\n    });\n\n    await trpcClient.lists.addToList.mutate({\n      listId: manualList.id,\n      bookmarkId: createBookmark1.id,\n    });\n\n    return { manualList, smartList, createBookmark1, createBookmark2 };\n  }\n\n  let apiKey: string;\n\n  beforeEach(async () => {\n    apiKey = await createTestUser();\n  });\n\n  it(\"should generate rss feed for manual lists\", async () => {\n    const { manualList } = await seedDatabase();\n    const trpcClient = getTrpcClient(apiKey);\n\n    // Enable rss feed\n    const token = await trpcClient.lists.regenRssToken.mutate({\n      listId: manualList.id,\n    });\n\n    const res = await fetchRssFeed(manualList.id, token.token);\n    expect(res.status).toBe(200);\n    expect(res.headers.get(\"Content-Type\")).toBe(\"application/rss+xml\");\n\n    const text = await res.text();\n    expect(text).toContain(\"Test Bookmark #1\");\n    expect(text).not.toContain(\"Test Bookmark #2\");\n  });\n\n  it(\"should generate rss feed for smart lists\", async () => {\n    const { smartList } = await seedDatabase();\n    const trpcClient = getTrpcClient(apiKey);\n\n    // Enable rss feed\n    const token = await trpcClient.lists.regenRssToken.mutate({\n      listId: smartList.id,\n    });\n\n    const res = await fetchRssFeed(smartList.id, token.token);\n    expect(res.status).toBe(200);\n    expect(res.headers.get(\"Content-Type\")).toBe(\"application/rss+xml\");\n\n    const text = await res.text();\n    expect(text).not.toContain(\"Test Bookmark #1\");\n    expect(text).toContain(\"Test Bookmark #2\");\n  });\n\n  it(\"should fail when the token is invalid\", async () => {\n    const { smartList } = await seedDatabase();\n    const trpcClient = getTrpcClient(apiKey);\n\n    // Enable rss feed\n    const token = await trpcClient.lists.regenRssToken.mutate({\n      listId: smartList.id,\n    });\n\n    let res = await fetchRssFeed(smartList.id, token.token);\n    expect(res.status).toBe(200);\n\n    // Invalidate the token\n    await trpcClient.lists.regenRssToken.mutate({\n      listId: smartList.id,\n    });\n\n    res = await fetchRssFeed(smartList.id, token.token);\n    expect(res.status).toBe(404);\n  });\n\n  it(\"should fail when rss gets disabled\", async () => {\n    const { smartList } = await seedDatabase();\n    const trpcClient = getTrpcClient(apiKey);\n\n    // Enable rss feed\n    const token = await trpcClient.lists.regenRssToken.mutate({\n      listId: smartList.id,\n    });\n\n    const res = await fetchRssFeed(smartList.id, token.token);\n    expect(res.status).toBe(200);\n\n    // Disable rss feed\n    await trpcClient.lists.clearRssToken.mutate({\n      listId: smartList.id,\n    });\n\n    const res2 = await fetchRssFeed(smartList.id, token.token);\n    expect(res2.status).toBe(404);\n  });\n\n  it(\"should fail when no token is provided\", async () => {\n    const { smartList } = await seedDatabase();\n    const trpcClient = getTrpcClient(apiKey);\n\n    // Enable rss feed\n    await trpcClient.lists.regenRssToken.mutate({\n      listId: smartList.id,\n    });\n\n    const res2 = await fetchRssFeed(smartList.id, \"\");\n    expect(res2.status).toBe(400);\n  });\n});\n"
  },
  {
    "path": "packages/e2e_tests/tests/api/tags.test.ts",
    "content": "import { beforeEach, describe, expect, inject, it } from \"vitest\";\n\nimport { createKarakeepClient } from \"@karakeep/sdk\";\n\nimport { createTestUser } from \"../../utils/api\";\n\ndescribe(\"Tags API\", () => {\n  const port = inject(\"karakeepPort\");\n\n  if (!port) {\n    throw new Error(\"Missing required environment variables\");\n  }\n\n  let client: ReturnType<typeof createKarakeepClient>;\n  let apiKey: string;\n\n  beforeEach(async () => {\n    apiKey = await createTestUser();\n    client = createKarakeepClient({\n      baseUrl: `http://localhost:${port}/api/v1/`,\n      headers: {\n        \"Content-Type\": \"application/json\",\n        authorization: `Bearer ${apiKey}`,\n      },\n    });\n  });\n\n  it(\"should get, update and delete a tag\", async () => {\n    // Create a tag by attaching it to the bookmark\n    const { data: tag } = await client.POST(\"/tags\", {\n      body: {\n        name: \"Test Tag\",\n      },\n    });\n    expect(tag).toBeDefined();\n    expect(tag!.name).toBe(\"Test Tag\");\n\n    const tagId = tag!.id;\n\n    // Get the tag\n    const { data: retrievedTag, response: getResponse } = await client.GET(\n      \"/tags/{tagId}\",\n      {\n        params: {\n          path: {\n            tagId,\n          },\n        },\n      },\n    );\n\n    expect(getResponse.status).toBe(200);\n    expect(retrievedTag!.id).toBe(tagId);\n    expect(retrievedTag!.name).toBe(\"Test Tag\");\n\n    // Update the tag\n    const { data: updatedTag, response: updateResponse } = await client.PATCH(\n      \"/tags/{tagId}\",\n      {\n        params: {\n          path: {\n            tagId,\n          },\n        },\n        body: {\n          name: \"Updated Tag\",\n        },\n      },\n    );\n\n    expect(updateResponse.status).toBe(200);\n    expect(updatedTag!.name).toBe(\"Updated Tag\");\n\n    // Delete the tag\n    const { response: deleteResponse } = await client.DELETE(\"/tags/{tagId}\", {\n      params: {\n        path: {\n          tagId,\n        },\n      },\n    });\n\n    expect(deleteResponse.status).toBe(204);\n\n    // Verify it's deleted\n    const { response: getDeletedResponse } = await client.GET(\"/tags/{tagId}\", {\n      params: {\n        path: {\n          tagId,\n        },\n      },\n    });\n\n    expect(getDeletedResponse.status).toBe(404);\n  });\n\n  it(\"should manage bookmarks with a tag\", async () => {\n    // Create a bookmark first\n    const { data: firstBookmark } = await client.POST(\"/bookmarks\", {\n      body: {\n        type: \"text\",\n        title: \"Test Bookmark\",\n        text: \"This is a test bookmark\",\n      },\n    });\n\n    // Create a tag by attaching it to the bookmark\n    const { data: addTagResponse } = await client.POST(\n      \"/bookmarks/{bookmarkId}/tags\",\n      {\n        params: {\n          path: {\n            bookmarkId: firstBookmark!.id,\n          },\n        },\n        body: {\n          tags: [{ tagName: \"Test Tag\" }],\n        },\n      },\n    );\n\n    const tagId = addTagResponse!.attached[0];\n\n    // Add tag to another bookmark\n    const { data: secondBookmark } = await client.POST(\"/bookmarks\", {\n      body: {\n        type: \"text\",\n        title: \"Second Bookmark\",\n        text: \"This is another test bookmark\",\n      },\n    });\n\n    const { data: addSecondTagResponse, response: addResponse } =\n      await client.POST(\"/bookmarks/{bookmarkId}/tags\", {\n        params: {\n          path: {\n            bookmarkId: secondBookmark!.id,\n          },\n        },\n        body: {\n          tags: [{ tagId }],\n        },\n      });\n\n    expect(addResponse.status).toBe(200);\n    expect(addSecondTagResponse!.attached.length).toBe(1);\n\n    // Get bookmarks with tag\n    const { data: taggedBookmarks, response: getResponse } = await client.GET(\n      \"/tags/{tagId}/bookmarks\",\n      {\n        params: {\n          path: {\n            tagId,\n          },\n        },\n      },\n    );\n\n    expect(getResponse.status).toBe(200);\n    expect(taggedBookmarks!.bookmarks.length).toBe(2);\n    expect(taggedBookmarks!.bookmarks.map((b) => b.id)).toContain(\n      firstBookmark!.id,\n    );\n    expect(taggedBookmarks!.bookmarks.map((b) => b.id)).toContain(\n      secondBookmark!.id,\n    );\n\n    // Remove tag from first bookmark\n    const { response: removeResponse } = await client.DELETE(\n      \"/bookmarks/{bookmarkId}/tags\",\n      {\n        params: {\n          path: {\n            bookmarkId: firstBookmark!.id,\n          },\n        },\n        body: {\n          tags: [{ tagId }],\n        },\n      },\n    );\n\n    expect(removeResponse.status).toBe(200);\n\n    // Verify tag is still on second bookmark\n    const { data: updatedTaggedBookmarks } = await client.GET(\n      \"/tags/{tagId}/bookmarks\",\n      {\n        params: {\n          path: {\n            tagId,\n          },\n        },\n      },\n    );\n\n    expect(updatedTaggedBookmarks!.bookmarks.length).toBe(1);\n    expect(updatedTaggedBookmarks!.bookmarks[0].id).toBe(secondBookmark!.id);\n  });\n\n  it(\"should paginate through tags\", async () => {\n    // Create multiple tags\n    const tagNames = [\"Tag A\", \"Tag B\", \"Tag C\", \"Tag D\", \"Tag E\"];\n    const createdTags = [];\n\n    for (const name of tagNames) {\n      const { data: tag } = await client.POST(\"/tags\", {\n        body: { name },\n      });\n      createdTags.push(tag!);\n    }\n\n    // Test pagination with limit of 2\n    const { data: firstPage, response: firstResponse } = await client.GET(\n      \"/tags\",\n      {\n        params: {\n          query: {\n            limit: 2,\n          },\n        },\n      },\n    );\n\n    expect(firstResponse.status).toBe(200);\n    expect(firstPage!.tags.length).toBe(2);\n    expect(firstPage!.nextCursor).toBeDefined();\n\n    // Get second page using cursor\n    const { data: secondPage, response: secondResponse } = await client.GET(\n      \"/tags\",\n      {\n        params: {\n          query: {\n            limit: 2,\n            cursor: firstPage!.nextCursor!,\n          },\n        },\n      },\n    );\n\n    expect(secondResponse.status).toBe(200);\n    expect(secondPage!.tags.length).toBe(2);\n    expect(secondPage!.nextCursor).toBeDefined();\n\n    // Get third page\n    const { data: thirdPage, response: thirdResponse } = await client.GET(\n      \"/tags\",\n      {\n        params: {\n          query: {\n            limit: 2,\n            cursor: secondPage!.nextCursor!,\n          },\n        },\n      },\n    );\n\n    expect(thirdResponse.status).toBe(200);\n    expect(thirdPage!.tags.length).toBe(1); // Only one tag remaining\n    expect(thirdPage!.nextCursor).toBeNull(); // No more pages\n\n    // Verify all tags are accounted for across pages\n    const allPagedTags = [\n      ...firstPage!.tags,\n      ...secondPage!.tags,\n      ...thirdPage!.tags,\n    ];\n    expect(allPagedTags.length).toBe(5);\n\n    // Verify all created tags are included\n    const allPagedTagIds = allPagedTags.map((tag) => tag.id);\n    const createdTagIds = createdTags.map((tag) => tag.id);\n    expect(allPagedTagIds.sort()).toEqual(createdTagIds.sort());\n  });\n\n  it(\"Invalid cursor should return 400\", async () => {\n    const { response } = await client.GET(\"/tags\", {\n      params: {\n        query: {\n          limit: 2,\n          cursor: \"{}\",\n        },\n      },\n    });\n    expect(response.status).toBe(400);\n  });\n\n  it(\"Listing without args returns all tags\", async () => {\n    const tagNames = [\"Tag A\", \"Tag B\", \"Tag C\", \"Tag D\", \"Tag E\"];\n\n    for (const name of tagNames) {\n      await client.POST(\"/tags\", {\n        body: { name },\n      });\n    }\n\n    const { data } = await client.GET(\"/tags\");\n    expect(data?.tags).toHaveLength(tagNames.length);\n  });\n});\n"
  },
  {
    "path": "packages/e2e_tests/tests/api/users.test.ts",
    "content": "import { beforeEach, describe, expect, inject, it } from \"vitest\";\n\nimport { createKarakeepClient } from \"@karakeep/sdk\";\n\nimport { createTestUser } from \"../../utils/api\";\n\ndescribe(\"Users API\", () => {\n  const port = inject(\"karakeepPort\");\n\n  if (!port) {\n    throw new Error(\"Missing required environment variables\");\n  }\n\n  let client: ReturnType<typeof createKarakeepClient>;\n  let apiKey: string;\n\n  beforeEach(async () => {\n    apiKey = await createTestUser();\n    client = createKarakeepClient({\n      baseUrl: `http://localhost:${port}/api/v1/`,\n      headers: {\n        \"Content-Type\": \"application/json\",\n        authorization: `Bearer ${apiKey}`,\n      },\n    });\n  });\n\n  it(\"should respond with user info\", async () => {\n    // Get the user info\n    const { data: userInfo } = await client.GET(\"/users/me\");\n    expect(userInfo).toBeDefined();\n    expect(userInfo?.name).toEqual(\"Test User\");\n  });\n\n  it(\"should respond with user stats\", async () => {\n    ////////////////////////////////////////////////////////////////////////////////////\n    // Prepare some data\n    ////////////////////////////////////////////////////////////////////////////////////\n    const { data: createdBookmark1 } = await client.POST(\"/bookmarks\", {\n      body: {\n        type: \"text\",\n        text: \"This is a test bookmark\",\n        favourited: true,\n      },\n    });\n    await client.POST(\"/bookmarks\", {\n      body: {\n        type: \"text\",\n        text: \"This is a test bookmark\",\n        archived: true,\n      },\n    });\n    // Create a highlight\n    await client.POST(\"/highlights\", {\n      body: {\n        bookmarkId: createdBookmark1!.id,\n        startOffset: 0,\n        endOffset: 5,\n        text: \"This is a test highlight\",\n        note: \"Test note\",\n        color: \"yellow\",\n      },\n    });\n    // attach a tag\n    await client.POST(\"/bookmarks/{bookmarkId}/tags\", {\n      params: {\n        path: {\n          bookmarkId: createdBookmark1!.id,\n        },\n      },\n      body: {\n        tags: [{ tagName: \"test-tag\" }],\n      },\n    });\n    // create two list\n    await client.POST(\"/lists\", {\n      body: {\n        name: \"Test List\",\n        icon: \"s\",\n      },\n    });\n    await client.POST(\"/lists\", {\n      body: {\n        name: \"Test List 2\",\n        icon: \"s\",\n      },\n    });\n\n    ////////////////////////////////////////////////////////////////////////////////////\n    // The actual test\n    ////////////////////////////////////////////////////////////////////////////////////\n\n    const { data: userStats } = await client.GET(\"/users/me/stats\");\n\n    expect(userStats).toBeDefined();\n    expect(userStats?.numBookmarks).toBe(2);\n    expect(userStats?.numFavorites).toBe(1);\n    expect(userStats?.numArchived).toBe(1);\n    expect(userStats?.numTags).toBe(1);\n    expect(userStats?.numLists).toBe(2);\n    expect(userStats?.numHighlights).toBe(1);\n  });\n});\n"
  },
  {
    "path": "packages/e2e_tests/tests/assetdb/assetdb-utils.ts",
    "content": "import * as fs from \"fs\";\nimport * as os from \"os\";\nimport * as path from \"path\";\nimport {\n  CreateBucketCommand,\n  DeleteBucketCommand,\n  DeleteObjectCommand,\n  ListObjectsV2Command,\n  S3Client,\n} from \"@aws-sdk/client-s3\";\n\nimport {\n  ASSET_TYPES,\n  AssetMetadata,\n  AssetStore,\n  LocalFileSystemAssetStore,\n  S3AssetStore,\n} from \"@karakeep/shared/assetdb\";\n\nexport interface TestAssetData {\n  userId: string;\n  assetId: string;\n  content: Buffer;\n  metadata: AssetMetadata;\n}\n\nexport function createTestAssetData(\n  overrides: Partial<TestAssetData> = {},\n): TestAssetData {\n  const defaultData: TestAssetData = {\n    userId: `user_${Math.random().toString(36).substring(7)}`,\n    assetId: `asset_${Math.random().toString(36).substring(7)}`,\n    content: Buffer.from(`Test content ${Math.random()}`),\n    metadata: {\n      contentType: ASSET_TYPES.TEXT_HTML,\n      fileName: \"test.html\",\n    },\n  };\n\n  return { ...defaultData, ...overrides };\n}\n\nexport function createTestImageData(): TestAssetData {\n  // Create a minimal PNG image (1x1 pixel)\n  const pngData = Buffer.from([\n    0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d,\n    0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,\n    0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, 0xde, 0x00, 0x00, 0x00,\n    0x0c, 0x49, 0x44, 0x41, 0x54, 0x08, 0xd7, 0x63, 0xf8, 0x0f, 0x00, 0x00,\n    0x01, 0x00, 0x01, 0x5c, 0xc2, 0x8a, 0x8e, 0x00, 0x00, 0x00, 0x00, 0x49,\n    0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,\n  ]);\n\n  return createTestAssetData({\n    content: pngData,\n    metadata: {\n      contentType: ASSET_TYPES.IMAGE_PNG,\n      fileName: \"test.png\",\n    },\n  });\n}\n\nexport function createTestPdfData(): TestAssetData {\n  // Create a minimal PDF\n  const pdfContent = `%PDF-1.4\n1 0 obj\n<<\n/Type /Catalog\n/Pages 2 0 R\n>>\nendobj\n\n2 0 obj\n<<\n/Type /Pages\n/Kids [3 0 R]\n/Count 1\n>>\nendobj\n\n3 0 obj\n<<\n/Type /Page\n/Parent 2 0 R\n/MediaBox [0 0 612 792]\n>>\nendobj\n\nxref\n0 4\n0000000000 65535 f \n0000000010 00000 n \n0000000053 00000 n \n0000000125 00000 n \ntrailer\n<<\n/Size 4\n/Root 1 0 R\n>>\nstartxref\n173\n%%EOF`;\n\n  return createTestAssetData({\n    content: Buffer.from(pdfContent),\n    metadata: {\n      contentType: ASSET_TYPES.APPLICATION_PDF,\n      fileName: \"test.pdf\",\n    },\n  });\n}\n\nexport async function createTempDirectory(): Promise<string> {\n  const tempDir = await fs.promises.mkdtemp(\n    path.join(os.tmpdir(), \"assetdb-test-\"),\n  );\n  return tempDir;\n}\n\nexport async function cleanupTempDirectory(tempDir: string): Promise<void> {\n  try {\n    await fs.promises.rm(tempDir, { recursive: true, force: true });\n  } catch (error) {\n    console.warn(`Failed to cleanup temp directory ${tempDir}:`, error);\n  }\n}\n\nexport function createLocalFileSystemStore(\n  rootPath: string,\n): LocalFileSystemAssetStore {\n  return new LocalFileSystemAssetStore(rootPath);\n}\n\nexport function createS3Store(bucketName: string): S3AssetStore {\n  const s3Client = new S3Client({\n    region: \"us-east-1\",\n    endpoint: \"http://localhost:9000\", // MinIO endpoint for testing\n    credentials: {\n      accessKeyId: \"minioadmin\",\n      secretAccessKey: \"minioadmin\",\n    },\n    forcePathStyle: true,\n  });\n\n  return new S3AssetStore(s3Client, bucketName);\n}\n\nexport async function createTestBucket(bucketName: string): Promise<S3Client> {\n  const s3Client = new S3Client({\n    region: \"us-east-1\",\n    endpoint: \"http://localhost:9000\",\n    credentials: {\n      accessKeyId: \"minioadmin\",\n      secretAccessKey: \"minioadmin\",\n    },\n    forcePathStyle: true,\n  });\n\n  try {\n    await s3Client.send(new CreateBucketCommand({ Bucket: bucketName }));\n  } catch (error: unknown) {\n    const err = error as { name?: string };\n    if (\n      err.name !== \"BucketAlreadyOwnedByYou\" &&\n      err.name !== \"BucketAlreadyExists\"\n    ) {\n      throw error;\n    }\n  }\n\n  return s3Client;\n}\n\nexport async function cleanupTestBucket(\n  s3Client: S3Client,\n  bucketName: string,\n): Promise<void> {\n  try {\n    // List and delete all objects\n    let continuationToken: string | undefined;\n    do {\n      const listResponse = await s3Client.send(\n        new ListObjectsV2Command({\n          Bucket: bucketName,\n          ContinuationToken: continuationToken,\n        }),\n      );\n\n      if (listResponse.Contents && listResponse.Contents.length > 0) {\n        const deletePromises = listResponse.Contents.map(\n          (obj: { Key?: string }) =>\n            s3Client.send(\n              new DeleteObjectCommand({\n                Bucket: bucketName,\n                Key: obj.Key!,\n              }),\n            ),\n        );\n        await Promise.all(deletePromises);\n      }\n\n      continuationToken = listResponse.NextContinuationToken;\n    } while (continuationToken);\n\n    // Delete the bucket\n    await s3Client.send(new DeleteBucketCommand({ Bucket: bucketName }));\n  } catch (error) {\n    console.warn(`Failed to cleanup S3 bucket ${bucketName}:`, error);\n  }\n}\n\nexport async function createTempFile(\n  content: Buffer,\n  fileName: string,\n): Promise<string> {\n  const tempDir = await createTempDirectory();\n  const filePath = path.join(tempDir, fileName);\n  await fs.promises.writeFile(filePath, content);\n  return filePath;\n}\n\nexport async function streamToBuffer(\n  stream: NodeJS.ReadableStream,\n): Promise<Buffer> {\n  const chunks: Buffer[] = [];\n  const readable = stream as AsyncIterable<Buffer>;\n\n  for await (const chunk of readable) {\n    chunks.push(chunk);\n  }\n\n  return Buffer.concat(chunks);\n}\n\nexport function generateLargeBuffer(sizeInMB: number): Buffer {\n  const sizeInBytes = sizeInMB * 1024 * 1024;\n  const buffer = Buffer.alloc(sizeInBytes);\n\n  // Fill with some pattern to make it compressible but not empty\n  for (let i = 0; i < sizeInBytes; i++) {\n    buffer[i] = i % 256;\n  }\n\n  return buffer;\n}\n\nexport async function assertAssetExists(\n  store: AssetStore,\n  userId: string,\n  assetId: string,\n): Promise<void> {\n  const { asset, metadata } = await store.readAsset({ userId, assetId });\n  if (!asset || !metadata) {\n    throw new Error(`Asset ${assetId} for user ${userId} does not exist`);\n  }\n}\n\nexport async function assertAssetNotExists(\n  store: AssetStore,\n  userId: string,\n  assetId: string,\n): Promise<void> {\n  try {\n    await store.readAsset({ userId, assetId });\n    throw new Error(`Asset ${assetId} for user ${userId} should not exist`);\n  } catch (error: unknown) {\n    // Expected to throw\n    const err = error as { message?: string };\n    if (err.message?.includes(\"should not exist\")) {\n      throw error;\n    }\n  }\n}\n\nexport async function getAllAssetsArray(store: AssetStore): Promise<\n  {\n    userId: string;\n    assetId: string;\n    contentType: string;\n    fileName?: string | null;\n    size: number;\n  }[]\n> {\n  const assets = [];\n  for await (const asset of store.getAllAssets()) {\n    assets.push(asset);\n  }\n  return assets;\n}\n"
  },
  {
    "path": "packages/e2e_tests/tests/assetdb/interface-compliance.test.ts",
    "content": "import * as fs from \"fs\";\nimport { afterEach, beforeEach, describe, expect, it } from \"vitest\";\n\nimport { ASSET_TYPES, AssetStore } from \"@karakeep/shared/assetdb\";\n\nimport {\n  assertAssetExists,\n  assertAssetNotExists,\n  cleanupTempDirectory,\n  cleanupTestBucket,\n  createLocalFileSystemStore,\n  createS3Store,\n  createTempDirectory,\n  createTempFile,\n  createTestAssetData,\n  createTestBucket,\n  createTestImageData,\n  createTestPdfData,\n  generateLargeBuffer,\n  getAllAssetsArray,\n  streamToBuffer,\n} from \"./assetdb-utils\";\n\ninterface TestContext {\n  store: AssetStore;\n  cleanup: () => Promise<void>;\n}\n\nasync function createLocalContext(): Promise<TestContext> {\n  const tempDir = await createTempDirectory();\n  const store = createLocalFileSystemStore(tempDir);\n\n  return {\n    store,\n    cleanup: async () => {\n      await cleanupTempDirectory(tempDir);\n    },\n  };\n}\n\nasync function createS3Context(): Promise<TestContext> {\n  const bucketName = `test-bucket-${Math.random().toString(36).substring(7)}`;\n  const s3Client = await createTestBucket(bucketName);\n  const store = createS3Store(bucketName);\n\n  return {\n    store,\n    cleanup: async () => {\n      await cleanupTestBucket(s3Client, bucketName);\n    },\n  };\n}\n\ndescribe.each([\n  { name: \"LocalFileSystemAssetStore\", createContext: createLocalContext },\n  { name: \"S3AssetStore\", createContext: createS3Context },\n])(\"AssetStore Interface Compliance - $name\", ({ createContext }) => {\n  let context: TestContext;\n\n  beforeEach(async () => {\n    context = await createContext();\n  });\n\n  afterEach(async () => {\n    await context.cleanup();\n  });\n\n  describe(\"Basic CRUD Operations\", () => {\n    it(\"should save and retrieve an asset\", async () => {\n      const testData = createTestAssetData();\n\n      await context.store.saveAsset({\n        userId: testData.userId,\n        assetId: testData.assetId,\n        asset: testData.content,\n        metadata: testData.metadata,\n      });\n\n      const { asset, metadata } = await context.store.readAsset({\n        userId: testData.userId,\n        assetId: testData.assetId,\n      });\n\n      expect(asset).toEqual(testData.content);\n      expect(metadata).toEqual(testData.metadata);\n    });\n\n    it(\"should delete an asset\", async () => {\n      const testData = createTestAssetData();\n\n      await context.store.saveAsset({\n        userId: testData.userId,\n        assetId: testData.assetId,\n        asset: testData.content,\n        metadata: testData.metadata,\n      });\n\n      await assertAssetExists(context.store, testData.userId, testData.assetId);\n\n      await context.store.deleteAsset({\n        userId: testData.userId,\n        assetId: testData.assetId,\n      });\n\n      await assertAssetNotExists(\n        context.store,\n        testData.userId,\n        testData.assetId,\n      );\n    });\n\n    it(\"should get asset size\", async () => {\n      const testData = createTestAssetData();\n\n      await context.store.saveAsset({\n        userId: testData.userId,\n        assetId: testData.assetId,\n        asset: testData.content,\n        metadata: testData.metadata,\n      });\n\n      const size = await context.store.getAssetSize({\n        userId: testData.userId,\n        assetId: testData.assetId,\n      });\n\n      expect(size).toBe(testData.content.length);\n    });\n\n    it(\"should read asset metadata\", async () => {\n      const testData = createTestAssetData();\n\n      await context.store.saveAsset({\n        userId: testData.userId,\n        assetId: testData.assetId,\n        asset: testData.content,\n        metadata: testData.metadata,\n      });\n\n      const metadata = await context.store.readAssetMetadata({\n        userId: testData.userId,\n        assetId: testData.assetId,\n      });\n\n      expect(metadata).toEqual(testData.metadata);\n    });\n  });\n\n  describe(\"Streaming Operations\", () => {\n    it(\"should create readable stream\", async () => {\n      const testData = createTestAssetData();\n\n      await context.store.saveAsset({\n        userId: testData.userId,\n        assetId: testData.assetId,\n        asset: testData.content,\n        metadata: testData.metadata,\n      });\n\n      const stream = await context.store.createAssetReadStream({\n        userId: testData.userId,\n        assetId: testData.assetId,\n      });\n\n      const streamedContent = await streamToBuffer(stream);\n      expect(streamedContent).toEqual(testData.content);\n    });\n\n    it(\"should support range requests in streams\", async () => {\n      const content = Buffer.from(\"0123456789abcdef\");\n      const testData = createTestAssetData({ content });\n\n      await context.store.saveAsset({\n        userId: testData.userId,\n        assetId: testData.assetId,\n        asset: testData.content,\n        metadata: testData.metadata,\n      });\n\n      const stream = await context.store.createAssetReadStream({\n        userId: testData.userId,\n        assetId: testData.assetId,\n        start: 5,\n        end: 10,\n      });\n\n      const streamedContent = await streamToBuffer(stream);\n      expect(streamedContent.toString()).toBe(\"56789a\");\n    });\n  });\n\n  describe(\"Asset Types Support\", () => {\n    it(\"should support all required asset types\", async () => {\n      const testCases = [\n        { contentType: ASSET_TYPES.IMAGE_JPEG, fileName: \"test.jpg\" },\n        { contentType: ASSET_TYPES.IMAGE_PNG, fileName: \"test.png\" },\n        { contentType: ASSET_TYPES.IMAGE_WEBP, fileName: \"test.webp\" },\n        { contentType: ASSET_TYPES.APPLICATION_PDF, fileName: \"test.pdf\" },\n        { contentType: ASSET_TYPES.TEXT_HTML, fileName: \"test.html\" },\n        { contentType: ASSET_TYPES.VIDEO_MP4, fileName: \"test.mp4\" },\n      ];\n\n      for (const { contentType, fileName } of testCases) {\n        const testData = createTestAssetData({\n          metadata: { contentType, fileName },\n        });\n\n        await context.store.saveAsset({\n          userId: testData.userId,\n          assetId: testData.assetId,\n          asset: testData.content,\n          metadata: testData.metadata,\n        });\n\n        const { metadata } = await context.store.readAsset({\n          userId: testData.userId,\n          assetId: testData.assetId,\n        });\n\n        expect(metadata.contentType).toBe(contentType);\n        expect(metadata.fileName).toBe(fileName);\n      }\n    });\n\n    it(\"should handle large assets\", async () => {\n      const largeContent = generateLargeBuffer(5); // 5MB\n      const testData = createTestAssetData({ content: largeContent });\n\n      await context.store.saveAsset({\n        userId: testData.userId,\n        assetId: testData.assetId,\n        asset: testData.content,\n        metadata: testData.metadata,\n      });\n\n      const { asset } = await context.store.readAsset({\n        userId: testData.userId,\n        assetId: testData.assetId,\n      });\n\n      expect(asset.length).toBe(largeContent.length);\n      expect(asset).toEqual(largeContent);\n    });\n\n    it(\"should reject unsupported asset types\", async () => {\n      const testData = createTestAssetData({\n        metadata: {\n          contentType: \"unsupported/type\",\n          fileName: \"test.unsupported\",\n        },\n      });\n\n      await expect(\n        context.store.saveAsset({\n          userId: testData.userId,\n          assetId: testData.assetId,\n          asset: testData.content,\n          metadata: testData.metadata,\n        }),\n      ).rejects.toThrow(\"Unsupported asset type\");\n    });\n  });\n\n  describe(\"Bulk Operations\", () => {\n    it(\"should delete all user assets\", async () => {\n      const userId = \"bulk-test-user\";\n      const testAssets = [\n        createTestAssetData({ userId }),\n        createTestAssetData({ userId }),\n        createTestAssetData({ userId }),\n      ];\n      const otherUserAsset = createTestAssetData(); // Different user\n\n      // Save all assets\n      await Promise.all([\n        ...testAssets.map((testData) =>\n          context.store.saveAsset({\n            userId: testData.userId,\n            assetId: testData.assetId,\n            asset: testData.content,\n            metadata: testData.metadata,\n          }),\n        ),\n        context.store.saveAsset({\n          userId: otherUserAsset.userId,\n          assetId: otherUserAsset.assetId,\n          asset: otherUserAsset.content,\n          metadata: otherUserAsset.metadata,\n        }),\n      ]);\n\n      // Delete user assets\n      await context.store.deleteUserAssets({ userId });\n\n      // Verify user assets are deleted\n      for (const testData of testAssets) {\n        await assertAssetNotExists(\n          context.store,\n          testData.userId,\n          testData.assetId,\n        );\n      }\n\n      // Verify other user's asset still exists\n      await assertAssetExists(\n        context.store,\n        otherUserAsset.userId,\n        otherUserAsset.assetId,\n      );\n    });\n\n    it(\"should list all assets\", async () => {\n      const testAssets = [\n        createTestAssetData(),\n        createTestImageData(),\n        createTestPdfData(),\n      ];\n\n      // Save all assets\n      await Promise.all(\n        testAssets.map((testData) =>\n          context.store.saveAsset({\n            userId: testData.userId,\n            assetId: testData.assetId,\n            asset: testData.content,\n            metadata: testData.metadata,\n          }),\n        ),\n      );\n\n      const assets = await getAllAssetsArray(context.store);\n\n      expect(assets).toHaveLength(3);\n\n      // Verify all assets are present\n      const assetIds = assets.map((a) => a.assetId);\n      for (const testData of testAssets) {\n        expect(assetIds).toContain(testData.assetId);\n      }\n\n      // Verify asset structure\n      for (const asset of assets) {\n        expect(asset).toHaveProperty(\"userId\");\n        expect(asset).toHaveProperty(\"assetId\");\n        expect(asset).toHaveProperty(\"contentType\");\n        expect(asset).toHaveProperty(\"size\");\n        expect(typeof asset.size).toBe(\"number\");\n        expect(asset.size).toBeGreaterThan(0);\n      }\n    });\n  });\n\n  describe(\"File Operations\", () => {\n    it(\"should save asset from file and delete original file\", async () => {\n      const testData = createTestAssetData();\n      const tempFile = await createTempFile(testData.content, \"temp-asset.bin\");\n\n      // Verify temp file exists before operation\n      expect(\n        await fs.promises\n          .access(tempFile)\n          .then(() => true)\n          .catch(() => false),\n      ).toBe(true);\n\n      await context.store.saveAssetFromFile({\n        userId: testData.userId,\n        assetId: testData.assetId,\n        assetPath: tempFile,\n        metadata: testData.metadata,\n      });\n\n      // Verify temp file was deleted\n      expect(\n        await fs.promises\n          .access(tempFile)\n          .then(() => true)\n          .catch(() => false),\n      ).toBe(false);\n\n      // Verify asset was saved correctly\n      const { asset, metadata } = await context.store.readAsset({\n        userId: testData.userId,\n        assetId: testData.assetId,\n      });\n\n      expect(asset).toEqual(testData.content);\n      expect(metadata).toEqual(testData.metadata);\n    });\n  });\n\n  describe(\"Error Handling\", () => {\n    it(\"should throw error for non-existent asset read\", async () => {\n      await expect(\n        context.store.readAsset({\n          userId: \"non-existent-user\",\n          assetId: \"non-existent-asset\",\n        }),\n      ).rejects.toThrow();\n    });\n\n    it(\"should throw error for non-existent asset metadata\", async () => {\n      await expect(\n        context.store.readAssetMetadata({\n          userId: \"non-existent-user\",\n          assetId: \"non-existent-asset\",\n        }),\n      ).rejects.toThrow();\n    });\n\n    it(\"should throw error for non-existent asset size\", async () => {\n      await expect(\n        context.store.getAssetSize({\n          userId: \"non-existent-user\",\n          assetId: \"non-existent-asset\",\n        }),\n      ).rejects.toThrow();\n    });\n\n    it(\"should handle deleting non-existent asset gracefully\", async () => {\n      // Filesystem implementation throws errors for non-existent files\n      await expect(\n        context.store.deleteAsset({\n          userId: \"non-existent-user\",\n          assetId: \"non-existent-asset\",\n        }),\n      ).resolves.not.toThrow();\n    });\n\n    it(\"should handle deletion of non-existent user directory gracefully\", async () => {\n      // Should not throw error when user directory doesn't exist\n      await expect(\n        context.store.deleteUserAssets({ userId: \"non-existent-user\" }),\n      ).resolves.not.toThrow();\n    });\n\n    it(\"should handle non-existent asset stream appropriately\", async () => {\n      const streamResult = context.store.createAssetReadStream({\n        userId: \"non-existent-user\",\n        assetId: \"non-existent-asset\",\n      });\n\n      await expect(streamResult).rejects.toThrow();\n    });\n  });\n\n  describe(\"Data Integrity\", () => {\n    it(\"should preserve binary data integrity\", async () => {\n      const testData = createTestImageData();\n\n      await context.store.saveAsset({\n        userId: testData.userId,\n        assetId: testData.assetId,\n        asset: testData.content,\n        metadata: testData.metadata,\n      });\n\n      const { asset } = await context.store.readAsset({\n        userId: testData.userId,\n        assetId: testData.assetId,\n      });\n\n      // Verify exact binary match\n      expect(asset).toEqual(testData.content);\n\n      // Verify PNG header is intact\n      expect(asset[0]).toBe(0x89);\n      expect(asset[1]).toBe(0x50);\n      expect(asset[2]).toBe(0x4e);\n      expect(asset[3]).toBe(0x47);\n    });\n\n    it(\"should preserve metadata exactly\", async () => {\n      const testData = createTestAssetData({\n        metadata: {\n          contentType: ASSET_TYPES.APPLICATION_PDF,\n          fileName: \"test-document.pdf\",\n        },\n      });\n\n      await context.store.saveAsset({\n        userId: testData.userId,\n        assetId: testData.assetId,\n        asset: testData.content,\n        metadata: testData.metadata,\n      });\n\n      const { metadata } = await context.store.readAsset({\n        userId: testData.userId,\n        assetId: testData.assetId,\n      });\n\n      expect(metadata).toEqual(testData.metadata);\n      expect(metadata.contentType).toBe(ASSET_TYPES.APPLICATION_PDF);\n      expect(metadata.fileName).toBe(\"test-document.pdf\");\n    });\n\n    it(\"should handle null fileName correctly\", async () => {\n      const testData = createTestAssetData({\n        metadata: {\n          contentType: ASSET_TYPES.TEXT_HTML,\n          fileName: null,\n        },\n      });\n\n      await context.store.saveAsset({\n        userId: testData.userId,\n        assetId: testData.assetId,\n        asset: testData.content,\n        metadata: testData.metadata,\n      });\n\n      const { metadata } = await context.store.readAsset({\n        userId: testData.userId,\n        assetId: testData.assetId,\n      });\n\n      expect(metadata.fileName).toBeNull();\n    });\n  });\n\n  describe(\"Concurrent Operations\", () => {\n    it(\"should handle concurrent saves safely\", async () => {\n      const testAssets = Array.from({ length: 5 }, () => createTestAssetData());\n\n      await Promise.all(\n        testAssets.map((testData) =>\n          context.store.saveAsset({\n            userId: testData.userId,\n            assetId: testData.assetId,\n            asset: testData.content,\n            metadata: testData.metadata,\n          }),\n        ),\n      );\n\n      // Verify all assets were saved correctly\n      for (const testData of testAssets) {\n        const { asset, metadata } = await context.store.readAsset({\n          userId: testData.userId,\n          assetId: testData.assetId,\n        });\n\n        expect(asset).toEqual(testData.content);\n        expect(metadata).toEqual(testData.metadata);\n      }\n    });\n\n    it(\"should handle concurrent reads safely\", async () => {\n      const testData = createTestAssetData();\n\n      await context.store.saveAsset({\n        userId: testData.userId,\n        assetId: testData.assetId,\n        asset: testData.content,\n        metadata: testData.metadata,\n      });\n\n      // Perform multiple concurrent reads\n      const readPromises = Array.from({ length: 10 }, () =>\n        context.store.readAsset({\n          userId: testData.userId,\n          assetId: testData.assetId,\n        }),\n      );\n\n      const results = await Promise.all(readPromises);\n\n      // Verify all reads returned the same data\n      for (const { asset, metadata } of results) {\n        expect(asset).toEqual(testData.content);\n        expect(metadata).toEqual(testData.metadata);\n      }\n    });\n  });\n\n  describe(\"Edge Cases\", () => {\n    it(\"should handle empty assets\", async () => {\n      const testData = createTestAssetData({\n        content: Buffer.alloc(0),\n      });\n\n      await context.store.saveAsset({\n        userId: testData.userId,\n        assetId: testData.assetId,\n        asset: testData.content,\n        metadata: testData.metadata,\n      });\n\n      const { asset } = await context.store.readAsset({\n        userId: testData.userId,\n        assetId: testData.assetId,\n      });\n\n      expect(asset.length).toBe(0);\n\n      const size = await context.store.getAssetSize({\n        userId: testData.userId,\n        assetId: testData.assetId,\n      });\n\n      expect(size).toBe(0);\n    });\n\n    it(\"should handle special characters in user and asset IDs\", async () => {\n      const testData = createTestAssetData({\n        userId: \"user-with-special_chars.123\",\n        assetId: \"asset_with-special.chars_456\",\n      });\n\n      await context.store.saveAsset({\n        userId: testData.userId,\n        assetId: testData.assetId,\n        asset: testData.content,\n        metadata: testData.metadata,\n      });\n\n      const { asset, metadata } = await context.store.readAsset({\n        userId: testData.userId,\n        assetId: testData.assetId,\n      });\n\n      expect(asset).toEqual(testData.content);\n      expect(metadata).toEqual(testData.metadata);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/e2e_tests/tests/assetdb/local-filesystem-store.test.ts",
    "content": "import * as fs from \"fs\";\nimport * as path from \"path\";\nimport { afterEach, beforeEach, describe, expect, it } from \"vitest\";\n\nimport { LocalFileSystemAssetStore } from \"@karakeep/shared/assetdb\";\n\nimport {\n  assertAssetNotExists,\n  cleanupTempDirectory,\n  createLocalFileSystemStore,\n  createTempDirectory,\n  createTestAssetData,\n} from \"./assetdb-utils\";\n\ndescribe(\"LocalFileSystemAssetStore - Filesystem-Specific Behaviors\", () => {\n  let tempDir: string;\n  let store: LocalFileSystemAssetStore;\n\n  beforeEach(async () => {\n    tempDir = await createTempDirectory();\n    store = createLocalFileSystemStore(tempDir);\n  });\n\n  afterEach(async () => {\n    await cleanupTempDirectory(tempDir);\n  });\n\n  describe(\"File System Structure\", () => {\n    it(\"should create correct directory structure and files\", async () => {\n      const testData = createTestAssetData();\n\n      await store.saveAsset({\n        userId: testData.userId,\n        assetId: testData.assetId,\n        asset: testData.content,\n        metadata: testData.metadata,\n      });\n\n      // Verify directory structure\n      const assetDir = path.join(tempDir, testData.userId, testData.assetId);\n      expect(\n        await fs.promises\n          .access(assetDir)\n          .then(() => true)\n          .catch(() => false),\n      ).toBe(true);\n\n      // Verify asset.bin file\n      const assetFile = path.join(assetDir, \"asset.bin\");\n      expect(\n        await fs.promises\n          .access(assetFile)\n          .then(() => true)\n          .catch(() => false),\n      ).toBe(true);\n\n      // Verify metadata.json file\n      const metadataFile = path.join(assetDir, \"metadata.json\");\n      expect(\n        await fs.promises\n          .access(metadataFile)\n          .then(() => true)\n          .catch(() => false),\n      ).toBe(true);\n\n      // Verify file contents\n      const savedContent = await fs.promises.readFile(assetFile);\n      expect(savedContent).toEqual(testData.content);\n\n      const savedMetadata = JSON.parse(\n        await fs.promises.readFile(metadataFile, \"utf8\"),\n      );\n      expect(savedMetadata).toEqual(testData.metadata);\n    });\n\n    it(\"should create nested directory structure for user/asset hierarchy\", async () => {\n      const userId = \"user123\";\n      const assetId = \"asset456\";\n      const testData = createTestAssetData({ userId, assetId });\n\n      await store.saveAsset({\n        userId: testData.userId,\n        assetId: testData.assetId,\n        asset: testData.content,\n        metadata: testData.metadata,\n      });\n\n      // Verify the exact directory structure\n      const userDir = path.join(tempDir, userId);\n      const assetDir = path.join(userDir, assetId);\n\n      expect(\n        await fs.promises\n          .access(userDir)\n          .then(() => true)\n          .catch(() => false),\n      ).toBe(true);\n\n      expect(\n        await fs.promises\n          .access(assetDir)\n          .then(() => true)\n          .catch(() => false),\n      ).toBe(true);\n\n      // Verify files exist in the correct location\n      expect(\n        await fs.promises\n          .access(path.join(assetDir, \"asset.bin\"))\n          .then(() => true)\n          .catch(() => false),\n      ).toBe(true);\n\n      expect(\n        await fs.promises\n          .access(path.join(assetDir, \"metadata.json\"))\n          .then(() => true)\n          .catch(() => false),\n      ).toBe(true);\n    });\n  });\n\n  describe(\"Directory Cleanup\", () => {\n    it(\"should remove entire asset directory when deleting asset\", async () => {\n      const testData = createTestAssetData();\n\n      await store.saveAsset({\n        userId: testData.userId,\n        assetId: testData.assetId,\n        asset: testData.content,\n        metadata: testData.metadata,\n      });\n\n      const assetDir = path.join(tempDir, testData.userId, testData.assetId);\n\n      // Verify directory exists\n      expect(\n        await fs.promises\n          .access(assetDir)\n          .then(() => true)\n          .catch(() => false),\n      ).toBe(true);\n\n      await store.deleteAsset({\n        userId: testData.userId,\n        assetId: testData.assetId,\n      });\n\n      // Verify entire directory was removed\n      expect(\n        await fs.promises\n          .access(assetDir)\n          .then(() => true)\n          .catch(() => false),\n      ).toBe(false);\n\n      await assertAssetNotExists(store, testData.userId, testData.assetId);\n    });\n\n    it(\"should remove entire user directory when deleting all user assets\", async () => {\n      const userId = \"test-user\";\n      const testData1 = createTestAssetData({ userId });\n      const testData2 = createTestAssetData({ userId });\n\n      await Promise.all([\n        store.saveAsset({\n          userId: testData1.userId,\n          assetId: testData1.assetId,\n          asset: testData1.content,\n          metadata: testData1.metadata,\n        }),\n        store.saveAsset({\n          userId: testData2.userId,\n          assetId: testData2.assetId,\n          asset: testData2.content,\n          metadata: testData2.metadata,\n        }),\n      ]);\n\n      const userDir = path.join(tempDir, userId);\n\n      // Verify user directory exists\n      expect(\n        await fs.promises\n          .access(userDir)\n          .then(() => true)\n          .catch(() => false),\n      ).toBe(true);\n\n      await store.deleteUserAssets({ userId });\n\n      // Verify entire user directory was removed\n      expect(\n        await fs.promises\n          .access(userDir)\n          .then(() => true)\n          .catch(() => false),\n      ).toBe(false);\n    });\n  });\n\n  describe(\"File System Permissions\", () => {\n    it(\"should create directories with appropriate permissions\", async () => {\n      const testData = createTestAssetData();\n\n      await store.saveAsset({\n        userId: testData.userId,\n        assetId: testData.assetId,\n        asset: testData.content,\n        metadata: testData.metadata,\n      });\n\n      const userDir = path.join(tempDir, testData.userId);\n      const assetDir = path.join(userDir, testData.assetId);\n\n      // Verify directories are readable and writable\n      const userStats = await fs.promises.stat(userDir);\n      const assetStats = await fs.promises.stat(assetDir);\n\n      expect(userStats.isDirectory()).toBe(true);\n      expect(assetStats.isDirectory()).toBe(true);\n\n      // Verify we can read and write to the directories\n      await fs.promises.access(userDir, fs.constants.R_OK | fs.constants.W_OK);\n      await fs.promises.access(assetDir, fs.constants.R_OK | fs.constants.W_OK);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/e2e_tests/tests/assetdb/s3-store.test.ts",
    "content": "import { HeadObjectCommand, S3Client } from \"@aws-sdk/client-s3\";\nimport { afterEach, beforeEach, describe, expect, it } from \"vitest\";\n\nimport { S3AssetStore } from \"@karakeep/shared/assetdb\";\n\nimport {\n  assertAssetExists,\n  cleanupTestBucket,\n  createS3Store,\n  createTestAssetData,\n  createTestBucket,\n} from \"./assetdb-utils\";\n\ndescribe(\"S3AssetStore - S3-Specific Behaviors\", () => {\n  let bucketName: string;\n  let s3Client: S3Client;\n  let store: S3AssetStore;\n\n  beforeEach(async () => {\n    bucketName = `test-bucket-${Math.random().toString(36).substring(7)}`;\n    s3Client = await createTestBucket(bucketName);\n    store = createS3Store(bucketName);\n  });\n\n  afterEach(async () => {\n    await cleanupTestBucket(s3Client, bucketName);\n  });\n\n  describe(\"S3 Object Structure\", () => {\n    it(\"should store asset as single object with user-defined metadata\", async () => {\n      const testData = createTestAssetData();\n\n      await store.saveAsset({\n        userId: testData.userId,\n        assetId: testData.assetId,\n        asset: testData.content,\n        metadata: testData.metadata,\n      });\n\n      // Verify the object exists with the expected key structure\n      const objectKey = `${testData.userId}/${testData.assetId}`;\n      const headResponse = await s3Client.send(\n        new HeadObjectCommand({\n          Bucket: bucketName,\n          Key: objectKey,\n        }),\n      );\n\n      // Verify metadata is stored in S3 user-defined metadata\n      expect(headResponse.Metadata).toBeDefined();\n      expect(headResponse.Metadata![\"x-amz-meta-content-type\"]).toBe(\n        testData.metadata.contentType,\n      );\n      expect(headResponse.Metadata![\"x-amz-meta-file-name\"]).toBe(\n        testData.metadata.fileName || \"\",\n      );\n\n      // Verify content type is set correctly on the S3 object\n      expect(headResponse.ContentType).toBe(testData.metadata.contentType);\n    });\n\n    it(\"should handle null fileName in metadata correctly\", async () => {\n      const testData = createTestAssetData({\n        metadata: {\n          contentType: \"text/html\",\n          fileName: null,\n        },\n      });\n\n      await store.saveAsset({\n        userId: testData.userId,\n        assetId: testData.assetId,\n        asset: testData.content,\n        metadata: testData.metadata,\n      });\n\n      const objectKey = `${testData.userId}/${testData.assetId}`;\n      const headResponse = await s3Client.send(\n        new HeadObjectCommand({\n          Bucket: bucketName,\n          Key: objectKey,\n        }),\n      );\n\n      // Verify null fileName are not stored in S3 metadata\n      expect(headResponse.Metadata![\"x-amz-meta-file-name\"]).toBeUndefined();\n\n      // Verify reading back gives us null fileName\n      const metadata = await store.readAssetMetadata({\n        userId: testData.userId,\n        assetId: testData.assetId,\n      });\n      expect(metadata.fileName).toBeNull();\n    });\n  });\n\n  describe(\"S3 Key Structure\", () => {\n    it(\"should use clean userId/assetId key structure\", async () => {\n      const userId = \"user123\";\n      const assetId = \"asset456\";\n      const testData = createTestAssetData({ userId, assetId });\n\n      await store.saveAsset({\n        userId: testData.userId,\n        assetId: testData.assetId,\n        asset: testData.content,\n        metadata: testData.metadata,\n      });\n\n      // Verify the exact key structure\n      const expectedKey = `${userId}/${assetId}`;\n      const headResponse = await s3Client.send(\n        new HeadObjectCommand({\n          Bucket: bucketName,\n          Key: expectedKey,\n        }),\n      );\n\n      expect(headResponse.ContentLength).toBe(testData.content.length);\n    });\n\n    it(\"should handle special characters in user and asset IDs\", async () => {\n      const userId = \"user/with/slashes\";\n      const assetId = \"asset-with-special-chars_123\";\n      const testData = createTestAssetData({ userId, assetId });\n\n      await store.saveAsset({\n        userId: testData.userId,\n        assetId: testData.assetId,\n        asset: testData.content,\n        metadata: testData.metadata,\n      });\n\n      await assertAssetExists(store, testData.userId, testData.assetId);\n    });\n  });\n\n  describe(\"S3 Eventual Consistency\", () => {\n    it(\"should handle immediate read after write (MinIO strong consistency)\", async () => {\n      const testData = createTestAssetData();\n\n      await store.saveAsset({\n        userId: testData.userId,\n        assetId: testData.assetId,\n        asset: testData.content,\n        metadata: testData.metadata,\n      });\n\n      // Immediately try to read - should work with MinIO's strong consistency\n      const { asset, metadata } = await store.readAsset({\n        userId: testData.userId,\n        assetId: testData.assetId,\n      });\n\n      expect(asset).toEqual(testData.content);\n      expect(metadata).toEqual(testData.metadata);\n    });\n  });\n\n  describe(\"S3 Metadata Conversion\", () => {\n    it(\"should correctly convert between AssetMetadata and S3 metadata\", async () => {\n      const testCases = [\n        {\n          contentType: \"image/jpeg\",\n          fileName: \"test-image.jpg\",\n        },\n        {\n          contentType: \"application/pdf\",\n          fileName: \"document.pdf\",\n        },\n        {\n          contentType: \"text/html\",\n          fileName: null,\n        },\n      ];\n\n      for (const metadata of testCases) {\n        const testData = createTestAssetData({ metadata });\n\n        await store.saveAsset({\n          userId: testData.userId,\n          assetId: testData.assetId,\n          asset: testData.content,\n          metadata: testData.metadata,\n        });\n\n        // Verify metadata round-trip\n        const retrievedMetadata = await store.readAssetMetadata({\n          userId: testData.userId,\n          assetId: testData.assetId,\n        });\n\n        expect(retrievedMetadata).toEqual(testData.metadata);\n      }\n    });\n  });\n});\n"
  },
  {
    "path": "packages/e2e_tests/tests/workers/crawler.test.ts",
    "content": "import { assert, beforeEach, describe, expect, inject, it } from \"vitest\";\n\nimport { createKarakeepClient } from \"@karakeep/sdk\";\n\nimport { createTestUser } from \"../../utils/api\";\nimport { waitUntil } from \"../../utils/general\";\n\ndescribe(\"Crawler Tests\", () => {\n  const port = inject(\"karakeepPort\");\n\n  if (!port) {\n    throw new Error(\"Missing required environment variables\");\n  }\n\n  let client: ReturnType<typeof createKarakeepClient>;\n  let apiKey: string;\n\n  async function getBookmark(bookmarkId: string) {\n    const { data } = await client.GET(`/bookmarks/{bookmarkId}`, {\n      params: {\n        path: {\n          bookmarkId,\n        },\n        query: {\n          includeContent: true,\n        },\n      },\n    });\n    return data;\n  }\n\n  beforeEach(async () => {\n    apiKey = await createTestUser();\n    client = createKarakeepClient({\n      baseUrl: `http://localhost:${port}/api/v1/`,\n      headers: {\n        \"Content-Type\": \"application/json\",\n        authorization: `Bearer ${apiKey}`,\n      },\n    });\n  });\n\n  it(\"should crawl a website\", async () => {\n    let { data: bookmark } = await client.POST(\"/bookmarks\", {\n      body: {\n        type: \"link\",\n        url: \"http://nginx:80/hello.html\",\n      },\n    });\n    assert(bookmark);\n\n    await waitUntil(async () => {\n      const data = await getBookmark(bookmark!.id);\n      assert(data);\n      assert(data.content.type === \"link\");\n      return data.content.crawledAt !== null;\n    }, \"Bookmark is crawled\");\n\n    bookmark = await getBookmark(bookmark.id);\n    assert(bookmark && bookmark.content.type === \"link\");\n    expect(bookmark.content.crawledAt).toBeDefined();\n    expect(bookmark.content.htmlContent).toContain(\"Hello World\");\n    expect(bookmark.content.title).toContain(\"My test title\");\n    expect(bookmark.content.url).toBe(\"http://nginx:80/hello.html\");\n    expect(\n      bookmark.assets.find((a) => a.assetType === \"screenshot\"),\n    ).toBeDefined();\n  });\n\n  it(\"image lings jobs be converted into images\", async () => {\n    let { data: bookmark } = await client.POST(\"/bookmarks\", {\n      body: {\n        type: \"link\",\n        url: \"http://nginx:80/image.png\",\n      },\n    });\n    assert(bookmark);\n\n    await waitUntil(async () => {\n      const data = await getBookmark(bookmark!.id);\n      assert(data);\n      return data.content.type === \"asset\";\n    }, \"Bookmark is crawled and converted to an image\");\n\n    bookmark = await getBookmark(bookmark.id);\n    assert(bookmark && bookmark.content.type === \"asset\");\n    expect(bookmark.content.assetType).toBe(\"image\");\n    expect(bookmark.content.assetId).toBeDefined();\n    expect(bookmark.content.fileName).toBe(\"image.png\");\n    expect(bookmark.content.sourceUrl).toBe(\"http://nginx:80/image.png\");\n  });\n});\n"
  },
  {
    "path": "packages/e2e_tests/tests/workers/feed.test.ts",
    "content": "import { assert, beforeEach, describe, expect, inject, it } from \"vitest\";\n\nimport { createTestUser } from \"../../utils/api\";\nimport { waitUntil } from \"../../utils/general\";\nimport { getTrpcClient } from \"../../utils/trpc\";\n\ndescribe(\"Feed Worker Tests\", () => {\n  const port = inject(\"karakeepPort\");\n\n  if (!port) {\n    throw new Error(\"Missing required environment variables\");\n  }\n\n  let apiKey: string;\n\n  beforeEach(async () => {\n    apiKey = await createTestUser();\n  });\n\n  it(\"should fetch and parse an RSS feed and create bookmarks\", async () => {\n    const trpcClient = getTrpcClient(apiKey);\n\n    // Create a feed pointing to the RSS file served by nginx\n    const feed = await trpcClient.feeds.create.mutate({\n      name: \"Test Feed\",\n      url: \"http://nginx:80/feed.xml\",\n      enabled: true,\n    });\n\n    expect(feed.id).toBeDefined();\n    expect(feed.lastFetchedStatus).toBe(\"pending\");\n\n    // Trigger a manual fetch\n    await trpcClient.feeds.fetchNow.mutate({ feedId: feed.id });\n\n    // Wait until the feed has been fetched successfully\n    await waitUntil(async () => {\n      const updated = await trpcClient.feeds.get.query({ feedId: feed.id });\n      return updated.lastFetchedStatus === \"success\";\n    }, \"Feed is fetched successfully\");\n\n    // Verify the feed status\n    const fetchedFeed = await trpcClient.feeds.get.query({ feedId: feed.id });\n    expect(fetchedFeed.lastFetchedStatus).toBe(\"success\");\n    expect(fetchedFeed.lastFetchedAt).toBeDefined();\n\n    // Verify bookmarks were created from the feed entries\n    const bookmarks = await trpcClient.bookmarks.getBookmarks.query({\n      archived: false,\n    });\n\n    expect(bookmarks.bookmarks.length).toBe(2);\n\n    const titles = bookmarks.bookmarks.map((b) => b.title);\n    expect(titles).toContain(\"First Test Article\");\n    expect(titles).toContain(\"Second Test Article\");\n\n    const urls = bookmarks.bookmarks.map((b) => {\n      assert(b.content.type === \"link\");\n      return b.content.url;\n    });\n    expect(urls).toContain(\"http://nginx:80/hello.html\");\n    expect(urls).toContain(\"http://nginx:80/hello.html?article=2\");\n  });\n\n  it(\"should import tags from RSS categories when importTags is enabled\", async () => {\n    const trpcClient = getTrpcClient(apiKey);\n\n    // Create a feed with importTags enabled\n    const feed = await trpcClient.feeds.create.mutate({\n      name: \"Test Feed With Tags\",\n      url: \"http://nginx:80/feed.xml\",\n      enabled: true,\n      importTags: true,\n    });\n\n    // Trigger a manual fetch\n    await trpcClient.feeds.fetchNow.mutate({ feedId: feed.id });\n\n    // Wait until the feed has been fetched successfully\n    await waitUntil(async () => {\n      const updated = await trpcClient.feeds.get.query({ feedId: feed.id });\n      return updated.lastFetchedStatus === \"success\";\n    }, \"Feed with tags is fetched successfully\");\n\n    // Verify bookmarks were created\n    const bookmarks = await trpcClient.bookmarks.getBookmarks.query({\n      archived: false,\n    });\n    expect(bookmarks.bookmarks.length).toBe(2);\n\n    // Find the first article and check its tags\n    const firstArticle = bookmarks.bookmarks.find(\n      (b) => b.title === \"First Test Article\",\n    );\n    assert(firstArticle);\n    const firstTags = firstArticle.tags.map((t) => t.name);\n    expect(firstTags).toContain(\"tech\");\n    expect(firstTags).toContain(\"testing\");\n\n    // Find the second article and check its tags\n    const secondArticle = bookmarks.bookmarks.find(\n      (b) => b.title === \"Second Test Article\",\n    );\n    assert(secondArticle);\n    const secondTags = secondArticle.tags.map((t) => t.name);\n    expect(secondTags).toContain(\"news\");\n  });\n\n  it(\"should not create duplicate bookmarks on re-fetch\", async () => {\n    const trpcClient = getTrpcClient(apiKey);\n\n    const feed = await trpcClient.feeds.create.mutate({\n      name: \"Test Feed Dedup\",\n      url: \"http://nginx:80/feed.xml\",\n      enabled: true,\n    });\n\n    // First fetch\n    await trpcClient.feeds.fetchNow.mutate({ feedId: feed.id });\n    await waitUntil(async () => {\n      const updated = await trpcClient.feeds.get.query({ feedId: feed.id });\n      return updated.lastFetchedStatus === \"success\";\n    }, \"First feed fetch completes\");\n\n    const afterFirstFetch = await trpcClient.feeds.get.query({\n      feedId: feed.id,\n    });\n    assert(afterFirstFetch.lastFetchedAt);\n    const firstFetchedAt = afterFirstFetch.lastFetchedAt;\n\n    const firstFetch = await trpcClient.bookmarks.getBookmarks.query({\n      archived: false,\n    });\n    expect(firstFetch.bookmarks.length).toBe(2);\n\n    // Second fetch of the same feed - should not create duplicates\n    await trpcClient.feeds.fetchNow.mutate({ feedId: feed.id });\n    await waitUntil(async () => {\n      const updated = await trpcClient.feeds.get.query({ feedId: feed.id });\n      assert(updated.lastFetchedAt);\n      return updated.lastFetchedAt > firstFetchedAt;\n    }, \"Second feed fetch completes\");\n\n    const secondFetch = await trpcClient.bookmarks.getBookmarks.query({\n      archived: false,\n    });\n    expect(secondFetch.bookmarks.length).toBe(2);\n  });\n});\n"
  },
  {
    "path": "packages/e2e_tests/tests/workers/import.test.ts",
    "content": "import { assert, beforeEach, describe, expect, inject, it } from \"vitest\";\n\nimport { createKarakeepClient } from \"@karakeep/sdk\";\n\nimport { createTestUser } from \"../../utils/api\";\nimport { waitUntil } from \"../../utils/general\";\nimport { getTrpcClient } from \"../../utils/trpc\";\n\ndescribe(\"Import Worker Tests\", () => {\n  const port = inject(\"karakeepPort\");\n\n  if (!port) {\n    throw new Error(\"Missing required environment variables\");\n  }\n\n  let client: ReturnType<typeof createKarakeepClient>;\n  let trpc: ReturnType<typeof getTrpcClient>;\n  let apiKey: string;\n\n  beforeEach(async () => {\n    apiKey = await createTestUser();\n    client = createKarakeepClient({\n      baseUrl: `http://localhost:${port}/api/v1/`,\n      headers: {\n        \"Content-Type\": \"application/json\",\n        authorization: `Bearer ${apiKey}`,\n      },\n    });\n    trpc = getTrpcClient(apiKey);\n  });\n\n  it(\"should import 15 bookmarks of different types\", async () => {\n    // Create lists first (lists require IDs, not paths)\n    const { data: parentList } = await client.POST(\"/lists\", {\n      body: {\n        name: \"Import Test List\",\n        icon: \"folder\",\n      },\n    });\n    assert(parentList);\n\n    const { data: nestedList } = await client.POST(\"/lists\", {\n      body: {\n        name: \"Nested\",\n        icon: \"folder\",\n        parentId: parentList.id,\n      },\n    });\n    assert(nestedList);\n\n    // Create a root list that all imported bookmarks will be attached to\n    const { data: rootList } = await client.POST(\"/lists\", {\n      body: {\n        name: \"Import Root List\",\n        icon: \"folder\",\n      },\n    });\n    assert(rootList);\n\n    // Create an import session with rootListId\n    const { id: importSessionId } =\n      await trpc.importSessions.createImportSession.mutate({\n        name: \"E2E Test Import\",\n        rootListId: rootList.id,\n      });\n    assert(importSessionId);\n\n    // Define 15 bookmarks of different types\n    const bookmarksToImport = [\n      // Links (7 total, with varying metadata)\n      {\n        type: \"link\" as const,\n        url: \"http://nginx:80/hello.html\",\n      },\n      {\n        type: \"link\" as const,\n        url: \"http://nginx:80/page1.html\",\n        title: \"Page 1 Title\",\n      },\n      {\n        type: \"link\" as const,\n        url: \"http://nginx:80/page2.html\",\n        title: \"Page 2 with Note\",\n        note: \"This is a note for page 2\",\n      },\n      {\n        type: \"link\" as const,\n        url: \"http://nginx:80/page3.html\",\n        title: \"Page 3 with Tags\",\n        tags: [\"tag1\", \"tag2\"],\n      },\n      {\n        type: \"link\" as const,\n        url: \"http://nginx:80/page4.html\",\n        title: \"Page 4 with List\",\n        listIds: [parentList.id],\n      },\n      {\n        type: \"link\" as const,\n        url: \"http://nginx:80/page5.html\",\n        title: \"Page 5 with Source Date\",\n        sourceAddedAt: new Date(\"2024-01-15T10:30:00Z\"),\n      },\n      {\n        type: \"link\" as const,\n        url: \"http://nginx:80/page6.html\",\n        title: \"Page 6 Full Metadata\",\n        note: \"Full metadata note\",\n        tags: [\"imported\", \"full\"],\n        listIds: [nestedList.id],\n        sourceAddedAt: new Date(\"2024-02-20T15:45:00Z\"),\n      },\n\n      // Text bookmarks (5 total)\n      {\n        type: \"text\" as const,\n        content: \"This is a basic text bookmark content.\",\n      },\n      {\n        type: \"text\" as const,\n        title: \"Text with Title\",\n        content: \"Text bookmark with a title.\",\n      },\n      {\n        type: \"text\" as const,\n        title: \"Text with Tags\",\n        content: \"Text bookmark with tags attached.\",\n        tags: [\"text-tag\", \"imported\"],\n      },\n      {\n        type: \"text\" as const,\n        title: \"Text with Note\",\n        content: \"Text bookmark content here.\",\n        note: \"A note attached to this text bookmark.\",\n      },\n      {\n        type: \"text\" as const,\n        title: \"Text Full Metadata\",\n        content: \"Text bookmark with all metadata fields.\",\n        note: \"Complete text note\",\n        tags: [\"complete\", \"text\"],\n        listIds: [parentList.id],\n        sourceAddedAt: new Date(\"2024-03-10T08:00:00Z\"),\n      },\n\n      // Duplicates (3 total - same URLs as earlier links)\n      {\n        type: \"link\" as const,\n        url: \"http://nginx:80/hello.html\", // Duplicate of link #1\n        title: \"Duplicate of Hello\",\n      },\n      {\n        type: \"link\" as const,\n        url: \"http://nginx:80/page1.html\", // Duplicate of link #2\n        title: \"Duplicate of Page 1\",\n      },\n      {\n        type: \"link\" as const,\n        url: \"http://nginx:80/page2.html\", // Duplicate of link #3\n        title: \"Duplicate of Page 2\",\n        note: \"Different note but same URL\",\n      },\n    ];\n\n    // Stage all bookmarks\n    await trpc.importSessions.stageImportedBookmarks.mutate({\n      importSessionId,\n      bookmarks: bookmarksToImport,\n    });\n\n    // Finalize the import to trigger processing\n    await trpc.importSessions.finalizeImportStaging.mutate({\n      importSessionId,\n    });\n\n    // Wait for all bookmarks to be processed\n    await waitUntil(\n      async () => {\n        const stats = await trpc.importSessions.getImportSessionStats.query({\n          importSessionId,\n        });\n        const allProcessed =\n          stats.completedBookmarks + stats.failedBookmarks ===\n          stats.totalBookmarks;\n        console.log(\n          `Import progress: ${stats.completedBookmarks} completed, ${stats.failedBookmarks} failed, ${stats.totalBookmarks} total`,\n        );\n        return allProcessed && stats.totalBookmarks === 15;\n      },\n      \"All bookmarks are processed\",\n      120000, // 2 minutes timeout\n    );\n\n    // Get final stats\n    const finalStats = await trpc.importSessions.getImportSessionStats.query({\n      importSessionId,\n    });\n\n    expect(finalStats.totalBookmarks).toBe(15);\n    expect(finalStats.completedBookmarks).toBe(15);\n    expect(finalStats.failedBookmarks).toBe(0);\n    expect(finalStats.status).toBe(\"completed\");\n\n    // Get results by filter\n    const acceptedResults =\n      await trpc.importSessions.getImportSessionResults.query({\n        importSessionId,\n        filter: \"accepted\",\n        limit: 50,\n      });\n\n    const duplicateResults =\n      await trpc.importSessions.getImportSessionResults.query({\n        importSessionId,\n        filter: \"skipped_duplicate\",\n        limit: 50,\n      });\n\n    // We expect 12 accepted (7 links + 5 text) and 3 duplicates\n    expect(acceptedResults.items.length).toBe(12);\n    expect(duplicateResults.items.length).toBe(3);\n\n    // Verify duplicates reference the original bookmarks\n    for (const dup of duplicateResults.items) {\n      expect(dup.resultBookmarkId).toBeDefined();\n      expect(dup.result).toBe(\"skipped_duplicate\");\n    }\n\n    // Verify accepted bookmarks have resultBookmarkId\n    for (const accepted of acceptedResults.items) {\n      expect(accepted.resultBookmarkId).toBeDefined();\n      expect(accepted.result).toBe(\"accepted\");\n    }\n\n    // Verify actual bookmarks were created via the API\n    const { data: allBookmarks } = await client.GET(\"/bookmarks\", {\n      params: {\n        query: {\n          limit: 50,\n        },\n      },\n    });\n    assert(allBookmarks);\n\n    // Should have 12 unique bookmarks (7 links + 5 text)\n    expect(allBookmarks.bookmarks.length).toBe(12);\n\n    // Verify link bookmarks\n    const linkBookmarks = allBookmarks.bookmarks.filter(\n      (b) => b.content.type === \"link\",\n    );\n    expect(linkBookmarks.length).toBe(7);\n\n    // Verify text bookmarks\n    const textBookmarks = allBookmarks.bookmarks.filter(\n      (b) => b.content.type === \"text\",\n    );\n    expect(textBookmarks.length).toBe(5);\n\n    // Verify tags were created\n    const { data: tagsResponse } = await client.GET(\"/tags\", {});\n    assert(tagsResponse);\n    const tagNames = tagsResponse.tags.map((t) => t.name);\n    expect(tagNames).toContain(\"tag1\");\n    expect(tagNames).toContain(\"tag2\");\n    expect(tagNames).toContain(\"imported\");\n    expect(tagNames).toContain(\"text-tag\");\n    expect(tagNames).toContain(\"complete\");\n\n    // Verify tags are actually attached to bookmarks\n    // Find a bookmark with tags and verify\n    const bookmarkWithTags = allBookmarks.bookmarks.find((b) =>\n      b.tags.some((t) => t.name === \"tag1\"),\n    );\n    assert(bookmarkWithTags, \"Should find a bookmark with tag1\");\n    expect(bookmarkWithTags.tags.map((t) => t.name)).toContain(\"tag1\");\n    expect(bookmarkWithTags.tags.map((t) => t.name)).toContain(\"tag2\");\n\n    // Verify \"imported\" tag is on multiple bookmarks (used in link and text)\n    const bookmarksWithImportedTag = allBookmarks.bookmarks.filter((b) =>\n      b.tags.some((t) => t.name === \"imported\"),\n    );\n    expect(bookmarksWithImportedTag.length).toBeGreaterThanOrEqual(2);\n\n    // Verify bookmarks are actually in the lists\n    const { data: parentListBookmarks } = await client.GET(\n      \"/lists/{listId}/bookmarks\",\n      {\n        params: {\n          path: { listId: parentList.id },\n        },\n      },\n    );\n    assert(parentListBookmarks);\n    // Should have bookmarks with listIds containing parentList.id\n    expect(parentListBookmarks.bookmarks.length).toBeGreaterThanOrEqual(2);\n\n    // Verify nested list has bookmarks\n    const { data: nestedListBookmarks } = await client.GET(\n      \"/lists/{listId}/bookmarks\",\n      {\n        params: {\n          path: { listId: nestedList.id },\n        },\n      },\n    );\n    assert(nestedListBookmarks);\n    // Should have the bookmark with listIds containing nestedList.id\n    expect(nestedListBookmarks.bookmarks.length).toBeGreaterThanOrEqual(1);\n\n    // Verify ALL imported bookmarks are in the root list (via rootListId)\n    const { data: rootListBookmarks } = await client.GET(\n      \"/lists/{listId}/bookmarks\",\n      {\n        params: {\n          path: { listId: rootList.id },\n        },\n      },\n    );\n    assert(rootListBookmarks);\n    // All 12 unique bookmarks should be in the root list\n    expect(rootListBookmarks.bookmarks.length).toBe(12);\n  });\n});\n"
  },
  {
    "path": "packages/e2e_tests/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@karakeep/tsconfig/node.json\",\n  \"include\": [\"**/*.ts\"],\n  \"exclude\": [\"node_modules\"],\n  \"compilerOptions\": {\n    \"tsBuildInfoFile\": \"node_modules/.cache/tsbuildinfo.json\"\n  }\n}\n"
  },
  {
    "path": "packages/e2e_tests/utils/api.ts",
    "content": "import { getTrpcClient } from \"./trpc\";\n\nexport function getAuthHeader(apiKey: string) {\n  return {\n    \"Content-Type\": \"application/json\",\n    authorization: `Bearer ${apiKey}`,\n  };\n}\n\nexport async function uploadTestAsset(\n  apiKey: string,\n  port: number,\n  file: File,\n) {\n  const formData = new FormData();\n  formData.append(\"file\", file);\n\n  const response = await fetch(`http://localhost:${port}/api/v1/assets`, {\n    method: \"POST\",\n    headers: {\n      authorization: `Bearer ${apiKey}`,\n    },\n    body: formData,\n  });\n\n  if (!response.ok) {\n    throw new Error(`Failed to upload asset: ${response.statusText}`);\n  }\n\n  return response.json() as Promise<{\n    assetId: string;\n    contentType: string;\n    fileName: string;\n  }>;\n}\n\nexport async function createTestUser() {\n  const trpc = getTrpcClient();\n\n  const random = Math.random().toString(36).substring(7);\n  const email = `testuser+${random}@example.com`;\n\n  await trpc.users.create.mutate({\n    name: \"Test User\",\n    email,\n    password: \"test1234\",\n    confirmPassword: \"test1234\",\n  });\n\n  const { key } = await trpc.apiKeys.exchange.mutate({\n    email,\n    password: \"test1234\",\n    keyName: \"test-key\",\n  });\n\n  return key;\n}\n"
  },
  {
    "path": "packages/e2e_tests/utils/assets.ts",
    "content": "import * as fs from \"fs\";\nimport * as path from \"path\";\n\nconst pdfFixturePath = path.join(__dirname, \"..\", \"fixtures\", \"test.pdf\");\nconst pdfContent = fs.readFileSync(pdfFixturePath);\n\nexport function createTestPdfFile(fileName = \"test.pdf\"): File {\n  return new File([pdfContent], fileName, {\n    type: \"application/pdf\",\n  });\n}\n"
  },
  {
    "path": "packages/e2e_tests/utils/general.ts",
    "content": "export async function waitUntil(\n  f: () => Promise<boolean>,\n  description: string,\n  timeoutMs = 60000,\n): Promise<void> {\n  const startTime = Date.now();\n\n  while (Date.now() - startTime < timeoutMs) {\n    console.log(`Waiting for ${description}...`);\n    try {\n      const res = await f();\n      if (res) {\n        console.log(`${description}: success`);\n        return;\n      }\n    } catch (error) {\n      // Ignore errors and retry\n      console.log(`${description}: error, retrying...: ${error}`);\n    }\n    await new Promise((resolve) => setTimeout(resolve, 1000));\n  }\n\n  throw new Error(`${description}: timeout after ${timeoutMs}ms`);\n}\n"
  },
  {
    "path": "packages/e2e_tests/utils/trpc.ts",
    "content": "import { createTRPCClient, httpBatchLink } from \"@trpc/client\";\nimport superjson from \"superjson\";\n\nimport type { AppRouter } from \"@karakeep/trpc/routers/_app\";\n\nexport function getTrpcClient(apiKey?: string) {\n  return createTRPCClient<AppRouter>({\n    links: [\n      httpBatchLink({\n        transformer: superjson,\n        url: `http://localhost:${process.env.KARAKEEP_PORT}/api/trpc`,\n        headers() {\n          return {\n            authorization: apiKey ? `Bearer ${apiKey}` : undefined,\n          };\n        },\n      }),\n    ],\n  });\n}\n"
  },
  {
    "path": "packages/e2e_tests/vitest.config.ts",
    "content": "/// <reference types=\"vitest\" />\n\nimport tsconfigPaths from \"vite-tsconfig-paths\";\nimport { defineConfig } from \"vitest/config\";\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  plugins: [tsconfigPaths()],\n  test: {\n    alias: {\n      \"@/*\": \"./*\",\n    },\n    globalSetup: [\"./setup/startContainers.ts\", \"./setup/seed.ts\"],\n    teardownTimeout: 30000,\n    include: [\"tests/**/*.test.ts\"],\n    testTimeout: 60000,\n    env: {\n      NEXTAUTH_SECRET: \"secret\",\n    },\n  },\n});\n"
  },
  {
    "path": "packages/open-api/.oxlintrc.json",
    "content": "{\n  \"$schema\": \"../../node_modules/oxlint/configuration_schema.json\",\n  \"extends\": [\n    \"../../tooling/oxlint/oxlint-base.json\"\n  ],\n  \"env\": {\n    \"builtin\": true,\n    \"commonjs\": true\n  },\n  \"ignorePatterns\": [\n    \"**/*.config.js\",\n    \"**/*.config.cjs\",\n    \"**/.eslintrc.cjs\",\n    \"**/.next\",\n    \"**/dist\",\n    \"**/build\",\n    \"**/pnpm-lock.yaml\"\n  ]\n}\n"
  },
  {
    "path": "packages/open-api/index.ts",
    "content": "import * as fs from \"fs\";\nimport * as process from \"process\";\nimport {\n  OpenApiGeneratorV3,\n  OpenAPIRegistry,\n} from \"@asteasolutions/zod-to-openapi\";\n\nimport { registry as adminRegistry } from \"./lib/admin\";\nimport { registry as assetsRegistry } from \"./lib/assets\";\nimport { registry as backupsRegistry } from \"./lib/backups\";\nimport { registry as bookmarksRegistry } from \"./lib/bookmarks\";\nimport { registry as commonRegistry } from \"./lib/common\";\nimport { registry as highlightsRegistry } from \"./lib/highlights\";\nimport { registry as listsRegistry } from \"./lib/lists\";\nimport { registry as tagsRegistry } from \"./lib/tags\";\nimport { registry as userRegistry } from \"./lib/users\";\n\nfunction getOpenApiDocumentation() {\n  const registry = new OpenAPIRegistry([\n    commonRegistry,\n    bookmarksRegistry,\n    listsRegistry,\n    tagsRegistry,\n    highlightsRegistry,\n    userRegistry,\n    assetsRegistry,\n    adminRegistry,\n    backupsRegistry,\n  ]);\n\n  const generator = new OpenApiGeneratorV3(registry.definitions);\n\n  return generator.generateDocument({\n    openapi: \"3.0.0\",\n    info: {\n      version: \"1.0.0\",\n      title: \"Karakeep API\",\n      description:\n        \"Karakeep is a self-hostable bookmarking and read-it-later service. \" +\n        \"This API allows you to manage bookmarks, lists, tags, highlights, assets, and backups programmatically.\\n\\n\" +\n        \"## Authentication\\n\\n\" +\n        \"All endpoints require a Bearer token passed in the `Authorization` header. \" +\n        \"You can generate an API key from the Karakeep web UI under **Settings > API Keys**.\\n\\n\" +\n        \"## Pagination\\n\\n\" +\n        \"List endpoints support cursor-based pagination via `cursor` and `limit` query parameters. \" +\n        \"The response includes a `nextCursor` field — pass it as the `cursor` parameter to fetch the next page. \" +\n        \"A `null` value for `nextCursor` indicates there are no more results.\\n\\n\" +\n        \"## Bookmark Types\\n\\n\" +\n        \"Bookmarks can be one of three types:\\n\" +\n        \"- **link** — A URL bookmark with optional crawled metadata.\\n\" +\n        \"- **text** — A plain text note.\\n\" +\n        \"- **asset** — An uploaded file (image or PDF).\\n\\n## Rate Limiting\\n\\nWhen rate limiting is enabled, the API enforces per-IP request limits. \" +\n        \"If you exceed the allowed number of requests within the time window, the API returns a `429 Too Many Requests` response with a message indicating how many seconds to wait before retrying.\",\n    },\n    tags: [\n      {\n        name: \"Bookmarks\",\n        description:\n          \"Manage bookmarks — create, retrieve, update, delete, search, and organize bookmarks with tags, lists, highlights, and assets.\",\n      },\n      {\n        name: \"Lists\",\n        description:\n          \"Manage bookmark lists. Lists can be manual (curated) or smart (query-based). Bookmarks can belong to multiple lists.\",\n      },\n      {\n        name: \"Tags\",\n        description:\n          \"Manage tags for categorizing bookmarks. Tags can be attached by users or automatically by AI.\",\n      },\n      {\n        name: \"Highlights\",\n        description:\n          \"Manage text highlights within bookmarks. Highlights support color coding and optional notes.\",\n      },\n      {\n        name: \"Assets\",\n        description:\n          \"Upload and retrieve binary assets (images, PDFs, screenshots) associated with bookmarks.\",\n      },\n      {\n        name: \"Users\",\n        description:\n          \"Retrieve information and statistics about the currently authenticated user.\",\n      },\n      {\n        name: \"Admin\",\n        description:\n          \"Administrative endpoints for managing users. Requires admin role.\",\n      },\n      {\n        name: \"Backups\",\n        description:\n          \"Create and manage full account backups as downloadable zip archives.\",\n      },\n    ],\n    servers: [\n      {\n        url: \"{address}/api/v1\",\n        variables: {\n          address: {\n            default: \"https://try.karakeep.app\",\n            description: \"The address of the Karakeep server\",\n          },\n        },\n      },\n    ],\n  });\n}\n\nfunction writeDocumentation() {\n  const docs = getOpenApiDocumentation();\n  const fileContent = JSON.stringify(docs, null, 2);\n  fs.writeFileSync(`./karakeep-openapi-spec.json`, fileContent, {\n    encoding: \"utf-8\",\n  });\n}\n\nfunction checkDocumentation() {\n  const docs = getOpenApiDocumentation();\n  const fileContent = JSON.stringify(docs, null, 2);\n  const oldContent = fs.readFileSync(`./karakeep-openapi-spec.json`, {\n    encoding: \"utf-8\",\n  });\n  if (oldContent !== fileContent) {\n    process.exit(1);\n  }\n}\n\nif (process.argv[2] === \"check\") {\n  checkDocumentation();\n} else {\n  writeDocumentation();\n}\n"
  },
  {
    "path": "packages/open-api/karakeep-openapi-spec.json",
    "content": "{\n  \"openapi\": \"3.0.0\",\n  \"info\": {\n    \"version\": \"1.0.0\",\n    \"title\": \"Karakeep API\",\n    \"description\": \"Karakeep is a self-hostable bookmarking and read-it-later service. This API allows you to manage bookmarks, lists, tags, highlights, assets, and backups programmatically.\\n\\n## Authentication\\n\\nAll endpoints require a Bearer token passed in the `Authorization` header. You can generate an API key from the Karakeep web UI under **Settings > API Keys**.\\n\\n## Pagination\\n\\nList endpoints support cursor-based pagination via `cursor` and `limit` query parameters. The response includes a `nextCursor` field — pass it as the `cursor` parameter to fetch the next page. A `null` value for `nextCursor` indicates there are no more results.\\n\\n## Bookmark Types\\n\\nBookmarks can be one of three types:\\n- **link** — A URL bookmark with optional crawled metadata.\\n- **text** — A plain text note.\\n- **asset** — An uploaded file (image or PDF).\\n\\n## Rate Limiting\\n\\nWhen rate limiting is enabled, the API enforces per-IP request limits. If you exceed the allowed number of requests within the time window, the API returns a `429 Too Many Requests` response with a message indicating how many seconds to wait before retrying.\"\n  },\n  \"tags\": [\n    {\n      \"name\": \"Bookmarks\",\n      \"description\": \"Manage bookmarks — create, retrieve, update, delete, search, and organize bookmarks with tags, lists, highlights, and assets.\"\n    },\n    {\n      \"name\": \"Lists\",\n      \"description\": \"Manage bookmark lists. Lists can be manual (curated) or smart (query-based). Bookmarks can belong to multiple lists.\"\n    },\n    {\n      \"name\": \"Tags\",\n      \"description\": \"Manage tags for categorizing bookmarks. Tags can be attached by users or automatically by AI.\"\n    },\n    {\n      \"name\": \"Highlights\",\n      \"description\": \"Manage text highlights within bookmarks. Highlights support color coding and optional notes.\"\n    },\n    {\n      \"name\": \"Assets\",\n      \"description\": \"Upload and retrieve binary assets (images, PDFs, screenshots) associated with bookmarks.\"\n    },\n    {\n      \"name\": \"Users\",\n      \"description\": \"Retrieve information and statistics about the currently authenticated user.\"\n    },\n    {\n      \"name\": \"Admin\",\n      \"description\": \"Administrative endpoints for managing users. Requires admin role.\"\n    },\n    {\n      \"name\": \"Backups\",\n      \"description\": \"Create and manage full account backups as downloadable zip archives.\"\n    }\n  ],\n  \"servers\": [\n    {\n      \"url\": \"{address}/api/v1\",\n      \"variables\": {\n        \"address\": {\n          \"default\": \"https://try.karakeep.app\",\n          \"description\": \"The address of the Karakeep server\"\n        }\n      }\n    }\n  ],\n  \"components\": {\n    \"securitySchemes\": {\n      \"bearerAuth\": {\n        \"type\": \"http\",\n        \"scheme\": \"bearer\",\n        \"bearerFormat\": \"JWT\"\n      }\n    },\n    \"schemas\": {\n      \"BookmarkId\": {\n        \"type\": \"string\",\n        \"description\": \"The unique identifier of the bookmark.\",\n        \"example\": \"ieidlxygmwj87oxz5hxttoc8\"\n      },\n      \"ListId\": {\n        \"type\": \"string\",\n        \"description\": \"The unique identifier of the list.\",\n        \"example\": \"ieidlxygmwj87oxz5hxttoc8\"\n      },\n      \"TagId\": {\n        \"type\": \"string\",\n        \"description\": \"The unique identifier of the tag.\",\n        \"example\": \"ieidlxygmwj87oxz5hxttoc8\"\n      },\n      \"HighlightId\": {\n        \"type\": \"string\",\n        \"description\": \"The unique identifier of the highlight.\",\n        \"example\": \"ieidlxygmwj87oxz5hxttoc8\"\n      },\n      \"AssetId\": {\n        \"type\": \"string\",\n        \"description\": \"The unique identifier of the asset.\",\n        \"example\": \"ieidlxygmwj87oxz5hxttoc8\"\n      },\n      \"BackupId\": {\n        \"type\": \"string\",\n        \"description\": \"The unique identifier of the backup.\",\n        \"example\": \"ieidlxygmwj87oxz5hxttoc8\"\n      },\n      \"Bookmark\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": {\n            \"type\": \"string\"\n          },\n          \"createdAt\": {\n            \"type\": \"string\"\n          },\n          \"modifiedAt\": {\n            \"type\": \"string\",\n            \"nullable\": true\n          },\n          \"title\": {\n            \"type\": \"string\",\n            \"nullable\": true\n          },\n          \"archived\": {\n            \"type\": \"boolean\"\n          },\n          \"favourited\": {\n            \"type\": \"boolean\"\n          },\n          \"taggingStatus\": {\n            \"type\": \"string\",\n            \"nullable\": true,\n            \"enum\": [\n              \"success\",\n              \"failure\",\n              \"pending\"\n            ]\n          },\n          \"summarizationStatus\": {\n            \"type\": \"string\",\n            \"nullable\": true,\n            \"enum\": [\n              \"success\",\n              \"failure\",\n              \"pending\"\n            ]\n          },\n          \"note\": {\n            \"type\": \"string\",\n            \"nullable\": true\n          },\n          \"summary\": {\n            \"type\": \"string\",\n            \"nullable\": true\n          },\n          \"source\": {\n            \"type\": \"string\",\n            \"nullable\": true,\n            \"enum\": [\n              \"api\",\n              \"web\",\n              \"cli\",\n              \"mobile\",\n              \"extension\",\n              \"singlefile\",\n              \"rss\",\n              \"import\"\n            ]\n          },\n          \"userId\": {\n            \"type\": \"string\"\n          },\n          \"tags\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"id\": {\n                  \"type\": \"string\"\n                },\n                \"name\": {\n                  \"type\": \"string\"\n                },\n                \"attachedBy\": {\n                  \"type\": \"string\",\n                  \"enum\": [\n                    \"ai\",\n                    \"human\"\n                  ]\n                }\n              },\n              \"required\": [\n                \"id\",\n                \"name\",\n                \"attachedBy\"\n              ]\n            }\n          },\n          \"content\": {\n            \"oneOf\": [\n              {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"type\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                      \"link\"\n                    ]\n                  },\n                  \"url\": {\n                    \"type\": \"string\"\n                  },\n                  \"title\": {\n                    \"type\": \"string\",\n                    \"nullable\": true\n                  },\n                  \"description\": {\n                    \"type\": \"string\",\n                    \"nullable\": true\n                  },\n                  \"imageUrl\": {\n                    \"type\": \"string\",\n                    \"nullable\": true\n                  },\n                  \"imageAssetId\": {\n                    \"type\": \"string\",\n                    \"nullable\": true\n                  },\n                  \"screenshotAssetId\": {\n                    \"type\": \"string\",\n                    \"nullable\": true\n                  },\n                  \"pdfAssetId\": {\n                    \"type\": \"string\",\n                    \"nullable\": true\n                  },\n                  \"fullPageArchiveAssetId\": {\n                    \"type\": \"string\",\n                    \"nullable\": true\n                  },\n                  \"precrawledArchiveAssetId\": {\n                    \"type\": \"string\",\n                    \"nullable\": true\n                  },\n                  \"videoAssetId\": {\n                    \"type\": \"string\",\n                    \"nullable\": true\n                  },\n                  \"favicon\": {\n                    \"type\": \"string\",\n                    \"nullable\": true\n                  },\n                  \"htmlContent\": {\n                    \"type\": \"string\",\n                    \"nullable\": true\n                  },\n                  \"contentAssetId\": {\n                    \"type\": \"string\",\n                    \"nullable\": true\n                  },\n                  \"crawledAt\": {\n                    \"type\": \"string\",\n                    \"nullable\": true\n                  },\n                  \"crawlStatus\": {\n                    \"type\": \"string\",\n                    \"nullable\": true,\n                    \"enum\": [\n                      \"success\",\n                      \"failure\",\n                      \"pending\"\n                    ]\n                  },\n                  \"author\": {\n                    \"type\": \"string\",\n                    \"nullable\": true\n                  },\n                  \"publisher\": {\n                    \"type\": \"string\",\n                    \"nullable\": true\n                  },\n                  \"datePublished\": {\n                    \"type\": \"string\",\n                    \"nullable\": true\n                  },\n                  \"dateModified\": {\n                    \"type\": \"string\",\n                    \"nullable\": true\n                  }\n                },\n                \"required\": [\n                  \"type\",\n                  \"url\"\n                ]\n              },\n              {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"type\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                      \"text\"\n                    ]\n                  },\n                  \"text\": {\n                    \"type\": \"string\"\n                  },\n                  \"sourceUrl\": {\n                    \"type\": \"string\",\n                    \"nullable\": true\n                  }\n                },\n                \"required\": [\n                  \"type\",\n                  \"text\"\n                ]\n              },\n              {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"type\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                      \"asset\"\n                    ]\n                  },\n                  \"assetType\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                      \"image\",\n                      \"pdf\"\n                    ]\n                  },\n                  \"assetId\": {\n                    \"type\": \"string\"\n                  },\n                  \"fileName\": {\n                    \"type\": \"string\",\n                    \"nullable\": true\n                  },\n                  \"sourceUrl\": {\n                    \"type\": \"string\",\n                    \"nullable\": true\n                  },\n                  \"size\": {\n                    \"type\": \"number\",\n                    \"nullable\": true\n                  },\n                  \"content\": {\n                    \"type\": \"string\",\n                    \"nullable\": true\n                  }\n                },\n                \"required\": [\n                  \"type\",\n                  \"assetType\",\n                  \"assetId\"\n                ]\n              },\n              {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"type\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                      \"unknown\"\n                    ]\n                  }\n                },\n                \"required\": [\n                  \"type\"\n                ]\n              }\n            ]\n          },\n          \"assets\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"id\": {\n                  \"type\": \"string\"\n                },\n                \"assetType\": {\n                  \"type\": \"string\",\n                  \"enum\": [\n                    \"linkHtmlContent\",\n                    \"screenshot\",\n                    \"pdf\",\n                    \"assetScreenshot\",\n                    \"bannerImage\",\n                    \"fullPageArchive\",\n                    \"video\",\n                    \"bookmarkAsset\",\n                    \"precrawledArchive\",\n                    \"userUploaded\",\n                    \"avatar\",\n                    \"unknown\"\n                  ]\n                },\n                \"fileName\": {\n                  \"type\": \"string\",\n                  \"nullable\": true\n                }\n              },\n              \"required\": [\n                \"id\",\n                \"assetType\"\n              ]\n            }\n          }\n        },\n        \"required\": [\n          \"id\",\n          \"createdAt\",\n          \"modifiedAt\",\n          \"archived\",\n          \"favourited\",\n          \"taggingStatus\",\n          \"summarizationStatus\",\n          \"userId\",\n          \"tags\",\n          \"content\",\n          \"assets\"\n        ]\n      },\n      \"PaginatedBookmarks\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"bookmarks\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Bookmark\"\n            }\n          },\n          \"nextCursor\": {\n            \"type\": \"string\",\n            \"nullable\": true,\n            \"description\": \"Cursor for the next page, or null if no more results.\"\n          }\n        },\n        \"required\": [\n          \"bookmarks\",\n          \"nextCursor\"\n        ]\n      },\n      \"Cursor\": {\n        \"type\": \"string\",\n        \"description\": \"Cursor from a previous response to fetch the next page.\"\n      },\n      \"Error\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"code\": {\n            \"type\": \"string\",\n            \"description\": \"A machine-readable error code.\"\n          },\n          \"message\": {\n            \"type\": \"string\",\n            \"description\": \"A human-readable error message.\"\n          }\n        },\n        \"required\": [\n          \"code\",\n          \"message\"\n        ]\n      },\n      \"List\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": {\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"type\": \"string\"\n          },\n          \"description\": {\n            \"type\": \"string\",\n            \"nullable\": true\n          },\n          \"icon\": {\n            \"type\": \"string\"\n          },\n          \"parentId\": {\n            \"type\": \"string\",\n            \"nullable\": true\n          },\n          \"type\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"manual\",\n              \"smart\"\n            ],\n            \"default\": \"manual\"\n          },\n          \"query\": {\n            \"type\": \"string\",\n            \"nullable\": true\n          },\n          \"public\": {\n            \"type\": \"boolean\"\n          },\n          \"hasCollaborators\": {\n            \"type\": \"boolean\"\n          },\n          \"userRole\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"owner\",\n              \"editor\",\n              \"viewer\",\n              \"public\"\n            ]\n          }\n        },\n        \"required\": [\n          \"id\",\n          \"name\",\n          \"icon\",\n          \"parentId\",\n          \"public\",\n          \"hasCollaborators\",\n          \"userRole\"\n        ]\n      },\n      \"Highlight\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"bookmarkId\": {\n            \"type\": \"string\"\n          },\n          \"startOffset\": {\n            \"type\": \"number\"\n          },\n          \"endOffset\": {\n            \"type\": \"number\"\n          },\n          \"color\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"yellow\",\n              \"red\",\n              \"green\",\n              \"blue\"\n            ],\n            \"default\": \"yellow\"\n          },\n          \"text\": {\n            \"type\": \"string\",\n            \"nullable\": true\n          },\n          \"note\": {\n            \"type\": \"string\",\n            \"nullable\": true\n          },\n          \"id\": {\n            \"type\": \"string\"\n          },\n          \"userId\": {\n            \"type\": \"string\"\n          },\n          \"createdAt\": {\n            \"type\": \"string\"\n          }\n        },\n        \"required\": [\n          \"bookmarkId\",\n          \"startOffset\",\n          \"endOffset\",\n          \"text\",\n          \"note\",\n          \"id\",\n          \"userId\",\n          \"createdAt\"\n        ]\n      },\n      \"Tag\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"id\": {\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"type\": \"string\"\n          },\n          \"numBookmarks\": {\n            \"type\": \"number\"\n          },\n          \"numBookmarksByAttachedType\": {\n            \"type\": \"object\",\n            \"properties\": {\n              \"ai\": {\n                \"type\": \"number\"\n              },\n              \"human\": {\n                \"type\": \"number\"\n              }\n            }\n          }\n        },\n        \"required\": [\n          \"id\",\n          \"name\",\n          \"numBookmarks\",\n          \"numBookmarksByAttachedType\"\n        ]\n      },\n      \"PaginatedHighlights\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"highlights\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/components/schemas/Highlight\"\n            }\n          },\n          \"nextCursor\": {\n            \"type\": \"string\",\n            \"nullable\": true,\n            \"description\": \"Cursor for the next page, or null if no more results.\"\n          }\n        },\n        \"required\": [\n          \"highlights\",\n          \"nextCursor\"\n        ]\n      },\n      \"UploadedAsset\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"assetId\": {\n            \"type\": \"string\",\n            \"description\": \"The unique identifier assigned to the uploaded asset.\"\n          },\n          \"contentType\": {\n            \"type\": \"string\",\n            \"description\": \"The MIME type of the uploaded file.\"\n          },\n          \"size\": {\n            \"type\": \"number\",\n            \"description\": \"The size of the uploaded file in bytes.\"\n          },\n          \"fileName\": {\n            \"type\": \"string\",\n            \"description\": \"The original file name of the uploaded file.\"\n          }\n        },\n        \"required\": [\n          \"assetId\",\n          \"contentType\",\n          \"size\",\n          \"fileName\"\n        ]\n      },\n      \"File to be uploaded\": {}\n    },\n    \"parameters\": {\n      \"BookmarkId\": {\n        \"schema\": {\n          \"$ref\": \"#/components/schemas/BookmarkId\"\n        },\n        \"required\": true,\n        \"name\": \"bookmarkId\",\n        \"in\": \"path\"\n      },\n      \"ListId\": {\n        \"schema\": {\n          \"$ref\": \"#/components/schemas/ListId\"\n        },\n        \"required\": true,\n        \"name\": \"listId\",\n        \"in\": \"path\"\n      },\n      \"TagId\": {\n        \"schema\": {\n          \"$ref\": \"#/components/schemas/TagId\"\n        },\n        \"required\": true,\n        \"name\": \"tagId\",\n        \"in\": \"path\"\n      },\n      \"HighlightId\": {\n        \"schema\": {\n          \"$ref\": \"#/components/schemas/HighlightId\"\n        },\n        \"required\": true,\n        \"name\": \"highlightId\",\n        \"in\": \"path\"\n      },\n      \"AssetId\": {\n        \"schema\": {\n          \"$ref\": \"#/components/schemas/AssetId\"\n        },\n        \"required\": true,\n        \"name\": \"assetId\",\n        \"in\": \"path\"\n      },\n      \"BackupId\": {\n        \"schema\": {\n          \"$ref\": \"#/components/schemas/BackupId\"\n        },\n        \"required\": true,\n        \"name\": \"backupId\",\n        \"in\": \"path\"\n      }\n    }\n  },\n  \"paths\": {\n    \"/bookmarks\": {\n      \"get\": {\n        \"operationId\": \"listBookmarks\",\n        \"description\": \"Retrieve a paginated list of all bookmarks for the authenticated user. Supports filtering by archived/favourited status and sorting by date.\",\n        \"summary\": \"Get all bookmarks\",\n        \"tags\": [\n          \"Bookmarks\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"boolean\",\n              \"description\": \"Filter by archived status.\"\n            },\n            \"required\": false,\n            \"description\": \"Filter by archived status.\",\n            \"name\": \"archived\",\n            \"in\": \"query\"\n          },\n          {\n            \"schema\": {\n              \"type\": \"boolean\",\n              \"description\": \"Filter by favourited status.\"\n            },\n            \"required\": false,\n            \"description\": \"Filter by favourited status.\",\n            \"name\": \"favourited\",\n            \"in\": \"query\"\n          },\n          {\n            \"schema\": {\n              \"type\": \"string\",\n              \"enum\": [\n                \"asc\",\n                \"desc\"\n              ],\n              \"default\": \"desc\",\n              \"description\": \"Sort order by creation date. Defaults to 'desc'.\"\n            },\n            \"required\": false,\n            \"description\": \"Sort order by creation date. Defaults to 'desc'.\",\n            \"name\": \"sortOrder\",\n            \"in\": \"query\"\n          },\n          {\n            \"schema\": {\n              \"type\": \"number\",\n              \"description\": \"Maximum number of items to return per page.\"\n            },\n            \"required\": false,\n            \"description\": \"Maximum number of items to return per page.\",\n            \"name\": \"limit\",\n            \"in\": \"query\"\n          },\n          {\n            \"schema\": {\n              \"$ref\": \"#/components/schemas/Cursor\"\n            },\n            \"required\": false,\n            \"description\": \"Cursor from a previous response to fetch the next page.\",\n            \"name\": \"cursor\",\n            \"in\": \"query\"\n          },\n          {\n            \"schema\": {\n              \"type\": \"boolean\",\n              \"default\": false,\n              \"description\": \"If set to true, the bookmark's full content (HTML, text, etc.) will be included in the response. Set to false for lighter responses when only metadata is needed.\"\n            },\n            \"required\": false,\n            \"description\": \"If set to true, the bookmark's full content (HTML, text, etc.) will be included in the response. Set to false for lighter responses when only metadata is needed.\",\n            \"name\": \"includeContent\",\n            \"in\": \"query\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"A paginated list of bookmarks.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PaginatedBookmarks\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          }\n        }\n      },\n      \"post\": {\n        \"operationId\": \"createBookmark\",\n        \"description\": \"Create a new bookmark. The bookmark type (link, text, or asset) is determined by the `type` field in the request body. For link bookmarks, if the URL already exists, the existing bookmark is returned with a 200 status.\",\n        \"summary\": \"Create a new bookmark\",\n        \"tags\": [\n          \"Bookmarks\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"requestBody\": {\n          \"description\": \"The bookmark to create.\",\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"allOf\": [\n                  {\n                    \"type\": \"object\",\n                    \"properties\": {\n                      \"title\": {\n                        \"type\": \"string\",\n                        \"nullable\": true,\n                        \"maxLength\": 1000\n                      },\n                      \"archived\": {\n                        \"type\": \"boolean\"\n                      },\n                      \"favourited\": {\n                        \"type\": \"boolean\"\n                      },\n                      \"note\": {\n                        \"type\": \"string\"\n                      },\n                      \"summary\": {\n                        \"type\": \"string\"\n                      },\n                      \"createdAt\": {\n                        \"type\": \"string\",\n                        \"nullable\": true\n                      },\n                      \"crawlPriority\": {\n                        \"type\": \"string\",\n                        \"enum\": [\n                          \"low\",\n                          \"normal\"\n                        ]\n                      },\n                      \"importSessionId\": {\n                        \"type\": \"string\"\n                      },\n                      \"source\": {\n                        \"type\": \"string\",\n                        \"enum\": [\n                          \"api\",\n                          \"web\",\n                          \"cli\",\n                          \"mobile\",\n                          \"extension\",\n                          \"singlefile\",\n                          \"rss\",\n                          \"import\"\n                        ]\n                      }\n                    }\n                  },\n                  {\n                    \"oneOf\": [\n                      {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"type\": {\n                            \"type\": \"string\",\n                            \"enum\": [\n                              \"link\"\n                            ]\n                          },\n                          \"url\": {\n                            \"type\": \"string\",\n                            \"format\": \"uri\"\n                          },\n                          \"precrawledArchiveId\": {\n                            \"type\": \"string\"\n                          }\n                        },\n                        \"required\": [\n                          \"type\",\n                          \"url\"\n                        ]\n                      },\n                      {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"type\": {\n                            \"type\": \"string\",\n                            \"enum\": [\n                              \"text\"\n                            ]\n                          },\n                          \"text\": {\n                            \"type\": \"string\"\n                          },\n                          \"sourceUrl\": {\n                            \"type\": \"string\"\n                          }\n                        },\n                        \"required\": [\n                          \"type\",\n                          \"text\"\n                        ]\n                      },\n                      {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"type\": {\n                            \"type\": \"string\",\n                            \"enum\": [\n                              \"asset\"\n                            ]\n                          },\n                          \"assetType\": {\n                            \"type\": \"string\",\n                            \"enum\": [\n                              \"image\",\n                              \"pdf\"\n                            ]\n                          },\n                          \"assetId\": {\n                            \"type\": \"string\"\n                          },\n                          \"fileName\": {\n                            \"type\": \"string\"\n                          },\n                          \"sourceUrl\": {\n                            \"type\": \"string\"\n                          }\n                        },\n                        \"required\": [\n                          \"type\",\n                          \"assetType\",\n                          \"assetId\"\n                        ]\n                      }\n                    ]\n                  }\n                ]\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"A bookmark with this URL already exists. The existing bookmark is returned.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Bookmark\"\n                }\n              }\n            }\n          },\n          \"201\": {\n            \"description\": \"The bookmark was created successfully.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Bookmark\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Bad request — invalid input data.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/bookmarks/search\": {\n      \"get\": {\n        \"operationId\": \"searchBookmarks\",\n        \"description\": \"Full-text search across all bookmarks. Searches bookmark titles, content, descriptions, and notes. Results default to relevance sorting.\",\n        \"summary\": \"Search bookmarks\",\n        \"tags\": [\n          \"Bookmarks\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"string\",\n              \"description\": \"The search query string.\"\n            },\n            \"required\": true,\n            \"description\": \"The search query string.\",\n            \"name\": \"q\",\n            \"in\": \"query\"\n          },\n          {\n            \"schema\": {\n              \"type\": \"string\",\n              \"enum\": [\n                \"asc\",\n                \"desc\",\n                \"relevance\"\n              ],\n              \"default\": \"relevance\",\n              \"description\": \"Sort order for results. Defaults to 'relevance'. Use 'asc' or 'desc' for date-based sorting.\"\n            },\n            \"required\": false,\n            \"description\": \"Sort order for results. Defaults to 'relevance'. Use 'asc' or 'desc' for date-based sorting.\",\n            \"name\": \"sortOrder\",\n            \"in\": \"query\"\n          },\n          {\n            \"schema\": {\n              \"type\": \"number\",\n              \"description\": \"Maximum number of items to return per page.\"\n            },\n            \"required\": false,\n            \"description\": \"Maximum number of items to return per page.\",\n            \"name\": \"limit\",\n            \"in\": \"query\"\n          },\n          {\n            \"schema\": {\n              \"$ref\": \"#/components/schemas/Cursor\"\n            },\n            \"required\": false,\n            \"description\": \"Cursor from a previous response to fetch the next page.\",\n            \"name\": \"cursor\",\n            \"in\": \"query\"\n          },\n          {\n            \"schema\": {\n              \"type\": \"boolean\",\n              \"default\": false,\n              \"description\": \"If set to true, the bookmark's full content (HTML, text, etc.) will be included in the response. Set to false for lighter responses when only metadata is needed.\"\n            },\n            \"required\": false,\n            \"description\": \"If set to true, the bookmark's full content (HTML, text, etc.) will be included in the response. Set to false for lighter responses when only metadata is needed.\",\n            \"name\": \"includeContent\",\n            \"in\": \"query\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"A paginated list of bookmarks matching the search query.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PaginatedBookmarks\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/bookmarks/check-url\": {\n      \"get\": {\n        \"operationId\": \"checkBookmarkUrl\",\n        \"description\": \"Check if a URL is already bookmarked. Uses substring matching to find candidates, then normalizes URLs (ignoring hash fragments and trailing slashes) for exact comparison.\",\n        \"summary\": \"Check if a URL exists in bookmarks\",\n        \"tags\": [\n          \"Bookmarks\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"string\",\n              \"description\": \"The URL to check against existing bookmarks.\"\n            },\n            \"required\": true,\n            \"description\": \"The URL to check against existing bookmarks.\",\n            \"name\": \"url\",\n            \"in\": \"query\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Object indicating whether the URL is bookmarked. `bookmarkId` is `null` if not found.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"bookmarkId\": {\n                      \"type\": \"string\",\n                      \"nullable\": true,\n                      \"description\": \"The ID of the existing bookmark, or null if the URL is not bookmarked.\"\n                    }\n                  },\n                  \"required\": [\n                    \"bookmarkId\"\n                  ]\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/bookmarks/{bookmarkId}\": {\n      \"get\": {\n        \"operationId\": \"getBookmark\",\n        \"description\": \"Retrieve a single bookmark by its ID, including its tags, content, and assets.\",\n        \"summary\": \"Get a single bookmark\",\n        \"tags\": [\n          \"Bookmarks\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/BookmarkId\"\n          },\n          {\n            \"schema\": {\n              \"type\": \"boolean\",\n              \"default\": false,\n              \"description\": \"If set to true, the bookmark's full content (HTML, text, etc.) will be included in the response. Set to false for lighter responses when only metadata is needed.\"\n            },\n            \"required\": false,\n            \"description\": \"If set to true, the bookmark's full content (HTML, text, etc.) will be included in the response. Set to false for lighter responses when only metadata is needed.\",\n            \"name\": \"includeContent\",\n            \"in\": \"query\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"The requested bookmark.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Bookmark\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Bookmark not found.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          }\n        }\n      },\n      \"delete\": {\n        \"operationId\": \"deleteBookmark\",\n        \"description\": \"Permanently delete a bookmark and all its associated data (tags, highlights, assets).\",\n        \"summary\": \"Delete a bookmark\",\n        \"tags\": [\n          \"Bookmarks\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/BookmarkId\"\n          }\n        ],\n        \"responses\": {\n          \"204\": {\n            \"description\": \"No content — the bookmark was deleted successfully.\"\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Bookmark not found.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          }\n        }\n      },\n      \"patch\": {\n        \"operationId\": \"updateBookmark\",\n        \"description\": \"Partially update a bookmark. Only the fields provided in the request body will be updated. Supports updating common fields (title, note, archived, favourited) as well as type-specific fields.\",\n        \"summary\": \"Update a bookmark\",\n        \"tags\": [\n          \"Bookmarks\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/BookmarkId\"\n          }\n        ],\n        \"requestBody\": {\n          \"description\": \"The fields to update. Only the fields you want to change need to be provided.\",\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"archived\": {\n                    \"type\": \"boolean\"\n                  },\n                  \"favourited\": {\n                    \"type\": \"boolean\"\n                  },\n                  \"summary\": {\n                    \"type\": \"string\",\n                    \"nullable\": true\n                  },\n                  \"note\": {\n                    \"type\": \"string\"\n                  },\n                  \"title\": {\n                    \"type\": \"string\",\n                    \"nullable\": true,\n                    \"maxLength\": 1000\n                  },\n                  \"createdAt\": {\n                    \"type\": \"string\",\n                    \"nullable\": true\n                  },\n                  \"url\": {\n                    \"type\": \"string\",\n                    \"format\": \"uri\"\n                  },\n                  \"description\": {\n                    \"type\": \"string\",\n                    \"nullable\": true\n                  },\n                  \"author\": {\n                    \"type\": \"string\",\n                    \"nullable\": true\n                  },\n                  \"publisher\": {\n                    \"type\": \"string\",\n                    \"nullable\": true\n                  },\n                  \"datePublished\": {\n                    \"type\": \"string\",\n                    \"nullable\": true\n                  },\n                  \"dateModified\": {\n                    \"type\": \"string\",\n                    \"nullable\": true\n                  },\n                  \"text\": {\n                    \"type\": \"string\",\n                    \"nullable\": true\n                  },\n                  \"assetContent\": {\n                    \"type\": \"string\",\n                    \"nullable\": true\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"The updated bookmark.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"id\": {\n                      \"type\": \"string\"\n                    },\n                    \"createdAt\": {\n                      \"type\": \"string\"\n                    },\n                    \"modifiedAt\": {\n                      \"type\": \"string\",\n                      \"nullable\": true\n                    },\n                    \"title\": {\n                      \"type\": \"string\",\n                      \"nullable\": true\n                    },\n                    \"archived\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"favourited\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"taggingStatus\": {\n                      \"type\": \"string\",\n                      \"nullable\": true,\n                      \"enum\": [\n                        \"success\",\n                        \"failure\",\n                        \"pending\"\n                      ]\n                    },\n                    \"summarizationStatus\": {\n                      \"type\": \"string\",\n                      \"nullable\": true,\n                      \"enum\": [\n                        \"success\",\n                        \"failure\",\n                        \"pending\"\n                      ]\n                    },\n                    \"note\": {\n                      \"type\": \"string\",\n                      \"nullable\": true\n                    },\n                    \"summary\": {\n                      \"type\": \"string\",\n                      \"nullable\": true\n                    },\n                    \"source\": {\n                      \"type\": \"string\",\n                      \"nullable\": true,\n                      \"enum\": [\n                        \"api\",\n                        \"web\",\n                        \"cli\",\n                        \"mobile\",\n                        \"extension\",\n                        \"singlefile\",\n                        \"rss\",\n                        \"import\"\n                      ]\n                    },\n                    \"userId\": {\n                      \"type\": \"string\"\n                    }\n                  },\n                  \"required\": [\n                    \"id\",\n                    \"createdAt\",\n                    \"modifiedAt\",\n                    \"archived\",\n                    \"favourited\",\n                    \"taggingStatus\",\n                    \"summarizationStatus\",\n                    \"userId\"\n                  ]\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Bookmark not found.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/bookmarks/{bookmarkId}/summarize\": {\n      \"post\": {\n        \"operationId\": \"summarizeBookmark\",\n        \"description\": \"Trigger AI summarization for a bookmark. The summary is generated asynchronously and attached to the bookmark. Returns the updated bookmark record.\",\n        \"summary\": \"Summarize a bookmark\",\n        \"tags\": [\n          \"Bookmarks\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/BookmarkId\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"The bookmark with the updated summary.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"id\": {\n                      \"type\": \"string\"\n                    },\n                    \"createdAt\": {\n                      \"type\": \"string\"\n                    },\n                    \"modifiedAt\": {\n                      \"type\": \"string\",\n                      \"nullable\": true\n                    },\n                    \"title\": {\n                      \"type\": \"string\",\n                      \"nullable\": true\n                    },\n                    \"archived\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"favourited\": {\n                      \"type\": \"boolean\"\n                    },\n                    \"taggingStatus\": {\n                      \"type\": \"string\",\n                      \"nullable\": true,\n                      \"enum\": [\n                        \"success\",\n                        \"failure\",\n                        \"pending\"\n                      ]\n                    },\n                    \"summarizationStatus\": {\n                      \"type\": \"string\",\n                      \"nullable\": true,\n                      \"enum\": [\n                        \"success\",\n                        \"failure\",\n                        \"pending\"\n                      ]\n                    },\n                    \"note\": {\n                      \"type\": \"string\",\n                      \"nullable\": true\n                    },\n                    \"summary\": {\n                      \"type\": \"string\",\n                      \"nullable\": true\n                    },\n                    \"source\": {\n                      \"type\": \"string\",\n                      \"nullable\": true,\n                      \"enum\": [\n                        \"api\",\n                        \"web\",\n                        \"cli\",\n                        \"mobile\",\n                        \"extension\",\n                        \"singlefile\",\n                        \"rss\",\n                        \"import\"\n                      ]\n                    },\n                    \"userId\": {\n                      \"type\": \"string\"\n                    }\n                  },\n                  \"required\": [\n                    \"id\",\n                    \"createdAt\",\n                    \"modifiedAt\",\n                    \"archived\",\n                    \"favourited\",\n                    \"taggingStatus\",\n                    \"summarizationStatus\",\n                    \"userId\"\n                  ]\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Bookmark not found.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/bookmarks/{bookmarkId}/tags\": {\n      \"post\": {\n        \"operationId\": \"attachTagsToBookmark\",\n        \"description\": \"Attach one or more tags to a bookmark. Tags can be identified by ID or name. If a tag name is provided and the tag doesn't exist, it will be created automatically.\",\n        \"summary\": \"Attach tags to a bookmark\",\n        \"tags\": [\n          \"Bookmarks\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/BookmarkId\"\n          }\n        ],\n        \"requestBody\": {\n          \"description\": \"The tags to attach. Each tag must have either a `tagId` or a `tagName`.\",\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"tags\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"tagId\": {\n                          \"type\": \"string\"\n                        },\n                        \"tagName\": {\n                          \"type\": \"string\"\n                        },\n                        \"attachedBy\": {\n                          \"type\": \"string\",\n                          \"enum\": [\n                            \"ai\",\n                            \"human\"\n                          ],\n                          \"default\": \"human\"\n                        }\n                      }\n                    }\n                  }\n                },\n                \"required\": [\n                  \"tags\"\n                ]\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"The IDs of the tags that were attached.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"attached\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"$ref\": \"#/components/schemas/TagId\"\n                      }\n                    }\n                  },\n                  \"required\": [\n                    \"attached\"\n                  ]\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Bookmark not found.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          }\n        }\n      },\n      \"delete\": {\n        \"operationId\": \"detachTagsFromBookmark\",\n        \"description\": \"Detach one or more tags from a bookmark. Tags can be identified by ID or name.\",\n        \"summary\": \"Detach tags from a bookmark\",\n        \"tags\": [\n          \"Bookmarks\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/BookmarkId\"\n          }\n        ],\n        \"requestBody\": {\n          \"description\": \"The tags to detach. Each tag must have either a `tagId` or a `tagName`.\",\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"tags\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"tagId\": {\n                          \"type\": \"string\"\n                        },\n                        \"tagName\": {\n                          \"type\": \"string\"\n                        },\n                        \"attachedBy\": {\n                          \"type\": \"string\",\n                          \"enum\": [\n                            \"ai\",\n                            \"human\"\n                          ],\n                          \"default\": \"human\"\n                        }\n                      }\n                    }\n                  }\n                },\n                \"required\": [\n                  \"tags\"\n                ]\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"The IDs of the tags that were detached.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"detached\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"$ref\": \"#/components/schemas/TagId\"\n                      }\n                    }\n                  },\n                  \"required\": [\n                    \"detached\"\n                  ]\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Bookmark not found.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/bookmarks/{bookmarkId}/lists\": {\n      \"get\": {\n        \"operationId\": \"getBookmarkLists\",\n        \"description\": \"Retrieve all lists that contain the specified bookmark.\",\n        \"summary\": \"Get lists of a bookmark\",\n        \"tags\": [\n          \"Bookmarks\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/BookmarkId\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"The lists that contain this bookmark.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"lists\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"$ref\": \"#/components/schemas/List\"\n                      }\n                    }\n                  },\n                  \"required\": [\n                    \"lists\"\n                  ]\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Bookmark not found.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/bookmarks/{bookmarkId}/highlights\": {\n      \"get\": {\n        \"operationId\": \"getBookmarkHighlights\",\n        \"description\": \"Retrieve all text highlights within the specified bookmark.\",\n        \"summary\": \"Get highlights of a bookmark\",\n        \"tags\": [\n          \"Bookmarks\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/BookmarkId\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"The highlights within this bookmark.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"highlights\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"$ref\": \"#/components/schemas/Highlight\"\n                      }\n                    }\n                  },\n                  \"required\": [\n                    \"highlights\"\n                  ]\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Bookmark not found.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/bookmarks/{bookmarkId}/assets\": {\n      \"post\": {\n        \"operationId\": \"attachAssetToBookmark\",\n        \"description\": \"Attach a previously uploaded asset to a bookmark. The asset must be uploaded first via the POST /assets endpoint.\",\n        \"summary\": \"Attach asset to a bookmark\",\n        \"tags\": [\n          \"Bookmarks\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/BookmarkId\"\n          }\n        ],\n        \"requestBody\": {\n          \"description\": \"The asset ID and type to attach.\",\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"id\": {\n                    \"type\": \"string\",\n                    \"description\": \"The ID of the previously uploaded asset.\"\n                  },\n                  \"assetType\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                      \"linkHtmlContent\",\n                      \"screenshot\",\n                      \"pdf\",\n                      \"assetScreenshot\",\n                      \"bannerImage\",\n                      \"fullPageArchive\",\n                      \"video\",\n                      \"bookmarkAsset\",\n                      \"precrawledArchive\",\n                      \"userUploaded\",\n                      \"avatar\",\n                      \"unknown\"\n                    ],\n                    \"description\": \"The type classification for this asset.\"\n                  }\n                },\n                \"required\": [\n                  \"id\",\n                  \"assetType\"\n                ]\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"201\": {\n            \"description\": \"The asset was attached successfully.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"id\": {\n                      \"type\": \"string\"\n                    },\n                    \"assetType\": {\n                      \"type\": \"string\",\n                      \"enum\": [\n                        \"linkHtmlContent\",\n                        \"screenshot\",\n                        \"pdf\",\n                        \"assetScreenshot\",\n                        \"bannerImage\",\n                        \"fullPageArchive\",\n                        \"video\",\n                        \"bookmarkAsset\",\n                        \"precrawledArchive\",\n                        \"userUploaded\",\n                        \"avatar\",\n                        \"unknown\"\n                      ]\n                    },\n                    \"fileName\": {\n                      \"type\": \"string\",\n                      \"nullable\": true\n                    }\n                  },\n                  \"required\": [\n                    \"id\",\n                    \"assetType\"\n                  ]\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Bookmark not found.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/bookmarks/{bookmarkId}/assets/{assetId}\": {\n      \"put\": {\n        \"operationId\": \"replaceAssetOnBookmark\",\n        \"description\": \"Replace an existing asset on a bookmark with a different previously uploaded asset.\",\n        \"summary\": \"Replace asset on a bookmark\",\n        \"tags\": [\n          \"Bookmarks\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/BookmarkId\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/AssetId\"\n          }\n        ],\n        \"requestBody\": {\n          \"description\": \"The ID of the new asset to replace the existing one.\",\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"assetId\": {\n                    \"type\": \"string\",\n                    \"description\": \"The ID of the new asset to use as a replacement.\"\n                  }\n                },\n                \"required\": [\n                  \"assetId\"\n                ]\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"204\": {\n            \"description\": \"No content — asset was replaced successfully.\"\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Bookmark or asset not found.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          }\n        }\n      },\n      \"delete\": {\n        \"operationId\": \"detachAssetFromBookmark\",\n        \"description\": \"Detach an asset from a bookmark.\",\n        \"summary\": \"Detach asset from a bookmark\",\n        \"tags\": [\n          \"Bookmarks\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/BookmarkId\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/AssetId\"\n          }\n        ],\n        \"responses\": {\n          \"204\": {\n            \"description\": \"No content — asset was detached successfully.\"\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Bookmark or asset not found.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/lists\": {\n      \"get\": {\n        \"operationId\": \"listLists\",\n        \"description\": \"Retrieve all bookmark lists for the authenticated user, including both manual and smart lists.\",\n        \"summary\": \"Get all lists\",\n        \"tags\": [\n          \"Lists\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"All lists owned by or shared with the current user.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"lists\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"$ref\": \"#/components/schemas/List\"\n                      }\n                    }\n                  },\n                  \"required\": [\n                    \"lists\"\n                  ]\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          }\n        }\n      },\n      \"post\": {\n        \"operationId\": \"createList\",\n        \"description\": \"Create a new bookmark list. Lists can be manual (bookmarks are added explicitly) or smart (bookmarks are matched automatically by a search query).\",\n        \"summary\": \"Create a new list\",\n        \"tags\": [\n          \"Lists\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"requestBody\": {\n          \"description\": \"The list to create. For smart lists, a `query` field is required. For manual lists, `query` must not be set.\",\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"name\": {\n                    \"type\": \"string\",\n                    \"minLength\": 1,\n                    \"maxLength\": 100\n                  },\n                  \"description\": {\n                    \"type\": \"string\",\n                    \"minLength\": 0,\n                    \"maxLength\": 500\n                  },\n                  \"icon\": {\n                    \"type\": \"string\"\n                  },\n                  \"type\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                      \"manual\",\n                      \"smart\"\n                    ],\n                    \"default\": \"manual\"\n                  },\n                  \"query\": {\n                    \"type\": \"string\",\n                    \"minLength\": 1\n                  },\n                  \"parentId\": {\n                    \"type\": \"string\",\n                    \"nullable\": true\n                  }\n                },\n                \"required\": [\n                  \"name\",\n                  \"icon\"\n                ]\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"201\": {\n            \"description\": \"The created list.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/List\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Bad request — invalid input data (e.g., smart list missing query, or manual list with a query).\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/lists/{listId}\": {\n      \"get\": {\n        \"operationId\": \"getList\",\n        \"description\": \"Retrieve a single list by its ID.\",\n        \"summary\": \"Get a single list\",\n        \"tags\": [\n          \"Lists\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/ListId\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"The requested list.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/List\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"List not found.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          }\n        }\n      },\n      \"delete\": {\n        \"operationId\": \"deleteList\",\n        \"description\": \"Delete a list. This removes the list only — bookmarks within it are not deleted.\",\n        \"summary\": \"Delete a list\",\n        \"tags\": [\n          \"Lists\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/ListId\"\n          }\n        ],\n        \"responses\": {\n          \"204\": {\n            \"description\": \"No content — the list was deleted successfully.\"\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"List not found.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          }\n        }\n      },\n      \"patch\": {\n        \"operationId\": \"updateList\",\n        \"description\": \"Partially update a list. Only the fields provided in the request body will be updated.\",\n        \"summary\": \"Update a list\",\n        \"tags\": [\n          \"Lists\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/ListId\"\n          }\n        ],\n        \"requestBody\": {\n          \"description\": \"The fields to update. Only the fields you want to change need to be provided.\",\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"name\": {\n                    \"type\": \"string\",\n                    \"minLength\": 1,\n                    \"maxLength\": 100\n                  },\n                  \"description\": {\n                    \"type\": \"string\",\n                    \"nullable\": true,\n                    \"minLength\": 0,\n                    \"maxLength\": 500\n                  },\n                  \"icon\": {\n                    \"type\": \"string\"\n                  },\n                  \"parentId\": {\n                    \"type\": \"string\",\n                    \"nullable\": true\n                  },\n                  \"query\": {\n                    \"type\": \"string\",\n                    \"minLength\": 1\n                  },\n                  \"public\": {\n                    \"type\": \"boolean\"\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"The updated list.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/List\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"List not found.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/lists/{listId}/bookmarks\": {\n      \"get\": {\n        \"operationId\": \"getListBookmarks\",\n        \"description\": \"Retrieve a paginated list of bookmarks within the specified list. For smart lists, bookmarks are computed from the list's query.\",\n        \"summary\": \"Get bookmarks in a list\",\n        \"tags\": [\n          \"Lists\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/ListId\"\n          },\n          {\n            \"schema\": {\n              \"type\": \"string\",\n              \"enum\": [\n                \"asc\",\n                \"desc\"\n              ],\n              \"default\": \"desc\",\n              \"description\": \"Sort order by creation date. Defaults to 'desc'.\"\n            },\n            \"required\": false,\n            \"description\": \"Sort order by creation date. Defaults to 'desc'.\",\n            \"name\": \"sortOrder\",\n            \"in\": \"query\"\n          },\n          {\n            \"schema\": {\n              \"type\": \"number\",\n              \"description\": \"Maximum number of items to return per page.\"\n            },\n            \"required\": false,\n            \"description\": \"Maximum number of items to return per page.\",\n            \"name\": \"limit\",\n            \"in\": \"query\"\n          },\n          {\n            \"schema\": {\n              \"$ref\": \"#/components/schemas/Cursor\"\n            },\n            \"required\": false,\n            \"description\": \"Cursor from a previous response to fetch the next page.\",\n            \"name\": \"cursor\",\n            \"in\": \"query\"\n          },\n          {\n            \"schema\": {\n              \"type\": \"boolean\",\n              \"default\": false,\n              \"description\": \"If set to true, the bookmark's full content (HTML, text, etc.) will be included in the response. Set to false for lighter responses when only metadata is needed.\"\n            },\n            \"required\": false,\n            \"description\": \"If set to true, the bookmark's full content (HTML, text, etc.) will be included in the response. Set to false for lighter responses when only metadata is needed.\",\n            \"name\": \"includeContent\",\n            \"in\": \"query\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"A paginated list of bookmarks in the specified list.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PaginatedBookmarks\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"List not found.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/lists/{listId}/bookmarks/{bookmarkId}\": {\n      \"put\": {\n        \"operationId\": \"addBookmarkToList\",\n        \"description\": \"Add a bookmark to a manual list. This operation is idempotent — adding an already-present bookmark has no effect.\",\n        \"summary\": \"Add a bookmark to a list\",\n        \"tags\": [\n          \"Lists\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/ListId\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/BookmarkId\"\n          }\n        ],\n        \"responses\": {\n          \"204\": {\n            \"description\": \"No content — the bookmark was added to the list successfully.\"\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"List or bookmark not found.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          }\n        }\n      },\n      \"delete\": {\n        \"operationId\": \"removeBookmarkFromList\",\n        \"description\": \"Remove a bookmark from a manual list.\",\n        \"summary\": \"Remove a bookmark from a list\",\n        \"tags\": [\n          \"Lists\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/ListId\"\n          },\n          {\n            \"$ref\": \"#/components/parameters/BookmarkId\"\n          }\n        ],\n        \"responses\": {\n          \"204\": {\n            \"description\": \"No content — the bookmark was removed from the list successfully.\"\n          },\n          \"400\": {\n            \"description\": \"Bookmark is not in the list.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"List or bookmark not found.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/tags\": {\n      \"get\": {\n        \"operationId\": \"listTags\",\n        \"description\": \"Retrieve a paginated list of all tags. Supports filtering by name, attached-by source, and sorting by name, usage count, or relevance.\",\n        \"summary\": \"Get all tags\",\n        \"tags\": [\n          \"Tags\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"required\": false,\n            \"name\": \"nameContains\",\n            \"in\": \"query\"\n          },\n          {\n            \"schema\": {\n              \"type\": \"string\",\n              \"enum\": [\n                \"name\",\n                \"usage\",\n                \"relevance\"\n              ],\n              \"default\": \"usage\"\n            },\n            \"required\": false,\n            \"name\": \"sort\",\n            \"in\": \"query\"\n          },\n          {\n            \"schema\": {\n              \"type\": \"string\",\n              \"enum\": [\n                \"ai\",\n                \"human\",\n                \"none\"\n              ]\n            },\n            \"required\": false,\n            \"name\": \"attachedBy\",\n            \"in\": \"query\"\n          },\n          {\n            \"schema\": {\n              \"type\": \"string\"\n            },\n            \"required\": false,\n            \"name\": \"cursor\",\n            \"in\": \"query\"\n          },\n          {\n            \"schema\": {\n              \"type\": \"number\",\n              \"nullable\": true\n            },\n            \"required\": false,\n            \"name\": \"limit\",\n            \"in\": \"query\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"A paginated list of tags with usage counts.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"tags\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"$ref\": \"#/components/schemas/Tag\"\n                      }\n                    },\n                    \"nextCursor\": {\n                      \"type\": \"string\",\n                      \"nullable\": true,\n                      \"description\": \"Cursor for the next page, or null if no more results.\"\n                    }\n                  },\n                  \"required\": [\n                    \"tags\",\n                    \"nextCursor\"\n                  ]\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          }\n        }\n      },\n      \"post\": {\n        \"operationId\": \"createTag\",\n        \"description\": \"Create a new tag. Tag names are normalized (trimmed and converted to the user's preferred tag style).\",\n        \"summary\": \"Create a new tag\",\n        \"tags\": [\n          \"Tags\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"requestBody\": {\n          \"description\": \"The tag name to create.\",\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"name\": {\n                    \"type\": \"string\"\n                  }\n                },\n                \"required\": [\n                  \"name\"\n                ]\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"201\": {\n            \"description\": \"The created tag.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"id\": {\n                      \"type\": \"string\"\n                    },\n                    \"name\": {\n                      \"type\": \"string\"\n                    }\n                  },\n                  \"required\": [\n                    \"id\",\n                    \"name\"\n                  ]\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/tags/{tagId}\": {\n      \"get\": {\n        \"operationId\": \"getTag\",\n        \"description\": \"Retrieve a single tag by its ID, including the number of bookmarks using it.\",\n        \"summary\": \"Get a single tag\",\n        \"tags\": [\n          \"Tags\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/TagId\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"The requested tag with usage statistics.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Tag\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Tag not found.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          }\n        }\n      },\n      \"delete\": {\n        \"operationId\": \"deleteTag\",\n        \"description\": \"Delete a tag. This removes the tag from all bookmarks it was attached to.\",\n        \"summary\": \"Delete a tag\",\n        \"tags\": [\n          \"Tags\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/TagId\"\n          }\n        ],\n        \"responses\": {\n          \"204\": {\n            \"description\": \"No content — the tag was deleted successfully.\"\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Tag not found.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          }\n        }\n      },\n      \"patch\": {\n        \"operationId\": \"updateTag\",\n        \"description\": \"Rename a tag. The new name will be normalized and trimmed.\",\n        \"summary\": \"Update a tag\",\n        \"tags\": [\n          \"Tags\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/TagId\"\n          }\n        ],\n        \"requestBody\": {\n          \"description\": \"The new tag name.\",\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"name\": {\n                    \"type\": \"string\"\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"The updated tag.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"id\": {\n                      \"type\": \"string\"\n                    },\n                    \"name\": {\n                      \"type\": \"string\"\n                    }\n                  },\n                  \"required\": [\n                    \"id\",\n                    \"name\"\n                  ]\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Tag not found.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/tags/{tagId}/bookmarks\": {\n      \"get\": {\n        \"operationId\": \"getTagBookmarks\",\n        \"description\": \"Retrieve a paginated list of all bookmarks that have the specified tag attached.\",\n        \"summary\": \"Get bookmarks with a tag\",\n        \"tags\": [\n          \"Tags\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/TagId\"\n          },\n          {\n            \"schema\": {\n              \"type\": \"string\",\n              \"enum\": [\n                \"asc\",\n                \"desc\"\n              ],\n              \"default\": \"desc\",\n              \"description\": \"Sort order by creation date. Defaults to 'desc'.\"\n            },\n            \"required\": false,\n            \"description\": \"Sort order by creation date. Defaults to 'desc'.\",\n            \"name\": \"sortOrder\",\n            \"in\": \"query\"\n          },\n          {\n            \"schema\": {\n              \"type\": \"number\",\n              \"description\": \"Maximum number of items to return per page.\"\n            },\n            \"required\": false,\n            \"description\": \"Maximum number of items to return per page.\",\n            \"name\": \"limit\",\n            \"in\": \"query\"\n          },\n          {\n            \"schema\": {\n              \"$ref\": \"#/components/schemas/Cursor\"\n            },\n            \"required\": false,\n            \"description\": \"Cursor from a previous response to fetch the next page.\",\n            \"name\": \"cursor\",\n            \"in\": \"query\"\n          },\n          {\n            \"schema\": {\n              \"type\": \"boolean\",\n              \"default\": false,\n              \"description\": \"If set to true, the bookmark's full content (HTML, text, etc.) will be included in the response. Set to false for lighter responses when only metadata is needed.\"\n            },\n            \"required\": false,\n            \"description\": \"If set to true, the bookmark's full content (HTML, text, etc.) will be included in the response. Set to false for lighter responses when only metadata is needed.\",\n            \"name\": \"includeContent\",\n            \"in\": \"query\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"A paginated list of bookmarks that have the specified tag.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PaginatedBookmarks\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Tag not found.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/highlights\": {\n      \"get\": {\n        \"operationId\": \"listHighlights\",\n        \"description\": \"Retrieve a paginated list of all highlights across all bookmarks for the authenticated user.\",\n        \"summary\": \"Get all highlights\",\n        \"tags\": [\n          \"Highlights\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"number\",\n              \"description\": \"Maximum number of items to return per page.\"\n            },\n            \"required\": false,\n            \"description\": \"Maximum number of items to return per page.\",\n            \"name\": \"limit\",\n            \"in\": \"query\"\n          },\n          {\n            \"schema\": {\n              \"$ref\": \"#/components/schemas/Cursor\"\n            },\n            \"required\": false,\n            \"description\": \"Cursor from a previous response to fetch the next page.\",\n            \"name\": \"cursor\",\n            \"in\": \"query\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"A paginated list of highlights.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/PaginatedHighlights\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          }\n        }\n      },\n      \"post\": {\n        \"operationId\": \"createHighlight\",\n        \"description\": \"Create a new text highlight on a bookmark. Highlights are defined by character offsets within the bookmark's content and support color coding.\",\n        \"summary\": \"Create a new highlight\",\n        \"tags\": [\n          \"Highlights\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"requestBody\": {\n          \"description\": \"The highlight to create, including the bookmark ID, text offsets, and optional color/note.\",\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"bookmarkId\": {\n                    \"type\": \"string\"\n                  },\n                  \"startOffset\": {\n                    \"type\": \"number\"\n                  },\n                  \"endOffset\": {\n                    \"type\": \"number\"\n                  },\n                  \"color\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                      \"yellow\",\n                      \"red\",\n                      \"green\",\n                      \"blue\"\n                    ],\n                    \"default\": \"yellow\"\n                  },\n                  \"text\": {\n                    \"type\": \"string\",\n                    \"nullable\": true\n                  },\n                  \"note\": {\n                    \"type\": \"string\",\n                    \"nullable\": true\n                  }\n                },\n                \"required\": [\n                  \"bookmarkId\",\n                  \"startOffset\",\n                  \"endOffset\",\n                  \"text\",\n                  \"note\"\n                ]\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"201\": {\n            \"description\": \"The created highlight.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Highlight\"\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Bad request — invalid offsets or missing required fields.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Bookmark not found — the specified bookmarkId does not exist.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/highlights/{highlightId}\": {\n      \"get\": {\n        \"operationId\": \"getHighlight\",\n        \"description\": \"Retrieve a single highlight by its ID.\",\n        \"summary\": \"Get a single highlight\",\n        \"tags\": [\n          \"Highlights\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/HighlightId\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"The requested highlight.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Highlight\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Highlight not found.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          }\n        }\n      },\n      \"delete\": {\n        \"operationId\": \"deleteHighlight\",\n        \"description\": \"Delete a highlight by its ID.\",\n        \"summary\": \"Delete a highlight\",\n        \"tags\": [\n          \"Highlights\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/HighlightId\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"The deleted highlight is returned.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Highlight\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Highlight not found.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          }\n        }\n      },\n      \"patch\": {\n        \"operationId\": \"updateHighlight\",\n        \"description\": \"Partially update a highlight. Supports changing the color or note.\",\n        \"summary\": \"Update a highlight\",\n        \"tags\": [\n          \"Highlights\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/HighlightId\"\n          }\n        ],\n        \"requestBody\": {\n          \"description\": \"The fields to update. Only the fields you want to change need to be provided.\",\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"color\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                      \"yellow\",\n                      \"red\",\n                      \"green\",\n                      \"blue\"\n                    ]\n                  },\n                  \"note\": {\n                    \"type\": \"string\",\n                    \"nullable\": true\n                  }\n                }\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"The updated highlight.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Highlight\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Highlight not found.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/users/me\": {\n      \"get\": {\n        \"operationId\": \"getCurrentUser\",\n        \"description\": \"Retrieve profile information for the currently authenticated user, including their name, email, and avatar.\",\n        \"summary\": \"Get current user info\",\n        \"tags\": [\n          \"Users\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"The current user's profile information.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"id\": {\n                      \"type\": \"string\"\n                    },\n                    \"name\": {\n                      \"type\": \"string\",\n                      \"nullable\": true\n                    },\n                    \"email\": {\n                      \"type\": \"string\",\n                      \"nullable\": true\n                    },\n                    \"image\": {\n                      \"type\": \"string\",\n                      \"nullable\": true\n                    },\n                    \"localUser\": {\n                      \"type\": \"boolean\"\n                    }\n                  },\n                  \"required\": [\n                    \"id\",\n                    \"localUser\"\n                  ]\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/users/me/stats\": {\n      \"get\": {\n        \"operationId\": \"getCurrentUserStats\",\n        \"description\": \"Retrieve usage statistics for the currently authenticated user, including bookmark counts by type, top domains, tag usage, bookmarking activity patterns, and storage usage.\",\n        \"summary\": \"Get current user stats\",\n        \"tags\": [\n          \"Users\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Detailed usage statistics for the current user.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"numBookmarks\": {\n                      \"type\": \"number\"\n                    },\n                    \"numFavorites\": {\n                      \"type\": \"number\"\n                    },\n                    \"numArchived\": {\n                      \"type\": \"number\"\n                    },\n                    \"numTags\": {\n                      \"type\": \"number\"\n                    },\n                    \"numLists\": {\n                      \"type\": \"number\"\n                    },\n                    \"numHighlights\": {\n                      \"type\": \"number\"\n                    },\n                    \"bookmarksByType\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"link\": {\n                          \"type\": \"number\"\n                        },\n                        \"text\": {\n                          \"type\": \"number\"\n                        },\n                        \"asset\": {\n                          \"type\": \"number\"\n                        }\n                      },\n                      \"required\": [\n                        \"link\",\n                        \"text\",\n                        \"asset\"\n                      ]\n                    },\n                    \"topDomains\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"domain\": {\n                            \"type\": \"string\"\n                          },\n                          \"count\": {\n                            \"type\": \"number\"\n                          }\n                        },\n                        \"required\": [\n                          \"domain\",\n                          \"count\"\n                        ]\n                      },\n                      \"maxItems\": 10\n                    },\n                    \"totalAssetSize\": {\n                      \"type\": \"number\"\n                    },\n                    \"assetsByType\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"type\": {\n                            \"type\": \"string\"\n                          },\n                          \"count\": {\n                            \"type\": \"number\"\n                          },\n                          \"totalSize\": {\n                            \"type\": \"number\"\n                          }\n                        },\n                        \"required\": [\n                          \"type\",\n                          \"count\",\n                          \"totalSize\"\n                        ]\n                      }\n                    },\n                    \"bookmarkingActivity\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"thisWeek\": {\n                          \"type\": \"number\"\n                        },\n                        \"thisMonth\": {\n                          \"type\": \"number\"\n                        },\n                        \"thisYear\": {\n                          \"type\": \"number\"\n                        },\n                        \"byHour\": {\n                          \"type\": \"array\",\n                          \"items\": {\n                            \"type\": \"object\",\n                            \"properties\": {\n                              \"hour\": {\n                                \"type\": \"number\"\n                              },\n                              \"count\": {\n                                \"type\": \"number\"\n                              }\n                            },\n                            \"required\": [\n                              \"hour\",\n                              \"count\"\n                            ]\n                          }\n                        },\n                        \"byDayOfWeek\": {\n                          \"type\": \"array\",\n                          \"items\": {\n                            \"type\": \"object\",\n                            \"properties\": {\n                              \"day\": {\n                                \"type\": \"number\"\n                              },\n                              \"count\": {\n                                \"type\": \"number\"\n                              }\n                            },\n                            \"required\": [\n                              \"day\",\n                              \"count\"\n                            ]\n                          }\n                        }\n                      },\n                      \"required\": [\n                        \"thisWeek\",\n                        \"thisMonth\",\n                        \"thisYear\",\n                        \"byHour\",\n                        \"byDayOfWeek\"\n                      ]\n                    },\n                    \"tagUsage\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"name\": {\n                            \"type\": \"string\"\n                          },\n                          \"count\": {\n                            \"type\": \"number\"\n                          }\n                        },\n                        \"required\": [\n                          \"name\",\n                          \"count\"\n                        ]\n                      },\n                      \"maxItems\": 10\n                    },\n                    \"bookmarksBySource\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"source\": {\n                            \"type\": \"string\",\n                            \"nullable\": true,\n                            \"enum\": [\n                              \"api\",\n                              \"web\",\n                              \"cli\",\n                              \"mobile\",\n                              \"extension\",\n                              \"singlefile\",\n                              \"rss\",\n                              \"import\"\n                            ]\n                          },\n                          \"count\": {\n                            \"type\": \"number\"\n                          }\n                        },\n                        \"required\": [\n                          \"source\",\n                          \"count\"\n                        ]\n                      }\n                    }\n                  },\n                  \"required\": [\n                    \"numBookmarks\",\n                    \"numFavorites\",\n                    \"numArchived\",\n                    \"numTags\",\n                    \"numLists\",\n                    \"numHighlights\",\n                    \"bookmarksByType\",\n                    \"topDomains\",\n                    \"totalAssetSize\",\n                    \"assetsByType\",\n                    \"bookmarkingActivity\",\n                    \"tagUsage\",\n                    \"bookmarksBySource\"\n                  ]\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/assets\": {\n      \"post\": {\n        \"operationId\": \"uploadAsset\",\n        \"description\": \"Upload a binary file as a new asset. The uploaded asset can then be attached to a bookmark via the POST /bookmarks/{bookmarkId}/assets endpoint.\",\n        \"summary\": \"Upload a new asset\",\n        \"tags\": [\n          \"Assets\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"requestBody\": {\n          \"description\": \"The file to upload as multipart/form-data.\",\n          \"content\": {\n            \"multipart/form-data\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"file\": {\n                    \"$ref\": \"#/components/schemas/File to be uploaded\"\n                  }\n                },\n                \"required\": [\n                  \"file\"\n                ]\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"The asset was uploaded successfully. Returns metadata about the uploaded asset.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/UploadedAsset\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/assets/{assetId}\": {\n      \"get\": {\n        \"operationId\": \"getAsset\",\n        \"description\": \"Download an asset's binary content. The response Content-Type header is set based on the asset's MIME type.\",\n        \"summary\": \"Get a single asset\",\n        \"tags\": [\n          \"Assets\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/AssetId\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"The asset's binary content. The Content-Type header reflects the asset's MIME type (e.g., image/png, application/pdf).\"\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/admin/users/{userId}\": {\n      \"put\": {\n        \"operationId\": \"adminUpdateUser\",\n        \"description\": \"Update a user's role, bookmark quota, storage quota, or browser crawling setting. Requires admin role. You cannot update your own user account via this endpoint.\",\n        \"summary\": \"Update a user (admin)\",\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"parameters\": [\n          {\n            \"schema\": {\n              \"type\": \"string\",\n              \"description\": \"The ID of the user to update.\",\n              \"example\": \"user_123\"\n            },\n            \"required\": true,\n            \"name\": \"userId\",\n            \"in\": \"path\"\n          }\n        ],\n        \"requestBody\": {\n          \"description\": \"The fields to update. All fields are optional — only provided fields will be changed.\",\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"role\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                      \"user\",\n                      \"admin\"\n                    ]\n                  },\n                  \"bookmarkQuota\": {\n                    \"type\": \"integer\",\n                    \"nullable\": true,\n                    \"minimum\": 0\n                  },\n                  \"storageQuota\": {\n                    \"type\": \"integer\",\n                    \"nullable\": true,\n                    \"minimum\": 0\n                  },\n                  \"browserCrawlingEnabled\": {\n                    \"type\": \"boolean\",\n                    \"nullable\": true\n                  }\n                },\n                \"description\": \"User update data\",\n                \"example\": {\n                  \"role\": \"admin\",\n                  \"bookmarkQuota\": 1000,\n                  \"storageQuota\": 5000000000\n                }\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"User updated successfully.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"success\": {\n                      \"type\": \"boolean\",\n                      \"description\": \"Whether the update was successful.\"\n                    }\n                  },\n                  \"required\": [\n                    \"success\"\n                  ]\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Bad request — invalid input data or attempted to update own user.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden — admin access required.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"User not found.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/admin/jobs/trigger/recrawl\": {\n      \"post\": {\n        \"operationId\": \"adminTriggerRecrawl\",\n        \"description\": \"Trigger a recrawl of link bookmarks. You can filter by crawl status to target specific bookmarks (e.g., only failed ones). Optionally run AI inference after crawling. Requires admin role.\",\n        \"summary\": \"Trigger recrawl of links (admin)\",\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"requestBody\": {\n          \"description\": \"Options for the recrawl job.\",\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"crawlStatus\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                      \"success\",\n                      \"failure\",\n                      \"pending\",\n                      \"all\"\n                    ],\n                    \"default\": \"all\",\n                    \"description\": \"Filter bookmarks by their crawl status. Use 'failure' to retry only failed crawls.\"\n                  },\n                  \"runInference\": {\n                    \"type\": \"boolean\",\n                    \"default\": false,\n                    \"description\": \"Whether to run AI inference after crawling.\"\n                  }\n                },\n                \"example\": {\n                  \"crawlStatus\": \"failure\",\n                  \"runInference\": false\n                }\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Recrawl jobs triggered successfully.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"success\": {\n                      \"type\": \"boolean\",\n                      \"description\": \"Whether the job was triggered successfully.\"\n                    }\n                  },\n                  \"required\": [\n                    \"success\"\n                  ]\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Bad request — invalid input data.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden — admin access required.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/admin/jobs/trigger/reindex\": {\n      \"post\": {\n        \"operationId\": \"adminTriggerReindex\",\n        \"description\": \"Trigger a reindex of all bookmarks in the search engine. This clears the existing index and re-queues all bookmarks for indexing. Requires admin role.\",\n        \"summary\": \"Trigger reindex of all bookmarks (admin)\",\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Reindex jobs triggered successfully.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"success\": {\n                      \"type\": \"boolean\",\n                      \"description\": \"Whether the job was triggered successfully.\"\n                    }\n                  },\n                  \"required\": [\n                    \"success\"\n                  ]\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden — admin access required.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/admin/jobs/trigger/inference\": {\n      \"post\": {\n        \"operationId\": \"adminTriggerInference\",\n        \"description\": \"Trigger AI inference (tagging or summarization) on bookmarks. You can filter by status to target specific bookmarks (e.g., only failed ones). Requires admin role.\",\n        \"summary\": \"Trigger AI inference on bookmarks (admin)\",\n        \"tags\": [\n          \"Admin\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"requestBody\": {\n          \"description\": \"Options for the inference job.\",\n          \"required\": true,\n          \"content\": {\n            \"application/json\": {\n              \"schema\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"type\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                      \"tag\",\n                      \"summarize\"\n                    ],\n                    \"description\": \"The type of inference to run: 'tag' for AI tagging, 'summarize' for AI summarization.\"\n                  },\n                  \"status\": {\n                    \"type\": \"string\",\n                    \"enum\": [\n                      \"success\",\n                      \"failure\",\n                      \"pending\",\n                      \"all\"\n                    ],\n                    \"default\": \"all\",\n                    \"description\": \"Filter bookmarks by their inference status. Use 'failure' to retry only failed ones.\"\n                  }\n                },\n                \"required\": [\n                  \"type\"\n                ],\n                \"example\": {\n                  \"type\": \"tag\",\n                  \"status\": \"failure\"\n                }\n              }\n            }\n          }\n        },\n        \"responses\": {\n          \"200\": {\n            \"description\": \"Inference jobs triggered successfully.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"success\": {\n                      \"type\": \"boolean\",\n                      \"description\": \"Whether the job was triggered successfully.\"\n                    }\n                  },\n                  \"required\": [\n                    \"success\"\n                  ]\n                }\n              }\n            }\n          },\n          \"400\": {\n            \"description\": \"Bad request — invalid input data.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          },\n          \"403\": {\n            \"description\": \"Forbidden — admin access required.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/backups\": {\n      \"get\": {\n        \"operationId\": \"listBackups\",\n        \"description\": \"Retrieve a list of all backups for the authenticated user, including their status and metadata.\",\n        \"summary\": \"Get all backups\",\n        \"tags\": [\n          \"Backups\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"A list of all backups.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"backups\": {\n                      \"type\": \"array\",\n                      \"items\": {\n                        \"type\": \"object\",\n                        \"properties\": {\n                          \"id\": {\n                            \"type\": \"string\"\n                          },\n                          \"userId\": {\n                            \"type\": \"string\"\n                          },\n                          \"assetId\": {\n                            \"type\": \"string\",\n                            \"nullable\": true\n                          },\n                          \"createdAt\": {\n                            \"type\": \"string\"\n                          },\n                          \"size\": {\n                            \"type\": \"number\"\n                          },\n                          \"bookmarkCount\": {\n                            \"type\": \"number\"\n                          },\n                          \"status\": {\n                            \"type\": \"string\",\n                            \"enum\": [\n                              \"pending\",\n                              \"success\",\n                              \"failure\"\n                            ]\n                          },\n                          \"errorMessage\": {\n                            \"type\": \"string\",\n                            \"nullable\": true\n                          }\n                        },\n                        \"required\": [\n                          \"id\",\n                          \"userId\",\n                          \"assetId\",\n                          \"createdAt\",\n                          \"size\",\n                          \"bookmarkCount\",\n                          \"status\"\n                        ]\n                      }\n                    }\n                  },\n                  \"required\": [\n                    \"backups\"\n                  ]\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          }\n        }\n      },\n      \"post\": {\n        \"operationId\": \"createBackup\",\n        \"description\": \"Trigger a new full account backup. The backup is created asynchronously — use GET /backups/{backupId} to check its status.\",\n        \"summary\": \"Trigger a new backup\",\n        \"tags\": [\n          \"Backups\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"responses\": {\n          \"201\": {\n            \"description\": \"Backup creation was triggered. The backup object is returned with a 'pending' status.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"id\": {\n                      \"type\": \"string\"\n                    },\n                    \"userId\": {\n                      \"type\": \"string\"\n                    },\n                    \"assetId\": {\n                      \"type\": \"string\",\n                      \"nullable\": true\n                    },\n                    \"createdAt\": {\n                      \"type\": \"string\"\n                    },\n                    \"size\": {\n                      \"type\": \"number\"\n                    },\n                    \"bookmarkCount\": {\n                      \"type\": \"number\"\n                    },\n                    \"status\": {\n                      \"type\": \"string\",\n                      \"enum\": [\n                        \"pending\",\n                        \"success\",\n                        \"failure\"\n                      ]\n                    },\n                    \"errorMessage\": {\n                      \"type\": \"string\",\n                      \"nullable\": true\n                    }\n                  },\n                  \"required\": [\n                    \"id\",\n                    \"userId\",\n                    \"assetId\",\n                    \"createdAt\",\n                    \"size\",\n                    \"bookmarkCount\",\n                    \"status\"\n                  ]\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/backups/{backupId}\": {\n      \"get\": {\n        \"operationId\": \"getBackup\",\n        \"description\": \"Retrieve metadata for a single backup, including its current status (pending, success, or failure).\",\n        \"summary\": \"Get a single backup\",\n        \"tags\": [\n          \"Backups\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/BackupId\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"The requested backup.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"type\": \"object\",\n                  \"properties\": {\n                    \"id\": {\n                      \"type\": \"string\"\n                    },\n                    \"userId\": {\n                      \"type\": \"string\"\n                    },\n                    \"assetId\": {\n                      \"type\": \"string\",\n                      \"nullable\": true\n                    },\n                    \"createdAt\": {\n                      \"type\": \"string\"\n                    },\n                    \"size\": {\n                      \"type\": \"number\"\n                    },\n                    \"bookmarkCount\": {\n                      \"type\": \"number\"\n                    },\n                    \"status\": {\n                      \"type\": \"string\",\n                      \"enum\": [\n                        \"pending\",\n                        \"success\",\n                        \"failure\"\n                      ]\n                    },\n                    \"errorMessage\": {\n                      \"type\": \"string\",\n                      \"nullable\": true\n                    }\n                  },\n                  \"required\": [\n                    \"id\",\n                    \"userId\",\n                    \"assetId\",\n                    \"createdAt\",\n                    \"size\",\n                    \"bookmarkCount\",\n                    \"status\"\n                  ]\n                }\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Backup not found.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          }\n        }\n      },\n      \"delete\": {\n        \"operationId\": \"deleteBackup\",\n        \"description\": \"Permanently delete a backup and its associated archive file.\",\n        \"summary\": \"Delete a backup\",\n        \"tags\": [\n          \"Backups\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/BackupId\"\n          }\n        ],\n        \"responses\": {\n          \"204\": {\n            \"description\": \"No content — the backup was deleted successfully.\"\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Backup not found.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          }\n        }\n      }\n    },\n    \"/backups/{backupId}/download\": {\n      \"get\": {\n        \"operationId\": \"downloadBackup\",\n        \"description\": \"Download a completed backup as a zip archive. The backup must have a 'success' status.\",\n        \"summary\": \"Download a backup\",\n        \"tags\": [\n          \"Backups\"\n        ],\n        \"security\": [\n          {\n            \"bearerAuth\": []\n          }\n        ],\n        \"parameters\": [\n          {\n            \"$ref\": \"#/components/parameters/BackupId\"\n          }\n        ],\n        \"responses\": {\n          \"200\": {\n            \"description\": \"The backup file as a zip archive.\",\n            \"content\": {\n              \"application/zip\": {\n                \"schema\": {}\n              }\n            }\n          },\n          \"401\": {\n            \"description\": \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n            \"content\": {\n              \"text/plain\": {\n                \"schema\": {\n                  \"type\": \"string\",\n                  \"example\": \"Unauthorized\"\n                }\n              }\n            }\n          },\n          \"404\": {\n            \"description\": \"Backup not found.\",\n            \"content\": {\n              \"application/json\": {\n                \"schema\": {\n                  \"$ref\": \"#/components/schemas/Error\"\n                }\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}"
  },
  {
    "path": "packages/open-api/lib/admin.ts",
    "content": "import {\n  extendZodWithOpenApi,\n  OpenAPIRegistry,\n} from \"@asteasolutions/zod-to-openapi\";\nimport { z } from \"zod\";\n\nimport { updateUserSchema } from \"@karakeep/shared/types/admin\";\n\nimport { BearerAuth } from \"./common\";\nimport { ErrorSchema, UnauthorizedResponse } from \"./errors\";\n\nexport const registry = new OpenAPIRegistry();\nextendZodWithOpenApi(z);\n\nconst updateUserRequestSchema = updateUserSchema.omit({ userId: true });\n\nconst updateUserResponseSchema = z.object({\n  success: z.boolean().describe(\"Whether the update was successful.\"),\n});\n\nconst adminJobSuccessResponseSchema = z.object({\n  success: z.boolean().describe(\"Whether the job was triggered successfully.\"),\n});\n\nconst adminForbiddenResponse = {\n  description: \"Forbidden — admin access required.\",\n  content: {\n    \"application/json\": {\n      schema: ErrorSchema,\n    },\n  },\n};\n\nregistry.registerPath({\n  operationId: \"adminUpdateUser\",\n  method: \"put\",\n  path: \"/admin/users/{userId}\",\n  description:\n    \"Update a user's role, bookmark quota, storage quota, or browser crawling setting. \" +\n    \"Requires admin role. You cannot update your own user account via this endpoint.\",\n  summary: \"Update a user (admin)\",\n  tags: [\"Admin\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    params: z.object({\n      userId: z.string().openapi({\n        description: \"The ID of the user to update.\",\n        example: \"user_123\",\n      }),\n    }),\n    body: {\n      description:\n        \"The fields to update. All fields are optional — only provided fields will be changed.\",\n      content: {\n        \"application/json\": {\n          schema: updateUserRequestSchema.openapi({\n            description: \"User update data\",\n            example: {\n              role: \"admin\",\n              bookmarkQuota: 1000,\n              storageQuota: 5000000000,\n            },\n          }),\n        },\n      },\n    },\n  },\n  responses: {\n    200: {\n      description: \"User updated successfully.\",\n      content: {\n        \"application/json\": {\n          schema: updateUserResponseSchema,\n        },\n      },\n    },\n    400: {\n      description:\n        \"Bad request — invalid input data or attempted to update own user.\",\n      content: {\n        \"application/json\": {\n          schema: ErrorSchema,\n        },\n      },\n    },\n    401: UnauthorizedResponse,\n    403: adminForbiddenResponse,\n    404: {\n      description: \"User not found.\",\n      content: {\n        \"application/json\": {\n          schema: ErrorSchema,\n        },\n      },\n    },\n  },\n});\n\nregistry.registerPath({\n  operationId: \"adminTriggerRecrawl\",\n  method: \"post\",\n  path: \"/admin/jobs/trigger/recrawl\",\n  description:\n    \"Trigger a recrawl of link bookmarks. You can filter by crawl status to target specific bookmarks \" +\n    \"(e.g., only failed ones). Optionally run AI inference after crawling. Requires admin role.\",\n  summary: \"Trigger recrawl of links (admin)\",\n  tags: [\"Admin\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    body: {\n      description: \"Options for the recrawl job.\",\n      content: {\n        \"application/json\": {\n          schema: z\n            .object({\n              crawlStatus: z\n                .enum([\"success\", \"failure\", \"pending\", \"all\"])\n                .default(\"all\")\n                .describe(\n                  \"Filter bookmarks by their crawl status. Use 'failure' to retry only failed crawls.\",\n                ),\n              runInference: z\n                .boolean()\n                .default(false)\n                .describe(\"Whether to run AI inference after crawling.\"),\n            })\n            .openapi({\n              example: {\n                crawlStatus: \"failure\",\n                runInference: false,\n              },\n            }),\n        },\n      },\n    },\n  },\n  responses: {\n    200: {\n      description: \"Recrawl jobs triggered successfully.\",\n      content: {\n        \"application/json\": {\n          schema: adminJobSuccessResponseSchema,\n        },\n      },\n    },\n    400: {\n      description: \"Bad request — invalid input data.\",\n      content: {\n        \"application/json\": {\n          schema: ErrorSchema,\n        },\n      },\n    },\n    401: UnauthorizedResponse,\n    403: adminForbiddenResponse,\n  },\n});\n\nregistry.registerPath({\n  operationId: \"adminTriggerReindex\",\n  method: \"post\",\n  path: \"/admin/jobs/trigger/reindex\",\n  description:\n    \"Trigger a reindex of all bookmarks in the search engine. This clears the existing index and \" +\n    \"re-queues all bookmarks for indexing. Requires admin role.\",\n  summary: \"Trigger reindex of all bookmarks (admin)\",\n  tags: [\"Admin\"],\n  security: [{ [BearerAuth.name]: [] }],\n  responses: {\n    200: {\n      description: \"Reindex jobs triggered successfully.\",\n      content: {\n        \"application/json\": {\n          schema: adminJobSuccessResponseSchema,\n        },\n      },\n    },\n    401: UnauthorizedResponse,\n    403: adminForbiddenResponse,\n  },\n});\n\nregistry.registerPath({\n  operationId: \"adminTriggerInference\",\n  method: \"post\",\n  path: \"/admin/jobs/trigger/inference\",\n  description:\n    \"Trigger AI inference (tagging or summarization) on bookmarks. You can filter by status \" +\n    \"to target specific bookmarks (e.g., only failed ones). Requires admin role.\",\n  summary: \"Trigger AI inference on bookmarks (admin)\",\n  tags: [\"Admin\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    body: {\n      description: \"Options for the inference job.\",\n      required: true,\n      content: {\n        \"application/json\": {\n          schema: z\n            .object({\n              type: z\n                .enum([\"tag\", \"summarize\"])\n                .describe(\n                  \"The type of inference to run: 'tag' for AI tagging, 'summarize' for AI summarization.\",\n                ),\n              status: z\n                .enum([\"success\", \"failure\", \"pending\", \"all\"])\n                .default(\"all\")\n                .describe(\n                  \"Filter bookmarks by their inference status. Use 'failure' to retry only failed ones.\",\n                ),\n            })\n            .openapi({\n              example: {\n                type: \"tag\",\n                status: \"failure\",\n              },\n            }),\n        },\n      },\n    },\n  },\n  responses: {\n    200: {\n      description: \"Inference jobs triggered successfully.\",\n      content: {\n        \"application/json\": {\n          schema: adminJobSuccessResponseSchema,\n        },\n      },\n    },\n    400: {\n      description: \"Bad request — invalid input data.\",\n      content: {\n        \"application/json\": {\n          schema: ErrorSchema,\n        },\n      },\n    },\n    401: UnauthorizedResponse,\n    403: adminForbiddenResponse,\n  },\n});\n"
  },
  {
    "path": "packages/open-api/lib/assets.ts",
    "content": "import {\n  extendZodWithOpenApi,\n  OpenAPIRegistry,\n} from \"@asteasolutions/zod-to-openapi\";\nimport { z } from \"zod\";\n\nimport { BearerAuth } from \"./common\";\nimport { UnauthorizedResponse } from \"./errors\";\n\nexport const registry = new OpenAPIRegistry();\nextendZodWithOpenApi(z);\n\nexport const AssetIdSchema = registry.registerParameter(\n  \"AssetId\",\n  z.string().openapi({\n    param: {\n      name: \"assetId\",\n      in: \"path\",\n    },\n    description: \"The unique identifier of the asset.\",\n    example: \"ieidlxygmwj87oxz5hxttoc8\",\n  }),\n);\n\nregistry.registerPath({\n  operationId: \"uploadAsset\",\n  method: \"post\",\n  path: \"/assets\",\n  description:\n    \"Upload a binary file as a new asset. The uploaded asset can then be attached to a bookmark via the POST /bookmarks/{bookmarkId}/assets endpoint.\",\n  summary: \"Upload a new asset\",\n  tags: [\"Assets\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    body: {\n      description: \"The file to upload as multipart/form-data.\",\n      content: {\n        \"multipart/form-data\": {\n          schema: z.object({\n            file: z.instanceof(File).openapi(\"File to be uploaded\"),\n          }),\n        },\n      },\n    },\n  },\n  responses: {\n    200: {\n      description:\n        \"The asset was uploaded successfully. Returns metadata about the uploaded asset.\",\n      content: {\n        \"application/json\": {\n          schema: z\n            .object({\n              assetId: z\n                .string()\n                .describe(\n                  \"The unique identifier assigned to the uploaded asset.\",\n                ),\n              contentType: z\n                .string()\n                .describe(\"The MIME type of the uploaded file.\"),\n              size: z\n                .number()\n                .describe(\"The size of the uploaded file in bytes.\"),\n              fileName: z\n                .string()\n                .describe(\"The original file name of the uploaded file.\"),\n            })\n            .openapi(\"UploadedAsset\"),\n        },\n      },\n    },\n    401: UnauthorizedResponse,\n  },\n});\n\nregistry.registerPath({\n  operationId: \"getAsset\",\n  method: \"get\",\n  path: \"/assets/{assetId}\",\n  description:\n    \"Download an asset's binary content. The response Content-Type header is set based on the asset's MIME type.\",\n  summary: \"Get a single asset\",\n  tags: [\"Assets\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    params: z.object({ assetId: AssetIdSchema }),\n  },\n  responses: {\n    200: {\n      description:\n        \"The asset's binary content. The Content-Type header reflects the asset's MIME type (e.g., image/png, application/pdf).\",\n    },\n    401: UnauthorizedResponse,\n  },\n});\n"
  },
  {
    "path": "packages/open-api/lib/backups.ts",
    "content": "import {\n  extendZodWithOpenApi,\n  OpenAPIRegistry,\n} from \"@asteasolutions/zod-to-openapi\";\nimport { z } from \"zod\";\n\nimport { zBackupSchema } from \"@karakeep/shared/types/backups\";\n\nimport { BearerAuth } from \"./common\";\nimport { ErrorSchema, UnauthorizedResponse } from \"./errors\";\n\nexport const registry = new OpenAPIRegistry();\nextendZodWithOpenApi(z);\n\nexport const BackupIdSchema = registry.registerParameter(\n  \"BackupId\",\n  z.string().openapi({\n    param: {\n      name: \"backupId\",\n      in: \"path\",\n    },\n    description: \"The unique identifier of the backup.\",\n    example: \"ieidlxygmwj87oxz5hxttoc8\",\n  }),\n);\n\nregistry.registerPath({\n  operationId: \"listBackups\",\n  method: \"get\",\n  path: \"/backups\",\n  description:\n    \"Retrieve a list of all backups for the authenticated user, including their status and metadata.\",\n  summary: \"Get all backups\",\n  tags: [\"Backups\"],\n  security: [{ [BearerAuth.name]: [] }],\n  responses: {\n    200: {\n      description: \"A list of all backups.\",\n      content: {\n        \"application/json\": {\n          schema: z.object({\n            backups: z.array(zBackupSchema),\n          }),\n        },\n      },\n    },\n    401: UnauthorizedResponse,\n  },\n});\n\nregistry.registerPath({\n  operationId: \"createBackup\",\n  method: \"post\",\n  path: \"/backups\",\n  description:\n    \"Trigger a new full account backup. The backup is created asynchronously — use GET /backups/{backupId} to check its status.\",\n  summary: \"Trigger a new backup\",\n  tags: [\"Backups\"],\n  security: [{ [BearerAuth.name]: [] }],\n  responses: {\n    201: {\n      description:\n        \"Backup creation was triggered. The backup object is returned with a 'pending' status.\",\n      content: {\n        \"application/json\": {\n          schema: zBackupSchema,\n        },\n      },\n    },\n    401: UnauthorizedResponse,\n  },\n});\n\nregistry.registerPath({\n  operationId: \"getBackup\",\n  method: \"get\",\n  path: \"/backups/{backupId}\",\n  description:\n    \"Retrieve metadata for a single backup, including its current status (pending, success, or failure).\",\n  summary: \"Get a single backup\",\n  tags: [\"Backups\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    params: z.object({ backupId: BackupIdSchema }),\n  },\n  responses: {\n    200: {\n      description: \"The requested backup.\",\n      content: {\n        \"application/json\": {\n          schema: zBackupSchema,\n        },\n      },\n    },\n    401: UnauthorizedResponse,\n    404: {\n      description: \"Backup not found.\",\n      content: {\n        \"application/json\": {\n          schema: ErrorSchema,\n        },\n      },\n    },\n  },\n});\n\nregistry.registerPath({\n  operationId: \"downloadBackup\",\n  method: \"get\",\n  path: \"/backups/{backupId}/download\",\n  description:\n    \"Download a completed backup as a zip archive. The backup must have a 'success' status.\",\n  summary: \"Download a backup\",\n  tags: [\"Backups\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    params: z.object({ backupId: BackupIdSchema }),\n  },\n  responses: {\n    200: {\n      description: \"The backup file as a zip archive.\",\n      content: {\n        \"application/zip\": {\n          schema: z.instanceof(Blob),\n        },\n      },\n    },\n    401: UnauthorizedResponse,\n    404: {\n      description: \"Backup not found.\",\n      content: {\n        \"application/json\": {\n          schema: ErrorSchema,\n        },\n      },\n    },\n  },\n});\n\nregistry.registerPath({\n  operationId: \"deleteBackup\",\n  method: \"delete\",\n  path: \"/backups/{backupId}\",\n  description: \"Permanently delete a backup and its associated archive file.\",\n  summary: \"Delete a backup\",\n  tags: [\"Backups\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    params: z.object({ backupId: BackupIdSchema }),\n  },\n  responses: {\n    204: {\n      description: \"No content — the backup was deleted successfully.\",\n    },\n    401: UnauthorizedResponse,\n    404: {\n      description: \"Backup not found.\",\n      content: {\n        \"application/json\": {\n          schema: ErrorSchema,\n        },\n      },\n    },\n  },\n});\n"
  },
  {
    "path": "packages/open-api/lib/bookmarks.ts",
    "content": "import {\n  extendZodWithOpenApi,\n  OpenAPIRegistry,\n} from \"@asteasolutions/zod-to-openapi\";\nimport { z } from \"zod\";\n\nimport {\n  zAssetSchema,\n  zAssetTypesSchema,\n  zBareBookmarkSchema,\n  zManipulatedTagSchema,\n  zNewBookmarkRequestSchema,\n  zSortOrder,\n  zUpdateBookmarksRequestSchema,\n} from \"@karakeep/shared/types/bookmarks\";\n\nimport { AssetIdSchema } from \"./assets\";\nimport { BearerAuth } from \"./common\";\nimport { ErrorSchema, UnauthorizedResponse } from \"./errors\";\nimport {\n  BookmarkSchema,\n  IncludeContentSearchParamSchema,\n  PaginatedBookmarksSchema,\n  PaginationSchema,\n} from \"./pagination\";\nimport { TagIdSchema } from \"./tags\";\nimport { HighlightSchema, ListSchema } from \"./types\";\n\nexport const registry = new OpenAPIRegistry();\nextendZodWithOpenApi(z);\n\nexport const BookmarkIdSchema = registry.registerParameter(\n  \"BookmarkId\",\n  z.string().openapi({\n    param: {\n      name: \"bookmarkId\",\n      in: \"path\",\n    },\n    description: \"The unique identifier of the bookmark.\",\n    example: \"ieidlxygmwj87oxz5hxttoc8\",\n  }),\n);\n\nregistry.registerPath({\n  operationId: \"listBookmarks\",\n  method: \"get\",\n  path: \"/bookmarks\",\n  description:\n    \"Retrieve a paginated list of all bookmarks for the authenticated user. \" +\n    \"Supports filtering by archived/favourited status and sorting by date.\",\n  summary: \"Get all bookmarks\",\n  tags: [\"Bookmarks\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    query: z\n      .object({\n        archived: z.boolean().optional().describe(\"Filter by archived status.\"),\n        favourited: z\n          .boolean()\n          .optional()\n          .describe(\"Filter by favourited status.\"),\n        sortOrder: zSortOrder\n          .exclude([\"relevance\"])\n          .optional()\n          .default(zSortOrder.Enum.desc)\n          .describe(\"Sort order by creation date. Defaults to 'desc'.\"),\n      })\n      .merge(PaginationSchema)\n      .merge(IncludeContentSearchParamSchema),\n  },\n  responses: {\n    200: {\n      description: \"A paginated list of bookmarks.\",\n      content: {\n        \"application/json\": {\n          schema: PaginatedBookmarksSchema,\n        },\n      },\n    },\n    401: UnauthorizedResponse,\n  },\n});\n\nregistry.registerPath({\n  operationId: \"searchBookmarks\",\n  method: \"get\",\n  path: \"/bookmarks/search\",\n  description:\n    \"Full-text search across all bookmarks. Searches bookmark titles, content, descriptions, and notes. \" +\n    \"Results default to relevance sorting.\",\n  summary: \"Search bookmarks\",\n  tags: [\"Bookmarks\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    query: z\n      .object({\n        q: z.string().describe(\"The search query string.\"),\n        sortOrder: zSortOrder\n          .optional()\n          .default(zSortOrder.Enum.relevance)\n          .describe(\n            \"Sort order for results. Defaults to 'relevance'. Use 'asc' or 'desc' for date-based sorting.\",\n          ),\n      })\n      .merge(PaginationSchema)\n      .merge(IncludeContentSearchParamSchema),\n  },\n  responses: {\n    200: {\n      description: \"A paginated list of bookmarks matching the search query.\",\n      content: {\n        \"application/json\": {\n          schema: PaginatedBookmarksSchema,\n        },\n      },\n    },\n    401: UnauthorizedResponse,\n  },\n});\n\nregistry.registerPath({\n  operationId: \"checkBookmarkUrl\",\n  method: \"get\",\n  path: \"/bookmarks/check-url\",\n  description:\n    \"Check if a URL is already bookmarked. Uses substring matching to find candidates, then normalizes URLs (ignoring hash fragments and trailing slashes) for exact comparison.\",\n  summary: \"Check if a URL exists in bookmarks\",\n  tags: [\"Bookmarks\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    query: z.object({\n      url: z.string().describe(\"The URL to check against existing bookmarks.\"),\n    }),\n  },\n  responses: {\n    200: {\n      description:\n        \"Object indicating whether the URL is bookmarked. `bookmarkId` is `null` if not found.\",\n      content: {\n        \"application/json\": {\n          schema: z.object({\n            bookmarkId: z\n              .string()\n              .nullable()\n              .describe(\n                \"The ID of the existing bookmark, or null if the URL is not bookmarked.\",\n              ),\n          }),\n        },\n      },\n    },\n    401: UnauthorizedResponse,\n  },\n});\n\nregistry.registerPath({\n  operationId: \"createBookmark\",\n  method: \"post\",\n  path: \"/bookmarks\",\n  description:\n    \"Create a new bookmark. The bookmark type (link, text, or asset) is determined by the `type` field in the request body. \" +\n    \"For link bookmarks, if the URL already exists, the existing bookmark is returned with a 200 status.\",\n  summary: \"Create a new bookmark\",\n  tags: [\"Bookmarks\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    body: {\n      description: \"The bookmark to create.\",\n      content: {\n        \"application/json\": {\n          schema: zNewBookmarkRequestSchema,\n        },\n      },\n    },\n  },\n  responses: {\n    200: {\n      description:\n        \"A bookmark with this URL already exists. The existing bookmark is returned.\",\n      content: {\n        \"application/json\": {\n          schema: BookmarkSchema,\n        },\n      },\n    },\n    201: {\n      description: \"The bookmark was created successfully.\",\n      content: {\n        \"application/json\": {\n          schema: BookmarkSchema,\n        },\n      },\n    },\n    400: {\n      description: \"Bad request — invalid input data.\",\n      content: {\n        \"application/json\": {\n          schema: ErrorSchema,\n        },\n      },\n    },\n    401: UnauthorizedResponse,\n  },\n});\n\nregistry.registerPath({\n  operationId: \"getBookmark\",\n  method: \"get\",\n  path: \"/bookmarks/{bookmarkId}\",\n  description:\n    \"Retrieve a single bookmark by its ID, including its tags, content, and assets.\",\n  summary: \"Get a single bookmark\",\n  tags: [\"Bookmarks\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    params: z.object({ bookmarkId: BookmarkIdSchema }),\n    query: IncludeContentSearchParamSchema,\n  },\n  responses: {\n    200: {\n      description: \"The requested bookmark.\",\n      content: {\n        \"application/json\": {\n          schema: BookmarkSchema,\n        },\n      },\n    },\n    401: UnauthorizedResponse,\n    404: {\n      description: \"Bookmark not found.\",\n      content: {\n        \"application/json\": {\n          schema: ErrorSchema,\n        },\n      },\n    },\n  },\n});\n\nregistry.registerPath({\n  operationId: \"deleteBookmark\",\n  method: \"delete\",\n  path: \"/bookmarks/{bookmarkId}\",\n  description:\n    \"Permanently delete a bookmark and all its associated data (tags, highlights, assets).\",\n  summary: \"Delete a bookmark\",\n  tags: [\"Bookmarks\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    params: z.object({ bookmarkId: BookmarkIdSchema }),\n  },\n  responses: {\n    204: {\n      description: \"No content — the bookmark was deleted successfully.\",\n    },\n    401: UnauthorizedResponse,\n    404: {\n      description: \"Bookmark not found.\",\n      content: {\n        \"application/json\": {\n          schema: ErrorSchema,\n        },\n      },\n    },\n  },\n});\n\nregistry.registerPath({\n  operationId: \"updateBookmark\",\n  method: \"patch\",\n  path: \"/bookmarks/{bookmarkId}\",\n  description:\n    \"Partially update a bookmark. Only the fields provided in the request body will be updated. \" +\n    \"Supports updating common fields (title, note, archived, favourited) as well as type-specific fields.\",\n  summary: \"Update a bookmark\",\n  tags: [\"Bookmarks\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    params: z.object({ bookmarkId: BookmarkIdSchema }),\n    body: {\n      description:\n        \"The fields to update. Only the fields you want to change need to be provided.\",\n      content: {\n        \"application/json\": {\n          schema: zUpdateBookmarksRequestSchema.omit({ bookmarkId: true }),\n        },\n      },\n    },\n  },\n  responses: {\n    200: {\n      description: \"The updated bookmark.\",\n      content: {\n        \"application/json\": {\n          schema: zBareBookmarkSchema,\n        },\n      },\n    },\n    401: UnauthorizedResponse,\n    404: {\n      description: \"Bookmark not found.\",\n      content: {\n        \"application/json\": {\n          schema: ErrorSchema,\n        },\n      },\n    },\n  },\n});\n\nregistry.registerPath({\n  operationId: \"summarizeBookmark\",\n  method: \"post\",\n  path: \"/bookmarks/{bookmarkId}/summarize\",\n  description:\n    \"Trigger AI summarization for a bookmark. The summary is generated asynchronously and attached to the bookmark. \" +\n    \"Returns the updated bookmark record.\",\n  summary: \"Summarize a bookmark\",\n  tags: [\"Bookmarks\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    params: z.object({ bookmarkId: BookmarkIdSchema }),\n  },\n  responses: {\n    200: {\n      description: \"The bookmark with the updated summary.\",\n      content: {\n        \"application/json\": {\n          schema: zBareBookmarkSchema,\n        },\n      },\n    },\n    401: UnauthorizedResponse,\n    404: {\n      description: \"Bookmark not found.\",\n      content: {\n        \"application/json\": {\n          schema: ErrorSchema,\n        },\n      },\n    },\n  },\n});\n\nregistry.registerPath({\n  operationId: \"attachTagsToBookmark\",\n  method: \"post\",\n  path: \"/bookmarks/{bookmarkId}/tags\",\n  description:\n    \"Attach one or more tags to a bookmark. Tags can be identified by ID or name. \" +\n    \"If a tag name is provided and the tag doesn't exist, it will be created automatically.\",\n  summary: \"Attach tags to a bookmark\",\n  tags: [\"Bookmarks\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    params: z.object({ bookmarkId: BookmarkIdSchema }),\n    body: {\n      description:\n        \"The tags to attach. Each tag must have either a `tagId` or a `tagName`.\",\n      content: {\n        \"application/json\": {\n          schema: z.object({ tags: z.array(zManipulatedTagSchema) }),\n        },\n      },\n    },\n  },\n  responses: {\n    200: {\n      description: \"The IDs of the tags that were attached.\",\n      content: {\n        \"application/json\": {\n          schema: z.object({ attached: z.array(TagIdSchema) }),\n        },\n      },\n    },\n    401: UnauthorizedResponse,\n    404: {\n      description: \"Bookmark not found.\",\n      content: {\n        \"application/json\": {\n          schema: ErrorSchema,\n        },\n      },\n    },\n  },\n});\n\nregistry.registerPath({\n  operationId: \"detachTagsFromBookmark\",\n  method: \"delete\",\n  path: \"/bookmarks/{bookmarkId}/tags\",\n  description:\n    \"Detach one or more tags from a bookmark. Tags can be identified by ID or name.\",\n  summary: \"Detach tags from a bookmark\",\n  tags: [\"Bookmarks\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    params: z.object({ bookmarkId: BookmarkIdSchema }),\n    body: {\n      description:\n        \"The tags to detach. Each tag must have either a `tagId` or a `tagName`.\",\n      content: {\n        \"application/json\": {\n          schema: z.object({ tags: z.array(zManipulatedTagSchema) }),\n        },\n      },\n    },\n  },\n  responses: {\n    200: {\n      description: \"The IDs of the tags that were detached.\",\n      content: {\n        \"application/json\": {\n          schema: z.object({ detached: z.array(TagIdSchema) }),\n        },\n      },\n    },\n    401: UnauthorizedResponse,\n    404: {\n      description: \"Bookmark not found.\",\n      content: {\n        \"application/json\": {\n          schema: ErrorSchema,\n        },\n      },\n    },\n  },\n});\n\nregistry.registerPath({\n  operationId: \"getBookmarkLists\",\n  method: \"get\",\n  path: \"/bookmarks/{bookmarkId}/lists\",\n  description: \"Retrieve all lists that contain the specified bookmark.\",\n  summary: \"Get lists of a bookmark\",\n  tags: [\"Bookmarks\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    params: z.object({ bookmarkId: BookmarkIdSchema }),\n  },\n  responses: {\n    200: {\n      description: \"The lists that contain this bookmark.\",\n      content: {\n        \"application/json\": {\n          schema: z.object({ lists: z.array(ListSchema) }),\n        },\n      },\n    },\n    401: UnauthorizedResponse,\n    404: {\n      description: \"Bookmark not found.\",\n      content: {\n        \"application/json\": {\n          schema: ErrorSchema,\n        },\n      },\n    },\n  },\n});\n\nregistry.registerPath({\n  operationId: \"getBookmarkHighlights\",\n  method: \"get\",\n  path: \"/bookmarks/{bookmarkId}/highlights\",\n  description: \"Retrieve all text highlights within the specified bookmark.\",\n  summary: \"Get highlights of a bookmark\",\n  tags: [\"Bookmarks\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    params: z.object({ bookmarkId: BookmarkIdSchema }),\n  },\n  responses: {\n    200: {\n      description: \"The highlights within this bookmark.\",\n      content: {\n        \"application/json\": {\n          schema: z.object({ highlights: z.array(HighlightSchema) }),\n        },\n      },\n    },\n    401: UnauthorizedResponse,\n    404: {\n      description: \"Bookmark not found.\",\n      content: {\n        \"application/json\": {\n          schema: ErrorSchema,\n        },\n      },\n    },\n  },\n});\n\nregistry.registerPath({\n  operationId: \"attachAssetToBookmark\",\n  method: \"post\",\n  path: \"/bookmarks/{bookmarkId}/assets\",\n  description:\n    \"Attach a previously uploaded asset to a bookmark. The asset must be uploaded first via the POST /assets endpoint.\",\n  summary: \"Attach asset to a bookmark\",\n  tags: [\"Bookmarks\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    params: z.object({ bookmarkId: BookmarkIdSchema }),\n    body: {\n      description: \"The asset ID and type to attach.\",\n      content: {\n        \"application/json\": {\n          schema: z.object({\n            id: z.string().describe(\"The ID of the previously uploaded asset.\"),\n            assetType: zAssetTypesSchema.describe(\n              \"The type classification for this asset.\",\n            ),\n          }),\n        },\n      },\n    },\n  },\n  responses: {\n    201: {\n      description: \"The asset was attached successfully.\",\n      content: {\n        \"application/json\": {\n          schema: zAssetSchema,\n        },\n      },\n    },\n    401: UnauthorizedResponse,\n    404: {\n      description: \"Bookmark not found.\",\n      content: {\n        \"application/json\": {\n          schema: ErrorSchema,\n        },\n      },\n    },\n  },\n});\n\nregistry.registerPath({\n  operationId: \"replaceAssetOnBookmark\",\n  method: \"put\",\n  path: \"/bookmarks/{bookmarkId}/assets/{assetId}\",\n  description:\n    \"Replace an existing asset on a bookmark with a different previously uploaded asset.\",\n  summary: \"Replace asset on a bookmark\",\n  tags: [\"Bookmarks\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    params: z.object({\n      bookmarkId: BookmarkIdSchema,\n      assetId: AssetIdSchema,\n    }),\n    body: {\n      description: \"The ID of the new asset to replace the existing one.\",\n      content: {\n        \"application/json\": {\n          schema: z.object({\n            assetId: z\n              .string()\n              .describe(\"The ID of the new asset to use as a replacement.\"),\n          }),\n        },\n      },\n    },\n  },\n  responses: {\n    204: {\n      description: \"No content — asset was replaced successfully.\",\n    },\n    401: UnauthorizedResponse,\n    404: {\n      description: \"Bookmark or asset not found.\",\n      content: {\n        \"application/json\": {\n          schema: ErrorSchema,\n        },\n      },\n    },\n  },\n});\n\nregistry.registerPath({\n  operationId: \"detachAssetFromBookmark\",\n  method: \"delete\",\n  path: \"/bookmarks/{bookmarkId}/assets/{assetId}\",\n  description: \"Detach an asset from a bookmark.\",\n  summary: \"Detach asset from a bookmark\",\n  tags: [\"Bookmarks\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    params: z.object({\n      bookmarkId: BookmarkIdSchema,\n      assetId: AssetIdSchema,\n    }),\n  },\n  responses: {\n    204: {\n      description: \"No content — asset was detached successfully.\",\n    },\n    401: UnauthorizedResponse,\n    404: {\n      description: \"Bookmark or asset not found.\",\n      content: {\n        \"application/json\": {\n          schema: ErrorSchema,\n        },\n      },\n    },\n  },\n});\n"
  },
  {
    "path": "packages/open-api/lib/common.ts",
    "content": "import { OpenAPIRegistry } from \"@asteasolutions/zod-to-openapi\";\n\nexport const registry = new OpenAPIRegistry();\nexport const BearerAuth = registry.registerComponent(\n  \"securitySchemes\",\n  \"bearerAuth\",\n  {\n    type: \"http\",\n    scheme: \"bearer\",\n    bearerFormat: \"JWT\",\n  },\n);\n"
  },
  {
    "path": "packages/open-api/lib/errors.ts",
    "content": "import { extendZodWithOpenApi } from \"@asteasolutions/zod-to-openapi\";\nimport { z } from \"zod\";\n\nextendZodWithOpenApi(z);\n\nexport const ErrorSchema = z\n  .object({\n    code: z.string().describe(\"A machine-readable error code.\"),\n    message: z.string().describe(\"A human-readable error message.\"),\n  })\n  .openapi(\"Error\");\n\nexport const UnauthorizedResponse = {\n  description:\n    \"Unauthorized — the Bearer token is missing, invalid, or expired.\",\n  content: {\n    \"text/plain\": {\n      schema: z.string().openapi({\n        example: \"Unauthorized\",\n      }),\n    },\n  },\n} as const;\n"
  },
  {
    "path": "packages/open-api/lib/highlights.ts",
    "content": "import {\n  extendZodWithOpenApi,\n  OpenAPIRegistry,\n} from \"@asteasolutions/zod-to-openapi\";\nimport { z } from \"zod\";\n\nimport {\n  zNewHighlightSchema,\n  zUpdateHighlightSchema,\n} from \"@karakeep/shared/types/highlights\";\n\nimport { BearerAuth } from \"./common\";\nimport { ErrorSchema, UnauthorizedResponse } from \"./errors\";\nimport { PaginationSchema } from \"./pagination\";\nimport { HighlightSchema, PaginatedHighlightsSchema } from \"./types\";\n\nexport const registry = new OpenAPIRegistry();\nextendZodWithOpenApi(z);\n\nexport const HighlightIdSchema = registry.registerParameter(\n  \"HighlightId\",\n  z.string().openapi({\n    param: {\n      name: \"highlightId\",\n      in: \"path\",\n    },\n    description: \"The unique identifier of the highlight.\",\n    example: \"ieidlxygmwj87oxz5hxttoc8\",\n  }),\n);\n\nregistry.registerPath({\n  operationId: \"listHighlights\",\n  method: \"get\",\n  path: \"/highlights\",\n  description:\n    \"Retrieve a paginated list of all highlights across all bookmarks for the authenticated user.\",\n  summary: \"Get all highlights\",\n  tags: [\"Highlights\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    query: PaginationSchema,\n  },\n  responses: {\n    200: {\n      description: \"A paginated list of highlights.\",\n      content: {\n        \"application/json\": {\n          schema: PaginatedHighlightsSchema,\n        },\n      },\n    },\n    401: UnauthorizedResponse,\n  },\n});\n\nregistry.registerPath({\n  operationId: \"createHighlight\",\n  method: \"post\",\n  path: \"/highlights\",\n  description:\n    \"Create a new text highlight on a bookmark. Highlights are defined by character offsets within the bookmark's content and support color coding.\",\n  summary: \"Create a new highlight\",\n  tags: [\"Highlights\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    body: {\n      description:\n        \"The highlight to create, including the bookmark ID, text offsets, and optional color/note.\",\n      content: {\n        \"application/json\": {\n          schema: zNewHighlightSchema,\n        },\n      },\n    },\n  },\n  responses: {\n    201: {\n      description: \"The created highlight.\",\n      content: {\n        \"application/json\": {\n          schema: HighlightSchema,\n        },\n      },\n    },\n    400: {\n      description: \"Bad request — invalid offsets or missing required fields.\",\n      content: {\n        \"application/json\": {\n          schema: ErrorSchema,\n        },\n      },\n    },\n    401: UnauthorizedResponse,\n    404: {\n      description:\n        \"Bookmark not found — the specified bookmarkId does not exist.\",\n      content: {\n        \"application/json\": {\n          schema: ErrorSchema,\n        },\n      },\n    },\n  },\n});\n\nregistry.registerPath({\n  operationId: \"getHighlight\",\n  method: \"get\",\n  path: \"/highlights/{highlightId}\",\n  description: \"Retrieve a single highlight by its ID.\",\n  summary: \"Get a single highlight\",\n  tags: [\"Highlights\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    params: z.object({ highlightId: HighlightIdSchema }),\n  },\n  responses: {\n    200: {\n      description: \"The requested highlight.\",\n      content: {\n        \"application/json\": {\n          schema: HighlightSchema,\n        },\n      },\n    },\n    401: UnauthorizedResponse,\n    404: {\n      description: \"Highlight not found.\",\n      content: {\n        \"application/json\": {\n          schema: ErrorSchema,\n        },\n      },\n    },\n  },\n});\n\nregistry.registerPath({\n  operationId: \"deleteHighlight\",\n  method: \"delete\",\n  path: \"/highlights/{highlightId}\",\n  description: \"Delete a highlight by its ID.\",\n  summary: \"Delete a highlight\",\n  tags: [\"Highlights\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    params: z.object({ highlightId: HighlightIdSchema }),\n  },\n  responses: {\n    200: {\n      description: \"The deleted highlight is returned.\",\n      content: {\n        \"application/json\": {\n          schema: HighlightSchema,\n        },\n      },\n    },\n    401: UnauthorizedResponse,\n    404: {\n      description: \"Highlight not found.\",\n      content: {\n        \"application/json\": {\n          schema: ErrorSchema,\n        },\n      },\n    },\n  },\n});\n\nregistry.registerPath({\n  operationId: \"updateHighlight\",\n  method: \"patch\",\n  path: \"/highlights/{highlightId}\",\n  description:\n    \"Partially update a highlight. Supports changing the color or note.\",\n  summary: \"Update a highlight\",\n  tags: [\"Highlights\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    params: z.object({ highlightId: HighlightIdSchema }),\n    body: {\n      description:\n        \"The fields to update. Only the fields you want to change need to be provided.\",\n      content: {\n        \"application/json\": {\n          schema: zUpdateHighlightSchema.omit({ highlightId: true }),\n        },\n      },\n    },\n  },\n  responses: {\n    200: {\n      description: \"The updated highlight.\",\n      content: {\n        \"application/json\": {\n          schema: HighlightSchema,\n        },\n      },\n    },\n    401: UnauthorizedResponse,\n    404: {\n      description: \"Highlight not found.\",\n      content: {\n        \"application/json\": {\n          schema: ErrorSchema,\n        },\n      },\n    },\n  },\n});\n"
  },
  {
    "path": "packages/open-api/lib/lists.ts",
    "content": "import {\n  extendZodWithOpenApi,\n  OpenAPIRegistry,\n} from \"@asteasolutions/zod-to-openapi\";\nimport { z } from \"zod\";\n\nimport { zSortOrder } from \"@karakeep/shared/types/bookmarks\";\nimport {\n  zEditBookmarkListSchema,\n  zNewBookmarkListSchema,\n} from \"@karakeep/shared/types/lists\";\n\nimport { BookmarkIdSchema } from \"./bookmarks\";\nimport { BearerAuth } from \"./common\";\nimport { ErrorSchema, UnauthorizedResponse } from \"./errors\";\nimport {\n  IncludeContentSearchParamSchema,\n  PaginatedBookmarksSchema,\n  PaginationSchema,\n} from \"./pagination\";\nimport { ListSchema } from \"./types\";\n\nexport const registry = new OpenAPIRegistry();\nextendZodWithOpenApi(z);\n\nexport const ListIdSchema = registry.registerParameter(\n  \"ListId\",\n  z.string().openapi({\n    param: {\n      name: \"listId\",\n      in: \"path\",\n    },\n    description: \"The unique identifier of the list.\",\n    example: \"ieidlxygmwj87oxz5hxttoc8\",\n  }),\n);\n\nregistry.registerPath({\n  operationId: \"listLists\",\n  method: \"get\",\n  path: \"/lists\",\n  description:\n    \"Retrieve all bookmark lists for the authenticated user, including both manual and smart lists.\",\n  summary: \"Get all lists\",\n  tags: [\"Lists\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {},\n  responses: {\n    200: {\n      description: \"All lists owned by or shared with the current user.\",\n      content: {\n        \"application/json\": {\n          schema: z.object({\n            lists: z.array(ListSchema),\n          }),\n        },\n      },\n    },\n    401: UnauthorizedResponse,\n  },\n});\n\nregistry.registerPath({\n  operationId: \"createList\",\n  method: \"post\",\n  path: \"/lists\",\n  description:\n    \"Create a new bookmark list. Lists can be manual (bookmarks are added explicitly) or smart (bookmarks are matched automatically by a search query).\",\n  summary: \"Create a new list\",\n  tags: [\"Lists\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    body: {\n      description:\n        \"The list to create. For smart lists, a `query` field is required. For manual lists, `query` must not be set.\",\n      content: {\n        \"application/json\": {\n          schema: zNewBookmarkListSchema,\n        },\n      },\n    },\n  },\n  responses: {\n    201: {\n      description: \"The created list.\",\n      content: {\n        \"application/json\": {\n          schema: ListSchema,\n        },\n      },\n    },\n    400: {\n      description:\n        \"Bad request — invalid input data (e.g., smart list missing query, or manual list with a query).\",\n      content: {\n        \"application/json\": {\n          schema: ErrorSchema,\n        },\n      },\n    },\n    401: UnauthorizedResponse,\n  },\n});\n\nregistry.registerPath({\n  operationId: \"getList\",\n  method: \"get\",\n  path: \"/lists/{listId}\",\n  description: \"Retrieve a single list by its ID.\",\n  summary: \"Get a single list\",\n  tags: [\"Lists\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    params: z.object({ listId: ListIdSchema }),\n  },\n  responses: {\n    200: {\n      description: \"The requested list.\",\n      content: {\n        \"application/json\": {\n          schema: ListSchema,\n        },\n      },\n    },\n    401: UnauthorizedResponse,\n    404: {\n      description: \"List not found.\",\n      content: {\n        \"application/json\": {\n          schema: ErrorSchema,\n        },\n      },\n    },\n  },\n});\n\nregistry.registerPath({\n  operationId: \"deleteList\",\n  method: \"delete\",\n  path: \"/lists/{listId}\",\n  description:\n    \"Delete a list. This removes the list only — bookmarks within it are not deleted.\",\n  summary: \"Delete a list\",\n  tags: [\"Lists\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    params: z.object({ listId: ListIdSchema }),\n  },\n  responses: {\n    204: {\n      description: \"No content — the list was deleted successfully.\",\n    },\n    401: UnauthorizedResponse,\n    404: {\n      description: \"List not found.\",\n      content: {\n        \"application/json\": {\n          schema: ErrorSchema,\n        },\n      },\n    },\n  },\n});\n\nregistry.registerPath({\n  operationId: \"updateList\",\n  method: \"patch\",\n  path: \"/lists/{listId}\",\n  description:\n    \"Partially update a list. Only the fields provided in the request body will be updated.\",\n  summary: \"Update a list\",\n  tags: [\"Lists\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    params: z.object({ listId: ListIdSchema }),\n    body: {\n      description:\n        \"The fields to update. Only the fields you want to change need to be provided.\",\n      content: {\n        \"application/json\": {\n          schema: zEditBookmarkListSchema.omit({ listId: true }),\n        },\n      },\n    },\n  },\n  responses: {\n    200: {\n      description: \"The updated list.\",\n      content: {\n        \"application/json\": {\n          schema: ListSchema,\n        },\n      },\n    },\n    401: UnauthorizedResponse,\n    404: {\n      description: \"List not found.\",\n      content: {\n        \"application/json\": {\n          schema: ErrorSchema,\n        },\n      },\n    },\n  },\n});\n\nregistry.registerPath({\n  operationId: \"getListBookmarks\",\n  method: \"get\",\n  path: \"/lists/{listId}/bookmarks\",\n  description:\n    \"Retrieve a paginated list of bookmarks within the specified list. For smart lists, bookmarks are computed from the list's query.\",\n  summary: \"Get bookmarks in a list\",\n  tags: [\"Lists\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    params: z.object({ listId: ListIdSchema }),\n    query: z\n      .object({\n        sortOrder: zSortOrder\n          .exclude([\"relevance\"])\n          .optional()\n          .default(zSortOrder.Enum.desc)\n          .describe(\"Sort order by creation date. Defaults to 'desc'.\"),\n      })\n      .merge(PaginationSchema)\n      .merge(IncludeContentSearchParamSchema),\n  },\n  responses: {\n    200: {\n      description: \"A paginated list of bookmarks in the specified list.\",\n      content: {\n        \"application/json\": {\n          schema: PaginatedBookmarksSchema,\n        },\n      },\n    },\n    401: UnauthorizedResponse,\n    404: {\n      description: \"List not found.\",\n      content: {\n        \"application/json\": {\n          schema: ErrorSchema,\n        },\n      },\n    },\n  },\n});\n\nregistry.registerPath({\n  operationId: \"addBookmarkToList\",\n  method: \"put\",\n  path: \"/lists/{listId}/bookmarks/{bookmarkId}\",\n  description:\n    \"Add a bookmark to a manual list. This operation is idempotent — adding an already-present bookmark has no effect.\",\n  summary: \"Add a bookmark to a list\",\n  tags: [\"Lists\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    params: z.object({ listId: ListIdSchema, bookmarkId: BookmarkIdSchema }),\n  },\n  responses: {\n    204: {\n      description:\n        \"No content — the bookmark was added to the list successfully.\",\n    },\n    401: UnauthorizedResponse,\n    404: {\n      description: \"List or bookmark not found.\",\n      content: {\n        \"application/json\": {\n          schema: ErrorSchema,\n        },\n      },\n    },\n  },\n});\n\nregistry.registerPath({\n  operationId: \"removeBookmarkFromList\",\n  method: \"delete\",\n  path: \"/lists/{listId}/bookmarks/{bookmarkId}\",\n  description: \"Remove a bookmark from a manual list.\",\n  summary: \"Remove a bookmark from a list\",\n  tags: [\"Lists\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    params: z.object({ listId: ListIdSchema, bookmarkId: BookmarkIdSchema }),\n  },\n  responses: {\n    204: {\n      description:\n        \"No content — the bookmark was removed from the list successfully.\",\n    },\n    400: {\n      description: \"Bookmark is not in the list.\",\n      content: {\n        \"application/json\": {\n          schema: ErrorSchema,\n        },\n      },\n    },\n    401: UnauthorizedResponse,\n    404: {\n      description: \"List or bookmark not found.\",\n      content: {\n        \"application/json\": {\n          schema: ErrorSchema,\n        },\n      },\n    },\n  },\n});\n"
  },
  {
    "path": "packages/open-api/lib/pagination.ts",
    "content": "export {\n  BookmarkSchema,\n  CursorSchema,\n  IncludeContentSearchParamSchema,\n  PaginatedBookmarksSchema,\n  PaginationSchema,\n} from \"./types\";\n"
  },
  {
    "path": "packages/open-api/lib/tags.ts",
    "content": "import {\n  extendZodWithOpenApi,\n  OpenAPIRegistry,\n} from \"@asteasolutions/zod-to-openapi\";\nimport { z } from \"zod\";\n\nimport { zSortOrder } from \"@karakeep/shared/types/bookmarks\";\nimport {\n  zCreateTagRequestSchema,\n  zTagBasicSchema,\n  zTagListQueryParamsSchema,\n  zUpdateTagRequestSchema,\n} from \"@karakeep/shared/types/tags\";\n\nimport { BearerAuth } from \"./common\";\nimport { ErrorSchema, UnauthorizedResponse } from \"./errors\";\nimport {\n  IncludeContentSearchParamSchema,\n  PaginatedBookmarksSchema,\n  PaginationSchema,\n} from \"./pagination\";\nimport { TagSchema } from \"./types\";\n\nexport const registry = new OpenAPIRegistry();\nextendZodWithOpenApi(z);\n\nexport const TagIdSchema = registry.registerParameter(\n  \"TagId\",\n  z.string().openapi({\n    param: {\n      name: \"tagId\",\n      in: \"path\",\n    },\n    description: \"The unique identifier of the tag.\",\n    example: \"ieidlxygmwj87oxz5hxttoc8\",\n  }),\n);\n\nregistry.registerPath({\n  operationId: \"listTags\",\n  method: \"get\",\n  path: \"/tags\",\n  description:\n    \"Retrieve a paginated list of all tags. Supports filtering by name, attached-by source, and sorting by name, usage count, or relevance.\",\n  summary: \"Get all tags\",\n  tags: [\"Tags\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    query: zTagListQueryParamsSchema,\n  },\n  responses: {\n    200: {\n      description: \"A paginated list of tags with usage counts.\",\n      content: {\n        \"application/json\": {\n          schema: z.object({\n            tags: z.array(TagSchema),\n            nextCursor: z\n              .string()\n              .nullable()\n              .describe(\n                \"Cursor for the next page, or null if no more results.\",\n              ),\n          }),\n        },\n      },\n    },\n    401: UnauthorizedResponse,\n  },\n});\n\nregistry.registerPath({\n  operationId: \"createTag\",\n  method: \"post\",\n  path: \"/tags\",\n  description:\n    \"Create a new tag. Tag names are normalized (trimmed and converted to the user's preferred tag style).\",\n  summary: \"Create a new tag\",\n  tags: [\"Tags\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    body: {\n      description: \"The tag name to create.\",\n      content: {\n        \"application/json\": {\n          schema: zCreateTagRequestSchema,\n        },\n      },\n    },\n  },\n  responses: {\n    201: {\n      description: \"The created tag.\",\n      content: {\n        \"application/json\": {\n          schema: zTagBasicSchema,\n        },\n      },\n    },\n    401: UnauthorizedResponse,\n  },\n});\n\nregistry.registerPath({\n  operationId: \"getTag\",\n  method: \"get\",\n  path: \"/tags/{tagId}\",\n  description:\n    \"Retrieve a single tag by its ID, including the number of bookmarks using it.\",\n  summary: \"Get a single tag\",\n  tags: [\"Tags\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    params: z.object({ tagId: TagIdSchema }),\n  },\n  responses: {\n    200: {\n      description: \"The requested tag with usage statistics.\",\n      content: {\n        \"application/json\": {\n          schema: TagSchema,\n        },\n      },\n    },\n    401: UnauthorizedResponse,\n    404: {\n      description: \"Tag not found.\",\n      content: {\n        \"application/json\": {\n          schema: ErrorSchema,\n        },\n      },\n    },\n  },\n});\n\nregistry.registerPath({\n  operationId: \"deleteTag\",\n  method: \"delete\",\n  path: \"/tags/{tagId}\",\n  description:\n    \"Delete a tag. This removes the tag from all bookmarks it was attached to.\",\n  summary: \"Delete a tag\",\n  tags: [\"Tags\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    params: z.object({ tagId: TagIdSchema }),\n  },\n  responses: {\n    204: {\n      description: \"No content — the tag was deleted successfully.\",\n    },\n    401: UnauthorizedResponse,\n    404: {\n      description: \"Tag not found.\",\n      content: {\n        \"application/json\": {\n          schema: ErrorSchema,\n        },\n      },\n    },\n  },\n});\n\nregistry.registerPath({\n  operationId: \"updateTag\",\n  method: \"patch\",\n  path: \"/tags/{tagId}\",\n  description: \"Rename a tag. The new name will be normalized and trimmed.\",\n  summary: \"Update a tag\",\n  tags: [\"Tags\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    params: z.object({ tagId: TagIdSchema }),\n    body: {\n      description: \"The new tag name.\",\n      content: {\n        \"application/json\": {\n          schema: zUpdateTagRequestSchema.omit({ tagId: true }),\n        },\n      },\n    },\n  },\n  responses: {\n    200: {\n      description: \"The updated tag.\",\n      content: {\n        \"application/json\": {\n          schema: zTagBasicSchema,\n        },\n      },\n    },\n    401: UnauthorizedResponse,\n    404: {\n      description: \"Tag not found.\",\n      content: {\n        \"application/json\": {\n          schema: ErrorSchema,\n        },\n      },\n    },\n  },\n});\n\nregistry.registerPath({\n  operationId: \"getTagBookmarks\",\n  method: \"get\",\n  path: \"/tags/{tagId}/bookmarks\",\n  description:\n    \"Retrieve a paginated list of all bookmarks that have the specified tag attached.\",\n  summary: \"Get bookmarks with a tag\",\n  tags: [\"Tags\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {\n    params: z.object({ tagId: TagIdSchema }),\n    query: z\n      .object({\n        sortOrder: zSortOrder\n          .exclude([\"relevance\"])\n          .optional()\n          .default(zSortOrder.Enum.desc)\n          .describe(\"Sort order by creation date. Defaults to 'desc'.\"),\n      })\n      .merge(PaginationSchema)\n      .merge(IncludeContentSearchParamSchema),\n  },\n  responses: {\n    200: {\n      description: \"A paginated list of bookmarks that have the specified tag.\",\n      content: {\n        \"application/json\": {\n          schema: PaginatedBookmarksSchema,\n        },\n      },\n    },\n    401: UnauthorizedResponse,\n    404: {\n      description: \"Tag not found.\",\n      content: {\n        \"application/json\": {\n          schema: ErrorSchema,\n        },\n      },\n    },\n  },\n});\n"
  },
  {
    "path": "packages/open-api/lib/types.ts",
    "content": "import { extendZodWithOpenApi } from \"@asteasolutions/zod-to-openapi\";\nimport { z } from \"zod\";\n\nimport { zBookmarkSchema } from \"@karakeep/shared/types/bookmarks\";\nimport { zHighlightSchema } from \"@karakeep/shared/types/highlights\";\nimport { zBookmarkListSchema } from \"@karakeep/shared/types/lists\";\nimport { zGetTagResponseSchema } from \"@karakeep/shared/types/tags\";\n\nextendZodWithOpenApi(z);\n\nexport const ListSchema = zBookmarkListSchema.openapi(\"List\");\n\nexport const BookmarkSchema = zBookmarkSchema.openapi(\"Bookmark\");\n\nexport const PaginatedBookmarksSchema = z\n  .object({\n    bookmarks: z.array(BookmarkSchema),\n    nextCursor: z\n      .string()\n      .nullable()\n      .describe(\"Cursor for the next page, or null if no more results.\"),\n  })\n  .openapi(\"PaginatedBookmarks\");\n\nexport const CursorSchema = z\n  .string()\n  .describe(\"An opaque cursor string for pagination.\")\n  .openapi(\"Cursor\");\n\nexport const PaginationSchema = z\n  .object({\n    limit: z\n      .number()\n      .optional()\n      .describe(\"Maximum number of items to return per page.\"),\n    cursor: CursorSchema.optional().describe(\n      \"Cursor from a previous response to fetch the next page.\",\n    ),\n  })\n  .openapi(\"Pagination\");\n\nexport const IncludeContentSearchParamSchema = z.object({\n  includeContent: z\n    .boolean()\n    .default(false)\n    .describe(\n      \"If set to true, the bookmark's full content (HTML, text, etc.) will be included in the response. \" +\n        \"Set to false for lighter responses when only metadata is needed.\",\n    ),\n});\n\nexport const HighlightSchema = zHighlightSchema.openapi(\"Highlight\");\n\nexport const PaginatedHighlightsSchema = z\n  .object({\n    highlights: z.array(HighlightSchema),\n    nextCursor: z\n      .string()\n      .nullable()\n      .describe(\"Cursor for the next page, or null if no more results.\"),\n  })\n  .openapi(\"PaginatedHighlights\");\n\nexport const TagSchema = zGetTagResponseSchema.openapi(\"Tag\");\n"
  },
  {
    "path": "packages/open-api/lib/users.ts",
    "content": "import {\n  extendZodWithOpenApi,\n  OpenAPIRegistry,\n} from \"@asteasolutions/zod-to-openapi\";\nimport { z } from \"zod\";\n\nimport {\n  zUserStatsResponseSchema,\n  zWhoAmIResponseSchema,\n} from \"@karakeep/shared/types/users\";\n\nimport { BearerAuth } from \"./common\";\nimport { UnauthorizedResponse } from \"./errors\";\n\nexport const registry = new OpenAPIRegistry();\nextendZodWithOpenApi(z);\n\nregistry.registerPath({\n  operationId: \"getCurrentUser\",\n  method: \"get\",\n  path: \"/users/me\",\n  description:\n    \"Retrieve profile information for the currently authenticated user, including their name, email, and avatar.\",\n  summary: \"Get current user info\",\n  tags: [\"Users\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {},\n  responses: {\n    200: {\n      description: \"The current user's profile information.\",\n      content: {\n        \"application/json\": {\n          schema: zWhoAmIResponseSchema,\n        },\n      },\n    },\n    401: UnauthorizedResponse,\n  },\n});\n\nregistry.registerPath({\n  operationId: \"getCurrentUserStats\",\n  method: \"get\",\n  path: \"/users/me/stats\",\n  description:\n    \"Retrieve usage statistics for the currently authenticated user, including bookmark counts by type, \" +\n    \"top domains, tag usage, bookmarking activity patterns, and storage usage.\",\n  summary: \"Get current user stats\",\n  tags: [\"Users\"],\n  security: [{ [BearerAuth.name]: [] }],\n  request: {},\n  responses: {\n    200: {\n      description: \"Detailed usage statistics for the current user.\",\n      content: {\n        \"application/json\": {\n          schema: zUserStatsResponseSchema,\n        },\n      },\n    },\n    401: UnauthorizedResponse,\n  },\n});\n"
  },
  {
    "path": "packages/open-api/package.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/package.json\",\n  \"name\": \"@karakeep/open-api\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"@asteasolutions/zod-to-openapi\": \"^7.2.0\",\n    \"@karakeep/shared\": \"workspace:^0.1.0\",\n    \"zod\": \"^3.24.2\"\n  },\n  \"devDependencies\": {\n    \"@karakeep/tsconfig\": \"workspace:^0.1.0\",\n    \"tsx\": \"^4.8.1\"\n  },\n  \"scripts\": {\n    \"typecheck\": \"tsc --noEmit\",\n    \"check\": \"tsx index.ts check\",\n    \"generate\": \"tsx index.ts\",\n    \"format\": \"oxfmt --check .\",\n    \"format:fix\": \"oxfmt .\",\n    \"lint\": \"oxlint .\",\n    \"lint:fix\": \"oxlint . --fix\"\n  },\n  \"main\": \"index.ts\"\n}\n"
  },
  {
    "path": "packages/open-api/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@karakeep/tsconfig/node.json\",\n  \"include\": [\"**/*.ts\"],\n  \"exclude\": [\"node_modules\"],\n  \"compilerOptions\": {\n    \"tsBuildInfoFile\": \"node_modules/.cache/tsbuildinfo.json\"\n  }\n}\n"
  },
  {
    "path": "packages/plugins/.oxlintrc.json",
    "content": "{\n  \"$schema\": \"../../node_modules/oxlint/configuration_schema.json\",\n  \"extends\": [\n    \"../../tooling/oxlint/oxlint-base.json\"\n  ],\n  \"env\": {\n    \"builtin\": true,\n    \"commonjs\": true\n  },\n  \"ignorePatterns\": [\n    \"**/*.config.js\",\n    \"**/*.config.cjs\",\n    \"**/.eslintrc.cjs\",\n    \"**/.next\",\n    \"**/dist\",\n    \"**/build\",\n    \"**/pnpm-lock.yaml\"\n  ]\n}\n"
  },
  {
    "path": "packages/plugins/package.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/package.json\",\n  \"name\": \"@karakeep/plugins\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"exports\": {\n    \"./queue-liteque\": \"./queue-liteque/index.ts\",\n    \"./queue-restate\": \"./queue-restate/index.ts\",\n    \"./ratelimit-memory\": \"./ratelimit-memory/index.ts\",\n    \"./ratelimit-redis\": \"./ratelimit-redis/index.ts\",\n    \"./search-meilisearch\": \"./search-meilisearch/index.ts\"\n  },\n  \"scripts\": {\n    \"typecheck\": \"tsc --noEmit\",\n    \"format\": \"oxfmt --check .\",\n    \"format:fix\": \"oxfmt .\",\n    \"lint\": \"oxlint .\",\n    \"lint:fix\": \"oxlint . --fix\",\n    \"test\": \"vitest\"\n  },\n  \"dependencies\": {\n    \"@karakeep/shared\": \"workspace:*\",\n    \"@restatedev/restate-sdk\": \"^1.10.3\",\n    \"@restatedev/restate-sdk-clients\": \"^1.10.3\",\n    \"async-mutex\": \"^0.4.1\",\n    \"liteque\": \"^0.7.0\",\n    \"meilisearch\": \"^0.45.0\",\n    \"redis\": \"^5.11.0\"\n  },\n  \"devDependencies\": {\n    \"@karakeep/tsconfig\": \"workspace:^0.1.0\",\n    \"vite-tsconfig-paths\": \"^4.3.1\",\n    \"vitest\": \"^3.2.4\"\n  }\n}\n"
  },
  {
    "path": "packages/plugins/queue-liteque/index.ts",
    "content": "// Auto-register the Liteque queue provider when this package is imported\nimport { PluginManager, PluginType } from \"@karakeep/shared/plugins\";\n\nimport { LitequeQueueProvider } from \"./src\";\n\nPluginManager.register({\n  type: PluginType.Queue,\n  name: \"Liteque\",\n  provider: new LitequeQueueProvider(),\n});\n"
  },
  {
    "path": "packages/plugins/queue-liteque/src/index.ts",
    "content": "import path from \"node:path\";\nimport {\n  buildDBClient,\n  SqliteQueue as LQ,\n  Runner as LQRunner,\n  migrateDB,\n  RetryAfterError,\n} from \"liteque\";\n\nimport type { PluginProvider } from \"@karakeep/shared/plugins\";\nimport type {\n  DequeuedJob,\n  EnqueueOptions,\n  Queue,\n  QueueClient,\n  QueueOptions,\n  Runner,\n  RunnerFuncs,\n  RunnerOptions,\n} from \"@karakeep/shared/queueing\";\nimport serverConfig from \"@karakeep/shared/config\";\nimport { QueueRetryAfterError } from \"@karakeep/shared/queueing\";\n\nclass LitequeQueueWrapper<T> implements Queue<T> {\n  constructor(\n    private readonly _name: string,\n    private readonly lq: LQ<T>,\n    public readonly opts: QueueOptions,\n  ) {}\n\n  ensureInit(): Promise<void> {\n    return Promise.resolve();\n  }\n\n  name(): string {\n    return this._name;\n  }\n\n  async enqueue(\n    payload: T,\n    options?: EnqueueOptions,\n  ): Promise<string | undefined> {\n    const job = await this.lq.enqueue(payload, options);\n    // liteque returns a Job with numeric id\n    return job ? String(job.id) : undefined;\n  }\n\n  async stats() {\n    return this.lq.stats();\n  }\n\n  async cancelAllNonRunning(): Promise<number> {\n    return this.lq.cancelAllNonRunning();\n  }\n\n  // Internal accessor for runner\n  get _impl(): LQ<T> {\n    return this.lq;\n  }\n}\n\nclass LitequeQueueClient implements QueueClient {\n  private db = buildDBClient(path.join(serverConfig.dataDir, \"queue.db\"), {\n    walEnabled: serverConfig.database.walMode,\n  });\n\n  private queues = new Map<string, LitequeQueueWrapper<unknown>>();\n\n  async prepare(): Promise<void> {\n    migrateDB(this.db);\n  }\n\n  async start(): Promise<void> {\n    // No-op for sqlite\n  }\n\n  createQueue<T>(name: string, options: QueueOptions): Queue<T> {\n    if (this.queues.has(name)) {\n      throw new Error(`Queue ${name} already exists`);\n    }\n    const lq = new LQ<T>(name, this.db, {\n      defaultJobArgs: { numRetries: options.defaultJobArgs.numRetries },\n      keepFailedJobs: options.keepFailedJobs,\n    });\n    const wrapper = new LitequeQueueWrapper<T>(name, lq, options);\n    this.queues.set(name, wrapper);\n    return wrapper;\n  }\n\n  createRunner<T, R = void>(\n    queue: Queue<T>,\n    funcs: RunnerFuncs<T, R>,\n    opts: RunnerOptions<T>,\n  ): Runner<T> {\n    const name = queue.name();\n    let wrapper = this.queues.get(name);\n    if (!wrapper) {\n      throw new Error(`Queue ${name} not found`);\n    }\n\n    // Wrap the run function to translate QueueRetryAfterError to liteque's RetryAfterError\n    const wrappedRun = async (job: DequeuedJob<T>): Promise<R> => {\n      try {\n        return await funcs.run(job);\n      } catch (error) {\n        if (error instanceof QueueRetryAfterError) {\n          // Translate to liteque's native RetryAfterError\n          // This will cause liteque to retry after the delay without counting against attempts\n          throw new RetryAfterError(error.delayMs);\n        }\n        // Re-throw any other errors\n        throw error;\n      }\n    };\n\n    const runner = new LQRunner<T, R>(\n      wrapper._impl,\n      {\n        run: wrappedRun,\n        onComplete: funcs.onComplete,\n        onError: funcs.onError,\n      },\n      {\n        pollIntervalMs: opts.pollIntervalMs ?? 1000,\n        timeoutSecs: opts.timeoutSecs,\n        concurrency: opts.concurrency,\n        validator: opts.validator,\n      },\n    );\n\n    return {\n      run: () => runner.run(),\n      stop: () => runner.stop(),\n      runUntilEmpty: () => runner.runUntilEmpty(),\n    };\n  }\n\n  async shutdown(): Promise<void> {\n    // No-op for sqlite\n  }\n}\n\nexport class LitequeQueueProvider implements PluginProvider<QueueClient> {\n  private client: QueueClient | null = null;\n\n  async getClient(): Promise<QueueClient | null> {\n    if (!this.client) {\n      const client = new LitequeQueueClient();\n      this.client = client;\n    }\n    return this.client;\n  }\n}\n"
  },
  {
    "path": "packages/plugins/queue-restate/index.ts",
    "content": "// Auto-register the Restate queue provider when this package is imported\nimport { PluginManager, PluginType } from \"@karakeep/shared/plugins\";\n\nimport { RestateQueueProvider } from \"./src\";\n\nif (RestateQueueProvider.isConfigured()) {\n  PluginManager.register({\n    type: PluginType.Queue,\n    name: \"Restate\",\n    provider: new RestateQueueProvider(),\n  });\n}\n"
  },
  {
    "path": "packages/plugins/queue-restate/src/admin.ts",
    "content": "import { z } from \"zod\";\n\nconst zStatus = z.enum([\n  \"pending\",\n  \"scheduled\",\n  \"ready\",\n  \"running\",\n  \"paused\",\n  \"backing-off\",\n  \"suspended\",\n  \"completed\",\n]);\n\ntype InvocationStatus = z.infer<typeof zStatus>;\n\nexport class AdminClient {\n  constructor(private addr: string) {}\n\n  async upsertDeployment(deploymentAddr: string) {\n    const res = await fetch(`${this.addr}/deployments`, {\n      method: \"POST\",\n      body: JSON.stringify({\n        uri: deploymentAddr,\n        force: true,\n      }),\n      headers: {\n        \"Content-Type\": \"application/json\",\n      },\n    });\n\n    if (!res.ok) {\n      throw new Error(`Failed to upsert deployment: ${res.status}`);\n    }\n  }\n\n  async getStats(\n    serviceName: string,\n  ): Promise<Record<InvocationStatus, number>> {\n    const query = `select status, count(*) as count from sys_invocation where target_service_name='${serviceName}' group by status`;\n    const res = await fetch(`${this.addr}/query`, {\n      method: \"POST\",\n      body: JSON.stringify({\n        query,\n      }),\n      headers: {\n        \"Content-Type\": \"application/json\",\n        Accept: \"application/json\",\n      },\n    });\n\n    if (!res.ok) {\n      throw new Error(`Failed to get stats: ${res.status}`);\n    }\n\n    const zSchema = z.object({\n      rows: z.array(\n        z.object({\n          status: zStatus,\n          count: z.number(),\n        }),\n      ),\n    });\n\n    return zSchema.parse(await res.json()).rows.reduce(\n      (acc, cur) => {\n        acc[cur.status] = cur.count;\n        return acc;\n      },\n      {\n        pending: 0,\n        scheduled: 0,\n        ready: 0,\n        running: 0,\n        paused: 0,\n        \"backing-off\": 0,\n        suspended: 0,\n        completed: 0,\n      } as Record<InvocationStatus, number>,\n    );\n  }\n}\n"
  },
  {
    "path": "packages/plugins/queue-restate/src/dispatcher.ts",
    "content": "import * as restate from \"@restatedev/restate-sdk\";\n\nimport type {\n  Queue,\n  QueueOptions,\n  RunnerOptions,\n} from \"@karakeep/shared/queueing\";\nimport logger from \"@karakeep/shared/logger\";\nimport { tryCatch } from \"@karakeep/shared/tryCatch\";\n\nimport type { RunnerJobData, RunnerResult, SerializedError } from \"./types\";\nimport { runnerServiceName } from \"./runner\";\nimport { RestateSemaphore } from \"./semaphore\";\n\nexport function buildDispatcherService<T, R>(\n  queue: Queue<T>,\n  opts: RunnerOptions<T>,\n  queueOpts: QueueOptions,\n) {\n  const NUM_RETRIES = queueOpts.defaultJobArgs.numRetries;\n  const runnerName = runnerServiceName(queue.name());\n\n  // Type definition for the runner service client\n  // Note: ctx parameter is required for Restate SDK to correctly infer client method signatures\n  interface RunnerService {\n    run: (\n      ctx: restate.Context,\n      data: RunnerJobData<T>,\n    ) => Promise<RunnerResult<R>>;\n    onCompleted: (\n      ctx: restate.Context,\n      data: { job: RunnerJobData<T>; result: R },\n    ) => Promise<void>;\n    onError: (\n      ctx: restate.Context,\n      data: { job: RunnerJobData<T>; error: SerializedError },\n    ) => Promise<void>;\n  }\n\n  return restate.service({\n    name: queue.name(),\n    options: {\n      inactivityTimeout: {\n        seconds: opts.timeoutSecs * 2,\n      },\n      retryPolicy: {\n        maxAttempts: NUM_RETRIES,\n        initialInterval: {\n          seconds: 5,\n        },\n        maxInterval: {\n          minutes: 1,\n        },\n      },\n      journalRetention: {\n        days: 3,\n      },\n    },\n    handlers: {\n      run: async (\n        ctx: restate.Context,\n        data: {\n          payload: T;\n          queuedIdempotencyKey?: string;\n          priority: number;\n          groupId?: string;\n        },\n      ) => {\n        const id = ctx.request().id;\n        const priority = data.priority ?? 0;\n        const logDebug = async (message: string) => {\n          await ctx.run(\n            \"log\",\n            async () => {\n              logger.debug(`[${queue.name()}][${id}] ${message}`);\n            },\n            {\n              maxRetryAttempts: 1,\n            },\n          );\n        };\n\n        const semaphore = new RestateSemaphore(\n          ctx,\n          `queue:${queue.name()}`,\n          opts.concurrency,\n          Math.ceil(opts.timeoutSecs * 1.5 * 1000),\n        );\n\n        const runner = ctx.serviceClient<RunnerService>({ name: runnerName });\n\n        let runNumber = 0;\n        while (runNumber <= NUM_RETRIES) {\n          await logDebug(\n            `Dispatcher attempt ${runNumber} for queue ${queue.name()} job ${id} (priority=${priority}, groupId=${data.groupId ?? \"none\"})`,\n          );\n          const leaseId = await semaphore.acquire(\n            priority,\n            data.groupId,\n            data.queuedIdempotencyKey,\n          );\n          if (!leaseId) {\n            // Idempotency key already exists, skip\n            await logDebug(\n              `Dispatcher skipping queue ${queue.name()} job ${id} due to existing idempotency key`,\n            );\n            return;\n          }\n          await logDebug(\n            `Dispatcher acquired lease ${leaseId} for queue ${queue.name()} job ${id}`,\n          );\n\n          const jobData: RunnerJobData<T> = {\n            id,\n            data: data.payload,\n            priority,\n            runNumber,\n            numRetriesLeft: NUM_RETRIES - runNumber,\n            timeoutSecs: opts.timeoutSecs,\n          };\n\n          // Call the runner service\n          const res = await tryCatch(runner.run(jobData));\n\n          // Handle RPC-level errors (e.g., runner service unavailable)\n          if (res.error) {\n            const errorMessage =\n              res.error instanceof Error\n                ? res.error.message\n                : String(res.error);\n            await logDebug(\n              `Dispatcher RPC error for queue ${queue.name()} job ${id}: ${errorMessage}`,\n            );\n            await semaphore.release(leaseId);\n            if (res.error instanceof restate.CancelledError) {\n              throw res.error;\n            }\n            // Notify the runner of the RPC error\n            await tryCatch(\n              runner.onError({\n                job: jobData,\n                error: {\n                  name:\n                    res.error instanceof Error ? res.error.name : \"RPCError\",\n                  message: errorMessage,\n                  stack:\n                    res.error instanceof Error ? res.error.stack : undefined,\n                },\n              }),\n            );\n            // Retry with exponential backoff + full jitter\n            const baseMs = Math.min(5000 * 2 ** runNumber, 60000);\n            const delayMs = Math.floor(ctx.rand.random() * baseMs);\n            await ctx.sleep(delayMs, \"rpc error retry\");\n            runNumber++;\n            continue;\n          }\n\n          const result = res.data;\n\n          if (result.type === \"rate_limit\") {\n            // Rate limit - release semaphore, sleep, and retry without incrementing runNumber\n            await logDebug(\n              `Dispatcher rate limit for queue ${queue.name()} job ${id} (delayMs=${result.delayMs})`,\n            );\n            await semaphore.release(leaseId);\n            await ctx.sleep(result.delayMs, \"rate limit retry\");\n            continue;\n          }\n\n          if (result.type === \"error\") {\n            // Call onError on the runner BEFORE releasing semaphore\n            // This ensures inFlight tracking stays consistent\n            await logDebug(\n              `Dispatcher runner error for queue ${queue.name()} job ${id}: ${result.error.message}`,\n            );\n            await tryCatch(\n              runner.onError({\n                job: jobData,\n                error: result.error,\n              }),\n            );\n            await semaphore.release(leaseId);\n\n            // Retry with backoff\n            await ctx.sleep(1000, \"error retry\");\n            runNumber++;\n            continue;\n          }\n\n          // Success - call onCompleted BEFORE releasing semaphore\n          // This ensures inFlight tracking stays consistent\n          await logDebug(\n            `Dispatcher completed queue ${queue.name()} job ${id}`,\n          );\n          await tryCatch(\n            runner.onCompleted({\n              job: jobData,\n              result: result.value,\n            }),\n          );\n          await semaphore.release(leaseId);\n          break;\n        }\n      },\n    },\n  });\n}\n"
  },
  {
    "path": "packages/plugins/queue-restate/src/env.ts",
    "content": "import { z } from \"zod\";\n\nconst stringBool = (defaultValue: string) =>\n  z\n    .string()\n    .default(defaultValue)\n    .refine((s) => s === \"true\" || s === \"false\")\n    .transform((s) => s === \"true\");\n\nexport const envConfig = z\n  .object({\n    RESTATE_LISTEN_PORT: z.coerce.number().optional(),\n    RESTATE_INGRESS_ADDR: z\n      .string()\n      .optional()\n      .default(\"http://localhost:8080\"),\n    RESTATE_ADMIN_ADDR: z.string().optional().default(\"http://localhost:9070\"),\n    RESTATE_PUB_KEY: z.string().optional(),\n    RESTATE_EXPOSE_CORE_SERVICES: stringBool(\"true\"),\n    // Deployment mode configuration - allows running dispatchers and runners separately\n    RESTATE_ENABLE_DISPATCHERS: stringBool(\"true\"),\n    RESTATE_ENABLE_RUNNERS: stringBool(\"true\"),\n  })\n  .parse(process.env);\n"
  },
  {
    "path": "packages/plugins/queue-restate/src/idProvider.ts",
    "content": "import { Context, object, ObjectContext } from \"@restatedev/restate-sdk\";\n\nexport const idProvider = object({\n  name: \"IdProvider\",\n  handlers: {\n    get: async (ctx: ObjectContext<{ nextId: number }>): Promise<number> => {\n      const state = (await ctx.get(\"nextId\")) ?? 0;\n      ctx.set(\"nextId\", state + 1);\n      return state;\n    },\n  },\n  options: {\n    ingressPrivate: true,\n    journalRetention: 0,\n  },\n});\n\nexport async function genId(ctx: Context) {\n  return ctx\n    .objectClient<typeof idProvider>({ name: \"IdProvider\" }, \"IdProvider\")\n    .get();\n}\n"
  },
  {
    "path": "packages/plugins/queue-restate/src/index.ts",
    "content": "import * as restate from \"@restatedev/restate-sdk\";\nimport * as restateClient from \"@restatedev/restate-sdk-clients\";\n\nimport type { PluginProvider } from \"@karakeep/shared/plugins\";\nimport type {\n  EnqueueOptions,\n  Queue,\n  QueueClient,\n  QueueOptions,\n  Runner,\n  RunnerFuncs,\n  RunnerOptions,\n} from \"@karakeep/shared/queueing\";\nimport logger from \"@karakeep/shared/logger\";\n\nimport { envConfig } from \"./env\";\nimport { idProvider } from \"./idProvider\";\nimport { semaphore } from \"./semaphore\";\nimport { buildRestateServices } from \"./service\";\n\nclass RestateQueueWrapper<T> implements Queue<T> {\n  constructor(\n    private readonly _name: string,\n    private readonly client: restateClient.Ingress,\n    public readonly opts: QueueOptions,\n  ) {}\n\n  ensureInit(): Promise<void> {\n    return Promise.resolve();\n  }\n\n  name(): string {\n    return this._name;\n  }\n\n  async enqueue(\n    payload: T,\n    options?: EnqueueOptions,\n  ): Promise<string | undefined> {\n    interface MyService {\n      run: (\n        ctx: restate.Context,\n        data: {\n          payload: T;\n          priority: number;\n          groupId?: string;\n          queuedIdempotencyKey?: string;\n        },\n      ) => Promise<void>;\n    }\n    const cl = this.client.serviceSendClient<MyService>({ name: this.name() });\n    const res = await cl.run(\n      {\n        payload,\n        priority: options?.priority ?? 0,\n        groupId: options?.groupId,\n        queuedIdempotencyKey: options?.idempotencyKey,\n      },\n      restateClient.rpc.sendOpts({\n        delay: options?.delayMs\n          ? {\n              milliseconds: options.delayMs,\n            }\n          : undefined,\n      }),\n    );\n    return res.invocationId;\n  }\n\n  async stats(): Promise<{\n    pending: number;\n    pending_retry: number;\n    running: number;\n    failed: number;\n  }> {\n    const semaphoreId = `queue:${this.name()}`;\n    const client = this.client.objectClient<typeof semaphore>(\n      { name: \"Semaphore\" },\n      semaphoreId,\n    );\n    const res = await client.queueSize();\n    return {\n      pending: res.pending,\n      pending_retry: 0,\n      running: res.running,\n      failed: 0,\n    };\n  }\n\n  async cancelAllNonRunning(): Promise<number> {\n    throw new Error(\"Method not implemented.\");\n  }\n}\n\nclass RestateRunnerWrapper<T> implements Runner<T> {\n  constructor(\n    private readonly dispatcherDef: restate.ServiceDefinition<\n      string,\n      {\n        run: (ctx: restate.Context, data: T) => Promise<void>;\n      }\n    >,\n    private readonly runnerDef: restate.ServiceDefinition<string, unknown>,\n  ) {}\n\n  async run(): Promise<void> {\n    // No-op for restate\n  }\n\n  async stop(): Promise<void> {\n    // No-op for restate\n  }\n\n  async runUntilEmpty(): Promise<void> {\n    throw new Error(\"Method not implemented.\");\n  }\n\n  get dispatcherService(): restate.ServiceDefinition<string, unknown> {\n    return this.dispatcherDef;\n  }\n\n  get runnerService(): restate.ServiceDefinition<string, unknown> {\n    return this.runnerDef;\n  }\n}\n\nclass RestateQueueClient implements QueueClient {\n  private client: restateClient.Ingress;\n  private queues = new Map<string, RestateQueueWrapper<unknown>>();\n  private services = new Map<string, RestateRunnerWrapper<unknown>>();\n\n  constructor() {\n    this.client = restateClient.connect({\n      url: envConfig.RESTATE_INGRESS_ADDR,\n    });\n  }\n\n  async prepare(): Promise<void> {\n    // No-op for restate\n  }\n\n  async start(): Promise<void> {\n    const servicesToExpose: restate.ServiceDefinition<string, unknown>[] = [];\n\n    for (const svc of this.services.values()) {\n      if (envConfig.RESTATE_ENABLE_DISPATCHERS) {\n        servicesToExpose.push(svc.dispatcherService);\n      }\n      if (envConfig.RESTATE_ENABLE_RUNNERS) {\n        servicesToExpose.push(svc.runnerService);\n      }\n    }\n\n    if (envConfig.RESTATE_EXPOSE_CORE_SERVICES) {\n      servicesToExpose.push(semaphore, idProvider);\n    }\n\n    const port = await restate.serve({\n      port: envConfig.RESTATE_LISTEN_PORT ?? 0,\n      services: servicesToExpose,\n      identityKeys: envConfig.RESTATE_PUB_KEY\n        ? [envConfig.RESTATE_PUB_KEY]\n        : undefined,\n      logger: (meta, msg) => {\n        if (meta.context) {\n          // No need to log invocation logs\n        } else {\n          logger.log(meta.level, `[restate] ${msg}`);\n        }\n      },\n    });\n    logger.info(`Restate listening on port ${port}`);\n  }\n\n  createQueue<T>(name: string, opts: QueueOptions): Queue<T> {\n    if (this.queues.has(name)) {\n      throw new Error(`Queue ${name} already exists`);\n    }\n    const wrapper = new RestateQueueWrapper<T>(name, this.client, opts);\n    this.queues.set(name, wrapper);\n    return wrapper;\n  }\n\n  createRunner<T, R = void>(\n    queue: Queue<T>,\n    funcs: RunnerFuncs<T, R>,\n    opts: RunnerOptions<T>,\n  ): Runner<T> {\n    const name = queue.name();\n    let wrapper = this.services.get(name);\n    if (wrapper) {\n      throw new Error(`Queue ${name} already exists`);\n    }\n    const services = buildRestateServices(queue, funcs, opts, queue.opts);\n    const svc = new RestateRunnerWrapper<T>(\n      services.dispatcher,\n      services.runner,\n    );\n    this.services.set(name, svc);\n    return svc;\n  }\n\n  async shutdown(): Promise<void> {\n    // No-op for sqlite\n  }\n}\n\nexport class RestateQueueProvider implements PluginProvider<QueueClient> {\n  private client: QueueClient | null = null;\n\n  static isConfigured(): boolean {\n    return envConfig.RESTATE_LISTEN_PORT !== undefined;\n  }\n\n  async getClient(): Promise<QueueClient | null> {\n    if (!this.client) {\n      const client = new RestateQueueClient();\n      this.client = client;\n    }\n    return this.client;\n  }\n}\n"
  },
  {
    "path": "packages/plugins/queue-restate/src/runner.ts",
    "content": "import * as restate from \"@restatedev/restate-sdk\";\n\nimport type { RunnerFuncs, RunnerOptions } from \"@karakeep/shared/queueing\";\nimport { QueueRetryAfterError } from \"@karakeep/shared/queueing\";\nimport { tryCatch } from \"@karakeep/shared/tryCatch\";\n\nimport type { RunnerJobData, RunnerResult, SerializedError } from \"./types\";\n\nfunction serializeError(error: Error): SerializedError {\n  return {\n    name: error.name,\n    message: error.message,\n    stack: error.stack,\n  };\n}\n\nexport function runnerServiceName(queueName: string): string {\n  return `${queueName}-runner`;\n}\n\nexport function buildRunnerService<T, R>(\n  queueName: string,\n  funcs: RunnerFuncs<T, R>,\n  opts: RunnerOptions<T>,\n) {\n  return restate.service({\n    name: runnerServiceName(queueName),\n    options: {\n      ingressPrivate: true,\n      inactivityTimeout: {\n        seconds: opts.timeoutSecs * 2,\n      },\n      journalRetention: {\n        days: 3,\n      },\n    },\n    handlers: {\n      run: restate.handlers.handler(\n        {\n          // No retries at runner level - dispatcher handles retry logic\n          retryPolicy: {\n            maxAttempts: 1,\n          },\n        },\n        async (\n          ctx: restate.Context,\n          jobData: RunnerJobData<T>,\n        ): Promise<RunnerResult<R>> => {\n          // Validate payload if validator provided\n          let payload = jobData.data;\n          if (opts.validator) {\n            const res = opts.validator.safeParse(jobData.data);\n            if (!res.success) {\n              return {\n                type: \"error\",\n                error: {\n                  name: \"ValidationError\",\n                  message: res.error.message,\n                },\n              };\n            }\n            payload = res.data;\n          }\n\n          const res = await tryCatch(\n            ctx\n              .run(\n                \"main logic\",\n                async () => {\n                  const result = await tryCatch(\n                    funcs.run({\n                      id: jobData.id,\n                      data: payload,\n                      priority: jobData.priority,\n                      runNumber: jobData.runNumber,\n                      abortSignal: AbortSignal.timeout(\n                        jobData.timeoutSecs * 1000,\n                      ),\n                    }),\n                  );\n                  if (result.error) {\n                    if (result.error instanceof QueueRetryAfterError) {\n                      return {\n                        type: \"rate_limit\" as const,\n                        delayMs: result.error.delayMs,\n                      };\n                    }\n                    throw result.error;\n                  }\n                  return { type: \"success\" as const, value: result.data };\n                },\n                {\n                  maxRetryAttempts: 1,\n                },\n              )\n              .orTimeout({\n                seconds: jobData.timeoutSecs * 1.1,\n              }),\n          );\n\n          if (res.error) {\n            return {\n              type: \"error\",\n              error: serializeError(res.error),\n            };\n          }\n\n          return res.data as RunnerResult<R>;\n        },\n      ),\n\n      onCompleted: async (\n        ctx: restate.Context,\n        data: { job: RunnerJobData<T>; result: R },\n      ): Promise<void> => {\n        await ctx.run(\"onComplete\", async () => {\n          await funcs.onComplete?.(\n            {\n              id: data.job.id,\n              data: data.job.data,\n              priority: data.job.priority,\n              runNumber: data.job.runNumber,\n              abortSignal: AbortSignal.timeout(data.job.timeoutSecs * 1000),\n            },\n            data.result,\n          );\n        });\n      },\n\n      onError: async (\n        ctx: restate.Context,\n        data: { job: RunnerJobData<T>; error: SerializedError },\n      ): Promise<void> => {\n        // Reconstruct the error\n        const reconstructedError = Object.assign(\n          new Error(data.error.message),\n          {\n            name: data.error.name,\n            stack: data.error.stack,\n          },\n        );\n\n        await ctx.run(\"onError\", async () => {\n          await funcs.onError?.({\n            id: data.job.id,\n            data: data.job.data,\n            priority: data.job.priority,\n            runNumber: data.job.runNumber,\n            numRetriesLeft: data.job.numRetriesLeft,\n            error: reconstructedError,\n          });\n        });\n      },\n    },\n  });\n}\n"
  },
  {
    "path": "packages/plugins/queue-restate/src/semaphore.ts",
    "content": "// Inspired from https://github.com/restatedev/examples/blob/main/typescript/patterns-use-cases/src/priorityqueue/queue.ts\n\nimport * as restate from \"@restatedev/restate-sdk\";\nimport {\n  Context,\n  object,\n  ObjectContext,\n  ObjectSharedContext,\n} from \"@restatedev/restate-sdk\";\n\ninterface QueueItem {\n  awakeable: string;\n  idempotencyKey?: string;\n  priority: number;\n  leaseDurationMs: number;\n}\n\ninterface LegacyQueueState {\n  itemsv2: Record<string, GroupState>;\n  inFlight: number;\n  paused: boolean;\n  leases: Record<string, number>;\n}\n\ninterface QueueState {\n  groups: Record<string, GroupState>;\n  paused: boolean;\n  leases: Record<string, number>;\n}\n\ninterface GroupState {\n  id: string;\n  items: QueueItem[];\n  lastServedTimestamp: number;\n}\n\nexport const semaphore = object({\n  name: \"Semaphore\",\n  handlers: {\n    acquire: restate.handlers.object.exclusive(\n      {\n        ingressPrivate: true,\n      },\n      async (\n        ctx: ObjectContext<LegacyQueueState>,\n        req: {\n          awakeableId: string;\n          priority: number;\n          capacity: number;\n          leaseDurationMs: number;\n          groupId?: string;\n          idempotencyKey?: string;\n        },\n      ): Promise<boolean> => {\n        const state = await getState(ctx);\n\n        if (\n          req.idempotencyKey &&\n          idempotencyKeyAlreadyExists(state.groups, req.idempotencyKey)\n        ) {\n          return false;\n        }\n\n        req.groupId = req.groupId ?? \"__ungrouped__\";\n\n        if (state.groups[req.groupId] === undefined) {\n          state.groups[req.groupId] = {\n            id: req.groupId,\n            items: [],\n            lastServedTimestamp: await ctx.date.now(),\n          };\n        }\n\n        state.groups[req.groupId].items.push({\n          awakeable: req.awakeableId,\n          priority: req.priority,\n          idempotencyKey: req.idempotencyKey,\n          leaseDurationMs: req.leaseDurationMs,\n        });\n\n        await tick(ctx, state, req.capacity);\n\n        setState(ctx, state);\n        return true;\n      },\n    ),\n\n    release: restate.handlers.object.exclusive(\n      {\n        ingressPrivate: true,\n      },\n      async (\n        ctx: ObjectContext<LegacyQueueState>,\n        req: {\n          leaseId: string;\n          capacity: number;\n        },\n      ): Promise<void> => {\n        const state = await getState(ctx);\n        delete state.leases[req.leaseId];\n        await tick(ctx, state, req.capacity);\n        setState(ctx, state);\n      },\n    ),\n    pause: restate.handlers.object.exclusive(\n      {},\n      async (ctx: ObjectContext<LegacyQueueState>): Promise<void> => {\n        const state = await getState(ctx);\n        state.paused = true;\n        setState(ctx, state);\n      },\n    ),\n    resume: restate.handlers.object.exclusive(\n      {},\n      async (ctx: ObjectContext<LegacyQueueState>): Promise<void> => {\n        const state = await getState(ctx);\n        state.paused = false;\n        await tick(ctx, state, 1);\n        setState(ctx, state);\n      },\n    ),\n    resetInflight: restate.handlers.object.exclusive(\n      {},\n      async (ctx: ObjectContext<LegacyQueueState>): Promise<void> => {\n        const state = await getState(ctx);\n        state.leases = {};\n        setState(ctx, state);\n      },\n    ),\n    tick: restate.handlers.object.exclusive(\n      {},\n      async (ctx: ObjectContext<LegacyQueueState>): Promise<void> => {\n        const state = await getState(ctx);\n        await tick(ctx, state, 1);\n        setState(ctx, state);\n      },\n    ),\n    queueSize: restate.handlers.object.shared(\n      {},\n      async (\n        ctx: ObjectSharedContext<LegacyQueueState>,\n      ): Promise<{\n        pending: number;\n        running: number;\n      }> => {\n        const groups = (await ctx.get(\"itemsv2\")) as Record<\n          string,\n          GroupState\n        > | null;\n        const leases = (await ctx.get(\"leases\")) as Record<\n          string,\n          number\n        > | null;\n\n        let pending = 0;\n        for (const group of Object.values(groups ?? {})) {\n          pending += group.items.length;\n        }\n\n        const now = Date.now();\n        let running = 0;\n        for (const expiry of Object.values(leases ?? {})) {\n          if (expiry > now) {\n            running++;\n          }\n        }\n\n        return { pending, running };\n      },\n    ),\n  },\n  options: {\n    journalRetention: 0,\n  },\n});\n\n// Lower numbers represent higher priority, mirroring Liteque’s semantics.\nfunction selectAndPopItem(\n  state: QueueState,\n  now: number,\n): {\n  item: QueueItem;\n  groupId: string;\n} {\n  let selected: {\n    priority: number;\n    groupId: string;\n    index: number;\n    groupLastServedTimestamp: number;\n  } = {\n    priority: Number.MAX_SAFE_INTEGER,\n    groupId: \"\",\n    index: 0,\n    groupLastServedTimestamp: 0,\n  };\n\n  for (const [groupId, group] of Object.entries(state.groups)) {\n    for (const [i, item] of group.items.entries()) {\n      if (item.priority < selected.priority) {\n        selected.priority = item.priority;\n        selected.groupId = groupId;\n        selected.index = i;\n        selected.groupLastServedTimestamp = group.lastServedTimestamp;\n      } else if (item.priority === selected.priority) {\n        if (group.lastServedTimestamp < selected.groupLastServedTimestamp) {\n          selected.priority = item.priority;\n          selected.groupId = groupId;\n          selected.index = i;\n          selected.groupLastServedTimestamp = group.lastServedTimestamp;\n        }\n      }\n    }\n  }\n\n  const [item] = state.groups[selected.groupId].items.splice(selected.index, 1);\n  state.groups[selected.groupId].lastServedTimestamp = now;\n  if (state.groups[selected.groupId].items.length === 0) {\n    delete state.groups[selected.groupId];\n  }\n  return { item, groupId: selected.groupId };\n}\n\nfunction pruneExpiredLeases(state: QueueState, now: number) {\n  for (const [leaseId, expiry] of Object.entries(state.leases)) {\n    if (expiry <= now) {\n      delete state.leases[leaseId];\n    }\n  }\n  return Object.keys(state.leases).length;\n}\n\nasync function tick(\n  ctx: ObjectContext<LegacyQueueState>,\n  state: QueueState,\n  capacity: number,\n): Promise<void> {\n  let activeLeases = pruneExpiredLeases(state, await ctx.date.now());\n  while (\n    !state.paused &&\n    activeLeases < capacity &&\n    Object.keys(state.groups).length > 0\n  ) {\n    const now = await ctx.date.now();\n    const { item } = selectAndPopItem(state, now);\n    state.leases[item.awakeable] = now + item.leaseDurationMs;\n    activeLeases++;\n    ctx.resolveAwakeable(item.awakeable);\n  }\n}\n\nasync function getState(\n  ctx: ObjectContext<LegacyQueueState>,\n): Promise<QueueState> {\n  const groups = (await ctx.get(\"itemsv2\")) ?? {};\n  const paused = (await ctx.get(\"paused\")) ?? false;\n  const leases = (await ctx.get(\"leases\")) ?? {};\n\n  return {\n    groups,\n    paused,\n    leases,\n  };\n}\n\nfunction idempotencyKeyAlreadyExists(\n  items: Record<string, GroupState>,\n  key: string,\n) {\n  for (const group of Object.values(items)) {\n    if (group.items.some((item) => item.idempotencyKey === key)) {\n      return true;\n    }\n  }\n  return false;\n}\n\nfunction setState(ctx: ObjectContext<LegacyQueueState>, state: QueueState) {\n  ctx.set(\"itemsv2\", state.groups);\n  ctx.set(\"leases\", state.leases);\n  ctx.set(\"paused\", state.paused);\n}\n\nexport class RestateSemaphore {\n  constructor(\n    private readonly ctx: Context,\n    private readonly id: string,\n    private readonly capacity: number,\n    private readonly leaseDurationMs: number,\n  ) {}\n\n  async acquire(priority: number, groupId?: string, idempotencyKey?: string) {\n    const awk = this.ctx.awakeable();\n    const res = await this.ctx\n      .objectClient<typeof semaphore>({ name: \"Semaphore\" }, this.id)\n      .acquire({\n        awakeableId: awk.id,\n        priority,\n        capacity: this.capacity,\n        leaseDurationMs: this.leaseDurationMs,\n        groupId,\n        idempotencyKey,\n      });\n\n    if (!res) {\n      return false;\n    }\n\n    try {\n      await awk.promise;\n    } catch (e) {\n      if (e instanceof restate.CancelledError) {\n        await this.release(awk.id);\n      }\n      throw e;\n    }\n    return awk.id;\n  }\n  async release(leaseId: string) {\n    await this.ctx\n      .objectClient<typeof semaphore>({ name: \"Semaphore\" }, this.id)\n      .release({ leaseId, capacity: this.capacity });\n  }\n}\n"
  },
  {
    "path": "packages/plugins/queue-restate/src/service.ts",
    "content": "import type {\n  Queue,\n  QueueOptions,\n  RunnerFuncs,\n  RunnerOptions,\n} from \"@karakeep/shared/queueing\";\n\nimport { buildDispatcherService } from \"./dispatcher\";\nimport { buildRunnerService } from \"./runner\";\n\nexport interface RestateServicePair<T, R> {\n  dispatcher: ReturnType<typeof buildDispatcherService<T, R>>;\n  runner: ReturnType<typeof buildRunnerService<T, R>>;\n}\n\nexport function buildRestateServices<T, R>(\n  queue: Queue<T>,\n  funcs: RunnerFuncs<T, R>,\n  opts: RunnerOptions<T>,\n  queueOpts: QueueOptions,\n): RestateServicePair<T, R> {\n  return {\n    dispatcher: buildDispatcherService<T, R>(queue, opts, queueOpts),\n    runner: buildRunnerService<T, R>(queue.name(), funcs, opts),\n  };\n}\n"
  },
  {
    "path": "packages/plugins/queue-restate/src/tests/docker-compose.yml",
    "content": "services:\n  restate:\n    image: docker.restate.dev/restatedev/restate:1.6.1\n    ports:\n      - \"${RESTATE_INGRESS_PORT:-8080}:8080\"\n      - \"${RESTATE_ADMIN_PORT:-9070}:9070\"\n    extra_hosts:\n      - \"host.docker.internal:host-gateway\"\n"
  },
  {
    "path": "packages/plugins/queue-restate/src/tests/queue.test.ts",
    "content": "import {\n  afterAll,\n  afterEach,\n  beforeAll,\n  beforeEach,\n  describe,\n  expect,\n  inject,\n  it,\n} from \"vitest\";\n\nimport type { Queue, QueueClient } from \"@karakeep/shared/queueing\";\nimport { QueueRetryAfterError } from \"@karakeep/shared/queueing\";\n\nimport { AdminClient } from \"../admin\";\nimport { RestateQueueProvider } from \"../index\";\nimport { waitUntil } from \"./utils\";\n\nclass Baton {\n  private promise: Promise<void>;\n  private resolve: () => void;\n  private waiting = 0;\n\n  constructor() {\n    this.resolve = () => {\n      /* empty */\n    };\n    this.promise = new Promise<void>((resolve) => {\n      this.resolve = resolve;\n    });\n  }\n\n  async acquire() {\n    this.waiting++;\n    await this.promise;\n  }\n\n  async waitUntilCountWaiting(count: number) {\n    while (this.waiting < count) {\n      await new Promise((resolve) => setTimeout(resolve, 100));\n    }\n  }\n\n  release() {\n    this.resolve();\n  }\n}\n\ntype TestAction =\n  | { type: \"val\"; val: number }\n  | { type: \"err\"; err: string }\n  | { type: \"stall\"; durSec: number }\n  | { type: \"semaphore-acquire\" }\n  | {\n      type: \"rate-limit\";\n      val: number;\n      delayMs: number;\n      attemptsBeforeSuccess: number;\n    }\n  | {\n      type: \"timeout-then-succeed\";\n      val: number;\n      timeoutDurSec: number;\n      attemptsBeforeSuccess: number;\n    };\n\ndescribe(\"Restate Queue Provider\", () => {\n  let queueClient: QueueClient;\n  let queue: Queue<TestAction>;\n  let adminClient: AdminClient;\n\n  const testState = {\n    results: [] as number[],\n    errors: [] as string[],\n    inFlight: 0,\n    maxInFlight: 0,\n    baton: new Baton(),\n    rateLimitAttempts: new Map<string, number>(),\n    timeoutAttempts: new Map<string, number>(),\n  };\n\n  async function waitUntilQueueEmpty() {\n    await waitUntil(\n      async () => {\n        const stats = await adminClient.getStats(queue.name());\n        return (\n          stats.pending +\n            stats.ready +\n            stats.running +\n            stats[\"backing-off\"] +\n            stats.paused +\n            stats.suspended ===\n          0\n        );\n      },\n      \"Queue to be empty\",\n      60000,\n    );\n  }\n\n  beforeEach(async () => {\n    testState.results = [];\n    testState.errors = [];\n    testState.inFlight = 0;\n    testState.maxInFlight = 0;\n    testState.baton = new Baton();\n    testState.rateLimitAttempts = new Map<string, number>();\n    testState.timeoutAttempts = new Map<string, number>();\n  });\n  afterEach(async () => {\n    await waitUntilQueueEmpty();\n  });\n\n  beforeAll(async () => {\n    const ingressPort = inject(\"restateIngressPort\");\n    const adminPort = inject(\"restateAdminPort\");\n\n    process.env.RESTATE_INGRESS_ADDR = `http://localhost:${ingressPort}`;\n    process.env.RESTATE_ADMIN_ADDR = `http://localhost:${adminPort}`;\n    process.env.RESTATE_LISTEN_PORT = \"9080\";\n\n    const provider = new RestateQueueProvider();\n    const client = await provider.getClient();\n\n    if (!client) {\n      throw new Error(\"Failed to create queue client\");\n    }\n\n    queueClient = client;\n    adminClient = new AdminClient(process.env.RESTATE_ADMIN_ADDR);\n\n    queue = queueClient.createQueue<TestAction>(\"test-queue\", {\n      defaultJobArgs: {\n        numRetries: 3,\n      },\n      keepFailedJobs: false,\n    });\n\n    queueClient.createRunner(\n      queue,\n      {\n        run: async (job) => {\n          testState.inFlight++;\n          testState.maxInFlight = Math.max(\n            testState.maxInFlight,\n            testState.inFlight,\n          );\n          const jobData = job.data;\n          switch (jobData.type) {\n            case \"val\":\n              return jobData.val;\n            case \"err\":\n              throw new Error(jobData.err);\n            case \"stall\":\n              await new Promise((resolve) =>\n                setTimeout(resolve, jobData.durSec * 1000),\n              );\n              break;\n            case \"semaphore-acquire\":\n              await testState.baton.acquire();\n              break;\n            case \"rate-limit\": {\n              const attemptKey = `${job.id}`;\n              const currentAttempts =\n                testState.rateLimitAttempts.get(attemptKey) || 0;\n              testState.rateLimitAttempts.set(attemptKey, currentAttempts + 1);\n\n              if (currentAttempts < jobData.attemptsBeforeSuccess) {\n                throw new QueueRetryAfterError(\n                  `Rate limited (attempt ${currentAttempts + 1})`,\n                  jobData.delayMs,\n                );\n              }\n              return jobData.val;\n            }\n            case \"timeout-then-succeed\": {\n              const attemptKey = `${job.id}`;\n              const currentAttempts =\n                testState.timeoutAttempts.get(attemptKey) || 0;\n              testState.timeoutAttempts.set(attemptKey, currentAttempts + 1);\n\n              if (currentAttempts < jobData.attemptsBeforeSuccess) {\n                // Stall longer than the timeout to trigger a timeout\n                await new Promise((resolve) =>\n                  setTimeout(resolve, jobData.timeoutDurSec * 1000),\n                );\n                // This should not be reached if timeout works correctly\n                throw new Error(\"Should have timed out\");\n              }\n              return jobData.val;\n            }\n          }\n        },\n        onError: async (job) => {\n          testState.inFlight--;\n          const jobData = job.data;\n          if (jobData && jobData.type === \"err\") {\n            testState.errors.push(jobData.err);\n          }\n        },\n        onComplete: async (_j, res) => {\n          testState.inFlight--;\n          if (res) {\n            testState.results.push(res);\n          }\n        },\n      },\n      {\n        concurrency: 3,\n        timeoutSecs: 2,\n        pollIntervalMs: 0 /* Doesn't matter */,\n      },\n    );\n\n    await queueClient.prepare();\n    await queueClient.start();\n\n    await adminClient.upsertDeployment(\"http://host.docker.internal:9080\");\n  }, 90000);\n\n  afterAll(async () => {\n    if (queueClient?.shutdown) {\n      await queueClient.shutdown();\n    }\n  });\n\n  it(\"should enqueue and process a job\", async () => {\n    const jobId = await queue.enqueue({ type: \"val\", val: 42 });\n\n    expect(jobId).toBeDefined();\n    expect(typeof jobId).toBe(\"string\");\n\n    await waitUntilQueueEmpty();\n\n    expect(testState.results).toEqual([42]);\n  }, 60000);\n\n  it(\"should process multiple jobs\", async () => {\n    await queue.enqueue({ type: \"val\", val: 1 });\n    await queue.enqueue({ type: \"val\", val: 2 });\n    await queue.enqueue({ type: \"val\", val: 3 });\n\n    await waitUntilQueueEmpty();\n\n    expect(testState.results.length).toEqual(3);\n    expect(testState.results).toContain(1);\n    expect(testState.results).toContain(2);\n    expect(testState.results).toContain(3);\n  }, 60000);\n\n  it(\"should retry failed jobs\", async () => {\n    await queue.enqueue({ type: \"err\", err: \"Test error\" });\n\n    await waitUntilQueueEmpty();\n\n    // Initial attempt + 3 retries\n    expect(testState.errors).toEqual([\n      \"Test error\",\n      \"Test error\",\n      \"Test error\",\n      \"Test error\",\n    ]);\n  }, 90000);\n\n  it(\"should use idempotency key\", async () => {\n    const idempotencyKey = `test-${Date.now()}`;\n\n    // hog the queue\n    await Promise.all([\n      queue.enqueue(\n        { type: \"semaphore-acquire\" },\n        { groupId: \"init\", priority: -10 },\n      ),\n      queue.enqueue(\n        { type: \"semaphore-acquire\" },\n        { groupId: \"init\", priority: -10 },\n      ),\n      queue.enqueue(\n        { type: \"semaphore-acquire\" },\n        { groupId: \"init\", priority: -10 },\n      ),\n    ]);\n    await testState.baton.waitUntilCountWaiting(3);\n\n    await queue.enqueue({ type: \"val\", val: 200 }, { idempotencyKey });\n    await queue.enqueue({ type: \"val\", val: 200 }, { idempotencyKey });\n\n    await new Promise((resolve) => setTimeout(resolve, 1000));\n\n    testState.baton.release();\n\n    await waitUntilQueueEmpty();\n\n    expect(testState.results).toEqual([200]);\n  }, 60000);\n\n  it(\"should handle concurrent jobs\", async () => {\n    const promises = [];\n    for (let i = 300; i < 320; i++) {\n      promises.push(queue.enqueue({ type: \"stall\", durSec: 0.1 }));\n    }\n    await Promise.all(promises);\n\n    await waitUntilQueueEmpty();\n\n    expect(testState.maxInFlight).toEqual(3);\n  }, 60000);\n\n  it(\"should handle priorities\", async () => {\n    // hog the queue\n    await Promise.all([\n      queue.enqueue(\n        { type: \"semaphore-acquire\" },\n        { groupId: \"init\", priority: -10 },\n      ),\n      queue.enqueue(\n        { type: \"semaphore-acquire\" },\n        { groupId: \"init\", priority: -10 },\n      ),\n      queue.enqueue(\n        { type: \"semaphore-acquire\" },\n        { groupId: \"init\", priority: -10 },\n      ),\n    ]);\n    await testState.baton.waitUntilCountWaiting(3);\n\n    // Then those will get reprioritized\n    await Promise.all([\n      queue.enqueue({ type: \"val\", val: 200 }, { priority: -1 }),\n      queue.enqueue({ type: \"val\", val: 201 }, { priority: -2 }),\n      queue.enqueue({ type: \"val\", val: 202 }, { priority: -3 }),\n\n      queue.enqueue({ type: \"val\", val: 300 }, { priority: 0 }),\n      queue.enqueue({ type: \"val\", val: 301 }, { priority: 1 }),\n      queue.enqueue({ type: \"val\", val: 302 }, { priority: 2 }),\n    ]);\n\n    // Wait for all jobs to be enqueued\n    await new Promise((resolve) => setTimeout(resolve, 1000));\n    testState.baton.release();\n\n    await waitUntilQueueEmpty();\n\n    expect(testState.results).toEqual([\n      // Lower numeric priority value should run first\n      202, 201, 200, 300, 301, 302,\n    ]);\n  }, 60000);\n\n  describe(\"Group Fairness\", () => {\n    it(\"should process jobs from different groups fairly with same priority\", async () => {\n      // hog the queue\n      await Promise.all([\n        queue.enqueue(\n          { type: \"semaphore-acquire\" },\n          { groupId: \"init\", priority: -10 },\n        ),\n        queue.enqueue(\n          { type: \"semaphore-acquire\" },\n          { groupId: \"init\", priority: -10 },\n        ),\n        queue.enqueue(\n          { type: \"semaphore-acquire\" },\n          { groupId: \"init\", priority: -10 },\n        ),\n      ]);\n      await testState.baton.waitUntilCountWaiting(3);\n\n      // Enqueue jobs from two different groups with same priority\n      // Group A has more jobs\n      await queue.enqueue(\n        { type: \"val\", val: 200 },\n        { priority: 0, groupId: \"B\" },\n      );\n      await queue.enqueue(\n        { type: \"val\", val: 201 },\n        { priority: 0, groupId: \"B\" },\n      );\n      await queue.enqueue(\n        { type: \"val\", val: 100 },\n        { priority: 0, groupId: \"A\" },\n      );\n      await queue.enqueue(\n        { type: \"val\", val: 101 },\n        { priority: 0, groupId: \"A\" },\n      );\n      await queue.enqueue(\n        { type: \"val\", val: 102 },\n        { priority: 0, groupId: \"A\" },\n      );\n      await queue.enqueue(\n        { type: \"val\", val: 103 },\n        { priority: 0, groupId: \"A\" },\n      );\n      await queue.enqueue(\n        { type: \"val\", val: 300 },\n        { priority: 0, groupId: \"C\" },\n      );\n      await queue.enqueue(\n        { type: \"val\", val: 301 },\n        { priority: 0, groupId: \"C\" },\n      );\n\n      // Wait for all jobs to be enqueued\n      await new Promise((resolve) => setTimeout(resolve, 1000));\n      testState.baton.release();\n\n      await waitUntilQueueEmpty();\n\n      expect(testState.results).toEqual([\n        200, 100, 300, 201, 101, 301, 102, 103,\n      ]);\n    }, 60000);\n\n    it(\"should respect priority over group fairness\", async () => {\n      // hog the queue\n      await Promise.all([\n        queue.enqueue(\n          { type: \"semaphore-acquire\" },\n          { groupId: \"init\", priority: -10 },\n        ),\n        queue.enqueue(\n          { type: \"semaphore-acquire\" },\n          { groupId: \"init\", priority: -10 },\n        ),\n        queue.enqueue(\n          { type: \"semaphore-acquire\" },\n          { groupId: \"init\", priority: -10 },\n        ),\n      ]);\n      await testState.baton.waitUntilCountWaiting(3);\n\n      await queue.enqueue(\n        { type: \"val\", val: 100 },\n        { priority: 1, groupId: \"A\" },\n      );\n      await queue.enqueue(\n        { type: \"val\", val: 101 },\n        { priority: 1, groupId: \"A\" },\n      );\n      await queue.enqueue(\n        { type: \"val\", val: 200 },\n        { priority: 0, groupId: \"B\" },\n      );\n      await queue.enqueue(\n        { type: \"val\", val: 201 },\n        { priority: 0, groupId: \"B\" },\n      );\n\n      // Wait for all jobs to be enqueued\n      await new Promise((resolve) => setTimeout(resolve, 1000));\n      testState.baton.release();\n\n      await waitUntilQueueEmpty();\n\n      // Priority 0 (higher) should run before priority 1 (lower)\n      expect(testState.results).toEqual([200, 201, 100, 101]);\n    }, 60000);\n\n    it(\"should handle jobs without groupId\", async () => {\n      // hog the queue\n      await Promise.all([\n        queue.enqueue(\n          { type: \"semaphore-acquire\" },\n          { groupId: \"init\", priority: -10 },\n        ),\n        queue.enqueue(\n          { type: \"semaphore-acquire\" },\n          { groupId: \"init\", priority: -10 },\n        ),\n        queue.enqueue(\n          { type: \"semaphore-acquire\" },\n          { groupId: \"init\", priority: -10 },\n        ),\n      ]);\n      await testState.baton.waitUntilCountWaiting(3);\n\n      // Mix of grouped and ungrouped jobs\n      await queue.enqueue({ type: \"val\", val: 100 }, { priority: 0 }); // ungrouped\n      await queue.enqueue({ type: \"val\", val: 101 }, { priority: 0 }); // ungrouped\n      await queue.enqueue(\n        { type: \"val\", val: 200 },\n        { priority: 0, groupId: \"A\" },\n      );\n      await queue.enqueue(\n        { type: \"val\", val: 201 },\n        { priority: 0, groupId: \"A\" },\n      );\n\n      // Wait for all jobs to be enqueued\n      await new Promise((resolve) => setTimeout(resolve, 1000));\n      testState.baton.release();\n\n      await waitUntilQueueEmpty();\n\n      // All jobs should complete successfully\n      expect(testState.results).toEqual([100, 200, 101, 201]);\n    }, 60000);\n\n    it(\"should work with jobs that don't specify groupId\", async () => {\n      // hog the queue\n      await Promise.all([\n        queue.enqueue(\n          { type: \"semaphore-acquire\" },\n          { groupId: \"init\", priority: -10 },\n        ),\n        queue.enqueue(\n          { type: \"semaphore-acquire\" },\n          { groupId: \"init\", priority: -10 },\n        ),\n        queue.enqueue(\n          { type: \"semaphore-acquire\" },\n          { groupId: \"init\", priority: -10 },\n        ),\n      ]);\n\n      await testState.baton.waitUntilCountWaiting(3);\n\n      // These should all go to the default \"__ungrouped__\" group\n      await queue.enqueue({ type: \"val\", val: 1 }, { priority: 0 });\n      await queue.enqueue({ type: \"val\", val: 2 }, { priority: 1 });\n      await queue.enqueue({ type: \"val\", val: 3 }, { priority: -1 });\n\n      // Wait for all jobs to be enqueued\n      await new Promise((resolve) => setTimeout(resolve, 1000));\n\n      testState.baton.release();\n\n      await waitUntilQueueEmpty();\n\n      // Should respect priority\n      expect(testState.results).toEqual([3, 1, 2]);\n    }, 60000);\n\n    it(\"should handle same job in same group with different priorities\", async () => {\n      // hog the queue\n      await Promise.all([\n        queue.enqueue(\n          { type: \"semaphore-acquire\" },\n          { groupId: \"init\", priority: -10 },\n        ),\n        queue.enqueue(\n          { type: \"semaphore-acquire\" },\n          { groupId: \"init\", priority: -10 },\n        ),\n        queue.enqueue(\n          { type: \"semaphore-acquire\" },\n          { groupId: \"init\", priority: -10 },\n        ),\n      ]);\n      await testState.baton.waitUntilCountWaiting(3);\n\n      await queue.enqueue(\n        { type: \"val\", val: 100 },\n        { priority: 2, groupId: \"A\" },\n      );\n      await queue.enqueue(\n        { type: \"val\", val: 101 },\n        { priority: 1, groupId: \"A\" },\n      );\n      await queue.enqueue(\n        { type: \"val\", val: 102 },\n        { priority: 0, groupId: \"A\" },\n      );\n\n      // Wait for all jobs to be enqueued\n      await new Promise((resolve) => setTimeout(resolve, 1000));\n      testState.baton.release();\n\n      await waitUntilQueueEmpty();\n\n      // Should respect priority even within the same group\n      expect(testState.results).toEqual([102, 101, 100]);\n    }, 60000);\n  });\n\n  describe(\"QueueRetryAfterError handling\", () => {\n    it(\"should retry after delay without counting against retry attempts\", async () => {\n      const startTime = Date.now();\n\n      // This job will fail with QueueRetryAfterError twice before succeeding\n      await queue.enqueue({\n        type: \"rate-limit\",\n        val: 42,\n        delayMs: 500, // 500ms delay\n        attemptsBeforeSuccess: 2, // Fail twice, succeed on third try\n      });\n\n      await waitUntilQueueEmpty();\n\n      const duration = Date.now() - startTime;\n\n      // Should have succeeded\n      expect(testState.results).toEqual([42]);\n\n      // Should have been called 3 times (2 rate limit failures + 1 success)\n      expect(testState.rateLimitAttempts.size).toBe(1);\n      const attempts = Array.from(testState.rateLimitAttempts.values())[0];\n      expect(attempts).toBe(3);\n\n      // Should have waited at least 1 second total (2 x 500ms delays)\n      expect(duration).toBeGreaterThanOrEqual(1000);\n\n      // onError should NOT have been called for rate limit retries\n      expect(testState.errors).toEqual([]);\n    }, 60000);\n\n    it(\"should not exhaust retries when rate limited\", async () => {\n      // This job will be rate limited many more times than the retry limit\n      // but should still eventually succeed\n      await queue.enqueue({\n        type: \"rate-limit\",\n        val: 100,\n        delayMs: 100, // Short delay for faster test\n        attemptsBeforeSuccess: 10, // Fail 10 times (more than the 3 retry limit)\n      });\n\n      await waitUntilQueueEmpty();\n\n      // Should have succeeded despite being \"retried\" more than the limit\n      expect(testState.results).toEqual([100]);\n\n      // Should have been called 11 times (10 rate limit failures + 1 success)\n      const attempts = Array.from(testState.rateLimitAttempts.values())[0];\n      expect(attempts).toBe(11);\n\n      // No errors should have been recorded\n      expect(testState.errors).toEqual([]);\n    }, 90000);\n\n    it(\"should still respect retry limit for non-rate-limit errors\", async () => {\n      // Enqueue a regular error job that should fail permanently\n      await queue.enqueue({ type: \"err\", err: \"Regular error\" });\n\n      await waitUntilQueueEmpty();\n\n      // Should have failed 4 times (initial + 3 retries) and not succeeded\n      expect(testState.errors).toEqual([\n        \"Regular error\",\n        \"Regular error\",\n        \"Regular error\",\n        \"Regular error\",\n      ]);\n      expect(testState.results).toEqual([]);\n    }, 90000);\n  });\n\n  describe(\"Timeout handling\", () => {\n    it(\"should retry timed out jobs and not waste semaphore slots\", async () => {\n      // This test verifies that:\n      // 1. Jobs that timeout get retried correctly\n      // 2. Semaphore slots are freed when jobs timeout (via lease expiry)\n      // 3. Other jobs can still run while a job is being retried\n\n      // Enqueue a job that will timeout on first attempt, succeed on second\n      // timeoutSecs is 2, so we stall for 5 seconds to ensure timeout\n      await queue.enqueue({\n        type: \"timeout-then-succeed\",\n        val: 42,\n        timeoutDurSec: 5,\n        attemptsBeforeSuccess: 1, // Timeout once, then succeed\n      });\n\n      // Wait a bit for the first attempt to start\n      await new Promise((resolve) => setTimeout(resolve, 500));\n\n      // Enqueue more jobs to verify semaphore slots are eventually freed\n      // With concurrency=3, these should be able to run after the timeout\n      await queue.enqueue({ type: \"val\", val: 100 });\n      await queue.enqueue({ type: \"val\", val: 101 });\n      await queue.enqueue({ type: \"val\", val: 102 });\n\n      await waitUntilQueueEmpty();\n\n      // The timeout job should have succeeded after retry\n      expect(testState.results).toContain(42);\n\n      // All other jobs should have completed\n      expect(testState.results).toContain(100);\n      expect(testState.results).toContain(101);\n      expect(testState.results).toContain(102);\n\n      // The timeout job should have been attempted twice\n      const attempts = Array.from(testState.timeoutAttempts.values())[0];\n      expect(attempts).toBe(2);\n\n      // Concurrency should not have exceeded the limit\n      expect(testState.maxInFlight).toBeLessThanOrEqual(3);\n    }, 120000);\n\n    it(\"should handle job that times out multiple times before succeeding\", async () => {\n      // Enqueue a single job that times out twice before succeeding\n      // This tests that the retry mechanism works correctly for timeouts\n      await queue.enqueue({\n        type: \"timeout-then-succeed\",\n        val: 99,\n        timeoutDurSec: 5,\n        attemptsBeforeSuccess: 2, // Timeout twice, then succeed\n      });\n\n      await waitUntilQueueEmpty();\n\n      // Job should eventually succeed\n      expect(testState.results).toEqual([99]);\n\n      // Should have been attempted 3 times (2 timeouts + 1 success)\n      const attempts = Array.from(testState.timeoutAttempts.values())[0];\n      expect(attempts).toBe(3);\n    }, 180000);\n  });\n});\n"
  },
  {
    "path": "packages/plugins/queue-restate/src/tests/setup/startContainers.ts",
    "content": "import { execSync } from \"child_process\";\nimport net from \"net\";\nimport path from \"path\";\nimport { fileURLToPath } from \"url\";\nimport type { GlobalSetupContext } from \"vitest/node\";\n\nimport { waitUntil } from \"../utils.js\";\n\nasync function getRandomPort(): Promise<number> {\n  const server = net.createServer();\n  return new Promise<number>((resolve, reject) => {\n    server.unref();\n    server.on(\"error\", reject);\n    server.listen(0, () => {\n      const port = (server.address() as net.AddressInfo).port;\n      server.close(() => resolve(port));\n    });\n  });\n}\n\nasync function waitForHealthy(\n  ingressPort: number,\n  adminPort: number,\n  timeout = 60000,\n): Promise<void> {\n  await waitUntil(\n    async () => {\n      const response = await fetch(`http://localhost:${adminPort}/health`);\n      return response.ok;\n    },\n    \"Restate admin API is healthy\",\n    timeout,\n  );\n\n  await waitUntil(\n    async () => {\n      const response = await fetch(\n        `http://localhost:${ingressPort}/restate/health`,\n      );\n      return response.ok;\n    },\n    \"Restate ingress is healthy\",\n    timeout,\n  );\n}\n\nexport default async function ({ provide }: GlobalSetupContext) {\n  const __dirname = path.dirname(fileURLToPath(import.meta.url));\n  const ingressPort = await getRandomPort();\n  const adminPort = await getRandomPort();\n\n  console.log(\n    `Starting Restate on ports ${ingressPort} (ingress) and ${adminPort} (admin)...`,\n  );\n  execSync(`docker compose up -d`, {\n    cwd: path.join(__dirname, \"..\"),\n    stdio: \"ignore\",\n    env: {\n      ...process.env,\n      RESTATE_INGRESS_PORT: ingressPort.toString(),\n      RESTATE_ADMIN_PORT: adminPort.toString(),\n    },\n  });\n\n  console.log(\"Waiting for Restate to become healthy...\");\n  await waitForHealthy(ingressPort, adminPort);\n\n  provide(\"restateIngressPort\", ingressPort);\n  provide(\"restateAdminPort\", adminPort);\n\n  process.env.RESTATE_INGRESS_ADDR = `http://localhost:${ingressPort}`;\n  process.env.RESTATE_ADMIN_ADDR = `http://localhost:${adminPort}`;\n  process.env.RESTATE_LISTEN_PORT = \"9080\";\n\n  return async () => {\n    console.log(\"Stopping Restate...\");\n    execSync(\"docker compose down\", {\n      cwd: path.join(__dirname, \"..\"),\n      stdio: \"ignore\",\n    });\n    return Promise.resolve();\n  };\n}\n\ndeclare module \"vitest\" {\n  export interface ProvidedContext {\n    restateIngressPort: number;\n    restateAdminPort: number;\n  }\n}\n"
  },
  {
    "path": "packages/plugins/queue-restate/src/tests/utils.ts",
    "content": "export async function waitUntil(\n  f: () => Promise<boolean>,\n  description: string,\n  timeoutMs = 60000,\n): Promise<void> {\n  const startTime = Date.now();\n\n  while (Date.now() - startTime < timeoutMs) {\n    console.log(`Waiting for ${description}...`);\n    try {\n      const res = await f();\n      if (res) {\n        console.log(`${description}: success`);\n        return;\n      }\n    } catch (error) {\n      console.log(`${description}: error, retrying...: ${error}`);\n    }\n    await new Promise((resolve) => setTimeout(resolve, 1000));\n  }\n\n  throw new Error(`${description}: timeout after ${timeoutMs}ms`);\n}\n"
  },
  {
    "path": "packages/plugins/queue-restate/src/types.ts",
    "content": "import { z } from \"zod\";\n\n/**\n * Zod schema for serialized errors that cross the RPC boundary.\n */\nexport const zSerializedError = z.object({\n  name: z.string(),\n  message: z.string(),\n  stack: z.string().optional(),\n});\n\nexport type SerializedError = z.infer<typeof zSerializedError>;\n\n/**\n * Zod schema for job data passed from dispatcher to runner.\n */\nexport function zRunnerJobData<T extends z.ZodTypeAny>(payloadSchema: T) {\n  return z.object({\n    id: z.string(),\n    data: payloadSchema,\n    priority: z.number(),\n    runNumber: z.number(),\n    numRetriesLeft: z.number(),\n    timeoutSecs: z.number(),\n  });\n}\n\nexport interface RunnerJobData<T> {\n  id: string;\n  data: T;\n  priority: number;\n  runNumber: number;\n  numRetriesLeft: number;\n  timeoutSecs: number;\n}\n\n/**\n * Zod schema for runner.run() response.\n */\nexport const zRunnerResult = z.discriminatedUnion(\"type\", [\n  z.object({\n    type: z.literal(\"success\"),\n    value: z.unknown(),\n  }),\n  z.object({\n    type: z.literal(\"rate_limit\"),\n    delayMs: z.number(),\n  }),\n  z.object({\n    type: z.literal(\"error\"),\n    error: zSerializedError,\n  }),\n]);\n\nexport type RunnerResult<R> =\n  | { type: \"success\"; value: R }\n  | { type: \"rate_limit\"; delayMs: number }\n  | { type: \"error\"; error: SerializedError };\n\n/**\n * Zod schema for runner.onCompleted() request.\n */\nexport function zOnCompletedRequest<T extends z.ZodTypeAny>(payloadSchema: T) {\n  return z.object({\n    job: zRunnerJobData(payloadSchema),\n    result: z.unknown(),\n  });\n}\n\n/**\n * Zod schema for runner.onError() request.\n */\nexport function zOnErrorRequest<T extends z.ZodTypeAny>(payloadSchema: T) {\n  return z.object({\n    job: zRunnerJobData(payloadSchema),\n    error: zSerializedError,\n  });\n}\n"
  },
  {
    "path": "packages/plugins/ratelimit-memory/index.ts",
    "content": "// Auto-register the RateLimit plugin when this package is imported\nimport { PluginManager, PluginType } from \"@karakeep/shared/plugins\";\n\nimport { RateLimitProvider } from \"./src\";\n\nPluginManager.register({\n  type: PluginType.RateLimit,\n  name: \"In-Memory Rate Limiter\",\n  provider: new RateLimitProvider(),\n});\n\n// Export the provider and rate limiter class for advanced usage\nexport { RateLimiter, RateLimitProvider } from \"./src\";\n"
  },
  {
    "path": "packages/plugins/ratelimit-memory/src/index.test.ts",
    "content": "import assert from \"assert\";\nimport { afterEach, beforeEach, describe, expect, it, vi } from \"vitest\";\n\nimport { RateLimiter } from \"./index\";\n\ndescribe(\"RateLimiter\", () => {\n  let rateLimiter: RateLimiter;\n\n  beforeEach(() => {\n    rateLimiter = new RateLimiter();\n    vi.useFakeTimers();\n  });\n\n  afterEach(() => {\n    vi.useRealTimers();\n    rateLimiter.clear();\n  });\n\n  describe(\"checkRateLimit\", () => {\n    it(\"should allow requests within rate limit\", () => {\n      const config = {\n        name: \"test\",\n        windowMs: 60000,\n        maxRequests: 3,\n      };\n\n      const result1 = rateLimiter.checkRateLimit(config, \"user1\");\n      const result2 = rateLimiter.checkRateLimit(config, \"user1\");\n      const result3 = rateLimiter.checkRateLimit(config, \"user1\");\n\n      expect(result1.allowed).toBe(true);\n      expect(result2.allowed).toBe(true);\n      expect(result3.allowed).toBe(true);\n    });\n\n    it(\"should block requests exceeding rate limit\", () => {\n      const config = {\n        name: \"test\",\n        windowMs: 60000,\n        maxRequests: 2,\n      };\n\n      const result1 = rateLimiter.checkRateLimit(config, \"user1\");\n      const result2 = rateLimiter.checkRateLimit(config, \"user1\");\n      const result3 = rateLimiter.checkRateLimit(config, \"user1\");\n\n      expect(result1.allowed).toBe(true);\n      expect(result2.allowed).toBe(true);\n      expect(result3.allowed).toBe(false);\n      assert(!result3.allowed);\n      expect(result3.resetInSeconds).toBeGreaterThan(0);\n    });\n\n    it(\"should reset after window expires\", () => {\n      const config = {\n        name: \"test\",\n        windowMs: 60000,\n        maxRequests: 2,\n      };\n\n      // First two requests allowed\n      const result1 = rateLimiter.checkRateLimit(config, \"user1\");\n      const result2 = rateLimiter.checkRateLimit(config, \"user1\");\n      expect(result1.allowed).toBe(true);\n      expect(result2.allowed).toBe(true);\n\n      // Third request blocked\n      const result3 = rateLimiter.checkRateLimit(config, \"user1\");\n      expect(result3.allowed).toBe(false);\n\n      // Advance time past the window\n      vi.advanceTimersByTime(61000);\n\n      // Should allow request after window reset\n      const result4 = rateLimiter.checkRateLimit(config, \"user1\");\n      expect(result4.allowed).toBe(true);\n    });\n\n    it(\"should isolate rate limits by identifier\", () => {\n      const config = {\n        name: \"test\",\n        windowMs: 60000,\n        maxRequests: 1,\n      };\n\n      const result1 = rateLimiter.checkRateLimit(config, \"user1\");\n      const result2 = rateLimiter.checkRateLimit(config, \"user2\");\n\n      expect(result1.allowed).toBe(true);\n      expect(result2.allowed).toBe(true);\n    });\n\n    it(\"should isolate rate limits by key\", () => {\n      const config = {\n        name: \"test\",\n        windowMs: 60000,\n        maxRequests: 1,\n      };\n\n      const result1 = rateLimiter.checkRateLimit(config, \"user1:/api/v1\");\n      const result2 = rateLimiter.checkRateLimit(config, \"user1:/api/v2\");\n\n      expect(result1.allowed).toBe(true);\n      expect(result2.allowed).toBe(true);\n    });\n\n    it(\"should isolate rate limits by config name\", () => {\n      const config1 = {\n        name: \"api\",\n        windowMs: 60000,\n        maxRequests: 1,\n      };\n      const config2 = {\n        name: \"auth\",\n        windowMs: 60000,\n        maxRequests: 1,\n      };\n\n      const result1 = rateLimiter.checkRateLimit(config1, \"user1\");\n      const result2 = rateLimiter.checkRateLimit(config2, \"user1\");\n\n      expect(result1.allowed).toBe(true);\n      expect(result2.allowed).toBe(true);\n    });\n\n    it(\"should calculate correct resetInSeconds\", () => {\n      const config = {\n        name: \"test\",\n        windowMs: 60000,\n        maxRequests: 1,\n      };\n\n      // First request allowed\n      rateLimiter.checkRateLimit(config, \"user1\");\n\n      // Advance time by 30 seconds\n      vi.advanceTimersByTime(30000);\n\n      // Second request blocked\n      const result = rateLimiter.checkRateLimit(config, \"user1\");\n      expect(result.allowed).toBe(false);\n      // Should have ~30 seconds remaining\n      assert(!result.allowed);\n      expect(result.resetInSeconds).toBeGreaterThan(29);\n      expect(result.resetInSeconds).toBeLessThanOrEqual(30);\n    });\n  });\n\n  describe(\"reset\", () => {\n    it(\"should reset rate limit for specific identifier\", () => {\n      const config = {\n        name: \"test\",\n        windowMs: 60000,\n        maxRequests: 1,\n      };\n\n      // Use up the limit\n      rateLimiter.checkRateLimit(config, \"user1\");\n      const result1 = rateLimiter.checkRateLimit(config, \"user1\");\n      expect(result1.allowed).toBe(false);\n\n      // Reset the limit\n      rateLimiter.reset(config, \"user1\");\n\n      // Should allow request again\n      const result2 = rateLimiter.checkRateLimit(config, \"user1\");\n      expect(result2.allowed).toBe(true);\n    });\n\n    it(\"should reset rate limit for specific key\", () => {\n      const config = {\n        name: \"test\",\n        windowMs: 60000,\n        maxRequests: 1,\n      };\n\n      // Use up the limit for key1\n      rateLimiter.checkRateLimit(config, \"user1:/path1\");\n      const result1 = rateLimiter.checkRateLimit(config, \"user1:/path1\");\n      expect(result1.allowed).toBe(false);\n\n      // Reset only key1\n      rateLimiter.reset(config, \"user1:/path1\");\n\n      // key1 should be allowed\n      const result2 = rateLimiter.checkRateLimit(config, \"user1:/path1\");\n      expect(result2.allowed).toBe(true);\n    });\n\n    it(\"should not affect other identifiers\", () => {\n      const config = {\n        name: \"test\",\n        windowMs: 60000,\n        maxRequests: 1,\n      };\n\n      // Use up limits for both users\n      rateLimiter.checkRateLimit(config, \"user1\");\n      rateLimiter.checkRateLimit(config, \"user2\");\n\n      // Reset only user1\n      rateLimiter.reset(config, \"user1\");\n\n      const result1 = rateLimiter.checkRateLimit(config, \"user1\");\n      const result2 = rateLimiter.checkRateLimit(config, \"user2\");\n\n      expect(result1.allowed).toBe(true);\n      expect(result2.allowed).toBe(false);\n    });\n  });\n\n  describe(\"clear\", () => {\n    it(\"should clear all rate limits\", () => {\n      const config = {\n        name: \"test\",\n        windowMs: 60000,\n        maxRequests: 1,\n      };\n\n      // Use up limits for multiple users\n      rateLimiter.checkRateLimit(config, \"user1\");\n      rateLimiter.checkRateLimit(config, \"user2\");\n\n      // Clear all limits\n      rateLimiter.clear();\n\n      // All should be allowed\n      const result1 = rateLimiter.checkRateLimit(config, \"user1\");\n      const result2 = rateLimiter.checkRateLimit(config, \"user2\");\n\n      expect(result1.allowed).toBe(true);\n      expect(result2.allowed).toBe(true);\n    });\n  });\n\n  describe(\"cleanup\", () => {\n    it(\"should cleanup expired entries\", () => {\n      const config = {\n        name: \"test\",\n        windowMs: 60000,\n        maxRequests: 1,\n      };\n\n      // Create an entry\n      rateLimiter.checkRateLimit(config, \"user1\");\n\n      // Advance time past window + cleanup interval\n      vi.advanceTimersByTime(61000 + 60000);\n\n      // Entry should be cleaned up and new request allowed\n      const result = rateLimiter.checkRateLimit(config, \"user1\");\n      expect(result.allowed).toBe(true);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/plugins/ratelimit-memory/src/index.ts",
    "content": "import type {\n  RateLimitClient,\n  RateLimitConfig,\n  RateLimitResult,\n} from \"@karakeep/shared/ratelimiting\";\nimport { PluginProvider } from \"@karakeep/shared/plugins\";\n\ninterface RateLimitEntry {\n  count: number;\n  resetTime: number;\n}\n\nexport class RateLimiter implements RateLimitClient {\n  private store = new Map<string, RateLimitEntry>();\n  private cleanupProbability: number;\n\n  constructor(cleanupProbability = 0.01) {\n    // Probability of cleanup on each check (default 1%)\n    this.cleanupProbability = cleanupProbability;\n  }\n\n  private cleanupExpiredEntries() {\n    const now = Date.now();\n    for (const [key, entry] of this.store.entries()) {\n      if (now > entry.resetTime) {\n        this.store.delete(key);\n      }\n    }\n  }\n\n  checkRateLimit(config: RateLimitConfig, key: string): RateLimitResult {\n    if (!key) {\n      return { allowed: true };\n    }\n\n    // Probabilistic cleanup\n    if (Math.random() < this.cleanupProbability) {\n      this.cleanupExpiredEntries();\n    }\n\n    const rateLimitKey = `${config.name}:${key}`;\n    const now = Date.now();\n\n    let entry = this.store.get(rateLimitKey);\n\n    if (!entry || now > entry.resetTime) {\n      entry = {\n        count: 1,\n        resetTime: now + config.windowMs,\n      };\n      this.store.set(rateLimitKey, entry);\n      return { allowed: true };\n    }\n\n    if (entry.count >= config.maxRequests) {\n      const resetInSeconds = Math.ceil((entry.resetTime - now) / 1000);\n      return {\n        allowed: false,\n        resetInSeconds,\n      };\n    }\n\n    entry.count++;\n    return { allowed: true };\n  }\n\n  reset(config: RateLimitConfig, key: string) {\n    const rateLimitKey = `${config.name}:${key}`;\n    this.store.delete(rateLimitKey);\n  }\n\n  clear() {\n    this.store.clear();\n  }\n}\n\nexport class RateLimitProvider implements PluginProvider<RateLimitClient> {\n  private client: RateLimiter | null = null;\n\n  async getClient(): Promise<RateLimitClient | null> {\n    if (!this.client) {\n      this.client = new RateLimiter();\n    }\n    return this.client;\n  }\n}\n"
  },
  {
    "path": "packages/plugins/ratelimit-redis/index.ts",
    "content": "// Auto-register the RateLimit plugin when this package is imported\nimport serverConfig from \"@karakeep/shared/config\";\nimport { PluginManager, PluginType } from \"@karakeep/shared/plugins\";\n\nimport { RedisRateLimitProvider } from \"./src\";\n\n// Only register if Redis configuration is provided\nif (serverConfig.redis?.url) {\n  PluginManager.register({\n    type: PluginType.RateLimit,\n    name: \"Redis Rate Limiter\",\n    provider: new RedisRateLimitProvider({\n      url: serverConfig.redis.url,\n    }),\n  });\n}\n\n// Export the provider and rate limiter class for advanced usage\nexport { RedisRateLimiter, RedisRateLimitProvider } from \"./src\";\n"
  },
  {
    "path": "packages/plugins/ratelimit-redis/src/index.ts",
    "content": "import type { RedisClientType } from \"redis\";\nimport { createClient } from \"redis\";\n\nimport type {\n  RateLimitClient,\n  RateLimitConfig,\n  RateLimitResult,\n} from \"@karakeep/shared/ratelimiting\";\nimport { throttledLogger } from \"@karakeep/shared/logger\";\nimport { PluginProvider } from \"@karakeep/shared/plugins\";\n\nconst KEY_PREFIX = \"ratelimit:v1\";\n\nconst failOpenLog = throttledLogger(30_000);\n\nexport class RedisRateLimiter implements RateLimitClient {\n  private redis: RedisClientType;\n\n  constructor(redis: RedisClientType) {\n    this.redis = redis;\n  }\n\n  async checkRateLimit(\n    config: RateLimitConfig,\n    key: string,\n  ): Promise<RateLimitResult> {\n    if (!key) {\n      return { allowed: true };\n    }\n\n    const rateLimitKey = `${KEY_PREFIX}:${config.name}:${key}`;\n    const rateLimitSequenceKey = `${rateLimitKey}:seq`;\n    const now = Date.now();\n\n    try {\n      // Use a Lua script to ensure atomicity\n      // This script:\n      // 1. Removes old entries outside the time window\n      // 2. Counts current entries\n      // 3. Adds new entry if under limit\n      // 4. Sets expiration on the key\n      const luaScript = `\n        local key = KEYS[1]\n        local sequenceKey = KEYS[2]\n        local now = tonumber(ARGV[1])\n        local window = tonumber(ARGV[2])\n        local maxRequests = tonumber(ARGV[3])\n        local windowStart = now - window\n\n        -- Remove old entries\n        redis.call('ZREMRANGEBYSCORE', key, '-inf', windowStart)\n\n        -- Count current requests\n        local current = redis.call('ZCARD', key)\n\n        if current < maxRequests then\n          -- Add new request\n          local seq = redis.call('INCR', sequenceKey)\n          redis.call('ZADD', key, now, now .. ':' .. seq)\n          -- Set expiration (window in milliseconds converted to seconds, plus 1 for safety)\n          redis.call('EXPIRE', key, math.ceil(window / 1000) + 1)\n          redis.call('EXPIRE', sequenceKey, math.ceil(window / 1000) + 1)\n          return {1, 0} -- allowed, resetInSeconds\n        else\n          -- Get the oldest entry to calculate reset time\n          local oldest = redis.call('ZRANGE', key, 0, 0, 'WITHSCORES')\n          local resetTime = tonumber(oldest[2]) + window\n          local resetInSeconds = math.ceil((resetTime - now) / 1000)\n          return {0, resetInSeconds} -- not allowed, resetInSeconds\n        end\n      `;\n\n      const result = await this.redis.eval(luaScript, {\n        keys: [rateLimitKey, rateLimitSequenceKey],\n        arguments: [\n          now.toString(),\n          config.windowMs.toString(),\n          config.maxRequests.toString(),\n        ],\n      });\n\n      if (!Array.isArray(result) || result.length < 2) {\n        throw new Error(\"Unexpected Redis eval result\");\n      }\n\n      const [allowed, resetInSeconds] = result.map((value) => Number(value));\n\n      if (allowed === 1) {\n        return { allowed: true };\n      } else {\n        return {\n          allowed: false,\n          resetInSeconds: resetInSeconds,\n        };\n      }\n    } catch (error) {\n      // On Redis error, fail open (allow the request)\n      failOpenLog(\n        \"warn\",\n        `Rate limiter failed open due to Redis error: ${error}`,\n      );\n      return { allowed: true };\n    }\n  }\n\n  async reset(config: RateLimitConfig, key: string) {\n    const rateLimitKey = `${KEY_PREFIX}:${config.name}:${key}`;\n    const rateLimitSequenceKey = `${rateLimitKey}:seq`;\n    try {\n      await this.redis.del([rateLimitKey, rateLimitSequenceKey]);\n    } catch (error) {\n      console.error(\"Redis rate limit reset error:\", error);\n    }\n  }\n\n  async clear() {\n    try {\n      let cursor = \"0\";\n      do {\n        const { cursor: nextCursor, keys } = await this.redis.scan(cursor, {\n          MATCH: `${KEY_PREFIX}:*`,\n          COUNT: 100,\n        });\n        cursor = nextCursor;\n        if (keys.length > 0) {\n          await this.redis.del(keys);\n        }\n      } while (cursor !== \"0\");\n    } catch (error) {\n      console.error(\"Redis rate limit clear error:\", error);\n    }\n  }\n\n  async disconnect() {\n    if (this.redis.isOpen) {\n      await this.redis.close();\n    }\n  }\n}\n\nexport interface RedisRateLimiterOptions {\n  url: string;\n}\n\nexport class RedisRateLimitProvider implements PluginProvider<RateLimitClient> {\n  private client: RedisRateLimiter | null = null;\n  private clientInitPromise: Promise<RedisRateLimiter | null> | null = null;\n  private nextRetryAt = 0;\n  private options: RedisRateLimiterOptions;\n  private static readonly RETRY_BACKOFF_MS = 5_000;\n\n  constructor(options: RedisRateLimiterOptions) {\n    this.options = options;\n  }\n\n  private createRedisClient(): RedisClientType {\n    return createClient({\n      url: this.options.url,\n      disableOfflineQueue: true,\n      socket: {\n        reconnectStrategy: () => 3_000,\n      },\n    });\n  }\n\n  private setupLifecycleHandlers(redis: RedisClientType): void {\n    redis.on(\"ready\", () => {\n      this.nextRetryAt = 0;\n    });\n\n    // \"end\" fires when the client is permanently closed\n    redis.on(\"end\", () => {\n      this.client = null;\n      this.nextRetryAt = Date.now() + RedisRateLimitProvider.RETRY_BACKOFF_MS;\n    });\n\n    redis.on(\"error\", (error) => {\n      console.error(\"Redis rate limiter client error:\", error);\n    });\n  }\n\n  private async initializeClient(): Promise<RedisRateLimiter | null> {\n    const redis = this.createRedisClient();\n    this.setupLifecycleHandlers(redis);\n\n    try {\n      // Test connection\n      await redis.connect();\n      await redis.ping();\n\n      this.nextRetryAt = 0;\n      console.log(\"Redis rate limiter connected successfully\");\n      return new RedisRateLimiter(redis);\n    } catch (error) {\n      this.nextRetryAt = Date.now() + RedisRateLimitProvider.RETRY_BACKOFF_MS;\n      redis.destroy();\n      console.error(\"Failed to connect to Redis for rate limiting:\", error);\n      return null;\n    }\n  }\n\n  async getClient(): Promise<RateLimitClient | null> {\n    if (this.client) {\n      return this.client;\n    }\n\n    if (this.clientInitPromise) {\n      return await this.clientInitPromise;\n    }\n\n    if (this.nextRetryAt > Date.now()) {\n      return null;\n    }\n\n    const initPromise = this.initializeClient();\n    this.clientInitPromise = initPromise;\n\n    try {\n      const client = await initPromise;\n      this.client = client;\n      return client;\n    } finally {\n      if (this.clientInitPromise === initPromise) {\n        this.clientInitPromise = null;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/plugins/ratelimit-redis/src/tests/docker-compose.yml",
    "content": "services:\n  redis:\n    image: redis:8-alpine\n    ports:\n      - \"${REDIS_PORT:-6379}:6379\"\n"
  },
  {
    "path": "packages/plugins/ratelimit-redis/src/tests/ratelimit-redis.test.ts",
    "content": "import assert from \"assert\";\nimport {\n  afterAll,\n  beforeAll,\n  beforeEach,\n  describe,\n  expect,\n  inject,\n  it,\n} from \"vitest\";\n\nimport { RedisRateLimiter, RedisRateLimitProvider } from \"../index\";\n\ndescribe(\"RedisRateLimiter\", () => {\n  let rateLimiter: RedisRateLimiter;\n\n  beforeAll(async () => {\n    const redisPort = inject(\"redisPort\");\n    const provider = new RedisRateLimitProvider({\n      url: `redis://localhost:${redisPort}`,\n    });\n    const client = await provider.getClient();\n    assert(client, \"Failed to connect to Redis\");\n    rateLimiter = client as RedisRateLimiter;\n  });\n\n  beforeEach(async () => {\n    await rateLimiter.clear();\n  });\n\n  afterAll(async () => {\n    if (rateLimiter) {\n      await rateLimiter.disconnect();\n    }\n  });\n\n  describe(\"checkRateLimit\", () => {\n    it(\"should allow requests within rate limit\", async () => {\n      const config = {\n        name: \"test-allow\",\n        windowMs: 60000,\n        maxRequests: 3,\n      };\n\n      const result1 = await rateLimiter.checkRateLimit(config, \"user1\");\n      const result2 = await rateLimiter.checkRateLimit(config, \"user1\");\n      const result3 = await rateLimiter.checkRateLimit(config, \"user1\");\n\n      expect(result1.allowed).toBe(true);\n      expect(result2.allowed).toBe(true);\n      expect(result3.allowed).toBe(true);\n    });\n\n    it(\"should block requests exceeding rate limit\", async () => {\n      const config = {\n        name: \"test-block\",\n        windowMs: 60000,\n        maxRequests: 2,\n      };\n\n      const result1 = await rateLimiter.checkRateLimit(config, \"user1\");\n      const result2 = await rateLimiter.checkRateLimit(config, \"user1\");\n      const result3 = await rateLimiter.checkRateLimit(config, \"user1\");\n\n      expect(result1.allowed).toBe(true);\n      expect(result2.allowed).toBe(true);\n      expect(result3.allowed).toBe(false);\n      assert(!result3.allowed);\n      expect(result3.resetInSeconds).toBeGreaterThan(0);\n    });\n\n    it(\"should reset after window expires\", async () => {\n      const config = {\n        name: \"test-window\",\n        windowMs: 2000, // 2 second window for faster test\n        maxRequests: 1,\n      };\n\n      const result1 = await rateLimiter.checkRateLimit(config, \"user1\");\n      expect(result1.allowed).toBe(true);\n\n      const result2 = await rateLimiter.checkRateLimit(config, \"user1\");\n      expect(result2.allowed).toBe(false);\n\n      // Wait for the window to expire\n      await new Promise((resolve) => setTimeout(resolve, 2500));\n\n      const result3 = await rateLimiter.checkRateLimit(config, \"user1\");\n      expect(result3.allowed).toBe(true);\n    });\n\n    it(\"should isolate rate limits by key\", async () => {\n      const config = {\n        name: \"test-isolate-key\",\n        windowMs: 60000,\n        maxRequests: 1,\n      };\n\n      const result1 = await rateLimiter.checkRateLimit(config, \"user1:/api/v1\");\n      const result2 = await rateLimiter.checkRateLimit(config, \"user1:/api/v2\");\n\n      expect(result1.allowed).toBe(true);\n      expect(result2.allowed).toBe(true);\n    });\n\n    it(\"should isolate rate limits by config name\", async () => {\n      const config1 = {\n        name: \"api-isolate\",\n        windowMs: 60000,\n        maxRequests: 1,\n      };\n      const config2 = {\n        name: \"auth-isolate\",\n        windowMs: 60000,\n        maxRequests: 1,\n      };\n\n      const result1 = await rateLimiter.checkRateLimit(config1, \"user1\");\n      const result2 = await rateLimiter.checkRateLimit(config2, \"user1\");\n\n      expect(result1.allowed).toBe(true);\n      expect(result2.allowed).toBe(true);\n    });\n\n    it(\"should calculate correct resetInSeconds\", async () => {\n      const config = {\n        name: \"test-reset-calc\",\n        windowMs: 60000,\n        maxRequests: 1,\n      };\n\n      await rateLimiter.checkRateLimit(config, \"user1\");\n      const result = await rateLimiter.checkRateLimit(config, \"user1\");\n\n      expect(result.allowed).toBe(false);\n      assert(!result.allowed);\n      expect(result.resetInSeconds).toBeGreaterThan(0);\n      expect(result.resetInSeconds).toBeLessThanOrEqual(60);\n    });\n\n    it(\"should allow empty key\", async () => {\n      const config = {\n        name: \"test-empty-key\",\n        windowMs: 60000,\n        maxRequests: 1,\n      };\n\n      const result = await rateLimiter.checkRateLimit(config, \"\");\n      expect(result.allowed).toBe(true);\n    });\n  });\n\n  describe(\"reset\", () => {\n    it(\"should reset rate limit for specific identifier\", async () => {\n      const config = {\n        name: \"test-reset\",\n        windowMs: 60000,\n        maxRequests: 1,\n      };\n\n      await rateLimiter.checkRateLimit(config, \"user1\");\n      const result1 = await rateLimiter.checkRateLimit(config, \"user1\");\n      expect(result1.allowed).toBe(false);\n\n      await rateLimiter.reset(config, \"user1\");\n\n      const result2 = await rateLimiter.checkRateLimit(config, \"user1\");\n      expect(result2.allowed).toBe(true);\n    });\n\n    it(\"should not affect other identifiers\", async () => {\n      const config = {\n        name: \"test-reset-isolation\",\n        windowMs: 60000,\n        maxRequests: 1,\n      };\n\n      await rateLimiter.checkRateLimit(config, \"user1\");\n      await rateLimiter.checkRateLimit(config, \"user2\");\n\n      await rateLimiter.reset(config, \"user1\");\n\n      const result1 = await rateLimiter.checkRateLimit(config, \"user1\");\n      const result2 = await rateLimiter.checkRateLimit(config, \"user2\");\n\n      expect(result1.allowed).toBe(true);\n      expect(result2.allowed).toBe(false);\n    });\n  });\n\n  describe(\"concurrent access\", () => {\n    it(\"should handle concurrent requests atomically\", async () => {\n      const config = {\n        name: \"test-concurrent\",\n        windowMs: 60000,\n        maxRequests: 5,\n      };\n\n      // Fire 10 requests concurrently\n      const results = await Promise.all(\n        Array.from({ length: 10 }, () =>\n          rateLimiter.checkRateLimit(config, \"user1\"),\n        ),\n      );\n\n      const allowed = results.filter((r) => r.allowed).length;\n      const blocked = results.filter((r) => !r.allowed).length;\n\n      expect(allowed).toBe(5);\n      expect(blocked).toBe(5);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/plugins/ratelimit-redis/src/tests/setup/startContainers.ts",
    "content": "import { execSync } from \"child_process\";\nimport net from \"net\";\nimport path from \"path\";\nimport { fileURLToPath } from \"url\";\nimport type { GlobalSetupContext } from \"vitest/node\";\n\nimport { waitUntil } from \"../utils.js\";\n\nasync function getRandomPort(): Promise<number> {\n  const server = net.createServer();\n  return new Promise<number>((resolve, reject) => {\n    server.unref();\n    server.on(\"error\", reject);\n    server.listen(0, () => {\n      const port = (server.address() as net.AddressInfo).port;\n      server.close(() => resolve(port));\n    });\n  });\n}\n\nasync function waitForHealthy(port: number, timeout = 30000): Promise<void> {\n  await waitUntil(\n    async () => {\n      const socket = net.createConnection({ port, host: \"localhost\" });\n      return new Promise<boolean>((resolve) => {\n        socket.on(\"connect\", () => {\n          socket.destroy();\n          resolve(true);\n        });\n        socket.on(\"error\", () => {\n          socket.destroy();\n          resolve(false);\n        });\n      });\n    },\n    \"Redis is healthy\",\n    timeout,\n  );\n}\n\nexport default async function ({ provide }: GlobalSetupContext) {\n  const __dirname = path.dirname(fileURLToPath(import.meta.url));\n  const redisPort = await getRandomPort();\n\n  console.log(`Starting Redis on port ${redisPort}...`);\n  try {\n    execSync(`docker compose up -d`, {\n      cwd: path.join(__dirname, \"..\"),\n      stdio: [\"ignore\", \"pipe\", \"pipe\"],\n      env: {\n        ...process.env,\n        REDIS_PORT: redisPort.toString(),\n      },\n    });\n  } catch (error) {\n    const execError = error as {\n      stdout?: Buffer;\n      stderr?: Buffer;\n      message?: string;\n    };\n    console.error(\"Failed to start Redis container\");\n    if (execError.stdout) {\n      console.error(execError.stdout.toString());\n    }\n    if (execError.stderr) {\n      console.error(execError.stderr.toString());\n    }\n    console.error(execError.message ?? error);\n    throw error;\n  }\n\n  console.log(\"Waiting for Redis to become healthy...\");\n  await waitForHealthy(redisPort);\n\n  provide(\"redisPort\", redisPort);\n\n  return async () => {\n    console.log(\"Stopping Redis...\");\n    execSync(\"docker compose down\", {\n      cwd: path.join(__dirname, \"..\"),\n      stdio: \"ignore\",\n    });\n  };\n}\n\ndeclare module \"vitest\" {\n  export interface ProvidedContext {\n    redisPort: number;\n  }\n}\n"
  },
  {
    "path": "packages/plugins/ratelimit-redis/src/tests/utils.ts",
    "content": "export async function waitUntil(\n  f: () => Promise<boolean>,\n  description: string,\n  timeoutMs = 60000,\n): Promise<void> {\n  const startTime = Date.now();\n\n  while (Date.now() - startTime < timeoutMs) {\n    console.log(`Waiting for ${description}...`);\n    try {\n      const res = await f();\n      if (res) {\n        console.log(`${description}: success`);\n        return;\n      }\n    } catch (error) {\n      console.log(`${description}: error, retrying...: ${error}`);\n    }\n    await new Promise((resolve) => setTimeout(resolve, 1000));\n  }\n\n  throw new Error(`${description}: timeout after ${timeoutMs}ms`);\n}\n"
  },
  {
    "path": "packages/plugins/search-meilisearch/index.ts",
    "content": "// Auto-register the MeiliSearch provider when this package is imported\nimport { PluginManager, PluginType } from \"@karakeep/shared/plugins\";\n\nimport { MeiliSearchProvider } from \"./src\";\n\nif (MeiliSearchProvider.isConfigured()) {\n  PluginManager.register({\n    type: PluginType.Search,\n    name: \"MeiliSearch\",\n    provider: new MeiliSearchProvider(),\n  });\n}\n"
  },
  {
    "path": "packages/plugins/search-meilisearch/src/env.ts",
    "content": "import { z } from \"zod\";\n\nexport const envConfig = z\n  .object({\n    MEILI_ADDR: z.string().optional(),\n    MEILI_MASTER_KEY: z.string().default(\"\"),\n    MEILI_BATCH_SIZE: z.coerce.number().int().positive().default(50),\n    MEILI_BATCH_TIMEOUT_MS: z.coerce.number().int().positive().default(500),\n  })\n  .parse(process.env);\n"
  },
  {
    "path": "packages/plugins/search-meilisearch/src/index.ts",
    "content": "import type { Index } from \"meilisearch\";\nimport { Mutex } from \"async-mutex\";\nimport { MeiliSearch } from \"meilisearch\";\n\nimport type {\n  BookmarkSearchDocument,\n  FilterQuery,\n  IndexingOptions,\n  SearchIndexClient,\n  SearchOptions,\n  SearchResponse,\n} from \"@karakeep/shared/search\";\nimport serverConfig from \"@karakeep/shared/config\";\nimport { PluginProvider } from \"@karakeep/shared/plugins\";\n\nimport { envConfig } from \"./env\";\n\nfunction filterToMeiliSearchFilter(filter: FilterQuery): string {\n  switch (filter.type) {\n    case \"eq\":\n      return `${filter.field} = \"${filter.value}\"`;\n    case \"in\":\n      return `${filter.field} IN [${filter.values.join(\",\")}]`;\n    default: {\n      const exhaustiveCheck: never = filter;\n      throw new Error(`Unhandled color case: ${exhaustiveCheck}`);\n    }\n  }\n}\n\ntype PendingOperation =\n  | {\n      type: \"add\";\n      document: BookmarkSearchDocument;\n      resolve: () => void;\n      reject: (error: Error) => void;\n    }\n  | {\n      type: \"delete\";\n      id: string;\n      resolve: () => void;\n      reject: (error: Error) => void;\n    };\n\nclass BatchingDocumentQueue {\n  private pendingOperations: PendingOperation[] = [];\n  private flushTimeout: ReturnType<typeof setTimeout> | null = null;\n  private mutex = new Mutex();\n\n  constructor(\n    private index: Index<BookmarkSearchDocument>,\n    private jobTimeoutSec: number,\n    private batchSize: number,\n    private batchTimeoutMs: number,\n  ) {}\n\n  async addDocument(document: BookmarkSearchDocument): Promise<void> {\n    return new Promise((resolve, reject) => {\n      this.pendingOperations.push({ type: \"add\", document, resolve, reject });\n      this.scheduleFlush();\n\n      if (this.pendingOperations.length >= this.batchSize) {\n        void this.flush();\n      }\n    });\n  }\n\n  async deleteDocument(id: string): Promise<void> {\n    return new Promise((resolve, reject) => {\n      this.pendingOperations.push({ type: \"delete\", id, resolve, reject });\n      this.scheduleFlush();\n\n      if (this.pendingOperations.length >= this.batchSize) {\n        void this.flush();\n      }\n    });\n  }\n\n  private scheduleFlush(): void {\n    if (this.flushTimeout === null) {\n      this.flushTimeout = setTimeout(() => {\n        void this.flush();\n      }, this.batchTimeoutMs);\n    }\n  }\n\n  private async flush(): Promise<void> {\n    await this.mutex.runExclusive(async () => {\n      if (this.flushTimeout) {\n        clearTimeout(this.flushTimeout);\n        this.flushTimeout = null;\n      }\n\n      // Process operations in order, batching consecutive operations of the same type\n      while (this.pendingOperations.length > 0) {\n        const currentType = this.pendingOperations[0].type;\n\n        // Collect consecutive operations of the same type (up to batchSize)\n        const batch: PendingOperation[] = [];\n        while (\n          batch.length < this.batchSize &&\n          this.pendingOperations.length > 0 &&\n          this.pendingOperations[0].type === currentType\n        ) {\n          batch.push(this.pendingOperations.shift()!);\n        }\n\n        if (currentType === \"add\") {\n          await this.flushAddBatch(\n            batch as Extract<PendingOperation, { type: \"add\" }>[],\n          );\n        } else {\n          await this.flushDeleteBatch(\n            batch as Extract<PendingOperation, { type: \"delete\" }>[],\n          );\n        }\n      }\n    });\n  }\n\n  private async flushAddBatch(\n    batch: Extract<PendingOperation, { type: \"add\" }>[],\n  ): Promise<void> {\n    if (batch.length === 0) return;\n\n    try {\n      const documents = batch.map((p) => p.document);\n      const task = await this.index.addDocuments(documents, {\n        primaryKey: \"id\",\n      });\n      await this.ensureTaskSuccess(task.taskUid);\n      batch.forEach((p) => p.resolve());\n    } catch (error) {\n      batch.forEach((p) => p.reject(error as Error));\n    }\n  }\n\n  private async flushDeleteBatch(\n    batch: Extract<PendingOperation, { type: \"delete\" }>[],\n  ): Promise<void> {\n    if (batch.length === 0) return;\n\n    try {\n      const ids = batch.map((p) => p.id);\n      const task = await this.index.deleteDocuments(ids);\n      await this.ensureTaskSuccess(task.taskUid);\n      batch.forEach((p) => p.resolve());\n    } catch (error) {\n      batch.forEach((p) => p.reject(error as Error));\n    }\n  }\n\n  private async ensureTaskSuccess(taskUid: number): Promise<void> {\n    const task = await this.index.waitForTask(taskUid, {\n      intervalMs: 200,\n      timeOutMs: this.jobTimeoutSec * 1000 * 0.9,\n    });\n    if (task.error) {\n      throw new Error(`Search task failed: ${task.error.message}`);\n    }\n  }\n}\n\nclass MeiliSearchIndexClient implements SearchIndexClient {\n  private batchQueue: BatchingDocumentQueue;\n  private jobTimeoutSec: number;\n\n  constructor(\n    private index: Index<BookmarkSearchDocument>,\n    jobTimeoutSec: number,\n    batchSize: number,\n    batchTimeoutMs: number,\n  ) {\n    this.jobTimeoutSec = jobTimeoutSec;\n    this.batchQueue = new BatchingDocumentQueue(\n      index,\n      jobTimeoutSec,\n      batchSize,\n      batchTimeoutMs,\n    );\n  }\n\n  async addDocuments(\n    documents: BookmarkSearchDocument[],\n    options?: IndexingOptions,\n  ): Promise<void> {\n    const shouldBatch = options?.batch !== false;\n\n    if (shouldBatch) {\n      await Promise.all(\n        documents.map((doc) => this.batchQueue.addDocument(doc)),\n      );\n    } else {\n      // Direct indexing without batching\n      const task = await this.index.addDocuments(documents, {\n        primaryKey: \"id\",\n      });\n      await this.ensureTaskSuccess(task.taskUid);\n    }\n  }\n\n  async deleteDocuments(\n    ids: string[],\n    options?: IndexingOptions,\n  ): Promise<void> {\n    const shouldBatch = options?.batch !== false;\n\n    if (shouldBatch) {\n      await Promise.all(ids.map((id) => this.batchQueue.deleteDocument(id)));\n    } else {\n      // Direct deletion without batching\n      const task = await this.index.deleteDocuments(ids);\n      await this.ensureTaskSuccess(task.taskUid);\n    }\n  }\n\n  async search(options: SearchOptions): Promise<SearchResponse> {\n    const result = await this.index.search(options.query, {\n      filter: options.filter?.map((f) => filterToMeiliSearchFilter(f)),\n      limit: options.limit,\n      offset: options.offset,\n      sort: options.sort?.map((s) => `${s.field}:${s.order}`),\n      attributesToRetrieve: [\"id\"],\n      showRankingScore: true,\n    });\n\n    return {\n      hits: result.hits.map((hit) => ({\n        id: hit.id,\n        score: hit._rankingScore,\n      })),\n      totalHits: result.estimatedTotalHits ?? 0,\n      processingTimeMs: result.processingTimeMs,\n    };\n  }\n\n  async clearIndex(): Promise<void> {\n    const task = await this.index.deleteAllDocuments();\n    await this.ensureTaskSuccess(task.taskUid);\n  }\n\n  private async ensureTaskSuccess(taskUid: number): Promise<void> {\n    const task = await this.index.waitForTask(taskUid, {\n      intervalMs: 200,\n      timeOutMs: this.jobTimeoutSec * 1000 * 0.9,\n    });\n    if (task.error) {\n      throw new Error(`Search task failed: ${task.error.message}`);\n    }\n  }\n}\n\nexport class MeiliSearchProvider implements PluginProvider<SearchIndexClient> {\n  private client: MeiliSearch | undefined;\n  private indexClient: SearchIndexClient | undefined;\n  private initPromise: Promise<SearchIndexClient | null> | undefined;\n  private readonly indexName = \"bookmarks\";\n\n  constructor() {\n    if (MeiliSearchProvider.isConfigured()) {\n      this.client = new MeiliSearch({\n        host: envConfig.MEILI_ADDR!,\n        apiKey: envConfig.MEILI_MASTER_KEY,\n      });\n    }\n  }\n\n  static isConfigured(): boolean {\n    return !!envConfig.MEILI_ADDR;\n  }\n\n  async getClient(): Promise<SearchIndexClient | null> {\n    if (this.indexClient) {\n      return this.indexClient;\n    }\n\n    if (this.initPromise) {\n      return this.initPromise;\n    }\n\n    this.initPromise = this.initClient();\n    const client = await this.initPromise;\n    this.initPromise = undefined;\n    return client;\n  }\n\n  private async initClient(): Promise<SearchIndexClient | null> {\n    if (!this.client) {\n      return null;\n    }\n\n    const indices = await this.client.getIndexes();\n    let indexFound = indices.results.find((i) => i.uid === this.indexName);\n\n    if (!indexFound) {\n      const idx = await this.client.createIndex(this.indexName, {\n        primaryKey: \"id\",\n      });\n      await this.client.waitForTask(idx.taskUid);\n      indexFound = await this.client.getIndex<BookmarkSearchDocument>(\n        this.indexName,\n      );\n    }\n\n    await this.configureIndex(indexFound);\n    this.indexClient = new MeiliSearchIndexClient(\n      indexFound,\n      serverConfig.search.jobTimeoutSec,\n      envConfig.MEILI_BATCH_SIZE,\n      envConfig.MEILI_BATCH_TIMEOUT_MS,\n    );\n    return this.indexClient;\n  }\n\n  private async configureIndex(\n    index: Index<BookmarkSearchDocument>,\n  ): Promise<void> {\n    const desiredFilterableAttributes = [\"id\", \"userId\"].sort();\n    const desiredSortableAttributes = [\"createdAt\"].sort();\n\n    const settings = await index.getSettings();\n\n    if (\n      JSON.stringify(settings.filterableAttributes?.sort()) !==\n      JSON.stringify(desiredFilterableAttributes)\n    ) {\n      console.log(\n        `[meilisearch] Updating desired filterable attributes to ${desiredFilterableAttributes} from ${settings.filterableAttributes}`,\n      );\n      const taskId = await index.updateFilterableAttributes(\n        desiredFilterableAttributes,\n      );\n      await this.client!.waitForTask(taskId.taskUid);\n    }\n\n    if (\n      JSON.stringify(settings.sortableAttributes?.sort()) !==\n      JSON.stringify(desiredSortableAttributes)\n    ) {\n      console.log(\n        `[meilisearch] Updating desired sortable attributes to ${desiredSortableAttributes} from ${settings.sortableAttributes}`,\n      );\n      const taskId = await index.updateSortableAttributes(\n        desiredSortableAttributes,\n      );\n      await this.client!.waitForTask(taskId.taskUid);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/plugins/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@karakeep/tsconfig/node.json\",\n  \"include\": [\"**/*.ts\"],\n  \"exclude\": [\"node_modules\"],\n  \"compilerOptions\": {\n    \"tsBuildInfoFile\": \"node_modules/.cache/tsbuildinfo.json\"\n  }\n}\n"
  },
  {
    "path": "packages/plugins/vitest.config.ts",
    "content": "/// <reference types=\"vitest\" />\n\nimport tsconfigPaths from \"vite-tsconfig-paths\";\nimport { defineConfig } from \"vitest/config\";\n\nexport default defineConfig({\n  plugins: [tsconfigPaths()],\n  test: {\n    globalSetup: [\n      \"./queue-restate/src/tests/setup/startContainers.ts\",\n      \"./ratelimit-redis/src/tests/setup/startContainers.ts\",\n    ],\n    teardownTimeout: 30000,\n    include: [\"**/src/tests/**/*.test.ts\"],\n    testTimeout: 60000,\n  },\n});\n"
  },
  {
    "path": "packages/sdk/.gitignore",
    "content": "dist\n"
  },
  {
    "path": "packages/sdk/.npmignore",
    "content": ".turbo/**\nsrc/**\nvite.config.mts\ntsconfig.json\n"
  },
  {
    "path": "packages/sdk/.oxlintrc.json",
    "content": "{\n  \"$schema\": \"../../node_modules/oxlint/configuration_schema.json\",\n  \"extends\": [\n    \"../../tooling/oxlint/oxlint-base.json\"\n  ],\n  \"env\": {\n    \"builtin\": true,\n    \"commonjs\": true\n  },\n  \"ignorePatterns\": [\n    \"**/*.config.js\",\n    \"**/*.config.cjs\",\n    \"**/.eslintrc.cjs\",\n    \"**/.next\",\n    \"**/dist\",\n    \"**/build\",\n    \"**/pnpm-lock.yaml\",\n    \"src/karakeep-api.d.ts\"\n  ]\n}\n"
  },
  {
    "path": "packages/sdk/README.md",
    "content": "# Karakeep SDK\n\nThis package contains the official typescript SDK for the karakeep API.\n\n## Installation\n\n```\nnpm install @karakeep/sdk\n```\n\n## Usage\n\n```typescript\nimport { createKarakeepClient } from \"@karakeep/sdk\";\n\n// Create a client\nconst apiKey = \"my-super-secret-key\";\nconst addr = `https://karakeep.mydomain.com`;\nconst client = createKarakeepClient({\n  baseUrl: `${addr}/api/v1/`,\n  headers: {\n    \"Content-Type\": \"application/json\",\n    authorization: `Bearer ${apiKey}`,\n  },\n});\n\n// Create a bookmark\nconst {\n  data: createdBookmark,\n  response: createResponse,\n  error: createError,\n} = await client.POST(\"/bookmarks\", {\n  body: {\n    type: \"text\",\n    title: \"Search Test 1\",\n    text: \"This is a test bookmark for search\",\n  },\n});\n\nconsole.log(createResponse.status, createdBookmark, createError);\n\n// Search for bookmarks\nconst {\n  data: searchResults,\n  response: searchResponse,\n  error: searchError,\n} = await client.GET(\"/bookmarks/search\", {\n  params: {\n    query: {\n      q: \"test bookmark\",\n    },\n  },\n});\nconsole.log(searchResponse.status, searchResults, searchError);\n```\n\n## Docs\n\nAPI docs can be found [here](https://docs.karakeep.app/api).\n\n## Versioning\n\n- This package follows the minor version of the karakeep server. So new APIs introduced in Karakeep version `0.21.0` will be available in this package starting from version `0.21.0`.\n- Karakeep strives to maintain backward compatibility in its APIs, so older versions of this package should continue working with newer karakeep server versions.\n"
  },
  {
    "path": "packages/sdk/package.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/package.json\",\n  \"name\": \"@karakeep/sdk\",\n  \"version\": \"0.31.0\",\n  \"description\": \"Typescript SDK for Karakeep\",\n  \"license\": \"GNU Affero General Public License version 3\",\n  \"keywords\": [\n    \"hoarder\",\n    \"karakeep\",\n    \"sdk\"\n  ],\n  \"main\": \"./src/index.ts\",\n  \"type\": \"module\",\n  \"publishConfig\": {\n    \"main\": \"./dist/index.js\",\n    \"exports\": \"./dist/index.js\",\n    \"module\": \"./dist/index.mjs\",\n    \"types\": \"./dist/index.d.ts\"\n  },\n  \"devDependencies\": {\n    \"@karakeep/tsconfig\": \"workspace:^0.1.0\",\n    \"@tsconfig/node22\": \"^22.0.0\",\n    \"openapi-typescript\": \"^7.6.1\",\n    \"tsx\": \"^4.8.1\",\n    \"vite\": \"^7.0.6\",\n    \"vite-plugin-dts\": \"^4.4.0\"\n  },\n  \"scripts\": {\n    \"build\": \"vite build\",\n    \"run\": \"tsx src/index.ts\",\n    \"lint\": \"oxlint .\",\n    \"lint:fix\": \"oxlint . --fix\",\n    \"format\": \"oxfmt --check .\",\n    \"format:fix\": \"oxfmt .\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/karakeep-app/karakeep.git\",\n    \"directory\": \"packages/sdk\"\n  },\n  \"dependencies\": {\n    \"openapi-fetch\": \"^0.13.3\"\n  }\n}\n"
  },
  {
    "path": "packages/sdk/src/index.ts",
    "content": "import createClient from \"openapi-fetch\";\n\nimport type { components, paths } from \"./karakeep-api.d.ts\";\n\n/**\n * @deprecated Use createKarakeepClient instead.\n */\nexport const createHoarderClient = createClient<paths>;\n\nexport const createKarakeepClient = createClient<paths>;\n\nexport type KarakeepAPISchemas = components[\"schemas\"];\n"
  },
  {
    "path": "packages/sdk/src/karakeep-api.d.ts",
    "content": "/**\n * This file was auto-generated by openapi-typescript.\n * Do not make direct changes to the file.\n */\n\nexport interface paths {\n  \"/bookmarks\": {\n    parameters: {\n      query?: never;\n      header?: never;\n      path?: never;\n      cookie?: never;\n    };\n    /**\n     * Get all bookmarks\n     * @description Get all bookmarks\n     */\n    get: {\n      parameters: {\n        query?: {\n          archived?: boolean;\n          favourited?: boolean;\n          sortOrder?: \"asc\" | \"desc\";\n          limit?: number;\n          cursor?: components[\"schemas\"][\"Cursor\"];\n          /** @description If set to true, bookmark's content will be included in the response. Note, this content can be large for some bookmarks. */\n          includeContent?: boolean;\n        };\n        header?: never;\n        path?: never;\n        cookie?: never;\n      };\n      requestBody?: never;\n      responses: {\n        /** @description Object with all bookmarks data. */\n        200: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": components[\"schemas\"][\"PaginatedBookmarks\"];\n          };\n        };\n      };\n    };\n    put?: never;\n    /**\n     * Create a new bookmark\n     * @description Create a new bookmark\n     */\n    post: {\n      parameters: {\n        query?: never;\n        header?: never;\n        path?: never;\n        cookie?: never;\n      };\n      /** @description The bookmark to create */\n      requestBody?: {\n        content: {\n          \"application/json\": {\n            title?: string | null;\n            archived?: boolean;\n            favourited?: boolean;\n            note?: string;\n            summary?: string;\n            createdAt?: string | null;\n            /** @enum {string} */\n            crawlPriority?: \"low\" | \"normal\";\n            importSessionId?: string;\n            /** @enum {string} */\n            source?:\n              | \"api\"\n              | \"web\"\n              | \"cli\"\n              | \"mobile\"\n              | \"extension\"\n              | \"singlefile\"\n              | \"rss\"\n              | \"import\";\n          } & (\n            | {\n                /** @enum {string} */\n                type: \"link\";\n                /** Format: uri */\n                url: string;\n                precrawledArchiveId?: string;\n              }\n            | {\n                /** @enum {string} */\n                type: \"text\";\n                text: string;\n                sourceUrl?: string;\n              }\n            | {\n                /** @enum {string} */\n                type: \"asset\";\n                /** @enum {string} */\n                assetType: \"image\" | \"pdf\";\n                assetId: string;\n                fileName?: string;\n                sourceUrl?: string;\n              }\n          );\n        };\n      };\n      responses: {\n        /** @description The bookmark already exists */\n        200: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": components[\"schemas\"][\"Bookmark\"];\n          };\n        };\n        /** @description The bookmark got created */\n        201: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": components[\"schemas\"][\"Bookmark\"];\n          };\n        };\n        /** @description Bad request */\n        400: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              code: string;\n              message: string;\n            };\n          };\n        };\n      };\n    };\n    delete?: never;\n    options?: never;\n    head?: never;\n    patch?: never;\n    trace?: never;\n  };\n  \"/bookmarks/search\": {\n    parameters: {\n      query?: never;\n      header?: never;\n      path?: never;\n      cookie?: never;\n    };\n    /**\n     * Search bookmarks\n     * @description Search bookmarks\n     */\n    get: {\n      parameters: {\n        query: {\n          q: string;\n          sortOrder?: \"asc\" | \"desc\" | \"relevance\";\n          limit?: number;\n          cursor?: components[\"schemas\"][\"Cursor\"];\n          /** @description If set to true, bookmark's content will be included in the response. Note, this content can be large for some bookmarks. */\n          includeContent?: boolean;\n        };\n        header?: never;\n        path?: never;\n        cookie?: never;\n      };\n      requestBody?: never;\n      responses: {\n        /** @description Object with the search results. */\n        200: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": components[\"schemas\"][\"PaginatedBookmarks\"];\n          };\n        };\n      };\n    };\n    put?: never;\n    post?: never;\n    delete?: never;\n    options?: never;\n    head?: never;\n    patch?: never;\n    trace?: never;\n  };\n  \"/bookmarks/check-url\": {\n    parameters: {\n      query?: never;\n      header?: never;\n      path?: never;\n      cookie?: never;\n    };\n    /**\n     * Check if a URL exists in bookmarks\n     * @description Check if a URL is already bookmarked. Uses substring matching to find candidates, then normalizes URLs (ignoring hash fragments and trailing slashes) for exact comparison.\n     */\n    get: {\n      parameters: {\n        query: {\n          url: string;\n        };\n        header?: never;\n        path?: never;\n        cookie?: never;\n      };\n      requestBody?: never;\n      responses: {\n        /** @description Object indicating whether the URL is bookmarked. bookmarkId is null if not found. */\n        200: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              bookmarkId: string | null;\n            };\n          };\n        };\n      };\n    };\n    put?: never;\n    post?: never;\n    delete?: never;\n    options?: never;\n    head?: never;\n    patch?: never;\n    trace?: never;\n  };\n  \"/bookmarks/{bookmarkId}\": {\n    parameters: {\n      query?: never;\n      header?: never;\n      path?: never;\n      cookie?: never;\n    };\n    /**\n     * Get a single bookmark\n     * @description Get bookmark by its id\n     */\n    get: {\n      parameters: {\n        query?: {\n          /** @description If set to true, bookmark's content will be included in the response. Note, this content can be large for some bookmarks. */\n          includeContent?: boolean;\n        };\n        header?: never;\n        path: {\n          bookmarkId: components[\"parameters\"][\"BookmarkId\"];\n        };\n        cookie?: never;\n      };\n      requestBody?: never;\n      responses: {\n        /** @description Object with bookmark data. */\n        200: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": components[\"schemas\"][\"Bookmark\"];\n          };\n        };\n        /** @description Bookmark not found */\n        404: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              code: string;\n              message: string;\n            };\n          };\n        };\n      };\n    };\n    put?: never;\n    post?: never;\n    /**\n     * Delete a bookmark\n     * @description Delete bookmark by its id\n     */\n    delete: {\n      parameters: {\n        query?: never;\n        header?: never;\n        path: {\n          bookmarkId: components[\"parameters\"][\"BookmarkId\"];\n        };\n        cookie?: never;\n      };\n      requestBody?: never;\n      responses: {\n        /** @description No content - the bookmark was deleted */\n        204: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content?: never;\n        };\n        /** @description Bookmark not found */\n        404: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              code: string;\n              message: string;\n            };\n          };\n        };\n      };\n    };\n    options?: never;\n    head?: never;\n    /**\n     * Update a bookmark\n     * @description Update bookmark by its id\n     */\n    patch: {\n      parameters: {\n        query?: never;\n        header?: never;\n        path: {\n          bookmarkId: components[\"parameters\"][\"BookmarkId\"];\n        };\n        cookie?: never;\n      };\n      /** @description The data to update. Only the fields you want to update need to be provided. */\n      requestBody?: {\n        content: {\n          \"application/json\": {\n            archived?: boolean;\n            favourited?: boolean;\n            summary?: string | null;\n            note?: string;\n            title?: string | null;\n            createdAt?: string | null;\n            /** Format: uri */\n            url?: string;\n            description?: string | null;\n            author?: string | null;\n            publisher?: string | null;\n            datePublished?: string | null;\n            dateModified?: string | null;\n            text?: string | null;\n            assetContent?: string | null;\n          };\n        };\n      };\n      responses: {\n        /** @description The updated bookmark */\n        200: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              id: string;\n              createdAt: string;\n              modifiedAt: string | null;\n              title?: string | null;\n              archived: boolean;\n              favourited: boolean;\n              /** @enum {string|null} */\n              taggingStatus: \"success\" | \"failure\" | \"pending\" | null;\n              /** @enum {string|null} */\n              summarizationStatus: \"success\" | \"failure\" | \"pending\" | null;\n              note?: string | null;\n              summary?: string | null;\n              /** @enum {string|null} */\n              source?:\n                | \"api\"\n                | \"web\"\n                | \"cli\"\n                | \"mobile\"\n                | \"extension\"\n                | \"singlefile\"\n                | \"rss\"\n                | \"import\"\n                | null;\n              userId: string;\n            };\n          };\n        };\n        /** @description Bookmark not found */\n        404: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              code: string;\n              message: string;\n            };\n          };\n        };\n      };\n    };\n    trace?: never;\n  };\n  \"/bookmarks/{bookmarkId}/summarize\": {\n    parameters: {\n      query?: never;\n      header?: never;\n      path?: never;\n      cookie?: never;\n    };\n    get?: never;\n    put?: never;\n    /**\n     * Summarize a bookmark\n     * @description Attaches a summary to the bookmark and returns the updated record.\n     */\n    post: {\n      parameters: {\n        query?: never;\n        header?: never;\n        path: {\n          bookmarkId: components[\"parameters\"][\"BookmarkId\"];\n        };\n        cookie?: never;\n      };\n      requestBody?: never;\n      responses: {\n        /** @description The updated bookmark with summary */\n        200: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              id: string;\n              createdAt: string;\n              modifiedAt: string | null;\n              title?: string | null;\n              archived: boolean;\n              favourited: boolean;\n              /** @enum {string|null} */\n              taggingStatus: \"success\" | \"failure\" | \"pending\" | null;\n              /** @enum {string|null} */\n              summarizationStatus: \"success\" | \"failure\" | \"pending\" | null;\n              note?: string | null;\n              summary?: string | null;\n              /** @enum {string|null} */\n              source?:\n                | \"api\"\n                | \"web\"\n                | \"cli\"\n                | \"mobile\"\n                | \"extension\"\n                | \"singlefile\"\n                | \"rss\"\n                | \"import\"\n                | null;\n              userId: string;\n            };\n          };\n        };\n        /** @description Bookmark not found */\n        404: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              code: string;\n              message: string;\n            };\n          };\n        };\n      };\n    };\n    delete?: never;\n    options?: never;\n    head?: never;\n    patch?: never;\n    trace?: never;\n  };\n  \"/bookmarks/{bookmarkId}/tags\": {\n    parameters: {\n      query?: never;\n      header?: never;\n      path?: never;\n      cookie?: never;\n    };\n    get?: never;\n    put?: never;\n    /**\n     * Attach tags to a bookmark\n     * @description Attach tags to a bookmark\n     */\n    post: {\n      parameters: {\n        query?: never;\n        header?: never;\n        path: {\n          bookmarkId: components[\"parameters\"][\"BookmarkId\"];\n        };\n        cookie?: never;\n      };\n      /** @description The tags to attach. */\n      requestBody?: {\n        content: {\n          \"application/json\": {\n            tags: {\n              tagId?: string;\n              tagName?: string;\n              /**\n               * @default human\n               * @enum {string}\n               */\n              attachedBy?: \"ai\" | \"human\";\n            }[];\n          };\n        };\n      };\n      responses: {\n        /** @description The list of attached tag ids */\n        200: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              attached: components[\"schemas\"][\"TagId\"][];\n            };\n          };\n        };\n        /** @description Bookmark not found */\n        404: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              code: string;\n              message: string;\n            };\n          };\n        };\n      };\n    };\n    /**\n     * Detach tags from a bookmark\n     * @description Detach tags from a bookmark\n     */\n    delete: {\n      parameters: {\n        query?: never;\n        header?: never;\n        path: {\n          bookmarkId: components[\"parameters\"][\"BookmarkId\"];\n        };\n        cookie?: never;\n      };\n      /** @description The tags to detach. */\n      requestBody?: {\n        content: {\n          \"application/json\": {\n            tags: {\n              tagId?: string;\n              tagName?: string;\n              /**\n               * @default human\n               * @enum {string}\n               */\n              attachedBy?: \"ai\" | \"human\";\n            }[];\n          };\n        };\n      };\n      responses: {\n        /** @description The list of detached tag ids */\n        200: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              detached: components[\"schemas\"][\"TagId\"][];\n            };\n          };\n        };\n        /** @description Bookmark not found */\n        404: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              code: string;\n              message: string;\n            };\n          };\n        };\n      };\n    };\n    options?: never;\n    head?: never;\n    patch?: never;\n    trace?: never;\n  };\n  \"/bookmarks/{bookmarkId}/lists\": {\n    parameters: {\n      query?: never;\n      header?: never;\n      path?: never;\n      cookie?: never;\n    };\n    /**\n     * Get lists of a bookmark\n     * @description Get lists of a bookmark\n     */\n    get: {\n      parameters: {\n        query?: never;\n        header?: never;\n        path: {\n          bookmarkId: components[\"parameters\"][\"BookmarkId\"];\n        };\n        cookie?: never;\n      };\n      requestBody?: never;\n      responses: {\n        /** @description The list of highlights */\n        200: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              lists: components[\"schemas\"][\"List\"][];\n            };\n          };\n        };\n        /** @description Bookmark not found */\n        404: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              code: string;\n              message: string;\n            };\n          };\n        };\n      };\n    };\n    put?: never;\n    post?: never;\n    delete?: never;\n    options?: never;\n    head?: never;\n    patch?: never;\n    trace?: never;\n  };\n  \"/bookmarks/{bookmarkId}/highlights\": {\n    parameters: {\n      query?: never;\n      header?: never;\n      path?: never;\n      cookie?: never;\n    };\n    /**\n     * Get highlights of a bookmark\n     * @description Get highlights of a bookmark\n     */\n    get: {\n      parameters: {\n        query?: never;\n        header?: never;\n        path: {\n          bookmarkId: components[\"parameters\"][\"BookmarkId\"];\n        };\n        cookie?: never;\n      };\n      requestBody?: never;\n      responses: {\n        /** @description The list of highlights */\n        200: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              highlights: components[\"schemas\"][\"Highlight\"][];\n            };\n          };\n        };\n        /** @description Bookmark not found */\n        404: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              code: string;\n              message: string;\n            };\n          };\n        };\n      };\n    };\n    put?: never;\n    post?: never;\n    delete?: never;\n    options?: never;\n    head?: never;\n    patch?: never;\n    trace?: never;\n  };\n  \"/bookmarks/{bookmarkId}/assets\": {\n    parameters: {\n      query?: never;\n      header?: never;\n      path?: never;\n      cookie?: never;\n    };\n    get?: never;\n    put?: never;\n    /**\n     * Attach asset\n     * @description Attach a new asset to a bookmark\n     */\n    post: {\n      parameters: {\n        query?: never;\n        header?: never;\n        path: {\n          bookmarkId: components[\"parameters\"][\"BookmarkId\"];\n        };\n        cookie?: never;\n      };\n      /** @description The asset to attach */\n      requestBody?: {\n        content: {\n          \"application/json\": {\n            id: string;\n            /** @enum {string} */\n            assetType:\n              | \"linkHtmlContent\"\n              | \"screenshot\"\n              | \"pdf\"\n              | \"assetScreenshot\"\n              | \"bannerImage\"\n              | \"fullPageArchive\"\n              | \"video\"\n              | \"bookmarkAsset\"\n              | \"precrawledArchive\"\n              | \"userUploaded\"\n              | \"avatar\"\n              | \"unknown\";\n          };\n        };\n      };\n      responses: {\n        /** @description The attached asset */\n        201: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              id: string;\n              /** @enum {string} */\n              assetType:\n                | \"linkHtmlContent\"\n                | \"screenshot\"\n                | \"pdf\"\n                | \"assetScreenshot\"\n                | \"bannerImage\"\n                | \"fullPageArchive\"\n                | \"video\"\n                | \"bookmarkAsset\"\n                | \"precrawledArchive\"\n                | \"userUploaded\"\n                | \"avatar\"\n                | \"unknown\";\n              fileName?: string | null;\n            };\n          };\n        };\n        /** @description Bookmark not found */\n        404: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              code: string;\n              message: string;\n            };\n          };\n        };\n      };\n    };\n    delete?: never;\n    options?: never;\n    head?: never;\n    patch?: never;\n    trace?: never;\n  };\n  \"/bookmarks/{bookmarkId}/assets/{assetId}\": {\n    parameters: {\n      query?: never;\n      header?: never;\n      path?: never;\n      cookie?: never;\n    };\n    get?: never;\n    /**\n     * Replace asset\n     * @description Replace an existing asset with a new one\n     */\n    put: {\n      parameters: {\n        query?: never;\n        header?: never;\n        path: {\n          bookmarkId: components[\"parameters\"][\"BookmarkId\"];\n          assetId: components[\"parameters\"][\"AssetId\"];\n        };\n        cookie?: never;\n      };\n      /** @description The new asset to replace with */\n      requestBody?: {\n        content: {\n          \"application/json\": {\n            assetId: string;\n          };\n        };\n      };\n      responses: {\n        /** @description No content - asset was replaced successfully */\n        204: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content?: never;\n        };\n        /** @description Bookmark not found */\n        404: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              code: string;\n              message: string;\n            };\n          };\n        };\n      };\n    };\n    post?: never;\n    /**\n     * Detach asset\n     * @description Detach an asset from a bookmark\n     */\n    delete: {\n      parameters: {\n        query?: never;\n        header?: never;\n        path: {\n          bookmarkId: components[\"parameters\"][\"BookmarkId\"];\n          assetId: components[\"parameters\"][\"AssetId\"];\n        };\n        cookie?: never;\n      };\n      requestBody?: never;\n      responses: {\n        /** @description No content - asset was detached successfully */\n        204: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content?: never;\n        };\n        /** @description Bookmark not found */\n        404: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              code: string;\n              message: string;\n            };\n          };\n        };\n      };\n    };\n    options?: never;\n    head?: never;\n    patch?: never;\n    trace?: never;\n  };\n  \"/lists\": {\n    parameters: {\n      query?: never;\n      header?: never;\n      path?: never;\n      cookie?: never;\n    };\n    /**\n     * Get all lists\n     * @description Get all lists\n     */\n    get: {\n      parameters: {\n        query?: never;\n        header?: never;\n        path?: never;\n        cookie?: never;\n      };\n      requestBody?: never;\n      responses: {\n        /** @description Object with all lists data. */\n        200: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              lists: components[\"schemas\"][\"List\"][];\n            };\n          };\n        };\n      };\n    };\n    put?: never;\n    /**\n     * Create a new list\n     * @description Create a new list\n     */\n    post: {\n      parameters: {\n        query?: never;\n        header?: never;\n        path?: never;\n        cookie?: never;\n      };\n      /** @description The list to create */\n      requestBody?: {\n        content: {\n          \"application/json\": {\n            name: string;\n            description?: string;\n            icon: string;\n            /**\n             * @default manual\n             * @enum {string}\n             */\n            type?: \"manual\" | \"smart\";\n            query?: string;\n            parentId?: string | null;\n          };\n        };\n      };\n      responses: {\n        /** @description The created list */\n        201: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": components[\"schemas\"][\"List\"];\n          };\n        };\n        /** @description Bad request */\n        400: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              code: string;\n              message: string;\n            };\n          };\n        };\n      };\n    };\n    delete?: never;\n    options?: never;\n    head?: never;\n    patch?: never;\n    trace?: never;\n  };\n  \"/lists/{listId}\": {\n    parameters: {\n      query?: never;\n      header?: never;\n      path?: never;\n      cookie?: never;\n    };\n    /**\n     * Get a single list\n     * @description Get list by its id\n     */\n    get: {\n      parameters: {\n        query?: never;\n        header?: never;\n        path: {\n          listId: components[\"parameters\"][\"ListId\"];\n        };\n        cookie?: never;\n      };\n      requestBody?: never;\n      responses: {\n        /** @description Object with list data. */\n        200: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": components[\"schemas\"][\"List\"];\n          };\n        };\n        /** @description List not found */\n        404: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              code: string;\n              message: string;\n            };\n          };\n        };\n      };\n    };\n    put?: never;\n    post?: never;\n    /**\n     * Delete a list\n     * @description Delete list by its id\n     */\n    delete: {\n      parameters: {\n        query?: never;\n        header?: never;\n        path: {\n          listId: components[\"parameters\"][\"ListId\"];\n        };\n        cookie?: never;\n      };\n      requestBody?: never;\n      responses: {\n        /** @description No content - the bookmark was deleted */\n        204: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content?: never;\n        };\n        /** @description List not found */\n        404: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              code: string;\n              message: string;\n            };\n          };\n        };\n      };\n    };\n    options?: never;\n    head?: never;\n    /**\n     * Update a list\n     * @description Update list by its id\n     */\n    patch: {\n      parameters: {\n        query?: never;\n        header?: never;\n        path: {\n          listId: components[\"parameters\"][\"ListId\"];\n        };\n        cookie?: never;\n      };\n      /** @description The data to update. Only the fields you want to update need to be provided. */\n      requestBody?: {\n        content: {\n          \"application/json\": {\n            name?: string;\n            description?: string | null;\n            icon?: string;\n            parentId?: string | null;\n            query?: string;\n            public?: boolean;\n          };\n        };\n      };\n      responses: {\n        /** @description The updated list */\n        200: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": components[\"schemas\"][\"List\"];\n          };\n        };\n        /** @description List not found */\n        404: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              code: string;\n              message: string;\n            };\n          };\n        };\n      };\n    };\n    trace?: never;\n  };\n  \"/lists/{listId}/bookmarks\": {\n    parameters: {\n      query?: never;\n      header?: never;\n      path?: never;\n      cookie?: never;\n    };\n    /**\n     * Get bookmarks in the list\n     * @description Get bookmarks in the list\n     */\n    get: {\n      parameters: {\n        query?: {\n          sortOrder?: \"asc\" | \"desc\";\n          limit?: number;\n          cursor?: components[\"schemas\"][\"Cursor\"];\n          /** @description If set to true, bookmark's content will be included in the response. Note, this content can be large for some bookmarks. */\n          includeContent?: boolean;\n        };\n        header?: never;\n        path: {\n          listId: components[\"parameters\"][\"ListId\"];\n        };\n        cookie?: never;\n      };\n      requestBody?: never;\n      responses: {\n        /** @description Object with list data. */\n        200: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": components[\"schemas\"][\"PaginatedBookmarks\"];\n          };\n        };\n        /** @description List not found */\n        404: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              code: string;\n              message: string;\n            };\n          };\n        };\n      };\n    };\n    put?: never;\n    post?: never;\n    delete?: never;\n    options?: never;\n    head?: never;\n    patch?: never;\n    trace?: never;\n  };\n  \"/lists/{listId}/bookmarks/{bookmarkId}\": {\n    parameters: {\n      query?: never;\n      header?: never;\n      path?: never;\n      cookie?: never;\n    };\n    get?: never;\n    /**\n     * Add a bookmark to a list\n     * @description Add the bookmarks to a list\n     */\n    put: {\n      parameters: {\n        query?: never;\n        header?: never;\n        path: {\n          listId: components[\"parameters\"][\"ListId\"];\n          bookmarkId: components[\"parameters\"][\"BookmarkId\"];\n        };\n        cookie?: never;\n      };\n      requestBody?: never;\n      responses: {\n        /** @description No content - the bookmark was added */\n        204: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content?: never;\n        };\n        /** @description List or bookmark not found */\n        404: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              code: string;\n              message: string;\n            };\n          };\n        };\n      };\n    };\n    post?: never;\n    /**\n     * Remove a bookmark from a list\n     * @description Remove the bookmarks from a list\n     */\n    delete: {\n      parameters: {\n        query?: never;\n        header?: never;\n        path: {\n          listId: components[\"parameters\"][\"ListId\"];\n          bookmarkId: components[\"parameters\"][\"BookmarkId\"];\n        };\n        cookie?: never;\n      };\n      requestBody?: never;\n      responses: {\n        /** @description No content - the bookmark was added */\n        204: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content?: never;\n        };\n        /** @description Bookmark already not in list */\n        400: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              code: string;\n              message: string;\n            };\n          };\n        };\n        /** @description List or bookmark not found */\n        404: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              code: string;\n              message: string;\n            };\n          };\n        };\n      };\n    };\n    options?: never;\n    head?: never;\n    patch?: never;\n    trace?: never;\n  };\n  \"/tags\": {\n    parameters: {\n      query?: never;\n      header?: never;\n      path?: never;\n      cookie?: never;\n    };\n    /**\n     * Get all tags\n     * @description Get all tags\n     */\n    get: {\n      parameters: {\n        query?: {\n          nameContains?: string;\n          sort?: \"name\" | \"usage\" | \"relevance\";\n          attachedBy?: \"ai\" | \"human\" | \"none\";\n          cursor?: string;\n          limit?: number | null;\n        };\n        header?: never;\n        path?: never;\n        cookie?: never;\n      };\n      requestBody?: never;\n      responses: {\n        /** @description Object with all tags data. */\n        200: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              tags: components[\"schemas\"][\"Tag\"][];\n              nextCursor: string | null;\n            };\n          };\n        };\n      };\n    };\n    put?: never;\n    /**\n     * Create a new tag\n     * @description Create a new tag\n     */\n    post: {\n      parameters: {\n        query?: never;\n        header?: never;\n        path?: never;\n        cookie?: never;\n      };\n      /** @description The data to create the tag with. */\n      requestBody?: {\n        content: {\n          \"application/json\": {\n            name: string;\n          };\n        };\n      };\n      responses: {\n        /** @description The created tag */\n        201: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              id: string;\n              name: string;\n            };\n          };\n        };\n      };\n    };\n    delete?: never;\n    options?: never;\n    head?: never;\n    patch?: never;\n    trace?: never;\n  };\n  \"/tags/{tagId}\": {\n    parameters: {\n      query?: never;\n      header?: never;\n      path?: never;\n      cookie?: never;\n    };\n    /**\n     * Get a single tag\n     * @description Get tag by its id\n     */\n    get: {\n      parameters: {\n        query?: never;\n        header?: never;\n        path: {\n          tagId: components[\"parameters\"][\"TagId\"];\n        };\n        cookie?: never;\n      };\n      requestBody?: never;\n      responses: {\n        /** @description Object with list data. */\n        200: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": components[\"schemas\"][\"Tag\"];\n          };\n        };\n        /** @description Tag not found */\n        404: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              code: string;\n              message: string;\n            };\n          };\n        };\n      };\n    };\n    put?: never;\n    post?: never;\n    /**\n     * Delete a tag\n     * @description Delete tag by its id\n     */\n    delete: {\n      parameters: {\n        query?: never;\n        header?: never;\n        path: {\n          tagId: components[\"parameters\"][\"TagId\"];\n        };\n        cookie?: never;\n      };\n      requestBody?: never;\n      responses: {\n        /** @description No content - the bookmark was deleted */\n        204: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content?: never;\n        };\n        /** @description Tag not found */\n        404: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              code: string;\n              message: string;\n            };\n          };\n        };\n      };\n    };\n    options?: never;\n    head?: never;\n    /**\n     * Update a tag\n     * @description Update tag by its id\n     */\n    patch: {\n      parameters: {\n        query?: never;\n        header?: never;\n        path: {\n          tagId: components[\"parameters\"][\"TagId\"];\n        };\n        cookie?: never;\n      };\n      /** @description The data to update. Only the fields you want to update need to be provided. */\n      requestBody?: {\n        content: {\n          \"application/json\": {\n            name?: string;\n          };\n        };\n      };\n      responses: {\n        /** @description The updated tag */\n        200: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              id: string;\n              name: string;\n            };\n          };\n        };\n        /** @description Tag not found */\n        404: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              code: string;\n              message: string;\n            };\n          };\n        };\n      };\n    };\n    trace?: never;\n  };\n  \"/tags/{tagId}/bookmarks\": {\n    parameters: {\n      query?: never;\n      header?: never;\n      path?: never;\n      cookie?: never;\n    };\n    /**\n     * Get bookmarks with the tag\n     * @description Get bookmarks with the tag\n     */\n    get: {\n      parameters: {\n        query?: {\n          sortOrder?: \"asc\" | \"desc\";\n          limit?: number;\n          cursor?: components[\"schemas\"][\"Cursor\"];\n          /** @description If set to true, bookmark's content will be included in the response. Note, this content can be large for some bookmarks. */\n          includeContent?: boolean;\n        };\n        header?: never;\n        path: {\n          tagId: components[\"parameters\"][\"TagId\"];\n        };\n        cookie?: never;\n      };\n      requestBody?: never;\n      responses: {\n        /** @description Object with list data. */\n        200: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": components[\"schemas\"][\"PaginatedBookmarks\"];\n          };\n        };\n        /** @description Tag not found */\n        404: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              code: string;\n              message: string;\n            };\n          };\n        };\n      };\n    };\n    put?: never;\n    post?: never;\n    delete?: never;\n    options?: never;\n    head?: never;\n    patch?: never;\n    trace?: never;\n  };\n  \"/highlights\": {\n    parameters: {\n      query?: never;\n      header?: never;\n      path?: never;\n      cookie?: never;\n    };\n    /**\n     * Get all highlights\n     * @description Get all highlights\n     */\n    get: {\n      parameters: {\n        query?: {\n          limit?: number;\n          cursor?: components[\"schemas\"][\"Cursor\"];\n        };\n        header?: never;\n        path?: never;\n        cookie?: never;\n      };\n      requestBody?: never;\n      responses: {\n        /** @description Object with all highlights data. */\n        200: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": components[\"schemas\"][\"PaginatedHighlights\"];\n          };\n        };\n      };\n    };\n    put?: never;\n    /**\n     * Create a new highlight\n     * @description Create a new highlight\n     */\n    post: {\n      parameters: {\n        query?: never;\n        header?: never;\n        path?: never;\n        cookie?: never;\n      };\n      /** @description The highlight to create */\n      requestBody?: {\n        content: {\n          \"application/json\": {\n            bookmarkId: string;\n            startOffset: number;\n            endOffset: number;\n            /**\n             * @default yellow\n             * @enum {string}\n             */\n            color?: \"yellow\" | \"red\" | \"green\" | \"blue\";\n            text: string | null;\n            note: string | null;\n          };\n        };\n      };\n      responses: {\n        /** @description The created highlight */\n        201: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": components[\"schemas\"][\"Highlight\"];\n          };\n        };\n        /** @description Bad highlight request */\n        400: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              code: string;\n              message: string;\n            };\n          };\n        };\n        /** @description Bookmark not found */\n        404: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              code: string;\n              message: string;\n            };\n          };\n        };\n      };\n    };\n    delete?: never;\n    options?: never;\n    head?: never;\n    patch?: never;\n    trace?: never;\n  };\n  \"/highlights/{highlightId}\": {\n    parameters: {\n      query?: never;\n      header?: never;\n      path?: never;\n      cookie?: never;\n    };\n    /**\n     * Get a single highlight\n     * @description Get highlight by its id\n     */\n    get: {\n      parameters: {\n        query?: never;\n        header?: never;\n        path: {\n          highlightId: components[\"parameters\"][\"HighlightId\"];\n        };\n        cookie?: never;\n      };\n      requestBody?: never;\n      responses: {\n        /** @description Object with highlight data. */\n        200: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": components[\"schemas\"][\"Highlight\"];\n          };\n        };\n        /** @description Highlight not found */\n        404: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              code: string;\n              message: string;\n            };\n          };\n        };\n      };\n    };\n    put?: never;\n    post?: never;\n    /**\n     * Delete a highlight\n     * @description Delete highlight by its id\n     */\n    delete: {\n      parameters: {\n        query?: never;\n        header?: never;\n        path: {\n          highlightId: components[\"parameters\"][\"HighlightId\"];\n        };\n        cookie?: never;\n      };\n      requestBody?: never;\n      responses: {\n        /** @description The deleted highlight */\n        200: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": components[\"schemas\"][\"Highlight\"];\n          };\n        };\n        /** @description Highlight not found */\n        404: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              code: string;\n              message: string;\n            };\n          };\n        };\n      };\n    };\n    options?: never;\n    head?: never;\n    /**\n     * Update a highlight\n     * @description Update highlight by its id\n     */\n    patch: {\n      parameters: {\n        query?: never;\n        header?: never;\n        path: {\n          highlightId: components[\"parameters\"][\"HighlightId\"];\n        };\n        cookie?: never;\n      };\n      /** @description The data to update. Only the fields you want to update need to be provided. */\n      requestBody?: {\n        content: {\n          \"application/json\": {\n            /** @enum {string} */\n            color?: \"yellow\" | \"red\" | \"green\" | \"blue\";\n            note?: string | null;\n          };\n        };\n      };\n      responses: {\n        /** @description The updated highlight */\n        200: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": components[\"schemas\"][\"Highlight\"];\n          };\n        };\n        /** @description Highlight not found */\n        404: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              code: string;\n              message: string;\n            };\n          };\n        };\n      };\n    };\n    trace?: never;\n  };\n  \"/users/me\": {\n    parameters: {\n      query?: never;\n      header?: never;\n      path?: never;\n      cookie?: never;\n    };\n    /**\n     * Get current user info\n     * @description Returns info about the current user\n     */\n    get: {\n      parameters: {\n        query?: never;\n        header?: never;\n        path?: never;\n        cookie?: never;\n      };\n      requestBody?: never;\n      responses: {\n        /** @description Object with user data. */\n        200: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              id: string;\n              name?: string | null;\n              email?: string | null;\n              image?: string | null;\n              localUser: boolean;\n            };\n          };\n        };\n      };\n    };\n    put?: never;\n    post?: never;\n    delete?: never;\n    options?: never;\n    head?: never;\n    patch?: never;\n    trace?: never;\n  };\n  \"/users/me/stats\": {\n    parameters: {\n      query?: never;\n      header?: never;\n      path?: never;\n      cookie?: never;\n    };\n    /**\n     * Get current user stats\n     * @description Returns stats about the current user\n     */\n    get: {\n      parameters: {\n        query?: never;\n        header?: never;\n        path?: never;\n        cookie?: never;\n      };\n      requestBody?: never;\n      responses: {\n        /** @description Object with user stats. */\n        200: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              numBookmarks: number;\n              numFavorites: number;\n              numArchived: number;\n              numTags: number;\n              numLists: number;\n              numHighlights: number;\n              bookmarksByType: {\n                link: number;\n                text: number;\n                asset: number;\n              };\n              topDomains: {\n                domain: string;\n                count: number;\n              }[];\n              totalAssetSize: number;\n              assetsByType: {\n                type: string;\n                count: number;\n                totalSize: number;\n              }[];\n              bookmarkingActivity: {\n                thisWeek: number;\n                thisMonth: number;\n                thisYear: number;\n                byHour: {\n                  hour: number;\n                  count: number;\n                }[];\n                byDayOfWeek: {\n                  day: number;\n                  count: number;\n                }[];\n              };\n              tagUsage: {\n                name: string;\n                count: number;\n              }[];\n              bookmarksBySource: {\n                /** @enum {string|null} */\n                source:\n                  | \"api\"\n                  | \"web\"\n                  | \"cli\"\n                  | \"mobile\"\n                  | \"extension\"\n                  | \"singlefile\"\n                  | \"rss\"\n                  | \"import\"\n                  | null;\n                count: number;\n              }[];\n            };\n          };\n        };\n      };\n    };\n    put?: never;\n    post?: never;\n    delete?: never;\n    options?: never;\n    head?: never;\n    patch?: never;\n    trace?: never;\n  };\n  \"/assets\": {\n    parameters: {\n      query?: never;\n      header?: never;\n      path?: never;\n      cookie?: never;\n    };\n    get?: never;\n    put?: never;\n    /**\n     * Upload a new asset\n     * @description Upload a new asset\n     */\n    post: {\n      parameters: {\n        query?: never;\n        header?: never;\n        path?: never;\n        cookie?: never;\n      };\n      /** @description The data to create the asset with. */\n      requestBody?: {\n        content: {\n          \"multipart/form-data\": {\n            file: components[\"schemas\"][\"File to be uploaded\"];\n          };\n        };\n      };\n      responses: {\n        /** @description Details about the created asset */\n        200: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": components[\"schemas\"][\"Asset\"];\n          };\n        };\n      };\n    };\n    delete?: never;\n    options?: never;\n    head?: never;\n    patch?: never;\n    trace?: never;\n  };\n  \"/assets/{assetId}\": {\n    parameters: {\n      query?: never;\n      header?: never;\n      path?: never;\n      cookie?: never;\n    };\n    /**\n     * Get a single asset\n     * @description Get asset by its id\n     */\n    get: {\n      parameters: {\n        query?: never;\n        header?: never;\n        path: {\n          assetId: components[\"parameters\"][\"AssetId\"];\n        };\n        cookie?: never;\n      };\n      requestBody?: never;\n      responses: {\n        /** @description Asset content. Content type is determined by the asset type. */\n        200: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content?: never;\n        };\n      };\n    };\n    put?: never;\n    post?: never;\n    delete?: never;\n    options?: never;\n    head?: never;\n    patch?: never;\n    trace?: never;\n  };\n  \"/admin/users/{userId}\": {\n    parameters: {\n      query?: never;\n      header?: never;\n      path?: never;\n      cookie?: never;\n    };\n    get?: never;\n    /**\n     * Update user\n     * @description Update a user's role, bookmark quota, or storage quota. Admin access required.\n     */\n    put: {\n      parameters: {\n        query?: never;\n        header?: never;\n        path: {\n          userId: string;\n        };\n        cookie?: never;\n      };\n      requestBody?: {\n        content: {\n          \"application/json\": {\n            /** @enum {string} */\n            role?: \"user\" | \"admin\";\n            bookmarkQuota?: number | null;\n            storageQuota?: number | null;\n            browserCrawlingEnabled?: boolean | null;\n          };\n        };\n      };\n      responses: {\n        /** @description User updated successfully */\n        200: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              success: boolean;\n            };\n          };\n        };\n        /** @description Bad request - Invalid input data or cannot update own user */\n        400: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              error: string;\n            };\n          };\n        };\n        /** @description Unauthorized - Authentication required */\n        401: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              error: string;\n            };\n          };\n        };\n        /** @description Forbidden - Admin access required */\n        403: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              error: string;\n            };\n          };\n        };\n        /** @description User not found */\n        404: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              error: string;\n            };\n          };\n        };\n      };\n    };\n    post?: never;\n    delete?: never;\n    options?: never;\n    head?: never;\n    patch?: never;\n    trace?: never;\n  };\n  \"/backups\": {\n    parameters: {\n      query?: never;\n      header?: never;\n      path?: never;\n      cookie?: never;\n    };\n    /**\n     * Get all backups\n     * @description Get all backups\n     */\n    get: {\n      parameters: {\n        query?: never;\n        header?: never;\n        path?: never;\n        cookie?: never;\n      };\n      requestBody?: never;\n      responses: {\n        /** @description Object with all backups data. */\n        200: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              backups: {\n                id: string;\n                userId: string;\n                assetId: string | null;\n                createdAt: string;\n                size: number;\n                bookmarkCount: number;\n                /** @enum {string} */\n                status: \"pending\" | \"success\" | \"failure\";\n                errorMessage?: string | null;\n              }[];\n            };\n          };\n        };\n      };\n    };\n    put?: never;\n    /**\n     * Trigger a new backup\n     * @description Trigger a new backup\n     */\n    post: {\n      parameters: {\n        query?: never;\n        header?: never;\n        path?: never;\n        cookie?: never;\n      };\n      requestBody?: never;\n      responses: {\n        /** @description Backup created successfully */\n        201: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              id: string;\n              userId: string;\n              assetId: string | null;\n              createdAt: string;\n              size: number;\n              bookmarkCount: number;\n              /** @enum {string} */\n              status: \"pending\" | \"success\" | \"failure\";\n              errorMessage?: string | null;\n            };\n          };\n        };\n      };\n    };\n    delete?: never;\n    options?: never;\n    head?: never;\n    patch?: never;\n    trace?: never;\n  };\n  \"/backups/{backupId}\": {\n    parameters: {\n      query?: never;\n      header?: never;\n      path?: never;\n      cookie?: never;\n    };\n    /**\n     * Get a single backup\n     * @description Get backup by its id\n     */\n    get: {\n      parameters: {\n        query?: never;\n        header?: never;\n        path: {\n          backupId: components[\"parameters\"][\"BackupId\"];\n        };\n        cookie?: never;\n      };\n      requestBody?: never;\n      responses: {\n        /** @description Object with backup data. */\n        200: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              id: string;\n              userId: string;\n              assetId: string | null;\n              createdAt: string;\n              size: number;\n              bookmarkCount: number;\n              /** @enum {string} */\n              status: \"pending\" | \"success\" | \"failure\";\n              errorMessage?: string | null;\n            };\n          };\n        };\n        /** @description Backup not found */\n        404: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              code: string;\n              message: string;\n            };\n          };\n        };\n      };\n    };\n    put?: never;\n    post?: never;\n    /**\n     * Delete a backup\n     * @description Delete backup by its id\n     */\n    delete: {\n      parameters: {\n        query?: never;\n        header?: never;\n        path: {\n          backupId: components[\"parameters\"][\"BackupId\"];\n        };\n        cookie?: never;\n      };\n      requestBody?: never;\n      responses: {\n        /** @description No content - the backup was deleted */\n        204: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content?: never;\n        };\n        /** @description Backup not found */\n        404: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              code: string;\n              message: string;\n            };\n          };\n        };\n      };\n    };\n    options?: never;\n    head?: never;\n    patch?: never;\n    trace?: never;\n  };\n  \"/backups/{backupId}/download\": {\n    parameters: {\n      query?: never;\n      header?: never;\n      path?: never;\n      cookie?: never;\n    };\n    /**\n     * Download a backup\n     * @description Download backup file\n     */\n    get: {\n      parameters: {\n        query?: never;\n        header?: never;\n        path: {\n          backupId: components[\"parameters\"][\"BackupId\"];\n        };\n        cookie?: never;\n      };\n      requestBody?: never;\n      responses: {\n        /** @description Backup file (zip archive) */\n        200: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/zip\": unknown;\n          };\n        };\n        /** @description Backup not found */\n        404: {\n          headers: {\n            [name: string]: unknown;\n          };\n          content: {\n            \"application/json\": {\n              code: string;\n              message: string;\n            };\n          };\n        };\n      };\n    };\n    put?: never;\n    post?: never;\n    delete?: never;\n    options?: never;\n    head?: never;\n    patch?: never;\n    trace?: never;\n  };\n}\nexport type webhooks = Record<string, never>;\nexport interface components {\n  schemas: {\n    /** @example ieidlxygmwj87oxz5hxttoc8 */\n    BookmarkId: string;\n    /** @example ieidlxygmwj87oxz5hxttoc8 */\n    ListId: string;\n    /** @example ieidlxygmwj87oxz5hxttoc8 */\n    TagId: string;\n    /** @example ieidlxygmwj87oxz5hxttoc8 */\n    HighlightId: string;\n    /** @example ieidlxygmwj87oxz5hxttoc8 */\n    AssetId: string;\n    /** @example ieidlxygmwj87oxz5hxttoc8 */\n    BackupId: string;\n    Bookmark: {\n      id: string;\n      createdAt: string;\n      modifiedAt: string | null;\n      title?: string | null;\n      archived: boolean;\n      favourited: boolean;\n      /** @enum {string|null} */\n      taggingStatus: \"success\" | \"failure\" | \"pending\" | null;\n      /** @enum {string|null} */\n      summarizationStatus: \"success\" | \"failure\" | \"pending\" | null;\n      note?: string | null;\n      summary?: string | null;\n      /** @enum {string|null} */\n      source?:\n        | \"api\"\n        | \"web\"\n        | \"cli\"\n        | \"mobile\"\n        | \"extension\"\n        | \"singlefile\"\n        | \"rss\"\n        | \"import\"\n        | null;\n      userId: string;\n      tags: {\n        id: string;\n        name: string;\n        /** @enum {string} */\n        attachedBy: \"ai\" | \"human\";\n      }[];\n      content:\n        | {\n            /** @enum {string} */\n            type: \"link\";\n            url: string;\n            title?: string | null;\n            description?: string | null;\n            imageUrl?: string | null;\n            imageAssetId?: string | null;\n            screenshotAssetId?: string | null;\n            pdfAssetId?: string | null;\n            fullPageArchiveAssetId?: string | null;\n            precrawledArchiveAssetId?: string | null;\n            videoAssetId?: string | null;\n            favicon?: string | null;\n            htmlContent?: string | null;\n            contentAssetId?: string | null;\n            crawledAt?: string | null;\n            /** @enum {string|null} */\n            crawlStatus?: \"success\" | \"failure\" | \"pending\" | null;\n            author?: string | null;\n            publisher?: string | null;\n            datePublished?: string | null;\n            dateModified?: string | null;\n          }\n        | {\n            /** @enum {string} */\n            type: \"text\";\n            text: string;\n            sourceUrl?: string | null;\n          }\n        | {\n            /** @enum {string} */\n            type: \"asset\";\n            /** @enum {string} */\n            assetType: \"image\" | \"pdf\";\n            assetId: string;\n            fileName?: string | null;\n            sourceUrl?: string | null;\n            size?: number | null;\n            content?: string | null;\n          }\n        | {\n            /** @enum {string} */\n            type: \"unknown\";\n          };\n      assets: {\n        id: string;\n        /** @enum {string} */\n        assetType:\n          | \"linkHtmlContent\"\n          | \"screenshot\"\n          | \"pdf\"\n          | \"assetScreenshot\"\n          | \"bannerImage\"\n          | \"fullPageArchive\"\n          | \"video\"\n          | \"bookmarkAsset\"\n          | \"precrawledArchive\"\n          | \"userUploaded\"\n          | \"avatar\"\n          | \"unknown\";\n        fileName?: string | null;\n      }[];\n    };\n    PaginatedBookmarks: {\n      bookmarks: components[\"schemas\"][\"Bookmark\"][];\n      nextCursor: string | null;\n    };\n    Cursor: string;\n    List: {\n      id: string;\n      name: string;\n      description?: string | null;\n      icon: string;\n      parentId: string | null;\n      /**\n       * @default manual\n       * @enum {string}\n       */\n      type: \"manual\" | \"smart\";\n      query?: string | null;\n      public: boolean;\n      hasCollaborators: boolean;\n      /** @enum {string} */\n      userRole: \"owner\" | \"editor\" | \"viewer\" | \"public\";\n    };\n    Highlight: {\n      bookmarkId: string;\n      startOffset: number;\n      endOffset: number;\n      /**\n       * @default yellow\n       * @enum {string}\n       */\n      color: \"yellow\" | \"red\" | \"green\" | \"blue\";\n      text: string | null;\n      note: string | null;\n      id: string;\n      userId: string;\n      createdAt: string;\n    };\n    Tag: {\n      id: string;\n      name: string;\n      numBookmarks: number;\n      numBookmarksByAttachedType: {\n        ai?: number;\n        human?: number;\n      };\n    };\n    PaginatedHighlights: {\n      highlights: components[\"schemas\"][\"Highlight\"][];\n      nextCursor: string | null;\n    };\n    Asset: {\n      assetId: string;\n      contentType: string;\n      size: number;\n      fileName: string;\n    };\n    \"File to be uploaded\": unknown;\n  };\n  responses: never;\n  parameters: {\n    BookmarkId: components[\"schemas\"][\"BookmarkId\"];\n    ListId: components[\"schemas\"][\"ListId\"];\n    TagId: components[\"schemas\"][\"TagId\"];\n    HighlightId: components[\"schemas\"][\"HighlightId\"];\n    AssetId: components[\"schemas\"][\"AssetId\"];\n    BackupId: components[\"schemas\"][\"BackupId\"];\n  };\n  requestBodies: never;\n  headers: never;\n  pathItems: never;\n}\nexport type $defs = Record<string, never>;\nexport type operations = Record<string, never>;\n"
  },
  {
    "path": "packages/sdk/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@karakeep/tsconfig/node.json\",\n  \"include\": [\"src\", \"vite.config.mts\"],\n  \"exclude\": [\"node_modules\", \"dist\"],\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"tsBuildInfoFile\": \"node_modules/.cache/tsbuildinfo.json\",\n    \"strictNullChecks\": true,\n    \"paths\": {\n      \"@/*\": [\"./src/*\"]\n    },\n    \"types\": [\"vite/client\"]\n  }\n}\n"
  },
  {
    "path": "packages/sdk/vite.config.mts",
    "content": "// This file is shamelessly copied from immich's CLI vite config\n// https://github.com/immich-app/immich/blob/main/cli/vite.config.ts\nimport { defineConfig } from \"vite\";\nimport dts from \"vite-plugin-dts\";\nimport tsconfigPaths from \"vite-tsconfig-paths\";\n\nexport default defineConfig({\n  build: {\n    lib: {\n      entry: \"src/index.ts\",\n      formats: [\"es\", \"cjs\"],\n      fileName: (format) => `index.${format === \"es\" ? \"mjs\" : \"js\"}`,\n    },\n    rollupOptions: {\n      external: [\"openapi-fetch\"],\n    },\n    ssr: true,\n    sourcemap: true,\n  },\n  plugins: [tsconfigPaths(), dts({ rollupTypes: true, copyDtsFiles: true })],\n});\n"
  },
  {
    "path": "packages/shared/.oxlintrc.json",
    "content": "{\n  \"$schema\": \"../../node_modules/oxlint/configuration_schema.json\",\n  \"extends\": [\n    \"../../tooling/oxlint/oxlint-base.json\"\n  ],\n  \"env\": {\n    \"builtin\": true,\n    \"commonjs\": true\n  },\n  \"ignorePatterns\": [\n    \"**/*.config.js\",\n    \"**/*.config.cjs\",\n    \"**/.eslintrc.cjs\",\n    \"**/.next\",\n    \"**/dist\",\n    \"**/build\",\n    \"**/pnpm-lock.yaml\"\n  ]\n}\n"
  },
  {
    "path": "packages/shared/assetdb.ts",
    "content": "import * as fs from \"fs\";\nimport * as path from \"path\";\nimport { Readable } from \"stream\";\nimport {\n  _Object,\n  DeleteObjectCommand,\n  DeleteObjectsCommand,\n  GetObjectCommand,\n  HeadObjectCommand,\n  ListObjectsV2Command,\n  PutObjectCommand,\n  S3Client,\n} from \"@aws-sdk/client-s3\";\nimport { Glob } from \"glob\";\nimport { z } from \"zod\";\n\nimport serverConfig from \"./config\";\nimport logger from \"./logger\";\nimport { QuotaApproved } from \"./storageQuota\";\n\nconst ROOT_PATH = serverConfig.assetsDir;\n\nexport const enum ASSET_TYPES {\n  IMAGE_GIF = \"image/gif\",\n  IMAGE_JPEG = \"image/jpeg\",\n  IMAGE_PNG = \"image/png\",\n  IMAGE_WEBP = \"image/webp\",\n  APPLICATION_PDF = \"application/pdf\",\n  APPLICATION_ZIP = \"application/zip\",\n  TEXT_HTML = \"text/html\",\n\n  VIDEO_MP4 = \"video/mp4\",\n  VIDEO_WEBM = \"video/webm\",\n  VIDEO_MKV = \"video/x-matroska\",\n}\n\nexport const VIDEO_ASSET_TYPES: Set<string> = new Set<string>([\n  ASSET_TYPES.VIDEO_MP4,\n  ASSET_TYPES.VIDEO_WEBM,\n  ASSET_TYPES.VIDEO_MKV,\n]);\n\nexport const IMAGE_ASSET_TYPES: Set<string> = new Set<string>([\n  ASSET_TYPES.IMAGE_GIF,\n  ASSET_TYPES.IMAGE_JPEG,\n  ASSET_TYPES.IMAGE_PNG,\n  ASSET_TYPES.IMAGE_WEBP,\n]);\n\n// The assets that we allow the users to upload\nexport const SUPPORTED_UPLOAD_ASSET_TYPES: Set<string> = new Set<string>([\n  ...IMAGE_ASSET_TYPES,\n  ...VIDEO_ASSET_TYPES,\n  ASSET_TYPES.TEXT_HTML,\n  ASSET_TYPES.APPLICATION_PDF,\n]);\n\n// The assets that we allow as a bookmark of type asset\nexport const SUPPORTED_BOOKMARK_ASSET_TYPES: Set<string> = new Set<string>([\n  ...IMAGE_ASSET_TYPES,\n  ASSET_TYPES.APPLICATION_PDF,\n]);\n\n// The assets that we support saving in the asset db\nexport const SUPPORTED_ASSET_TYPES: Set<string> = new Set<string>([\n  ...SUPPORTED_UPLOAD_ASSET_TYPES,\n  ASSET_TYPES.TEXT_HTML,\n  ASSET_TYPES.VIDEO_MP4,\n  ASSET_TYPES.APPLICATION_ZIP,\n]);\n\nexport const zAssetMetadataSchema = z.object({\n  contentType: z.string(),\n  fileName: z.string().nullish(),\n});\n\nexport type AssetMetadata = z.infer<typeof zAssetMetadataSchema>;\n\nexport interface AssetInfo {\n  userId: string;\n  assetId: string;\n  contentType: string;\n  fileName?: string | null;\n  size: number;\n}\n\nexport interface AssetStore {\n  saveAsset(params: {\n    userId: string;\n    assetId: string;\n    asset: Buffer;\n    metadata: AssetMetadata;\n  }): Promise<void>;\n\n  saveAssetFromFile(params: {\n    userId: string;\n    assetId: string;\n    assetPath: string;\n    metadata: AssetMetadata;\n  }): Promise<void>;\n\n  readAsset(params: {\n    userId: string;\n    assetId: string;\n  }): Promise<{ asset: Buffer; metadata: AssetMetadata }>;\n\n  createAssetReadStream(params: {\n    userId: string;\n    assetId: string;\n    start?: number;\n    end?: number;\n  }): Promise<NodeJS.ReadableStream>;\n\n  readAssetMetadata(params: {\n    userId: string;\n    assetId: string;\n  }): Promise<AssetMetadata>;\n\n  getAssetSize(params: { userId: string; assetId: string }): Promise<number>;\n\n  deleteAsset(params: { userId: string; assetId: string }): Promise<void>;\n\n  deleteUserAssets(params: { userId: string }): Promise<void>;\n\n  getAllAssets(): AsyncGenerator<AssetInfo>;\n}\n\nexport function newAssetId() {\n  return crypto.randomUUID();\n}\n\nclass LocalFileSystemAssetStore implements AssetStore {\n  private rootPath: string;\n\n  constructor(rootPath: string) {\n    this.rootPath = rootPath;\n  }\n\n  private getAssetDir(userId: string, assetId: string) {\n    return path.join(this.rootPath, userId, assetId);\n  }\n\n  private async isPathExists(filePath: string) {\n    return fs.promises\n      .access(filePath)\n      .then(() => true)\n      .catch(() => false);\n  }\n\n  async saveAsset({\n    userId,\n    assetId,\n    asset,\n    metadata,\n  }: {\n    userId: string;\n    assetId: string;\n    asset: Buffer;\n    metadata: AssetMetadata;\n  }) {\n    if (!SUPPORTED_ASSET_TYPES.has(metadata.contentType)) {\n      throw new Error(\"Unsupported asset type\");\n    }\n    const assetDir = this.getAssetDir(userId, assetId);\n    await fs.promises.mkdir(assetDir, { recursive: true });\n\n    await Promise.all([\n      fs.promises.writeFile(\n        path.join(assetDir, \"asset.bin\"),\n        Uint8Array.from(asset),\n      ),\n      fs.promises.writeFile(\n        path.join(assetDir, \"metadata.json\"),\n        JSON.stringify(metadata),\n      ),\n    ]);\n  }\n\n  async saveAssetFromFile({\n    userId,\n    assetId,\n    assetPath,\n    metadata,\n  }: {\n    userId: string;\n    assetId: string;\n    assetPath: string;\n    metadata: AssetMetadata;\n  }) {\n    if (!SUPPORTED_ASSET_TYPES.has(metadata.contentType)) {\n      throw new Error(\"Unsupported asset type\");\n    }\n    const assetDir = this.getAssetDir(userId, assetId);\n    await fs.promises.mkdir(assetDir, { recursive: true });\n\n    await Promise.all([\n      fs.promises.copyFile(assetPath, path.join(assetDir, \"asset.bin\")),\n      fs.promises.writeFile(\n        path.join(assetDir, \"metadata.json\"),\n        JSON.stringify(metadata),\n      ),\n    ]);\n    await fs.promises.rm(assetPath);\n  }\n\n  async readAsset({ userId, assetId }: { userId: string; assetId: string }) {\n    const assetDir = this.getAssetDir(userId, assetId);\n\n    const [asset, metadataStr] = await Promise.all([\n      fs.promises.readFile(path.join(assetDir, \"asset.bin\")),\n      fs.promises.readFile(path.join(assetDir, \"metadata.json\"), {\n        encoding: \"utf8\",\n      }),\n    ]);\n\n    const metadata = zAssetMetadataSchema.parse(JSON.parse(metadataStr));\n    return { asset, metadata };\n  }\n\n  async createAssetReadStream({\n    userId,\n    assetId,\n    start,\n    end,\n  }: {\n    userId: string;\n    assetId: string;\n    start?: number;\n    end?: number;\n  }) {\n    const assetDir = this.getAssetDir(userId, assetId);\n    const assetPath = path.join(assetDir, \"asset.bin\");\n    if (!(await this.isPathExists(assetPath))) {\n      throw new Error(`Asset ${assetId} not found`);\n    }\n\n    return fs.createReadStream(path.join(assetDir, \"asset.bin\"), {\n      start,\n      end,\n    });\n  }\n\n  async readAssetMetadata({\n    userId,\n    assetId,\n  }: {\n    userId: string;\n    assetId: string;\n  }) {\n    const assetDir = this.getAssetDir(userId, assetId);\n\n    const metadataStr = await fs.promises.readFile(\n      path.join(assetDir, \"metadata.json\"),\n      {\n        encoding: \"utf8\",\n      },\n    );\n\n    return zAssetMetadataSchema.parse(JSON.parse(metadataStr));\n  }\n\n  async getAssetSize({ userId, assetId }: { userId: string; assetId: string }) {\n    const assetDir = this.getAssetDir(userId, assetId);\n    const stat = await fs.promises.stat(path.join(assetDir, \"asset.bin\"));\n    return stat.size;\n  }\n\n  async deleteAsset({ userId, assetId }: { userId: string; assetId: string }) {\n    const assetDir = this.getAssetDir(userId, assetId);\n    if (!(await this.isPathExists(assetDir))) {\n      return;\n    }\n    await fs.promises.rm(assetDir, { recursive: true });\n  }\n\n  async deleteUserAssets({ userId }: { userId: string }) {\n    const userDir = path.join(this.rootPath, userId);\n    const dirExists = await this.isPathExists(userDir);\n    if (!dirExists) {\n      return;\n    }\n    await fs.promises.rm(userDir, { recursive: true });\n  }\n\n  async *getAllAssets() {\n    const g = new Glob(`/**/**/asset.bin`, {\n      maxDepth: 3,\n      root: this.rootPath,\n      cwd: this.rootPath,\n      absolute: false,\n    });\n    for await (const file of g) {\n      const [userId, assetId] = file.split(\"/\").slice(0, 2);\n      const [size, metadata] = await Promise.all([\n        this.getAssetSize({ userId, assetId }),\n        this.readAssetMetadata({ userId, assetId }),\n      ]);\n      yield {\n        userId,\n        assetId,\n        ...metadata,\n        size,\n      };\n    }\n  }\n}\n\nclass S3AssetStore implements AssetStore {\n  private s3Client: S3Client;\n  private bucketName: string;\n\n  constructor(s3Client: S3Client, bucketName: string) {\n    this.s3Client = s3Client;\n    this.bucketName = bucketName;\n  }\n\n  private getAssetKey(userId: string, assetId: string) {\n    return `${userId}/${assetId}`;\n  }\n\n  private metadataToS3Metadata(\n    metadata: AssetMetadata,\n  ): Record<string, string> {\n    return {\n      ...(metadata.fileName\n        ? { \"x-amz-meta-file-name\": metadata.fileName }\n        : {}),\n      \"x-amz-meta-content-type\": metadata.contentType,\n    };\n  }\n\n  private s3MetadataToMetadata(\n    s3Metadata: Record<string, string> | undefined,\n  ): AssetMetadata {\n    if (!s3Metadata) {\n      throw new Error(\"No metadata found in S3 object\");\n    }\n\n    return {\n      contentType: s3Metadata[\"x-amz-meta-content-type\"] || \"\",\n      fileName: s3Metadata[\"x-amz-meta-file-name\"] ?? null,\n    };\n  }\n\n  async saveAsset({\n    userId,\n    assetId,\n    asset,\n    metadata,\n  }: {\n    userId: string;\n    assetId: string;\n    asset: Buffer;\n    metadata: AssetMetadata;\n  }) {\n    if (!SUPPORTED_ASSET_TYPES.has(metadata.contentType)) {\n      throw new Error(\"Unsupported asset type\");\n    }\n\n    await this.s3Client.send(\n      new PutObjectCommand({\n        Bucket: this.bucketName,\n        Key: this.getAssetKey(userId, assetId),\n        Body: asset,\n        ContentType: metadata.contentType,\n        Metadata: this.metadataToS3Metadata(metadata),\n      }),\n    );\n  }\n\n  async saveAssetFromFile({\n    userId,\n    assetId,\n    assetPath,\n    metadata,\n  }: {\n    userId: string;\n    assetId: string;\n    assetPath: string;\n    metadata: AssetMetadata;\n  }) {\n    if (!SUPPORTED_ASSET_TYPES.has(metadata.contentType)) {\n      throw new Error(\"Unsupported asset type\");\n    }\n\n    const asset = fs.createReadStream(assetPath);\n    await this.s3Client.send(\n      new PutObjectCommand({\n        Bucket: this.bucketName,\n        Key: this.getAssetKey(userId, assetId),\n        Body: asset,\n        ContentType: metadata.contentType,\n        Metadata: this.metadataToS3Metadata(metadata),\n      }),\n    );\n    await fs.promises.rm(assetPath);\n  }\n\n  async readAsset({ userId, assetId }: { userId: string; assetId: string }) {\n    const response = await this.s3Client.send(\n      new GetObjectCommand({\n        Bucket: this.bucketName,\n        Key: this.getAssetKey(userId, assetId),\n      }),\n    );\n\n    if (!response.Body) {\n      throw new Error(\"Asset not found\");\n    }\n\n    const assetBuffer = await this.streamToBuffer(response.Body as Readable);\n    const metadata = this.s3MetadataToMetadata(response.Metadata);\n\n    return { asset: assetBuffer, metadata };\n  }\n\n  async createAssetReadStream({\n    userId,\n    assetId,\n    start,\n    end,\n  }: {\n    userId: string;\n    assetId: string;\n    start?: number;\n    end?: number;\n  }) {\n    const range =\n      start !== undefined && end !== undefined\n        ? `bytes=${start}-${end}`\n        : undefined;\n\n    const command = new GetObjectCommand({\n      Bucket: this.bucketName,\n      Key: this.getAssetKey(userId, assetId),\n      Range: range,\n    });\n\n    const response = await this.s3Client.send(command);\n    if (!response.Body) {\n      throw new Error(\"Asset not found\");\n    }\n    return response.Body as NodeJS.ReadableStream;\n  }\n\n  async readAssetMetadata({\n    userId,\n    assetId,\n  }: {\n    userId: string;\n    assetId: string;\n  }) {\n    const response = await this.s3Client.send(\n      new HeadObjectCommand({\n        Bucket: this.bucketName,\n        Key: this.getAssetKey(userId, assetId),\n      }),\n    );\n\n    return this.s3MetadataToMetadata(response.Metadata);\n  }\n\n  async getAssetSize({ userId, assetId }: { userId: string; assetId: string }) {\n    const response = await this.s3Client.send(\n      new HeadObjectCommand({\n        Bucket: this.bucketName,\n        Key: this.getAssetKey(userId, assetId),\n      }),\n    );\n\n    return response.ContentLength || 0;\n  }\n\n  async deleteAsset({ userId, assetId }: { userId: string; assetId: string }) {\n    await this.s3Client.send(\n      new DeleteObjectCommand({\n        Bucket: this.bucketName,\n        Key: this.getAssetKey(userId, assetId),\n      }),\n    );\n  }\n\n  async deleteUserAssets({ userId }: { userId: string }) {\n    let continuationToken: string | undefined;\n\n    do {\n      const listResponse = await this.s3Client.send(\n        new ListObjectsV2Command({\n          Bucket: this.bucketName,\n          Prefix: `${userId}/`,\n          ContinuationToken: continuationToken,\n        }),\n      );\n\n      if (listResponse.Contents && listResponse.Contents.length > 0) {\n        await this.s3Client.send(\n          new DeleteObjectsCommand({\n            Bucket: this.bucketName,\n            Delete: {\n              Objects: listResponse.Contents.map((obj) => ({\n                Key: obj.Key!,\n              })),\n            },\n          }),\n        );\n      }\n\n      continuationToken = listResponse.NextContinuationToken;\n    } while (continuationToken);\n  }\n\n  async *getAllAssets() {\n    let continuationToken: string | undefined;\n\n    do {\n      const listResponse = await this.s3Client.send(\n        new ListObjectsV2Command({\n          Bucket: this.bucketName,\n          ContinuationToken: continuationToken,\n        }),\n      );\n\n      if (listResponse.Contents) {\n        for (const obj of listResponse.Contents) {\n          if (!obj.Key) continue;\n\n          const pathParts = obj.Key.split(\"/\");\n          if (pathParts.length === 2) {\n            const userId = pathParts[0];\n            const assetId = pathParts[1];\n\n            try {\n              const headResponse = await this.s3Client.send(\n                new HeadObjectCommand({\n                  Bucket: this.bucketName,\n                  Key: obj.Key,\n                }),\n              );\n\n              const metadata = this.s3MetadataToMetadata(headResponse.Metadata);\n              const size = headResponse.ContentLength || 0;\n\n              yield {\n                userId,\n                assetId,\n                ...metadata,\n                size,\n              };\n            } catch (error) {\n              logger.warn(`Failed to read asset ${userId}/${assetId}:`, error);\n            }\n          }\n        }\n      }\n\n      continuationToken = listResponse.NextContinuationToken;\n    } while (continuationToken);\n  }\n\n  private async streamToBuffer(stream: Readable): Promise<Buffer> {\n    const chunks: Buffer[] = [];\n    for await (const chunk of stream) {\n      chunks.push(chunk);\n    }\n    return Buffer.concat(chunks);\n  }\n}\n\nfunction createDefaultAssetStore(): AssetStore {\n  const config = serverConfig.assetStore;\n\n  if (config.type === \"s3\") {\n    if (!config.s3.bucket) {\n      throw new Error(\n        \"ASSET_STORE_S3_BUCKET is required when using S3 asset store\",\n      );\n    }\n    if (!config.s3.accessKeyId || !config.s3.secretAccessKey) {\n      throw new Error(\n        \"ASSET_STORE_S3_ACCESS_KEY_ID and ASSET_STORE_S3_SECRET_ACCESS_KEY are required when using S3 asset store\",\n      );\n    }\n\n    const s3Client = new S3Client({\n      region: config.s3.region,\n      endpoint: config.s3.endpoint,\n      forcePathStyle: config.s3.forcePathStyle,\n      credentials: {\n        accessKeyId: config.s3.accessKeyId,\n        secretAccessKey: config.s3.secretAccessKey,\n      },\n    });\n\n    return new S3AssetStore(s3Client, config.s3.bucket);\n  }\n\n  return new LocalFileSystemAssetStore(ROOT_PATH);\n}\n\nconst defaultAssetStore = createDefaultAssetStore();\n\nexport { LocalFileSystemAssetStore, S3AssetStore };\n\n/**\n * Example usage of S3AssetStore:\n *\n * import { S3Client } from \"@aws-sdk/client-s3\";\n * import { S3AssetStore } from \"@karakeep/shared/assetdb\";\n *\n * const s3Client = new S3Client({\n *   region: \"us-east-1\",\n *   credentials: {\n *     accessKeyId: \"your-access-key\",\n *     secretAccessKey: \"your-secret-key\"\n *   }\n * });\n *\n * const s3AssetStore = new S3AssetStore(s3Client, \"your-bucket-name\");\n *\n * // Use s3AssetStore instead of the default file system store\n * await s3AssetStore.saveAsset({\n *   userId: \"user123\",\n *   assetId: \"asset456\",\n *   asset: buffer,\n *   metadata: { contentType: \"image/jpeg\", fileName: \"photo.jpg\" }\n * });\n */\n\nexport async function saveAsset({\n  userId,\n  assetId,\n  asset,\n  metadata,\n  quotaApproved,\n}: {\n  userId: string;\n  assetId: string;\n  asset: Buffer;\n  metadata: z.infer<typeof zAssetMetadataSchema>;\n  quotaApproved: QuotaApproved;\n}) {\n  // Verify the quota approval is for the correct user and size\n  if (quotaApproved.userId !== userId) {\n    throw new Error(\"Quota approval is for a different user\");\n  }\n  if (quotaApproved.approvedSize < asset.byteLength) {\n    throw new Error(\"Asset size exceeds approved quota\");\n  }\n\n  return defaultAssetStore.saveAsset({ userId, assetId, asset, metadata });\n}\n\nexport async function saveAssetFromFile({\n  userId,\n  assetId,\n  assetPath,\n  metadata,\n  quotaApproved,\n}: {\n  userId: string;\n  assetId: string;\n  assetPath: string;\n  metadata: z.infer<typeof zAssetMetadataSchema>;\n  quotaApproved: QuotaApproved;\n}) {\n  // Verify the quota approval is for the correct user\n  if (quotaApproved.userId !== userId) {\n    throw new Error(\"Quota approval is for a different user\");\n  }\n\n  // For file-based saves, we'll verify the file size matches the approved size\n  // when the underlying store implementation reads the file\n\n  return defaultAssetStore.saveAssetFromFile({\n    userId,\n    assetId,\n    assetPath,\n    metadata,\n  });\n}\n\nexport async function readAsset({\n  userId,\n  assetId,\n}: {\n  userId: string;\n  assetId: string;\n}) {\n  return defaultAssetStore.readAsset({ userId, assetId });\n}\n\nexport async function createAssetReadStream({\n  userId,\n  assetId,\n  start,\n  end,\n}: {\n  userId: string;\n  assetId: string;\n  start?: number;\n  end?: number;\n}) {\n  return defaultAssetStore.createAssetReadStream({\n    userId,\n    assetId,\n    start,\n    end,\n  });\n}\n\nexport async function readAssetMetadata({\n  userId,\n  assetId,\n}: {\n  userId: string;\n  assetId: string;\n}) {\n  return defaultAssetStore.readAssetMetadata({ userId, assetId });\n}\n\nexport async function getAssetSize({\n  userId,\n  assetId,\n}: {\n  userId: string;\n  assetId: string;\n}) {\n  return defaultAssetStore.getAssetSize({ userId, assetId });\n}\n\n/**\n * Deletes the passed in asset if it exists and ignores any errors\n * @param userId the id of the user the asset belongs to\n * @param assetId the id of the asset to delete\n */\nexport async function silentDeleteAsset(\n  userId: string,\n  assetId: string | undefined,\n) {\n  if (assetId) {\n    await deleteAsset({ userId, assetId }).catch(() => ({}));\n  }\n}\n\nexport async function deleteAsset({\n  userId,\n  assetId,\n}: {\n  userId: string;\n  assetId: string;\n}) {\n  return defaultAssetStore.deleteAsset({ userId, assetId });\n}\n\nexport async function deleteUserAssets({ userId }: { userId: string }) {\n  return defaultAssetStore.deleteUserAssets({ userId });\n}\n\nexport async function* getAllAssets() {\n  yield* defaultAssetStore.getAllAssets();\n}\n"
  },
  {
    "path": "packages/shared/concurrency.test.ts",
    "content": "// semaphore.test.ts\n\nimport { describe, expect, it } from \"vitest\";\n\nimport { AsyncSemaphore, limitConcurrency } from \"./concurrency\";\n\ndescribe(\"AsyncSemaphore\", () => {\n  it(\"should acquire a permit if available\", async () => {\n    const semaphore = new AsyncSemaphore(1);\n    await semaphore.acquire();\n    expect(semaphore.available).toBe(0);\n  });\n\n  it(\"should wait if no permit is available\", async () => {\n    const semaphore = new AsyncSemaphore(1);\n    await semaphore.acquire();\n\n    let acquired = false;\n    const acquirePromise = semaphore.acquire().then(() => {\n      acquired = true;\n    });\n\n    expect(acquired).toBe(false); // Should not resolve right away\n    semaphore.release();\n\n    await acquirePromise; // wait for the resolution of the promise\n    expect(acquired).toBe(true);\n    expect(semaphore.available).toBe(0);\n  });\n\n  it(\"should release a permit\", async () => {\n    const semaphore = new AsyncSemaphore(1);\n    await semaphore.acquire();\n    expect(semaphore.available).toBe(0);\n    semaphore.release();\n    expect(semaphore.available).toBe(1);\n  });\n\n  it(\"should handle multiple acquires and releases\", async () => {\n    const semaphore = new AsyncSemaphore(2);\n    await semaphore.acquire();\n    await semaphore.acquire();\n    expect(semaphore.available).toBe(0);\n\n    let resolved1 = false;\n    let resolved2 = false;\n    const promise1 = semaphore.acquire().then(() => {\n      resolved1 = true;\n    });\n    const promise2 = semaphore.acquire().then(() => {\n      resolved2 = true;\n    });\n\n    expect(resolved1).toBe(false);\n    expect(resolved2).toBe(false);\n\n    semaphore.release();\n    await promise1;\n    expect(resolved1).toBe(true);\n    expect(resolved2).toBe(false);\n\n    semaphore.release();\n    await promise2;\n    expect(resolved2).toBe(true);\n    expect(semaphore.available).toBe(0);\n  });\n\n  it(\"should acquire immediately if there is an available permit\", async () => {\n    const semaphore = new AsyncSemaphore(2);\n    await semaphore.acquire();\n    expect(semaphore.available).toBe(1);\n    await semaphore.acquire();\n    expect(semaphore.available).toBe(0);\n  });\n});\n\ndescribe(\"limitConcurrency\", () => {\n  it(\"should execute all promises with concurrency limit\", async () => {\n    const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));\n\n    const promiseFunctions = [\n      async () => {\n        await delay(10);\n        return 1;\n      },\n      async () => {\n        await delay(5);\n        return 2;\n      },\n      async () => {\n        await delay(15);\n        return 3;\n      },\n      async () => {\n        await delay(10);\n        return 4;\n      },\n    ];\n\n    const concurrencyLimit = 2;\n    const results = limitConcurrency(promiseFunctions, concurrencyLimit);\n    expect(results).toHaveLength(promiseFunctions.length);\n\n    const resolvedResults = await Promise.all(results);\n    expect(resolvedResults).toEqual([1, 2, 3, 4]);\n  });\n\n  it(\"should limit concurrency\", async () => {\n    const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));\n    let runningCount = 0;\n    let maxCounter = 0;\n    const promiseFunctions = [...Array(50).keys()].map(() => async () => {\n      runningCount++;\n      maxCounter = Math.max(maxCounter, runningCount);\n      await delay(100);\n      runningCount--;\n    });\n    const concurrencyLimit = 2;\n    const results = limitConcurrency(promiseFunctions, concurrencyLimit);\n\n    await Promise.all(results);\n    expect(runningCount).toBe(0);\n    expect(maxCounter).toBe(concurrencyLimit);\n  });\n\n  it(\"should handle errors in promise functions\", async () => {\n    const promiseFunctions = [\n      async () => {\n        return Promise.resolve(1);\n      },\n      async () => {\n        return Promise.resolve(2);\n      },\n      async () => {\n        return Promise.reject(new Error(\"Test Error\"));\n      },\n      async () => {\n        return Promise.resolve(4);\n      },\n    ];\n    const concurrencyLimit = 2;\n\n    const results = limitConcurrency(promiseFunctions, concurrencyLimit);\n\n    await expect(Promise.all(results)).rejects.toThrow(\"Test Error\"); // test that promise fails.\n\n    const resolveResults = await Promise.allSettled(results); // check that the other promises resolve even if the function fails\n\n    expect(resolveResults.map((r) => r.status)).toEqual([\n      \"fulfilled\",\n      \"fulfilled\",\n      \"rejected\",\n      \"fulfilled\",\n    ]);\n    expect(\n      resolveResults[0].status === \"fulfilled\" && resolveResults[0].value,\n    ).toBe(1);\n    expect(\n      resolveResults[1].status === \"fulfilled\" && resolveResults[1].value,\n    ).toBe(2);\n    expect(\n      resolveResults[2].status === \"rejected\" &&\n        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access\n        resolveResults[2].reason.message,\n    ).toBe(\"Test Error\");\n    expect(\n      resolveResults[3].status === \"fulfilled\" && resolveResults[3].value,\n    ).toBe(4);\n  });\n});\n"
  },
  {
    "path": "packages/shared/concurrency.ts",
    "content": "export class AsyncSemaphore {\n  private permits: number;\n  private queue: (() => void)[] = [];\n\n  constructor(permits: number) {\n    this.permits = permits;\n  }\n\n  acquire(): Promise<void> {\n    if (this.permits > 0) {\n      this.permits--;\n      return Promise.resolve();\n    } else {\n      return new Promise<void>((resolve) => {\n        this.queue.push(resolve);\n      });\n    }\n  }\n\n  release(): void {\n    if (this.queue.length > 0) {\n      const resolve = this.queue.shift();\n      if (resolve) {\n        resolve();\n      }\n    } else {\n      this.permits++;\n    }\n  }\n\n  get available(): number {\n    return this.permits;\n  }\n}\n\nexport function limitConcurrency<T>(\n  promises: (() => Promise<T>)[],\n  concurrencyLimit: number,\n): Promise<T>[] {\n  const semaphore = new AsyncSemaphore(concurrencyLimit);\n  const results: Promise<T>[] = [];\n\n  for (const promiseFunction of promises) {\n    results.push(\n      semaphore\n        .acquire()\n        .then(() => {\n          return promiseFunction();\n        })\n        .finally(() => {\n          semaphore.release();\n        }),\n    );\n  }\n  return results;\n}\n"
  },
  {
    "path": "packages/shared/config.ts",
    "content": "import crypto from \"node:crypto\";\nimport path from \"path\";\nimport { z } from \"zod\";\n\nconst stringBool = (defaultValue: string) =>\n  z\n    .string()\n    .default(defaultValue)\n    .refine((s) => s === \"true\" || s === \"false\")\n    .transform((s) => s === \"true\");\n\nconst optionalStringBool = () =>\n  z\n    .string()\n    .refine((s) => s === \"true\" || s === \"false\")\n    .transform((s) => s === \"true\")\n    .optional();\n\nconst allEnv = z.object({\n  PORT: z.coerce.number().default(3000),\n  WORKERS_HOST: z.string().default(\"127.0.0.1\"),\n  WORKERS_PORT: z.coerce.number().default(0),\n  WORKERS_ENABLED_WORKERS: z\n    .string()\n    .default(\"\")\n    .transform((val) =>\n      val\n        .split(\",\")\n        .map((w) => w.trim())\n        .filter((w) => w),\n    ),\n  WORKERS_DISABLED_WORKERS: z\n    .string()\n    .default(\"\")\n    .transform((val) =>\n      val\n        .split(\",\")\n        .map((w) => w.trim())\n        .filter((w) => w),\n    ),\n  API_URL: z.string().url().default(\"http://localhost:3000\"),\n  NEXTAUTH_URL: z\n    .string()\n    .url()\n    .default(\"http://localhost:3000\")\n    .transform((s) => s.replace(/\\/+$/, \"\")),\n  NEXTAUTH_SECRET: z.string().optional(),\n  DISABLE_SIGNUPS: stringBool(\"false\"),\n  DISABLE_PASSWORD_AUTH: stringBool(\"false\"),\n  OAUTH_AUTO_REDIRECT: stringBool(\"false\"),\n  OAUTH_ALLOW_DANGEROUS_EMAIL_ACCOUNT_LINKING: stringBool(\"false\"),\n  OAUTH_WELLKNOWN_URL: z.string().url().optional(),\n  OAUTH_CLIENT_SECRET: z.string().optional(),\n  OAUTH_CLIENT_ID: z.string().optional(),\n  OAUTH_TIMEOUT: z.coerce.number().optional().default(3500),\n  OAUTH_SCOPE: z.string().default(\"openid email profile\"),\n  OAUTH_PROVIDER_NAME: z.string().default(\"Custom Provider\"),\n  TURNSTILE_SITE_KEY: z.string().optional(),\n  TURNSTILE_SECRET_KEY: z.string().optional(),\n  OPENAI_API_KEY: z.string().optional(),\n  OPENAI_BASE_URL: z.string().url().optional(),\n  OPENAI_PROXY_URL: z.string().url().optional(),\n  OPENAI_SERVICE_TIER: z.enum([\"auto\", \"default\", \"flex\"]).optional(),\n  OLLAMA_BASE_URL: z.string().url().optional(),\n  OLLAMA_KEEP_ALIVE: z.string().optional(),\n  INFERENCE_JOB_TIMEOUT_SEC: z.coerce.number().default(30),\n  INFERENCE_FETCH_TIMEOUT_SEC: z.coerce.number().default(300),\n  INFERENCE_TEXT_MODEL: z.string().default(\"gpt-4.1-mini\"),\n  INFERENCE_IMAGE_MODEL: z.string().default(\"gpt-4o-mini\"),\n  EMBEDDING_TEXT_MODEL: z.string().default(\"text-embedding-3-small\"),\n  INFERENCE_CONTEXT_LENGTH: z.coerce.number().default(2048),\n  INFERENCE_MAX_OUTPUT_TOKENS: z.coerce.number().default(2048),\n  INFERENCE_USE_MAX_COMPLETION_TOKENS: stringBool(\"false\"),\n  INFERENCE_SUPPORTS_STRUCTURED_OUTPUT: optionalStringBool(),\n  INFERENCE_OUTPUT_SCHEMA: z\n    .enum([\"structured\", \"json\", \"plain\"])\n    .default(\"structured\"),\n  INFERENCE_ENABLE_AUTO_TAGGING: stringBool(\"true\"),\n  INFERENCE_ENABLE_AUTO_SUMMARIZATION: stringBool(\"false\"),\n  OCR_CACHE_DIR: z.string().optional(),\n  OCR_LANGS: z\n    .string()\n    .default(\"eng\")\n    .transform((val) => val.split(\",\")),\n  OCR_CONFIDENCE_THRESHOLD: z.coerce.number().default(50),\n  OCR_USE_LLM: stringBool(\"false\"),\n  CRAWLER_HEADLESS_BROWSER: stringBool(\"true\"),\n  BROWSER_WEB_URL: z.string().optional(),\n  BROWSER_WEBSOCKET_URL: z.string().optional(),\n  BROWSER_CONNECT_ONDEMAND: stringBool(\"false\"),\n  BROWSER_COOKIE_PATH: z.string().optional(),\n  CRAWLER_JOB_TIMEOUT_SEC: z.coerce.number().default(60),\n  CRAWLER_NAVIGATE_TIMEOUT_SEC: z.coerce.number().default(30),\n  CRAWLER_NUM_WORKERS: z.coerce.number().default(1),\n  INFERENCE_NUM_WORKERS: z.coerce.number().default(1),\n  SEARCH_NUM_WORKERS: z.coerce.number().default(1),\n  SEARCH_JOB_TIMEOUT_SEC: z.coerce.number().default(30),\n  WEBHOOK_NUM_WORKERS: z.coerce.number().default(1),\n  ASSET_PREPROCESSING_NUM_WORKERS: z.coerce.number().default(1),\n  ASSET_PREPROCESSING_JOB_TIMEOUT_SEC: z.coerce.number().default(60),\n  RULE_ENGINE_NUM_WORKERS: z.coerce.number().default(1),\n  CRAWLER_DOWNLOAD_BANNER_IMAGE: stringBool(\"true\"),\n  CRAWLER_STORE_SCREENSHOT: stringBool(\"true\"),\n  CRAWLER_FULL_PAGE_SCREENSHOT: stringBool(\"false\"),\n  CRAWLER_STORE_PDF: stringBool(\"false\"),\n  CRAWLER_FULL_PAGE_ARCHIVE: stringBool(\"false\"),\n  CRAWLER_VIDEO_DOWNLOAD: stringBool(\"false\"),\n  CRAWLER_VIDEO_DOWNLOAD_MAX_SIZE: z.coerce.number().default(50),\n  CRAWLER_VIDEO_DOWNLOAD_TIMEOUT_SEC: z.coerce.number().default(10 * 60),\n  CRAWLER_ENABLE_ADBLOCKER: stringBool(\"true\"),\n  CRAWLER_YTDLP_ARGS: z\n    .string()\n    .default(\"\")\n    .transform((t) => t.split(\"%%\").filter((a) => a)),\n  CRAWLER_PARSER_MEM_LIMIT_MB: z.coerce.number().default(512),\n  CRAWLER_PARSE_TIMEOUT_SEC: z.coerce.number().default(60),\n  CRAWLER_SCREENSHOT_TIMEOUT_SEC: z.coerce.number().default(5),\n  CRAWLER_IP_VALIDATION_DNS_RESOLVER_TIMEOUT_SEC: z.coerce.number().default(1),\n  CRAWLER_DOMAIN_RATE_LIMIT_WINDOW_MS: z.coerce.number().min(1).optional(),\n  CRAWLER_DOMAIN_RATE_LIMIT_MAX_REQUESTS: z.coerce.number().min(1).optional(),\n  LOG_LEVEL: z.string().default(\"debug\"),\n  NO_COLOR: stringBool(\"false\"),\n  DEMO_MODE: stringBool(\"false\"),\n  DEMO_MODE_EMAIL: z.string().optional(),\n  DEMO_MODE_PASSWORD: z.string().optional(),\n  DATA_DIR: z.string().default(\"\"),\n  ASSETS_DIR: z.string().optional(),\n  MAX_ASSET_SIZE_MB: z.coerce.number().default(50),\n  HTML_CONTENT_SIZE_INLINE_THRESHOLD_BYTES: z.coerce.number().default(5 * 1024),\n  INFERENCE_LANG: z.string().default(\"english\"),\n  WEBHOOK_TIMEOUT_SEC: z.coerce.number().default(5),\n  WEBHOOK_RETRY_TIMES: z.coerce.number().int().min(0).default(3),\n  MAX_RSS_FEEDS_PER_USER: z.coerce.number().default(1000),\n  MAX_WEBHOOKS_PER_USER: z.coerce.number().default(100),\n  // Legal\n  TERMS_OF_SERVICE_URL: z.string().url().optional(),\n  PRIVACY_POLICY_URL: z.string().url().optional(),\n\n  // Build only flag\n  SERVER_VERSION: z.string().optional(),\n  CHANGELOG_VERSION: z.string().optional(),\n  DISABLE_NEW_RELEASE_CHECK: stringBool(\"false\"),\n\n  // A flag to detect if the user is running in the old separete containers setup\n  USING_LEGACY_SEPARATE_CONTAINERS: stringBool(\"false\"),\n\n  // Prometheus metrics configuration\n  PROMETHEUS_AUTH_TOKEN: z.string().optional(),\n\n  // Email configuration\n  SMTP_HOST: z.string().optional(),\n  SMTP_PORT: z.coerce.number().optional().default(587),\n  SMTP_SECURE: stringBool(\"false\"),\n  SMTP_USER: z.string().optional(),\n  SMTP_PASSWORD: z.string().optional(),\n  SMTP_FROM: z.string().optional(),\n  EMAIL_VERIFICATION_REQUIRED: stringBool(\"false\"),\n\n  // Asset storage configuration\n  ASSET_STORE_S3_ENDPOINT: z.string().optional(),\n  ASSET_STORE_S3_REGION: z.string().optional(),\n  ASSET_STORE_S3_BUCKET: z.string().optional(),\n  ASSET_STORE_S3_ACCESS_KEY_ID: z.string().optional(),\n  ASSET_STORE_S3_SECRET_ACCESS_KEY: z.string().optional(),\n  ASSET_STORE_S3_FORCE_PATH_STYLE: stringBool(\"false\"),\n\n  // Rate limiting configuration\n  RATE_LIMITING_ENABLED: stringBool(\"false\"),\n\n  // Redis configuration\n  REDIS_URL: z.string().url().optional(),\n\n  // Stripe configuration\n  STRIPE_SECRET_KEY: z.string().optional(),\n  STRIPE_PUBLISHABLE_KEY: z.string().optional(),\n  STRIPE_WEBHOOK_SECRET: z.string().optional(),\n  STRIPE_PRICE_ID: z.string().optional(),\n\n  FREE_QUOTA_BOOKMARK_LIMIT: z.coerce.number().optional(),\n  FREE_QUOTA_ASSET_SIZE_BYTES: z.coerce.number().optional(),\n  FREE_BROWSER_CRAWLING_ENABLED: optionalStringBool(),\n  PAID_QUOTA_BOOKMARK_LIMIT: z.coerce.number().optional(),\n  PAID_QUOTA_ASSET_SIZE_BYTES: z.coerce.number().optional(),\n  PAID_BROWSER_CRAWLING_ENABLED: optionalStringBool(),\n\n  // Proxy configuration\n  CRAWLER_HTTP_PROXY: z\n    .string()\n    .transform((val) =>\n      val\n        .split(\",\")\n        .map((p) => p.trim())\n        .filter((p) => p),\n    )\n    .optional(),\n  CRAWLER_HTTPS_PROXY: z\n    .string()\n    .transform((val) =>\n      val\n        .split(\",\")\n        .map((p) => p.trim())\n        .filter((p) => p),\n    )\n    .optional(),\n  CRAWLER_NO_PROXY: z\n    .string()\n    .transform((val) =>\n      val\n        .split(\",\")\n        .map((p) => p.trim())\n        .filter((p) => p),\n    )\n    .optional(),\n  CRAWLER_ALLOWED_INTERNAL_HOSTNAMES: z\n    .string()\n    .transform((val) =>\n      val\n        .split(\",\")\n        .map((p) => p.trim())\n        .filter((p) => p),\n    )\n    .optional(),\n\n  // Database configuration\n  DB_WAL_MODE: stringBool(\"false\"),\n\n  // OpenTelemetry tracing configuration\n  OTEL_TRACING_ENABLED: stringBool(\"false\"),\n  OTEL_EXPORTER_OTLP_ENDPOINT: z.string().url().optional(),\n  OTEL_SERVICE_NAME: z.string().default(\"karakeep\"),\n  OTEL_SAMPLE_RATE: z.coerce.number().min(0).max(1).default(1.0),\n});\n\nconst serverConfigSchema = allEnv.transform((val, ctx) => {\n  const obj = {\n    port: val.PORT,\n    workers: {\n      host: val.WORKERS_HOST,\n      port: val.WORKERS_PORT,\n      enabledWorkers: val.WORKERS_ENABLED_WORKERS,\n      disabledWorkers: val.WORKERS_DISABLED_WORKERS,\n    },\n    apiUrl: val.API_URL,\n    publicUrl: val.NEXTAUTH_URL,\n    publicApiUrl: `${val.NEXTAUTH_URL}/api`,\n    signingSecret: () => {\n      if (!val.NEXTAUTH_SECRET) {\n        throw new Error(\"NEXTAUTH_SECRET is not set\");\n      }\n      return val.NEXTAUTH_SECRET;\n    },\n    auth: {\n      disableSignups: val.DISABLE_SIGNUPS,\n      disablePasswordAuth: val.DISABLE_PASSWORD_AUTH,\n      emailVerificationRequired: val.EMAIL_VERIFICATION_REQUIRED,\n      oauth: {\n        autoRedirect: val.OAUTH_AUTO_REDIRECT,\n        allowDangerousEmailAccountLinking:\n          val.OAUTH_ALLOW_DANGEROUS_EMAIL_ACCOUNT_LINKING,\n        wellKnownUrl: val.OAUTH_WELLKNOWN_URL,\n        clientSecret: val.OAUTH_CLIENT_SECRET,\n        clientId: val.OAUTH_CLIENT_ID,\n        scope: val.OAUTH_SCOPE,\n        name: val.OAUTH_PROVIDER_NAME,\n        timeout: val.OAUTH_TIMEOUT,\n      },\n      turnstile: {\n        enabled: val.TURNSTILE_SITE_KEY !== undefined,\n        siteKey: val.TURNSTILE_SITE_KEY,\n        secretKey: val.TURNSTILE_SECRET_KEY,\n      },\n    },\n    email: {\n      smtp: val.SMTP_HOST\n        ? {\n            host: val.SMTP_HOST,\n            port: val.SMTP_PORT,\n            secure: val.SMTP_SECURE,\n            user: val.SMTP_USER,\n            password: val.SMTP_PASSWORD,\n            from: val.SMTP_FROM,\n          }\n        : undefined,\n    },\n    inference: {\n      isConfigured: !!val.OPENAI_API_KEY || !!val.OLLAMA_BASE_URL,\n      numWorkers: val.INFERENCE_NUM_WORKERS,\n      jobTimeoutSec: val.INFERENCE_JOB_TIMEOUT_SEC,\n      fetchTimeoutSec: val.INFERENCE_FETCH_TIMEOUT_SEC,\n      openAIApiKey: val.OPENAI_API_KEY,\n      openAIBaseUrl: val.OPENAI_BASE_URL,\n      openAIProxyUrl: val.OPENAI_PROXY_URL,\n      openAIServiceTier: val.OPENAI_SERVICE_TIER,\n      ollamaBaseUrl: val.OLLAMA_BASE_URL,\n      ollamaKeepAlive: val.OLLAMA_KEEP_ALIVE,\n      textModel: val.INFERENCE_TEXT_MODEL,\n      imageModel: val.INFERENCE_IMAGE_MODEL,\n      inferredTagLang: val.INFERENCE_LANG,\n      contextLength: val.INFERENCE_CONTEXT_LENGTH,\n      maxOutputTokens: val.INFERENCE_MAX_OUTPUT_TOKENS,\n      useMaxCompletionTokens: val.INFERENCE_USE_MAX_COMPLETION_TOKENS,\n      outputSchema:\n        val.INFERENCE_SUPPORTS_STRUCTURED_OUTPUT !== undefined\n          ? val.INFERENCE_SUPPORTS_STRUCTURED_OUTPUT\n            ? (\"structured\" as const)\n            : (\"plain\" as const)\n          : val.INFERENCE_OUTPUT_SCHEMA,\n      enableAutoTagging: val.INFERENCE_ENABLE_AUTO_TAGGING,\n      enableAutoSummarization: val.INFERENCE_ENABLE_AUTO_SUMMARIZATION,\n    },\n    embedding: {\n      textModel: val.EMBEDDING_TEXT_MODEL,\n    },\n    crawler: {\n      numWorkers: val.CRAWLER_NUM_WORKERS,\n      headlessBrowser: val.CRAWLER_HEADLESS_BROWSER,\n      browserWebUrl: val.BROWSER_WEB_URL,\n      browserWebSocketUrl: val.BROWSER_WEBSOCKET_URL,\n      browserConnectOnDemand: val.BROWSER_CONNECT_ONDEMAND,\n      browserCookiePath: val.BROWSER_COOKIE_PATH,\n      jobTimeoutSec: val.CRAWLER_JOB_TIMEOUT_SEC,\n      navigateTimeoutSec: val.CRAWLER_NAVIGATE_TIMEOUT_SEC,\n      downloadBannerImage: val.CRAWLER_DOWNLOAD_BANNER_IMAGE,\n      storeScreenshot: val.CRAWLER_STORE_SCREENSHOT,\n      fullPageScreenshot: val.CRAWLER_FULL_PAGE_SCREENSHOT,\n      storePdf: val.CRAWLER_STORE_PDF,\n      fullPageArchive: val.CRAWLER_FULL_PAGE_ARCHIVE,\n      downloadVideo: val.CRAWLER_VIDEO_DOWNLOAD,\n      maxVideoDownloadSize: val.CRAWLER_VIDEO_DOWNLOAD_MAX_SIZE,\n      downloadVideoTimeout: val.CRAWLER_VIDEO_DOWNLOAD_TIMEOUT_SEC,\n      enableAdblocker: val.CRAWLER_ENABLE_ADBLOCKER,\n      ytDlpArguments: val.CRAWLER_YTDLP_ARGS,\n      parserMemLimitMb: val.CRAWLER_PARSER_MEM_LIMIT_MB,\n      parseTimeoutSec: val.CRAWLER_PARSE_TIMEOUT_SEC,\n      screenshotTimeoutSec: val.CRAWLER_SCREENSHOT_TIMEOUT_SEC,\n      htmlContentSizeThreshold: val.HTML_CONTENT_SIZE_INLINE_THRESHOLD_BYTES,\n      ipValidation: {\n        dnsResolverTimeoutSec:\n          val.CRAWLER_IP_VALIDATION_DNS_RESOLVER_TIMEOUT_SEC,\n      },\n      domainRatelimiting:\n        val.CRAWLER_DOMAIN_RATE_LIMIT_WINDOW_MS !== undefined &&\n        val.CRAWLER_DOMAIN_RATE_LIMIT_MAX_REQUESTS !== undefined\n          ? {\n              windowMs: val.CRAWLER_DOMAIN_RATE_LIMIT_WINDOW_MS,\n              maxRequests: val.CRAWLER_DOMAIN_RATE_LIMIT_MAX_REQUESTS,\n            }\n          : null,\n    },\n    ocr: {\n      langs: val.OCR_LANGS,\n      cacheDir: val.OCR_CACHE_DIR,\n      confidenceThreshold: val.OCR_CONFIDENCE_THRESHOLD,\n      useLLM: val.OCR_USE_LLM,\n    },\n    search: {\n      numWorkers: val.SEARCH_NUM_WORKERS,\n      jobTimeoutSec: val.SEARCH_JOB_TIMEOUT_SEC,\n    },\n    logLevel: val.LOG_LEVEL,\n    logNoColor: val.NO_COLOR,\n    demoMode: val.DEMO_MODE\n      ? {\n          email: val.DEMO_MODE_EMAIL,\n          password: val.DEMO_MODE_PASSWORD,\n        }\n      : undefined,\n    dataDir: val.DATA_DIR,\n    assetsDir: val.ASSETS_DIR ?? path.join(val.DATA_DIR, \"assets\"),\n    maxAssetSizeMb: val.MAX_ASSET_SIZE_MB,\n    legal: {\n      termsOfServiceUrl: val.TERMS_OF_SERVICE_URL,\n      privacyPolicyUrl: val.PRIVACY_POLICY_URL,\n    },\n    serverVersion: val.SERVER_VERSION,\n    changelogVersion: val.CHANGELOG_VERSION,\n    disableNewReleaseCheck: val.DISABLE_NEW_RELEASE_CHECK,\n    usingLegacySeparateContainers: val.USING_LEGACY_SEPARATE_CONTAINERS,\n    webhook: {\n      timeoutSec: val.WEBHOOK_TIMEOUT_SEC,\n      retryTimes: val.WEBHOOK_RETRY_TIMES,\n      numWorkers: val.WEBHOOK_NUM_WORKERS,\n      maxWebhooksPerUser: val.MAX_WEBHOOKS_PER_USER,\n    },\n    feeds: {\n      maxRssFeedsPerUser: val.MAX_RSS_FEEDS_PER_USER,\n    },\n    proxy: {\n      httpProxy: val.CRAWLER_HTTP_PROXY,\n      httpsProxy: val.CRAWLER_HTTPS_PROXY,\n      noProxy: val.CRAWLER_NO_PROXY,\n    },\n    allowedInternalHostnames: val.CRAWLER_ALLOWED_INTERNAL_HOSTNAMES,\n    assetPreprocessing: {\n      numWorkers: val.ASSET_PREPROCESSING_NUM_WORKERS,\n      jobTimeoutSec: val.ASSET_PREPROCESSING_JOB_TIMEOUT_SEC,\n    },\n    ruleEngine: {\n      numWorkers: val.RULE_ENGINE_NUM_WORKERS,\n    },\n    assetStore: {\n      type: val.ASSET_STORE_S3_ENDPOINT\n        ? (\"s3\" as const)\n        : (\"filesystem\" as const),\n      s3: {\n        endpoint: val.ASSET_STORE_S3_ENDPOINT,\n        region: val.ASSET_STORE_S3_REGION,\n        bucket: val.ASSET_STORE_S3_BUCKET,\n        accessKeyId: val.ASSET_STORE_S3_ACCESS_KEY_ID,\n        secretAccessKey: val.ASSET_STORE_S3_SECRET_ACCESS_KEY,\n        forcePathStyle: val.ASSET_STORE_S3_FORCE_PATH_STYLE,\n      },\n    },\n    prometheus: {\n      metricsToken:\n        val.PROMETHEUS_AUTH_TOKEN ?? crypto.randomBytes(64).toString(\"hex\"),\n    },\n    rateLimiting: {\n      enabled: val.RATE_LIMITING_ENABLED,\n    },\n    redis: {\n      url: val.REDIS_URL,\n    },\n    stripe: {\n      secretKey: val.STRIPE_SECRET_KEY,\n      publishableKey: val.STRIPE_PUBLISHABLE_KEY,\n      webhookSecret: val.STRIPE_WEBHOOK_SECRET,\n      priceId: val.STRIPE_PRICE_ID,\n      isConfigured: !!val.STRIPE_SECRET_KEY && !!val.STRIPE_PUBLISHABLE_KEY,\n    },\n    quotas: {\n      free: {\n        bookmarkLimit: val.FREE_QUOTA_BOOKMARK_LIMIT ?? null,\n        assetSizeBytes: val.FREE_QUOTA_ASSET_SIZE_BYTES ?? null,\n        browserCrawlingEnabled: val.FREE_BROWSER_CRAWLING_ENABLED ?? null,\n      },\n      paid: {\n        bookmarkLimit: val.PAID_QUOTA_BOOKMARK_LIMIT ?? null,\n        assetSizeBytes: val.PAID_QUOTA_ASSET_SIZE_BYTES ?? null,\n        browserCrawlingEnabled: val.PAID_BROWSER_CRAWLING_ENABLED ?? null,\n      },\n    },\n    database: {\n      walMode: val.DB_WAL_MODE,\n    },\n    tracing: {\n      enabled: val.OTEL_TRACING_ENABLED,\n      otlpEndpoint: val.OTEL_EXPORTER_OTLP_ENDPOINT,\n      serviceName: val.OTEL_SERVICE_NAME,\n      sampleRate: val.OTEL_SAMPLE_RATE,\n    },\n  };\n  if (obj.auth.emailVerificationRequired && !obj.email.smtp) {\n    ctx.addIssue({\n      code: z.ZodIssueCode.custom,\n      message: \"To enable email verification, SMTP settings must be configured\",\n      fatal: true,\n    });\n    return z.NEVER;\n  }\n  if (obj.auth.turnstile.enabled && !obj.auth.turnstile.secretKey) {\n    ctx.addIssue({\n      code: z.ZodIssueCode.custom,\n      message:\n        \"TURNSTILE_SECRET_KEY is required when TURNSTILE_SITE_KEY is set\",\n      fatal: true,\n    });\n    return z.NEVER;\n  }\n  return obj;\n});\n\nconst serverConfig: Readonly<z.infer<typeof serverConfigSchema>> =\n  serverConfigSchema.parse(process.env);\n\n// Always explicitly pick up stuff from server config to avoid accidentally leaking stuff\nexport const clientConfig = {\n  publicUrl: serverConfig.publicUrl,\n  publicApiUrl: serverConfig.publicApiUrl,\n  demoMode: serverConfig.demoMode,\n  auth: {\n    disableSignups: serverConfig.auth.disableSignups,\n    disablePasswordAuth: serverConfig.auth.disablePasswordAuth,\n    oauthAutoRedirect: serverConfig.auth.oauth.autoRedirect,\n  },\n  turnstile:\n    serverConfig.auth.turnstile.enabled && serverConfig.auth.turnstile.siteKey\n      ? {\n          siteKey: serverConfig.auth.turnstile.siteKey,\n        }\n      : null,\n  inference: {\n    isConfigured: serverConfig.inference.isConfigured,\n    inferredTagLang: serverConfig.inference.inferredTagLang,\n    enableAutoTagging: serverConfig.inference.enableAutoTagging,\n    enableAutoSummarization: serverConfig.inference.enableAutoSummarization,\n  },\n  legal: {\n    termsOfServiceUrl: serverConfig.legal.termsOfServiceUrl,\n    privacyPolicyUrl: serverConfig.legal.privacyPolicyUrl,\n  },\n  serverVersion: serverConfig.serverVersion,\n  disableNewReleaseCheck: serverConfig.disableNewReleaseCheck,\n};\nexport type ClientConfig = typeof clientConfig;\n\nexport default serverConfig;\n"
  },
  {
    "path": "packages/shared/customFetch.ts",
    "content": "import serverConfig from \"./config\";\n\n// Generic fetch function type that works across environments\ntype FetchFunction = (\n  input: RequestInfo | URL | string,\n  init?: RequestInit,\n) => Promise<Response>;\n\n// Factory function to create a custom fetch with timeout for any fetch implementation\nexport function createCustomFetch(fetchImpl: FetchFunction = globalThis.fetch) {\n  return function customFetch(\n    input: Parameters<typeof fetchImpl>[0],\n    init?: Parameters<typeof fetchImpl>[1],\n  ): ReturnType<typeof fetchImpl> {\n    const timeout = serverConfig.inference.fetchTimeoutSec * 1000; // Convert to milliseconds\n    return fetchImpl(input, {\n      signal: AbortSignal.timeout(timeout),\n      ...init,\n    });\n  };\n}\n\n// Default export for backward compatibility - uses global fetch\nexport const customFetch = createCustomFetch();\n"
  },
  {
    "path": "packages/shared/debug.ts",
    "content": "export function tap<T>(t: T, cb: (t: T) => void): T {\n  cb(t);\n  return t;\n}\n\nexport function debugPrint<T>(t: T): T {\n  return tap(t, (t) => {\n    console.log(t);\n  });\n}\n"
  },
  {
    "path": "packages/shared/import-export/exporters.ts",
    "content": "import { z } from \"zod\";\n\nimport { BookmarkTypes, ZBookmark } from \"../types/bookmarks\";\nimport { ZBookmarkList } from \"../types/lists\";\n\nexport const zExportListSchema = z.object({\n  id: z.string(),\n  name: z.string(),\n  description: z.string().nullable(),\n  icon: z.string(),\n  type: z.enum([\"manual\", \"smart\"]),\n  query: z.string().nullable(),\n  parentId: z.string().nullable(),\n});\n\nexport const zExportBookmarkSchema = z.object({\n  createdAt: z.number(),\n  title: z.string().nullable(),\n  tags: z.array(z.string()),\n  lists: z.array(z.string()).optional().default([]),\n  content: z\n    .discriminatedUnion(\"type\", [\n      z.object({\n        type: z.literal(BookmarkTypes.LINK),\n        url: z.string(),\n      }),\n      z.object({\n        type: z.literal(BookmarkTypes.TEXT),\n        text: z.string(),\n      }),\n    ])\n    .nullable(),\n  note: z.string().nullable(),\n  archived: z.boolean().optional().default(false),\n});\n\nexport const zExportSchema = z.object({\n  bookmarks: z.array(zExportBookmarkSchema),\n  lists: z.array(zExportListSchema).optional().default([]),\n});\n\nexport function toExportFormat(\n  bookmark: ZBookmark,\n  listIds?: string[],\n): z.infer<typeof zExportBookmarkSchema> {\n  let content = null;\n  switch (bookmark.content.type) {\n    case BookmarkTypes.LINK: {\n      content = {\n        type: bookmark.content.type,\n        url: bookmark.content.url,\n      };\n      break;\n    }\n    case BookmarkTypes.TEXT: {\n      content = {\n        type: bookmark.content.type,\n        text: bookmark.content.text,\n      };\n      break;\n    }\n    // Exclude asset types for now\n  }\n  return {\n    createdAt: Math.floor(bookmark.createdAt.getTime() / 1000),\n    title:\n      bookmark.title ??\n      (bookmark.content.type === BookmarkTypes.LINK\n        ? (bookmark.content.title ?? null)\n        : null),\n    tags: bookmark.tags.map((t) => t.name),\n    lists: listIds ?? [],\n    content,\n    note: bookmark.note ?? null,\n    archived: bookmark.archived,\n  };\n}\n\nexport function toExportListFormat(\n  list: ZBookmarkList,\n): z.infer<typeof zExportListSchema> {\n  return {\n    id: list.id,\n    name: list.name,\n    description: list.description ?? null,\n    icon: list.icon,\n    type: list.type,\n    query: list.query ?? null,\n    parentId: list.parentId,\n  };\n}\n\nexport function toNetscapeFormat(bookmarks: ZBookmark[]): string {\n  const header = `<!DOCTYPE NETSCAPE-Bookmark-file-1>\n<!-- This is an automatically generated file.\n     It will be read and overwritten.\n     DO NOT EDIT! -->\n<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">\n<TITLE>Bookmarks</TITLE>\n<H1>Bookmarks</H1>\n<DL><p>`;\n\n  const footer = `</DL><p>`;\n\n  const bookmarkEntries = bookmarks\n    .map((bookmark) => {\n      if (bookmark.content?.type !== BookmarkTypes.LINK) {\n        return \"\";\n      }\n      const addDate = bookmark.createdAt\n        ? `ADD_DATE=\"${Math.floor(bookmark.createdAt.getTime() / 1000)}\"`\n        : \"\";\n\n      const tagNames = bookmark.tags.map((t) => t.name).join(\",\");\n      const tags = tagNames.length > 0 ? `TAGS=\"${tagNames}\"` : \"\";\n\n      const encodedUrl = encodeURI(bookmark.content.url);\n      const displayTitle = bookmark.title ?? bookmark.content.url;\n      const encodedTitle = escapeHtml(displayTitle);\n\n      return `    <DT><A HREF=\"${encodedUrl}\" ${addDate} ${tags}>${encodedTitle}</A>`;\n    })\n    .filter(Boolean)\n    .join(\"\\n\");\n\n  return `${header}\\n${bookmarkEntries}\\n${footer}`;\n}\n\nfunction escapeHtml(input: string): string {\n  const escapeMap: Record<string, string> = {\n    \"&\": \"&amp;\",\n    \"'\": \"&#x27;\",\n    \"`\": \"&#x60;\",\n    '\"': \"&quot;\",\n    \"<\": \"&lt;\",\n    \">\": \"&gt;\",\n  };\n\n  return input.replace(/[&'`\"<>]/g, (match) => escapeMap[match] || \"\");\n}\n"
  },
  {
    "path": "packages/shared/import-export/fixtures/linkwarden-export.json",
    "content": "{\n  \"collections\": [\n    {\n      \"id\": 1,\n      \"name\": \"Tech\",\n      \"parentId\": null,\n      \"links\": [\n        {\n          \"name\": \"GitHub\",\n          \"url\": \"https://github.com\",\n          \"tags\": [{ \"name\": \"dev\" }],\n          \"createdAt\": \"2025-01-01T00:00:00.000Z\"\n        }\n      ]\n    },\n    {\n      \"id\": 2,\n      \"name\": \"Frontend\",\n      \"parentId\": 1,\n      \"links\": [\n        {\n          \"name\": \"React\",\n          \"url\": \"https://react.dev\",\n          \"tags\": [{ \"name\": \"js\" }, { \"name\": \"ui\" }],\n          \"createdAt\": \"2025-02-15T12:00:00.000Z\"\n        }\n      ]\n    },\n    {\n      \"id\": 3,\n      \"name\": \"Frameworks\",\n      \"parentId\": 2,\n      \"links\": [\n        {\n          \"name\": \"Next.js\",\n          \"url\": \"https://nextjs.org\",\n          \"tags\": [],\n          \"createdAt\": \"2025-03-10T08:30:00.000Z\"\n        }\n      ]\n    },\n    {\n      \"id\": 4,\n      \"name\": \"News\",\n      \"parentId\": null,\n      \"links\": [\n        {\n          \"name\": \"Hacker News\",\n          \"url\": \"https://news.ycombinator.com\",\n          \"tags\": [{ \"name\": \"news\" }],\n          \"createdAt\": \"2025-01-20T00:00:00.000Z\"\n        }\n      ]\n    },\n    {\n      \"id\": 5,\n      \"name\": \"Work\",\n      \"parentId\": null,\n      \"links\": [\n        {\n          \"name\": \"Example\",\n          \"url\": \"https://example.com\",\n          \"tags\": [{ \"name\": \"work\" }],\n          \"createdAt\": \"2025-01-01T00:00:00.000Z\"\n        }\n      ]\n    },\n    {\n      \"id\": 6,\n      \"name\": \"Personal\",\n      \"parentId\": null,\n      \"links\": [\n        {\n          \"name\": \"Example\",\n          \"url\": \"https://example.com\",\n          \"tags\": [{ \"name\": \"personal\" }],\n          \"createdAt\": \"2025-02-01T00:00:00.000Z\"\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/shared/import-export/importer.test.ts",
    "content": "import { describe, expect, it, vi } from \"vitest\";\n\nimport type { StagedBookmark } from \".\";\nimport { importBookmarksFromFile } from \".\";\n\nconst fakeFile = {\n  text: vi.fn().mockResolvedValue(\"fake file content\"),\n} as unknown as File;\n\ndescribe(\"importBookmarksFromFile\", () => {\n  it(\"creates root list, folders and stages bookmarks with progress\", async () => {\n    const parsers = {\n      pocket: vi.fn().mockReturnValue({\n        bookmarks: [\n          {\n            title: \"GitHub Repository\",\n            content: { type: \"link\", url: \"https://github.com/example/repo\" },\n            tags: [\"dev\", \"github\"],\n            addDate: 100,\n            paths: [[\"Development\", \"Projects\"]],\n          },\n          {\n            title: \"My Notes\",\n            content: {\n              type: \"text\",\n              text: \"Important notes about the project\",\n            },\n            tags: [\"notes\"],\n            addDate: 200,\n            paths: [[\"Personal\"]],\n            notes: \"Additional context\",\n            archived: true,\n          },\n          {\n            title: \"Blog Post\",\n            content: { type: \"link\", url: \"https://example.com/blog\" },\n            tags: [\"reading\", \"tech\"],\n            addDate: 300,\n            paths: [[\"Reading\", \"Tech\"]],\n          },\n          {\n            title: \"No Category Item\",\n            content: { type: \"link\", url: \"https://example.com/misc\" },\n            tags: [],\n            addDate: 400,\n            paths: [],\n          },\n          {\n            title: \"Duplicate URL Test\",\n            content: { type: \"link\", url: \"https://github.com/example/repo\" },\n            tags: [\"duplicate\"],\n            addDate: 50, // Earlier date\n            paths: [[\"Development\", \"Duplicates\"]],\n          },\n        ],\n        lists: [],\n      }),\n    };\n\n    const createdLists: { name: string; icon: string; parentId?: string }[] =\n      [];\n    const createList = vi.fn(\n      async (input: { name: string; icon: string; parentId?: string }) => {\n        createdLists.push(input);\n        return {\n          id: `${input.parentId ? input.parentId + \"/\" : \"\"}${input.name}`,\n        };\n      },\n    );\n\n    const stagedBookmarks: StagedBookmark[] = [];\n    const stageImportedBookmarks = vi.fn(\n      async (input: {\n        importSessionId: string;\n        bookmarks: StagedBookmark[];\n      }) => {\n        stagedBookmarks.push(...input.bookmarks);\n      },\n    );\n\n    const finalizeImportStaging = vi.fn();\n    const createImportSession = vi.fn(\n      async (_input: { name: string; rootListId: string }) => ({\n        id: \"session-1\",\n      }),\n    );\n\n    const progress: number[] = [];\n    const res = await importBookmarksFromFile(\n      {\n        file: fakeFile,\n        source: \"pocket\",\n        rootListName: \"Imported\",\n        deps: {\n          createList,\n          stageImportedBookmarks,\n          finalizeImportStaging,\n          createImportSession,\n        },\n        onProgress: (d, t) => progress.push(d / t),\n      },\n      { parsers },\n    );\n\n    expect(res.rootListId).toBe(\"Imported\");\n    expect(res.importSessionId).toBe(\"session-1\");\n    expect(res.counts).toEqual({\n      successes: 0,\n      failures: 0,\n      alreadyExisted: 0,\n      total: 5, // Using custom parser, no deduplication\n    });\n\n    // Root + all unique folders from paths\n    expect(createdLists).toEqual([\n      { name: \"Imported\", icon: \"⬆️\" },\n      { name: \"Development\", parentId: \"Imported\", icon: \"📁\" },\n      { name: \"Personal\", parentId: \"Imported\", icon: \"📁\" },\n      { name: \"Reading\", parentId: \"Imported\", icon: \"📁\" },\n      { name: \"Projects\", parentId: \"Imported/Development\", icon: \"📁\" },\n      { name: \"Tech\", parentId: \"Imported/Reading\", icon: \"📁\" },\n      { name: \"Duplicates\", parentId: \"Imported/Development\", icon: \"📁\" },\n    ]);\n\n    // Verify 5 bookmarks were staged (in 1 batch since < 50)\n    expect(stagedBookmarks).toHaveLength(5);\n    expect(stageImportedBookmarks).toHaveBeenCalledTimes(1);\n\n    // Verify GitHub link bookmark was staged correctly\n    const githubBookmark = stagedBookmarks.find(\n      (b) => b.url === \"https://github.com/example/repo\" && b.type === \"link\",\n    );\n    expect(githubBookmark).toBeDefined();\n    if (!githubBookmark) {\n      throw new Error(\"Expected GitHub bookmark to be staged\");\n    }\n    expect(githubBookmark.title).toBe(\"GitHub Repository\");\n    expect(githubBookmark.tags).toEqual([\"dev\", \"github\"]);\n    expect(githubBookmark.listIds).toEqual([\"Imported/Development/Projects\"]);\n\n    // Verify text bookmark was staged correctly\n    const textBookmark = stagedBookmarks.find((b) => b.type === \"text\");\n    expect(textBookmark).toBeDefined();\n    if (!textBookmark) {\n      throw new Error(\"Expected text bookmark to be staged\");\n    }\n    expect(textBookmark.content).toBe(\"Important notes about the project\");\n    expect(textBookmark.note).toBe(\"Additional context\");\n    expect(textBookmark.listIds).toEqual([\"Imported/Personal\"]);\n\n    // Verify bookmark with empty paths gets root list ID\n    const noCategoryBookmark = stagedBookmarks.find(\n      (b) => b.url === \"https://example.com/misc\",\n    );\n    expect(noCategoryBookmark).toBeDefined();\n    expect(noCategoryBookmark!.listIds).toEqual([\"Imported\"]);\n\n    // Verify finalizeImportStaging was called\n    expect(finalizeImportStaging).toHaveBeenCalledWith(\"session-1\");\n\n    expect(progress).toContain(0);\n    expect(progress.at(-1)).toBe(1);\n  });\n\n  it(\"returns zero counts and null rootListId when no bookmarks\", async () => {\n    const parsers = {\n      html: vi.fn().mockReturnValue({ bookmarks: [], lists: [] }),\n    };\n    const res = await importBookmarksFromFile(\n      {\n        file: fakeFile,\n        source: \"html\",\n        rootListName: \"Imported\",\n        deps: {\n          createList: vi.fn(),\n          stageImportedBookmarks: vi.fn(),\n          finalizeImportStaging: vi.fn(),\n          createImportSession: vi.fn(async () => ({ id: \"session-1\" })),\n        },\n      },\n      { parsers },\n    );\n    expect(res).toEqual({\n      counts: { successes: 0, failures: 0, alreadyExisted: 0, total: 0 },\n      rootListId: null,\n      importSessionId: null,\n    });\n  });\n\n  it(\"stages all bookmarks successfully\", async () => {\n    const parsers = {\n      pocket: vi.fn().mockReturnValue({\n        bookmarks: [\n          {\n            title: \"Bookmark 1\",\n            content: { type: \"link\", url: \"https://example.com/1\" },\n            tags: [\"tag1\"],\n            addDate: 100,\n            paths: [[\"Category1\"]],\n          },\n          {\n            title: \"Bookmark 2\",\n            content: { type: \"link\", url: \"https://example.com/2\" },\n            tags: [\"tag2\"],\n            addDate: 200,\n            paths: [[\"Category2\"]],\n          },\n          {\n            title: \"Bookmark 3\",\n            content: { type: \"link\", url: \"https://example.com/3\" },\n            tags: [\"tag3\"],\n            addDate: 300,\n            paths: [[\"Category1\"]],\n          },\n        ],\n        lists: [],\n      }),\n    };\n\n    const createdLists: { name: string; icon: string; parentId?: string }[] =\n      [];\n    const createList = vi.fn(\n      async (input: { name: string; icon: string; parentId?: string }) => {\n        createdLists.push(input);\n        return {\n          id: `${input.parentId ? input.parentId + \"/\" : \"\"}${input.name}`,\n        };\n      },\n    );\n\n    const stagedBookmarks: StagedBookmark[] = [];\n    const stageImportedBookmarks = vi.fn(\n      async (input: {\n        importSessionId: string;\n        bookmarks: StagedBookmark[];\n      }) => {\n        stagedBookmarks.push(...input.bookmarks);\n      },\n    );\n\n    const finalizeImportStaging = vi.fn();\n    const createImportSession = vi.fn(\n      async (_input: { name: string; rootListId: string }) => ({\n        id: \"session-1\",\n      }),\n    );\n\n    const progress: number[] = [];\n    const res = await importBookmarksFromFile(\n      {\n        file: fakeFile,\n        source: \"pocket\",\n        rootListName: \"Imported\",\n        deps: {\n          createList,\n          stageImportedBookmarks,\n          finalizeImportStaging,\n          createImportSession,\n        },\n        onProgress: (d, t) => progress.push(d / t),\n      },\n      { parsers },\n    );\n\n    expect(res.rootListId).toBe(\"Imported\");\n    expect(res.importSessionId).toBe(\"session-1\");\n    expect(res.counts).toEqual({\n      successes: 0,\n      failures: 0,\n      alreadyExisted: 0,\n      total: 3,\n    });\n\n    // Should create folders for all bookmarks\n    expect(createdLists).toEqual([\n      { name: \"Imported\", icon: \"⬆️\" },\n      { name: \"Category1\", parentId: \"Imported\", icon: \"📁\" },\n      { name: \"Category2\", parentId: \"Imported\", icon: \"📁\" },\n    ]);\n\n    // All bookmarks should be staged (in 1 batch since < 50)\n    expect(stagedBookmarks).toHaveLength(3);\n    expect(stageImportedBookmarks).toHaveBeenCalledTimes(1);\n\n    // Verify finalizeImportStaging was called\n    expect(finalizeImportStaging).toHaveBeenCalledWith(\"session-1\");\n\n    // Progress should complete\n    expect(progress).toContain(0);\n    expect(progress.at(-1)).toBe(1);\n  });\n\n  it(\"stages bookmarks with different paths\", async () => {\n    const parsers = {\n      pocket: vi.fn().mockReturnValue({\n        bookmarks: [\n          {\n            title: \"Bookmark 1\",\n            content: { type: \"link\", url: \"https://example.com/1\" },\n            tags: [\"tag1\"],\n            addDate: 100,\n            paths: [[\"Path1\"]],\n          },\n          {\n            title: \"Bookmark 2\",\n            content: { type: \"link\", url: \"https://example.com/2\" },\n            tags: [\"tag2\"],\n            addDate: 200,\n            paths: [[\"Path2\"]],\n          },\n          {\n            title: \"Bookmark 3\",\n            content: { type: \"link\", url: \"https://example.com/3\" },\n            tags: [\"tag3\"],\n            addDate: 300,\n            paths: [[\"Path2\"]],\n          },\n        ],\n        lists: [],\n      }),\n    };\n\n    const createList = vi.fn(\n      async (input: { name: string; icon: string; parentId?: string }) => {\n        return {\n          id: `${input.parentId ? input.parentId + \"/\" : \"\"}${input.name}`,\n        };\n      },\n    );\n\n    const stagedBookmarks: StagedBookmark[] = [];\n    const stageImportedBookmarks = vi.fn(\n      async (input: {\n        importSessionId: string;\n        bookmarks: StagedBookmark[];\n      }) => {\n        stagedBookmarks.push(...input.bookmarks);\n      },\n    );\n\n    const finalizeImportStaging = vi.fn();\n    const createImportSession = vi.fn(\n      async (_input: { name: string; rootListId: string }) => ({\n        id: \"session-1\",\n      }),\n    );\n\n    const progress: number[] = [];\n    const res = await importBookmarksFromFile(\n      {\n        file: fakeFile,\n        source: \"pocket\",\n        rootListName: \"Imported\",\n        deps: {\n          createList,\n          stageImportedBookmarks,\n          finalizeImportStaging,\n          createImportSession,\n        },\n        onProgress: (d, t) => progress.push(d / t),\n      },\n      { parsers },\n    );\n\n    expect(res.rootListId).toBe(\"Imported\");\n    expect(res.importSessionId).toBe(\"session-1\");\n    expect(res.counts).toEqual({\n      successes: 0,\n      failures: 0,\n      alreadyExisted: 0,\n      total: 3,\n    });\n\n    // All bookmarks should be staged (in 1 batch since < 50)\n    expect(stagedBookmarks).toHaveLength(3);\n    expect(stageImportedBookmarks).toHaveBeenCalledTimes(1);\n\n    // Verify finalizeImportStaging was called\n    expect(finalizeImportStaging).toHaveBeenCalledWith(\"session-1\");\n  });\n\n  it(\"preserves separate list memberships when external list IDs differ\", async () => {\n    const parsers = {\n      pocket: vi.fn().mockReturnValue({\n        bookmarks: [\n          {\n            title: \"Bookmark 1\",\n            content: { type: \"link\", url: \"https://example.com/1\" },\n            tags: [],\n            addDate: 100,\n            paths: [],\n            listExternalIds: [\"child-1-id\"],\n          },\n          {\n            title: \"Bookmark 2\",\n            content: { type: \"link\", url: \"https://example.com/2\" },\n            tags: [],\n            addDate: 200,\n            paths: [],\n            listExternalIds: [\"child-2-id\"],\n          },\n        ],\n        lists: [\n          {\n            externalId: \"parent-id\",\n            name: \"Projects\",\n            parentExternalId: null,\n            type: \"manual\",\n          },\n          {\n            externalId: \"child-1-id\",\n            name: \"Inbox\",\n            parentExternalId: \"parent-id\",\n            type: \"manual\",\n          },\n          {\n            externalId: \"child-2-id\",\n            name: \"Inbox\",\n            parentExternalId: \"parent-id\",\n            type: \"manual\",\n          },\n        ],\n      }),\n    };\n\n    let idCounter = 0;\n    const createdLists: { id: string; name: string; parentId?: string }[] = [];\n    const createList = vi.fn(\n      async (input: { name: string; icon: string; parentId?: string }) => {\n        const id = `list-${idCounter++}`;\n        createdLists.push({ id, name: input.name, parentId: input.parentId });\n        return { id };\n      },\n    );\n\n    const stagedBookmarks: StagedBookmark[] = [];\n    const stageImportedBookmarks = vi.fn(\n      async (input: {\n        importSessionId: string;\n        bookmarks: StagedBookmark[];\n      }) => {\n        stagedBookmarks.push(...input.bookmarks);\n      },\n    );\n\n    await importBookmarksFromFile(\n      {\n        file: fakeFile,\n        source: \"pocket\",\n        rootListName: \"Imported\",\n        deps: {\n          createList,\n          stageImportedBookmarks,\n          finalizeImportStaging: vi.fn(),\n          createImportSession: vi.fn(async () => ({ id: \"session-1\" })),\n        },\n      },\n      { parsers },\n    );\n\n    const projectsFolder = createdLists.find(\n      (list) => list.name === \"Projects\",\n    );\n    expect(projectsFolder).toBeDefined();\n\n    const duplicateFolders = createdLists.filter(\n      (list) => list.name === \"Inbox\" && list.parentId === projectsFolder?.id,\n    );\n    expect(duplicateFolders).toHaveLength(2);\n\n    const firstBookmark = stagedBookmarks.find(\n      (bookmark) => bookmark.url === \"https://example.com/1\",\n    );\n    const secondBookmark = stagedBookmarks.find(\n      (bookmark) => bookmark.url === \"https://example.com/2\",\n    );\n\n    expect(firstBookmark).toBeDefined();\n    expect(secondBookmark).toBeDefined();\n    expect(firstBookmark?.listIds[0]).not.toEqual(secondBookmark?.listIds[0]);\n    expect(duplicateFolders.map((list) => list.id)).toContain(\n      firstBookmark?.listIds[0],\n    );\n    expect(duplicateFolders.map((list) => list.id)).toContain(\n      secondBookmark?.listIds[0],\n    );\n  });\n\n  it(\"creates smart lists with their queries during import\", async () => {\n    const parsers = {\n      karakeep: vi.fn().mockReturnValue({\n        bookmarks: [\n          {\n            title: \"Bookmark 1\",\n            content: { type: \"link\", url: \"https://example.com/1\" },\n            tags: [],\n            addDate: 100,\n            paths: [],\n            listExternalIds: [\"manual-list-id\"],\n          },\n        ],\n        lists: [\n          {\n            externalId: \"manual-list-id\",\n            name: \"Manual\",\n            icon: \"⭐\",\n            description: \"Manual list description\",\n            parentExternalId: null,\n            type: \"manual\",\n          },\n          {\n            externalId: \"smart-list-id\",\n            name: \"Smart\",\n            icon: \"⚡\",\n            description: \"Smart list description\",\n            parentExternalId: null,\n            type: \"smart\",\n            query: \"tag:read-later\",\n          },\n        ],\n      }),\n    };\n\n    const createdLists: {\n      name: string;\n      icon: string;\n      description?: string;\n      parentId?: string;\n      type?: \"manual\" | \"smart\";\n      query?: string;\n    }[] = [];\n\n    const createList = vi.fn(\n      async (input: {\n        name: string;\n        icon: string;\n        description?: string;\n        parentId?: string;\n        type?: \"manual\" | \"smart\";\n        query?: string;\n      }) => {\n        createdLists.push(input);\n        return {\n          id: `${input.parentId ? input.parentId + \"/\" : \"\"}${input.name}`,\n        };\n      },\n    );\n\n    await importBookmarksFromFile(\n      {\n        file: fakeFile,\n        source: \"karakeep\",\n        rootListName: \"Imported\",\n        deps: {\n          createList,\n          stageImportedBookmarks: vi.fn(async () => undefined),\n          finalizeImportStaging: vi.fn(),\n          createImportSession: vi.fn(async () => ({ id: \"session-1\" })),\n        },\n      },\n      { parsers },\n    );\n\n    expect(createdLists).toContainEqual({\n      name: \"Smart\",\n      parentId: \"Imported\",\n      icon: \"⚡\",\n      description: \"Smart list description\",\n      type: \"smart\",\n      query: \"tag:read-later\",\n    });\n\n    expect(createdLists).toContainEqual({\n      name: \"Manual\",\n      parentId: \"Imported\",\n      icon: \"⭐\",\n      description: \"Manual list description\",\n    });\n  });\n\n  it(\"handles HTML bookmarks with empty folder names\", async () => {\n    const htmlContent = `<!DOCTYPE NETSCAPE-Bookmark-file-1>\n<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">\n<TITLE>Bookmarks</TITLE>\n<H1>Bookmarks</H1>\n<DL><p>\n    <DT><H3 ADD_DATE=\"1765995928\" LAST_MODIFIED=\"1765995928\">Bluetooth Fernbedienung</H3>\n    <DL><p>\n        <DT><H3 ADD_DATE=\"1765995928\" LAST_MODIFIED=\"0\"></H3>\n        <DL><p>\n            <DT><A HREF=\"https://www.example.com/product.html\" ADD_DATE=\"1593444456\">Example Product</A>\n        </DL><p>\n    </DL><p>\n</DL><p>`;\n\n    const mockFile = {\n      text: vi.fn().mockResolvedValue(htmlContent),\n    } as unknown as File;\n\n    const createdLists: { name: string; icon: string; parentId?: string }[] =\n      [];\n    const createList = vi.fn(\n      async (input: { name: string; icon: string; parentId?: string }) => {\n        createdLists.push(input);\n        return {\n          id: `${input.parentId ? input.parentId + \"/\" : \"\"}${input.name}`,\n        };\n      },\n    );\n\n    const stagedBookmarks: StagedBookmark[] = [];\n    const stageImportedBookmarks = vi.fn(\n      async (input: {\n        importSessionId: string;\n        bookmarks: StagedBookmark[];\n      }) => {\n        stagedBookmarks.push(...input.bookmarks);\n      },\n    );\n\n    const finalizeImportStaging = vi.fn();\n    const createImportSession = vi.fn(\n      async (_input: { name: string; rootListId: string }) => ({\n        id: \"session-1\",\n      }),\n    );\n\n    const res = await importBookmarksFromFile({\n      file: mockFile,\n      source: \"html\",\n      rootListName: \"HTML Import\",\n      deps: {\n        createList,\n        stageImportedBookmarks,\n        finalizeImportStaging,\n        createImportSession,\n      },\n    });\n\n    expect(res.counts).toEqual({\n      successes: 0,\n      failures: 0,\n      alreadyExisted: 0,\n      total: 1,\n    });\n\n    // Verify that the empty folder name was replaced with \"Unnamed\"\n    expect(createdLists).toEqual([\n      { name: \"HTML Import\", icon: \"⬆️\" },\n      { name: \"Bluetooth Fernbedienung\", parentId: \"HTML Import\", icon: \"📁\" },\n      {\n        name: \"Unnamed\",\n        parentId: \"HTML Import/Bluetooth Fernbedienung\",\n        icon: \"📁\",\n      },\n    ]);\n\n    // Verify the bookmark was staged with correct listIds\n    expect(stagedBookmarks).toHaveLength(1);\n    expect(stagedBookmarks[0]).toMatchObject({\n      title: \"Example Product\",\n      url: \"https://www.example.com/product.html\",\n      type: \"link\",\n      tags: [],\n      listIds: [\"HTML Import/Bluetooth Fernbedienung/Unnamed\"],\n    });\n\n    // Verify finalizeImportStaging was called\n    expect(finalizeImportStaging).toHaveBeenCalledWith(\"session-1\");\n  });\n\n  it(\"parses mymind CSV export correctly\", async () => {\n    const mymindCsv = `id,type,title,url,content,note,tags,created\n1pYm0O0hY4WnmKN,WebPage,mymind,https://access.mymind.com/everything,,,\"Wellness,Self-Improvement,Psychology\",2024-12-04T23:02:10Z\n1pYm0O0hY5ltduL,WebPage,Movies / TV / Anime,https://fmhy.pages.dev/videopiracyguide,,\"Free Media!\",\"Tools,media,Entertainment\",2024-12-04T23:02:32Z\n1pYm0O0hY8oFq9C,Note,,,\"• Critical Thinking\n• Empathy\",,,2024-12-04T23:05:23Z`;\n\n    const mockFile = {\n      text: vi.fn().mockResolvedValue(mymindCsv),\n    } as unknown as File;\n\n    const stagedBookmarks: StagedBookmark[] = [];\n    const stageImportedBookmarks = vi.fn(\n      async (input: {\n        importSessionId: string;\n        bookmarks: StagedBookmark[];\n      }) => {\n        stagedBookmarks.push(...input.bookmarks);\n      },\n    );\n\n    const finalizeImportStaging = vi.fn();\n    const createImportSession = vi.fn(\n      async (_input: { name: string; rootListId: string }) => ({\n        id: \"session-1\",\n      }),\n    );\n\n    const res = await importBookmarksFromFile({\n      file: mockFile,\n      source: \"mymind\",\n      rootListName: \"mymind Import\",\n      deps: {\n        createList: vi.fn(\n          async (input: { name: string; icon: string; parentId?: string }) => ({\n            id: `${input.parentId ? input.parentId + \"/\" : \"\"}${input.name}`,\n          }),\n        ),\n        stageImportedBookmarks,\n        finalizeImportStaging,\n        createImportSession,\n      },\n    });\n\n    expect(res.counts).toEqual({\n      successes: 0,\n      failures: 0,\n      alreadyExisted: 0,\n      total: 3,\n    });\n\n    // Verify 3 bookmarks were staged\n    expect(stagedBookmarks).toHaveLength(3);\n\n    // Verify first bookmark (WebPage with URL) - mymind has no paths, so root list\n    expect(stagedBookmarks[0]).toMatchObject({\n      title: \"mymind\",\n      url: \"https://access.mymind.com/everything\",\n      type: \"link\",\n      tags: [\"Wellness\", \"Self-Improvement\", \"Psychology\"],\n      listIds: [\"mymind Import\"],\n    });\n    expect(stagedBookmarks[0].sourceAddedAt).toEqual(\n      new Date(\"2024-12-04T23:02:10Z\"),\n    );\n\n    // Verify second bookmark (WebPage with note)\n    expect(stagedBookmarks[1]).toMatchObject({\n      title: \"Movies / TV / Anime\",\n      url: \"https://fmhy.pages.dev/videopiracyguide\",\n      type: \"link\",\n      tags: [\"Tools\", \"media\", \"Entertainment\"],\n      note: \"Free Media!\",\n      listIds: [\"mymind Import\"],\n    });\n\n    // Verify third bookmark (Note with text content)\n    expect(stagedBookmarks[2]).toMatchObject({\n      title: \"\",\n      content: \"• Critical Thinking\\n• Empathy\",\n      type: \"text\",\n      tags: [],\n      listIds: [\"mymind Import\"],\n    });\n\n    // Verify finalizeImportStaging was called\n    expect(finalizeImportStaging).toHaveBeenCalledWith(\"session-1\");\n  });\n});\n"
  },
  {
    "path": "packages/shared/import-export/importer.ts",
    "content": "import { MAX_LIST_NAME_LENGTH } from \"../types/lists\";\nimport { ImportSource, ParsedImportFile, parseImportFile } from \"./parsers\";\n\nexport interface ImportCounts {\n  successes: number;\n  failures: number;\n  alreadyExisted: number;\n  total: number;\n}\n\nexport interface StagedBookmark {\n  type: \"link\" | \"text\" | \"asset\";\n  url?: string;\n  title?: string;\n  content?: string;\n  note?: string;\n  tags: string[];\n  listIds: string[];\n  sourceAddedAt?: Date;\n}\n\nexport interface ImportDeps {\n  createList: (input: {\n    name: string;\n    icon: string;\n    description?: string;\n    parentId?: string;\n    type?: \"manual\" | \"smart\";\n    query?: string;\n  }) => Promise<{ id: string }>;\n  stageImportedBookmarks: (input: {\n    importSessionId: string;\n    bookmarks: StagedBookmark[];\n  }) => Promise<void>;\n  createImportSession: (input: {\n    name: string;\n    rootListId: string;\n  }) => Promise<{ id: string }>;\n  finalizeImportStaging: (sessionId: string) => Promise<void>;\n}\n\nexport interface ImportOptions {\n  concurrencyLimit?: number;\n  parsers?: Partial<\n    Record<ImportSource, (textContent: string) => ParsedImportFile>\n  >;\n}\n\nexport interface ImportResult {\n  counts: ImportCounts;\n  rootListId: string | null;\n  importSessionId: string | null;\n}\n\nexport async function importBookmarksFromFile(\n  {\n    file,\n    source,\n    rootListName,\n    deps,\n    onProgress,\n  }: {\n    file: { text: () => Promise<string> };\n    source: ImportSource;\n    rootListName: string;\n    deps: ImportDeps;\n    onProgress?: (done: number, total: number) => void;\n  },\n  options: ImportOptions = {},\n): Promise<ImportResult> {\n  const { parsers } = options;\n\n  const textContent = await file.text();\n  const parsedImport = parsers?.[source]\n    ? parsers[source]!(textContent)\n    : parseImportFile(source, textContent);\n  const parsedBookmarks = parsedImport.bookmarks;\n  const parsedLists = parsedImport.lists;\n  if (parsedBookmarks.length === 0) {\n    return {\n      counts: { successes: 0, failures: 0, alreadyExisted: 0, total: 0 },\n      rootListId: null,\n      importSessionId: null,\n    };\n  }\n\n  const rootList = await deps.createList({ name: rootListName, icon: \"⬆️\" });\n  const session = await deps.createImportSession({\n    name: `${source.charAt(0).toUpperCase() + source.slice(1)} Import - ${new Date().toLocaleDateString()}`,\n    rootListId: rootList.id,\n  });\n\n  onProgress?.(0, parsedBookmarks.length);\n\n  const externalListIdToCreatedListId: Record<string, string> = {};\n  if (parsedLists.length > 0) {\n    const unresolvedLists = new Map(\n      parsedLists.map((list) => [list.externalId, list]),\n    );\n\n    while (unresolvedLists.size > 0) {\n      let createdAny = false;\n\n      for (const [externalId, list] of unresolvedLists) {\n        if (\n          list.parentExternalId &&\n          !externalListIdToCreatedListId[list.parentExternalId]\n        ) {\n          continue;\n        }\n\n        const parentId = list.parentExternalId\n          ? externalListIdToCreatedListId[list.parentExternalId]\n          : rootList.id;\n\n        const createdList = await deps.createList({\n          name: list.name.substring(0, MAX_LIST_NAME_LENGTH),\n          parentId,\n          icon: list.icon ?? \"📁\",\n          description: list.description,\n          ...(list.type === \"smart\" && list.query\n            ? { type: \"smart\", query: list.query }\n            : {}),\n        });\n        externalListIdToCreatedListId[externalId] = createdList.id;\n        unresolvedLists.delete(externalId);\n        createdAny = true;\n      }\n\n      // Break cycles or unresolved parent references by attaching remaining\n      // lists to the import root.\n      if (!createdAny) {\n        for (const [externalId, list] of unresolvedLists) {\n          const createdList = await deps.createList({\n            name: list.name.substring(0, MAX_LIST_NAME_LENGTH),\n            parentId: rootList.id,\n            icon: list.icon ?? \"📁\",\n            description: list.description,\n            ...(list.type === \"smart\" && list.query\n              ? { type: \"smart\", query: list.query }\n              : {}),\n          });\n          externalListIdToCreatedListId[externalId] = createdList.id;\n        }\n        unresolvedLists.clear();\n      }\n    }\n  }\n\n  const PATH_DELIMITER = \"$$__$$\";\n  const getPathKey = (parts: string[]) => parts.join(PATH_DELIMITER);\n  const bookmarksWithPathMembership = parsedBookmarks.filter(\n    (bookmark) =>\n      !bookmark.listExternalIds || bookmark.listExternalIds.length === 0,\n  );\n\n  // Build required paths\n  const allRequiredPaths = new Map<string, string>();\n  for (const bookmark of bookmarksWithPathMembership) {\n    for (const path of bookmark.paths) {\n      if (path && path.length > 0) {\n        for (let i = 1; i <= path.length; i++) {\n          const subPath = path.slice(0, i);\n          const pathKey = getPathKey(subPath);\n          const folderName = subPath[subPath.length - 1];\n          if (!allRequiredPaths.has(pathKey)) {\n            allRequiredPaths.set(pathKey, folderName);\n          }\n        }\n      }\n    }\n  }\n\n  const allRequiredPathsArray = Array.from(allRequiredPaths.entries())\n    .map(([pathKey, folderName]) => ({ pathKey, folderName }))\n    .sort(\n      (a, b) =>\n        a.pathKey.split(PATH_DELIMITER).length -\n        b.pathKey.split(PATH_DELIMITER).length,\n    );\n\n  const pathMap: Record<string, string> = { \"\": rootList.id };\n\n  for (const { pathKey, folderName } of allRequiredPathsArray) {\n    const parts = pathKey.split(PATH_DELIMITER);\n    const parentKey = parts.slice(0, -1).join(PATH_DELIMITER);\n    const parentId = pathMap[parentKey] || rootList.id;\n\n    const folderList = await deps.createList({\n      name: folderName.substring(0, MAX_LIST_NAME_LENGTH),\n      parentId,\n      icon: \"📁\",\n    });\n    pathMap[pathKey] = folderList.id;\n  }\n\n  // Prepare all bookmarks for staging\n  const bookmarksToStage: StagedBookmark[] = parsedBookmarks.map((bookmark) => {\n    // Convert paths to list IDs using pathMap\n    // If no paths, assign to root list\n    const listIdsFromPaths =\n      bookmark.paths.length === 0\n        ? [rootList.id]\n        : bookmark.paths\n            .map((path) => {\n              if (path.length === 0) {\n                return rootList.id;\n              }\n              const pathKey = getPathKey(path);\n              return pathMap[pathKey] || rootList.id;\n            })\n            .filter((id, index, arr) => arr.indexOf(id) === index); // dedupe\n\n    const externalListIds = bookmark.listExternalIds ?? [];\n    const listIdsFromExternalListIds =\n      externalListIds.length > 0\n        ? [\n            ...new Set(\n              externalListIds.map((id) => externalListIdToCreatedListId[id]),\n            ),\n          ].filter((id): id is string => Boolean(id))\n        : [];\n\n    const listIds =\n      listIdsFromExternalListIds.length > 0\n        ? listIdsFromExternalListIds\n        : listIdsFromPaths;\n\n    // Determine type and extract content appropriately\n    let type: \"link\" | \"text\" | \"asset\" = \"link\";\n    let url: string | undefined;\n    let textContent: string | undefined;\n\n    if (bookmark.content) {\n      if (bookmark.content.type === \"link\") {\n        type = \"link\";\n        url = bookmark.content.url;\n      } else if (bookmark.content.type === \"text\") {\n        type = \"text\";\n        textContent = bookmark.content.text;\n      }\n    }\n\n    return {\n      type,\n      url,\n      title: bookmark.title,\n      content: textContent,\n      note: bookmark.notes,\n      tags: bookmark.tags ?? [],\n      listIds,\n      sourceAddedAt: bookmark.addDate\n        ? new Date(bookmark.addDate * 1000)\n        : undefined,\n    };\n  });\n\n  // Stage bookmarks in batches of 50\n  const BATCH_SIZE = 50;\n  let staged = 0;\n\n  for (let i = 0; i < bookmarksToStage.length; i += BATCH_SIZE) {\n    const batch = bookmarksToStage.slice(i, i + BATCH_SIZE);\n    await deps.stageImportedBookmarks({\n      importSessionId: session.id,\n      bookmarks: batch,\n    });\n    staged += batch.length;\n    onProgress?.(staged, parsedBookmarks.length);\n  }\n\n  // Finalize staging - marks session as \"pending\" for worker pickup\n  await deps.finalizeImportStaging(session.id);\n\n  return {\n    counts: {\n      successes: 0,\n      failures: 0,\n      alreadyExisted: 0,\n      total: parsedBookmarks.length,\n    },\n    rootListId: rootList.id,\n    importSessionId: session.id,\n  };\n}\n"
  },
  {
    "path": "packages/shared/import-export/index.ts",
    "content": "export * from \"./exporters\";\nexport * from \"./importer\";\nexport type {\n  ImportSource,\n  ParsedBookmark,\n  ParsedImportFile,\n  ParsedImportList,\n} from \"./parsers\";\nexport { parseImportFile } from \"./parsers\";\n"
  },
  {
    "path": "packages/shared/import-export/parsers.test.ts",
    "content": "import fs from \"node:fs\";\nimport path from \"node:path\";\nimport { describe, expect, it } from \"vitest\";\n\nimport { parseImportFile } from \"./parsers\";\n\ndescribe(\"parseNetscapeBookmarkFile\", () => {\n  it(\"parses a simple bookmark file with single bookmark\", () => {\n    const html = `<!DOCTYPE NETSCAPE-Bookmark-file-1>\n<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">\n<TITLE>Bookmarks</TITLE>\n<H1>Bookmarks</H1>\n<DL><p>\n    <DT><A HREF=\"https://example.com\" ADD_DATE=\"1234567890\">Example Site</A>\n</DL><p>`;\n\n    const parsed = parseImportFile(\"html\", html);\n\n    const result = parsed.bookmarks;\n\n    expect(result).toHaveLength(1);\n    expect(result[0]).toMatchObject({\n      title: \"Example Site\",\n      content: {\n        type: \"link\",\n        url: \"https://example.com\",\n      },\n      tags: [],\n      addDate: 1234567890,\n      paths: [[]],\n    });\n  });\n\n  it(\"parses bookmarks with tags\", () => {\n    const html = `<!DOCTYPE NETSCAPE-Bookmark-file-1>\n<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">\n<TITLE>Bookmarks</TITLE>\n<H1>Bookmarks</H1>\n<DL><p>\n    <DT><A HREF=\"https://example.com\" ADD_DATE=\"1234567890\" TAGS=\"tag1,tag2,tag3\">Example Site</A>\n</DL><p>`;\n\n    const parsed = parseImportFile(\"html\", html);\n\n    const result = parsed.bookmarks;\n\n    expect(result).toHaveLength(1);\n    expect(result[0].tags).toEqual([\"tag1\", \"tag2\", \"tag3\"]);\n  });\n\n  it(\"parses bookmarks in nested folders\", () => {\n    const html = `<!DOCTYPE NETSCAPE-Bookmark-file-1>\n<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">\n<TITLE>Bookmarks</TITLE>\n<H1>Bookmarks</H1>\n<DL><p>\n    <DT><H3 ADD_DATE=\"1234567890\" LAST_MODIFIED=\"1234567891\">Folder1</H3>\n    <DL><p>\n        <DT><H3 ADD_DATE=\"1234567892\" LAST_MODIFIED=\"1234567893\">Folder2</H3>\n        <DL><p>\n            <DT><A HREF=\"https://example.com\" ADD_DATE=\"1234567894\">Nested Bookmark</A>\n        </DL><p>\n    </DL><p>\n</DL><p>`;\n\n    const parsed = parseImportFile(\"html\", html);\n\n    const result = parsed.bookmarks;\n\n    expect(result).toHaveLength(1);\n    expect(result[0]).toMatchObject({\n      title: \"Nested Bookmark\",\n      content: {\n        type: \"link\",\n        url: \"https://example.com\",\n      },\n      paths: [[\"Folder1\", \"Folder2\"]],\n    });\n  });\n\n  it(\"handles empty folder names by replacing with 'Unnamed'\", () => {\n    const html = `<!DOCTYPE NETSCAPE-Bookmark-file-1>\n<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">\n<TITLE>Bookmarks</TITLE>\n<H1>Bookmarks</H1>\n<DL><p>\n    <DT><H3 ADD_DATE=\"1234567890\" LAST_MODIFIED=\"1234567891\">Named Folder</H3>\n    <DL><p>\n        <DT><H3 ADD_DATE=\"1234567892\" LAST_MODIFIED=\"0\"></H3>\n        <DL><p>\n            <DT><A HREF=\"https://example.com\" ADD_DATE=\"1234567894\">Bookmark</A>\n        </DL><p>\n    </DL><p>\n</DL><p>`;\n\n    const parsed = parseImportFile(\"html\", html);\n\n    const result = parsed.bookmarks;\n\n    expect(result).toHaveLength(1);\n    expect(result[0].paths).toEqual([[\"Named Folder\", \"Unnamed\"]]);\n  });\n\n  it(\"parses multiple bookmarks in different folders\", () => {\n    const html = `<!DOCTYPE NETSCAPE-Bookmark-file-1>\n<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">\n<TITLE>Bookmarks</TITLE>\n<H1>Bookmarks</H1>\n<DL><p>\n    <DT><H3 ADD_DATE=\"1234567890\">Tech</H3>\n    <DL><p>\n        <DT><A HREF=\"https://github.com\" ADD_DATE=\"1234567891\">GitHub</A>\n        <DT><A HREF=\"https://stackoverflow.com\" ADD_DATE=\"1234567892\">Stack Overflow</A>\n    </DL><p>\n    <DT><H3 ADD_DATE=\"1234567893\">News</H3>\n    <DL><p>\n        <DT><A HREF=\"https://news.ycombinator.com\" ADD_DATE=\"1234567894\">Hacker News</A>\n    </DL><p>\n</DL><p>`;\n\n    const parsed = parseImportFile(\"html\", html);\n\n    const result = parsed.bookmarks;\n\n    expect(result).toHaveLength(3);\n\n    expect(result[0]).toMatchObject({\n      title: \"GitHub\",\n      content: { type: \"link\", url: \"https://github.com\" },\n      paths: [[\"Tech\"]],\n    });\n\n    expect(result[1]).toMatchObject({\n      title: \"Stack Overflow\",\n      content: { type: \"link\", url: \"https://stackoverflow.com\" },\n      paths: [[\"Tech\"]],\n    });\n\n    expect(result[2]).toMatchObject({\n      title: \"Hacker News\",\n      content: { type: \"link\", url: \"https://news.ycombinator.com\" },\n      paths: [[\"News\"]],\n    });\n  });\n\n  it(\"parses bookmarks at root level (no folders)\", () => {\n    const html = `<!DOCTYPE NETSCAPE-Bookmark-file-1>\n<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">\n<TITLE>Bookmarks</TITLE>\n<H1>Bookmarks</H1>\n<DL><p>\n    <DT><A HREF=\"https://example1.com\" ADD_DATE=\"1234567890\">Bookmark 1</A>\n    <DT><A HREF=\"https://example2.com\" ADD_DATE=\"1234567891\">Bookmark 2</A>\n</DL><p>`;\n\n    const parsed = parseImportFile(\"html\", html);\n\n    const result = parsed.bookmarks;\n\n    expect(result).toHaveLength(2);\n    expect(result[0].paths).toEqual([[]]);\n    expect(result[1].paths).toEqual([[]]);\n  });\n\n  it(\"handles deeply nested folder structures\", () => {\n    const html = `<!DOCTYPE NETSCAPE-Bookmark-file-1>\n<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">\n<TITLE>Bookmarks</TITLE>\n<H1>Bookmarks</H1>\n<DL><p>\n    <DT><H3>Level1</H3>\n    <DL><p>\n        <DT><H3>Level2</H3>\n        <DL><p>\n            <DT><H3>Level3</H3>\n            <DL><p>\n                <DT><H3>Level4</H3>\n                <DL><p>\n                    <DT><A HREF=\"https://example.com\" ADD_DATE=\"1234567890\">Deep Bookmark</A>\n                </DL><p>\n            </DL><p>\n        </DL><p>\n    </DL><p>\n</DL><p>`;\n\n    const parsed = parseImportFile(\"html\", html);\n\n    const result = parsed.bookmarks;\n\n    expect(result).toHaveLength(1);\n    expect(result[0].paths).toEqual([[\"Level1\", \"Level2\", \"Level3\", \"Level4\"]]);\n  });\n\n  it(\"deduplicates bookmarks with the same URL\", () => {\n    const html = `<!DOCTYPE NETSCAPE-Bookmark-file-1>\n<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">\n<TITLE>Bookmarks</TITLE>\n<H1>Bookmarks</H1>\n<DL><p>\n    <DT><H3>Folder1</H3>\n    <DL><p>\n        <DT><A HREF=\"https://example.com\" ADD_DATE=\"1234567890\" TAGS=\"tag1\">First Instance</A>\n    </DL><p>\n    <DT><H3>Folder2</H3>\n    <DL><p>\n        <DT><A HREF=\"https://example.com\" ADD_DATE=\"1234567891\" TAGS=\"tag2\">Second Instance</A>\n    </DL><p>\n</DL><p>`;\n\n    const parsed = parseImportFile(\"html\", html);\n\n    const result = parsed.bookmarks;\n\n    expect(result).toHaveLength(1);\n    expect(result[0]).toMatchObject({\n      content: { type: \"link\", url: \"https://example.com\" },\n      tags: [\"tag1\", \"tag2\"],\n      addDate: 1234567890, // Should keep the earlier date\n    });\n    expect(result[0].paths).toHaveLength(2);\n    expect(result[0].paths).toContainEqual([\"Folder1\"]);\n    expect(result[0].paths).toContainEqual([\"Folder2\"]);\n  });\n\n  it(\"merges notes from duplicate bookmarks\", () => {\n    const html = `<!DOCTYPE NETSCAPE-Bookmark-file-1>\n<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">\n<TITLE>Bookmarks</TITLE>\n<H1>Bookmarks</H1>\n<DL><p>\n    <DT><A HREF=\"https://example.com\" ADD_DATE=\"1234567890\">Bookmark</A>\n    <DD>First note\n    <DT><A HREF=\"https://example.com\" ADD_DATE=\"1234567891\">Bookmark</A>\n    <DD>Second note\n</DL><p>`;\n\n    // Note: The current parser doesn't extract DD notes, but this test\n    // documents the expected behavior if/when DD parsing is added\n    const parsed = parseImportFile(\"html\", html);\n\n    const result = parsed.bookmarks;\n\n    expect(result).toHaveLength(1);\n    expect(result[0].content).toMatchObject({\n      type: \"link\",\n      url: \"https://example.com\",\n    });\n  });\n\n  it(\"handles bookmarks without ADD_DATE attribute\", () => {\n    const html = `<!DOCTYPE NETSCAPE-Bookmark-file-1>\n<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">\n<TITLE>Bookmarks</TITLE>\n<H1>Bookmarks</H1>\n<DL><p>\n    <DT><A HREF=\"https://example.com\">No Date Bookmark</A>\n</DL><p>`;\n\n    const parsed = parseImportFile(\"html\", html);\n\n    const result = parsed.bookmarks;\n\n    expect(result).toHaveLength(1);\n    expect(result[0].addDate).toBeUndefined();\n  });\n\n  it(\"handles bookmarks without HREF attribute\", () => {\n    const html = `<!DOCTYPE NETSCAPE-Bookmark-file-1>\n<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">\n<TITLE>Bookmarks</TITLE>\n<H1>Bookmarks</H1>\n<DL><p>\n    <DT><A ADD_DATE=\"1234567890\">No URL Bookmark</A>\n</DL><p>`;\n\n    const parsed = parseImportFile(\"html\", html);\n\n    const result = parsed.bookmarks;\n\n    expect(result).toHaveLength(1);\n    expect(result[0].content).toBeUndefined();\n  });\n\n  it(\"handles mixed structure with folders and root-level bookmarks\", () => {\n    const html = `<!DOCTYPE NETSCAPE-Bookmark-file-1>\n<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">\n<TITLE>Bookmarks</TITLE>\n<H1>Bookmarks</H1>\n<DL><p>\n    <DT><A HREF=\"https://root1.com\" ADD_DATE=\"1234567890\">Root Bookmark 1</A>\n    <DT><H3>Folder</H3>\n    <DL><p>\n        <DT><A HREF=\"https://folder1.com\" ADD_DATE=\"1234567891\">Folder Bookmark</A>\n    </DL><p>\n    <DT><A HREF=\"https://root2.com\" ADD_DATE=\"1234567892\">Root Bookmark 2</A>\n</DL><p>`;\n\n    const parsed = parseImportFile(\"html\", html);\n\n    const result = parsed.bookmarks;\n\n    expect(result).toHaveLength(3);\n    expect(result[0]).toMatchObject({\n      title: \"Root Bookmark 1\",\n      paths: [[]],\n    });\n    expect(result[1]).toMatchObject({\n      title: \"Folder Bookmark\",\n      paths: [[\"Folder\"]],\n    });\n    expect(result[2]).toMatchObject({\n      title: \"Root Bookmark 2\",\n      paths: [[]],\n    });\n  });\n\n  it(\"throws error for non-Netscape bookmark files\", () => {\n    const html = `<html>\n<head><title>Not a bookmark file</title></head>\n<body>Just a regular HTML file</body>\n</html>`;\n\n    expect(() => parseImportFile(\"html\", html)).toThrow(\n      \"The uploaded html file does not seem to be a bookmark file\",\n    );\n  });\n});\n\ndescribe(\"parseLinkwardenBookmarkFile\", () => {\n  const fixture = fs.readFileSync(\n    path.join(__dirname, \"fixtures/linkwarden-export.json\"),\n    \"utf-8\",\n  );\n\n  it(\"parses collections with nested hierarchy into paths\", () => {\n    const result = parseImportFile(\"linkwarden\", fixture);\n\n    // 6 links total, but example.com appears in two collections so deduped to 5\n    expect(result.bookmarks).toHaveLength(5);\n\n    // Root-level collection\n    expect(result.bookmarks[0]).toMatchObject({\n      title: \"GitHub\",\n      content: { type: \"link\", url: \"https://github.com\" },\n      tags: [\"dev\"],\n      addDate: new Date(\"2025-01-01T00:00:00.000Z\").getTime() / 1000,\n      paths: [[\"Tech\"]],\n    });\n\n    // Nested one level deep\n    expect(result.bookmarks[1]).toMatchObject({\n      title: \"React\",\n      content: { type: \"link\", url: \"https://react.dev\" },\n      tags: [\"js\", \"ui\"],\n      paths: [[\"Tech\", \"Frontend\"]],\n    });\n\n    // Nested two levels deep\n    expect(result.bookmarks[2]).toMatchObject({\n      title: \"Next.js\",\n      content: { type: \"link\", url: \"https://nextjs.org\" },\n      tags: [],\n      paths: [[\"Tech\", \"Frontend\", \"Frameworks\"]],\n    });\n\n    // Another root-level collection\n    expect(result.bookmarks[3]).toMatchObject({\n      title: \"Hacker News\",\n      content: { type: \"link\", url: \"https://news.ycombinator.com\" },\n      tags: [\"news\"],\n      paths: [[\"News\"]],\n    });\n  });\n\n  it(\"deduplicates bookmarks across collections and merges metadata\", () => {\n    const result = parseImportFile(\"linkwarden\", fixture);\n\n    // example.com appears in both \"Work\" and \"Personal\" collections\n    const example = result.bookmarks.find(\n      (b) =>\n        b.content?.type === \"link\" && b.content.url === \"https://example.com\",\n    )!;\n    expect(example.tags).toEqual([\"work\", \"personal\"]);\n    expect(example.paths).toEqual([[\"Work\"], [\"Personal\"]]);\n    // Should keep the earlier date\n    expect(example.addDate).toEqual(\n      new Date(\"2025-01-01T00:00:00.000Z\").getTime() / 1000,\n    );\n  });\n});\n\ndescribe(\"parseKarakeepBookmarkFile\", () => {\n  it(\"keeps distinct identities for duplicate sibling list names\", () => {\n    const json = JSON.stringify({\n      lists: [\n        {\n          id: \"parent\",\n          name: \"Projects\",\n          description: null,\n          icon: \"📁\",\n          type: \"manual\",\n          query: null,\n          parentId: null,\n        },\n        {\n          id: \"child-1\",\n          name: \"Inbox\",\n          description: null,\n          icon: \"📁\",\n          type: \"manual\",\n          query: null,\n          parentId: \"parent\",\n        },\n        {\n          id: \"child-2\",\n          name: \"Inbox\",\n          description: null,\n          icon: \"📁\",\n          type: \"manual\",\n          query: null,\n          parentId: \"parent\",\n        },\n      ],\n      bookmarks: [\n        {\n          createdAt: 123,\n          title: \"One\",\n          tags: [],\n          lists: [\"child-1\"],\n          content: { type: \"link\", url: \"https://one.example\" },\n          note: null,\n          archived: false,\n        },\n        {\n          createdAt: 456,\n          title: \"Two\",\n          tags: [],\n          lists: [\"child-2\"],\n          content: { type: \"link\", url: \"https://two.example\" },\n          note: null,\n          archived: false,\n        },\n      ],\n    });\n\n    const result = parseImportFile(\"karakeep\", json);\n\n    expect(result.lists).toEqual([\n      {\n        externalId: \"parent\",\n        name: \"Projects\",\n        icon: \"📁\",\n        description: undefined,\n        parentExternalId: null,\n        type: \"manual\",\n      },\n      {\n        externalId: \"child-1\",\n        name: \"Inbox\",\n        icon: \"📁\",\n        description: undefined,\n        parentExternalId: \"parent\",\n        type: \"manual\",\n      },\n      {\n        externalId: \"child-2\",\n        name: \"Inbox\",\n        icon: \"📁\",\n        description: undefined,\n        parentExternalId: \"parent\",\n        type: \"manual\",\n      },\n    ]);\n\n    expect(result.bookmarks).toHaveLength(2);\n    expect(result.bookmarks[0].listExternalIds).toEqual([\"child-1\"]);\n    expect(result.bookmarks[1].listExternalIds).toEqual([\"child-2\"]);\n  });\n\n  it(\"preserves smart list query metadata\", () => {\n    const json = JSON.stringify({\n      lists: [\n        {\n          id: \"manual\",\n          name: \"Manual\",\n          description: null,\n          icon: \"📁\",\n          type: \"manual\",\n          query: null,\n          parentId: null,\n        },\n        {\n          id: \"smart\",\n          name: \"Smart\",\n          description: \"Smart list description\",\n          icon: \"⚡\",\n          type: \"smart\",\n          query: \"tag:read-later\",\n          parentId: null,\n        },\n      ],\n      bookmarks: [\n        {\n          createdAt: 123,\n          title: \"One\",\n          tags: [],\n          lists: [\"manual\", \"smart\"],\n          content: { type: \"link\", url: \"https://one.example\" },\n          note: null,\n          archived: false,\n        },\n      ],\n    });\n\n    const result = parseImportFile(\"karakeep\", json);\n\n    expect(result.lists).toContainEqual({\n      externalId: \"smart\",\n      name: \"Smart\",\n      icon: \"⚡\",\n      description: \"Smart list description\",\n      parentExternalId: null,\n      type: \"smart\",\n      query: \"tag:read-later\",\n    });\n    expect(result.bookmarks[0].listExternalIds).toEqual([\"manual\"]);\n  });\n});\n"
  },
  {
    "path": "packages/shared/import-export/parsers.ts",
    "content": "// Copied from https://gist.github.com/devster31/4e8c6548fd16ffb75c02e6f24e27f9b9\n\nimport type { AnyNode } from \"domhandler\";\nimport * as cheerio from \"cheerio\";\nimport { parse } from \"csv-parse/sync\";\nimport { z } from \"zod\";\n\nimport { BookmarkTypes } from \"../types/bookmarks\";\nimport { zExportSchema } from \"./exporters\";\n\nexport type ImportSource =\n  | \"html\"\n  | \"pocket\"\n  | \"matter\"\n  | \"omnivore\"\n  | \"karakeep\"\n  | \"linkwarden\"\n  | \"tab-session-manager\"\n  | \"mymind\"\n  | \"instapaper\";\n\nexport interface ParsedBookmark {\n  title: string;\n  content?:\n    | { type: BookmarkTypes.LINK; url: string }\n    | { type: BookmarkTypes.TEXT; text: string };\n  tags: string[];\n  addDate?: number;\n  notes?: string;\n  archived?: boolean;\n  paths: string[][];\n  // Optional list IDs from the source file (used with top-level `lists`).\n  listExternalIds?: string[];\n}\n\nexport interface ParsedImportList {\n  externalId: string;\n  name: string;\n  icon?: string;\n  description?: string;\n  parentExternalId: string | null;\n  type: \"manual\" | \"smart\";\n  query?: string;\n}\n\nexport interface ParsedImportFile {\n  bookmarks: ParsedBookmark[];\n  lists: ParsedImportList[];\n}\n\nfunction parseNetscapeBookmarkFile(textContent: string): ParsedBookmark[] {\n  if (!textContent.startsWith(\"<!DOCTYPE NETSCAPE-Bookmark-file-1>\")) {\n    throw Error(\"The uploaded html file does not seem to be a bookmark file\");\n  }\n\n  const $ = cheerio.load(textContent);\n  const bookmarks: ParsedBookmark[] = [];\n\n  // Recursively traverse the bookmark hierarchy top-down\n  function traverseFolder(\n    element: cheerio.Cheerio<AnyNode>,\n    currentPath: string[],\n  ) {\n    element.children().each((_index, child) => {\n      const $child = $(child);\n\n      // Check if this is a folder (DT with H3)\n      const h3 = $child.children(\"h3\").first();\n      if (h3.length > 0) {\n        const folderName = h3.text().trim() || \"Unnamed\";\n        const newPath = [...currentPath, folderName];\n\n        // Find the DL that follows this folder and recurse into it\n        const dl = $child.children(\"dl\").first();\n        if (dl.length > 0) {\n          traverseFolder(dl, newPath);\n        }\n      } else {\n        // Check if this is a bookmark (DT with A)\n        const anchor = $child.children(\"a\").first();\n        if (anchor.length > 0) {\n          const addDate = anchor.attr(\"add_date\");\n          const tagsStr = anchor.attr(\"tags\");\n          const tags = tagsStr && tagsStr.length > 0 ? tagsStr.split(\",\") : [];\n          const url = anchor.attr(\"href\");\n\n          bookmarks.push({\n            title: anchor.text(),\n            content: url\n              ? { type: BookmarkTypes.LINK as const, url }\n              : undefined,\n            tags,\n            addDate:\n              typeof addDate === \"undefined\" ? undefined : parseInt(addDate),\n            paths: [currentPath],\n          });\n        }\n      }\n    });\n  }\n\n  // Start traversal from the root DL element\n  const rootDl = $(\"dl\").first();\n  if (rootDl.length > 0) {\n    traverseFolder(rootDl, []);\n  }\n\n  return bookmarks;\n}\n\nfunction parsePocketBookmarkFile(textContent: string): ParsedBookmark[] {\n  const records = parse(textContent, {\n    columns: true,\n    skip_empty_lines: true,\n  }) as {\n    title: string;\n    url: string;\n    time_added: string;\n    tags: string;\n    status?: string;\n  }[];\n\n  return records.map((record) => {\n    return {\n      title: record.title,\n      content: { type: BookmarkTypes.LINK as const, url: record.url },\n      tags: record.tags.length > 0 ? record.tags.split(\"|\") : [],\n      addDate: parseInt(record.time_added),\n      archived: record.status === \"archive\",\n      paths: [], // TODO\n    };\n  });\n}\n\nfunction parseMatterBookmarkFile(textContent: string): ParsedBookmark[] {\n  const zMatterRecordSchema = z.object({\n    Title: z.string(),\n    Author: z.string(),\n    Publisher: z.string(),\n    URL: z.string(),\n    Tags: z\n      .string()\n      .transform((tags) => (tags.length > 0 ? tags.split(\";\") : [])),\n    \"Word Count\": z.string(),\n    \"In Queue\": z.string().transform((inQueue) => inQueue === \"False\"),\n    Favorited: z.string(),\n    Read: z.string(),\n    Highlight_Count: z.string(),\n    \"Last Interaction Date\": z\n      .string()\n      .transform((date) => Date.parse(date) / 1000),\n    \"File Id\": z.string(),\n  });\n\n  const zMatterExportSchema = z.array(zMatterRecordSchema);\n\n  const records = parse(textContent, {\n    columns: true,\n    skip_empty_lines: true,\n  });\n\n  const parsed = zMatterExportSchema.safeParse(records);\n  if (!parsed.success) {\n    throw new Error(\n      `The uploaded CSV file contains an invalid Matter bookmark file: ${parsed.error.toString()}`,\n    );\n  }\n\n  return parsed.data.map((record) => {\n    return {\n      title: record.Title,\n      content: { type: BookmarkTypes.LINK as const, url: record.URL },\n      tags: record.Tags,\n      addDate: record[\"Last Interaction Date\"],\n      archived: record[\"In Queue\"],\n      paths: [], // TODO\n    };\n  });\n}\n\nfunction parseKarakeepBookmarkFile(textContent: string): ParsedImportFile {\n  const parsed = zExportSchema.safeParse(JSON.parse(textContent));\n  if (!parsed.success) {\n    throw new Error(\n      `The uploaded JSON file contains an invalid bookmark file: ${parsed.error.toString()}`,\n    );\n  }\n\n  const exportedLists = parsed.data.lists ?? [];\n  const parsedLists: ParsedImportList[] = exportedLists.map((list) => ({\n    externalId: list.id,\n    name: list.name,\n    icon: list.icon,\n    description: list.description ?? undefined,\n    parentExternalId: list.parentId,\n    type: list.type,\n    query: list.type === \"smart\" ? (list.query ?? undefined) : undefined,\n  }));\n\n  const manualListIds = new Set(\n    exportedLists.filter((l) => l.type === \"manual\").map((l) => l.id),\n  );\n\n  const parsedBookmarks = parsed.data.bookmarks.map((bookmark) => {\n    let content = undefined;\n    if (bookmark.content?.type == BookmarkTypes.LINK) {\n      content = {\n        type: BookmarkTypes.LINK as const,\n        url: bookmark.content.url,\n      };\n    } else if (bookmark.content?.type == BookmarkTypes.TEXT) {\n      content = {\n        type: BookmarkTypes.TEXT as const,\n        text: bookmark.content.text,\n      };\n    }\n\n    return {\n      title: bookmark.title ?? \"\",\n      content,\n      tags: bookmark.tags,\n      addDate: bookmark.createdAt,\n      notes: bookmark.note ?? undefined,\n      archived: bookmark.archived,\n      paths: [],\n      listExternalIds: (bookmark.lists ?? []).filter((listId) =>\n        manualListIds.has(listId),\n      ),\n    };\n  });\n\n  return {\n    bookmarks: parsedBookmarks,\n    lists: parsedLists,\n  };\n}\n\nfunction parseOmnivoreBookmarkFile(textContent: string): ParsedBookmark[] {\n  const zOmnivoreExportSchema = z.array(\n    z.object({\n      title: z.string(),\n      url: z.string(),\n      labels: z.array(z.string()),\n      savedAt: z.coerce.date(),\n      state: z.string().optional(),\n    }),\n  );\n\n  const parsed = zOmnivoreExportSchema.safeParse(JSON.parse(textContent));\n  if (!parsed.success) {\n    throw new Error(\n      `The uploaded JSON file contains an invalid omnivore bookmark file: ${parsed.error.toString()}`,\n    );\n  }\n\n  return parsed.data.map((bookmark) => {\n    return {\n      title: bookmark.title ?? \"\",\n      content: { type: BookmarkTypes.LINK as const, url: bookmark.url },\n      tags: bookmark.labels,\n      addDate: bookmark.savedAt.getTime() / 1000,\n      archived: bookmark.state === \"Archived\",\n      paths: [],\n    };\n  });\n}\n\nfunction parseLinkwardenBookmarkFile(textContent: string): ParsedBookmark[] {\n  const zLinkwardenExportSchema = z.object({\n    collections: z.array(\n      z.object({\n        id: z.number(),\n        name: z.string(),\n        parentId: z.number().nullable(),\n        links: z.array(\n          z.object({\n            name: z.string(),\n            url: z.string(),\n            tags: z.array(z.object({ name: z.string() })),\n            createdAt: z.coerce.date(),\n          }),\n        ),\n      }),\n    ),\n  });\n\n  const parsed = zLinkwardenExportSchema.safeParse(JSON.parse(textContent));\n  if (!parsed.success) {\n    throw new Error(\n      `The uploaded JSON file contains an invalid Linkwarden bookmark file: ${parsed.error.toString()}`,\n    );\n  }\n\n  // Build a map of collection id -> collection for path resolution\n  const collectionsById = new Map(\n    parsed.data.collections.map((c) => [c.id, c]),\n  );\n\n  // Resolve the full path for a collection by walking up the parent chain\n  function getCollectionPath(collectionId: number): string[] {\n    const path: string[] = [];\n    let currentId: number | null = collectionId;\n    while (currentId !== null) {\n      const collection = collectionsById.get(currentId);\n      if (!collection) break;\n      path.unshift(collection.name);\n      currentId = collection.parentId;\n    }\n    return path;\n  }\n\n  return parsed.data.collections.flatMap((collection) => {\n    const collectionPath = getCollectionPath(collection.id);\n    return collection.links.map((bookmark) => ({\n      title: bookmark.name ?? \"\",\n      content: { type: BookmarkTypes.LINK as const, url: bookmark.url },\n      tags: bookmark.tags.map((tag) => tag.name),\n      addDate: bookmark.createdAt.getTime() / 1000,\n      paths: [collectionPath],\n    }));\n  });\n}\n\nfunction parseTabSessionManagerStateFile(\n  textContent: string,\n): ParsedBookmark[] {\n  const zTab = z.object({\n    url: z.string(),\n    title: z.string(),\n    lastAccessed: z.number(),\n  });\n\n  const zSession = z.object({\n    windows: z.record(z.string(), z.record(z.string(), zTab)),\n    date: z.number(),\n  });\n\n  const zTabSessionManagerSchema = z.array(zSession);\n\n  const parsed = zTabSessionManagerSchema.safeParse(JSON.parse(textContent));\n  if (!parsed.success) {\n    throw new Error(\n      `The uploaded JSON file contains an invalid Tab Session Manager bookmark file: ${parsed.error.toString()}`,\n    );\n  }\n\n  // Get the object in data that has the most recent `date`\n  const { windows } = parsed.data.reduce((prev, curr) =>\n    prev.date > curr.date ? prev : curr,\n  );\n\n  return Object.values(windows).flatMap((window) =>\n    Object.values(window).map((tab) => ({\n      title: tab.title,\n      content: { type: BookmarkTypes.LINK as const, url: tab.url },\n      tags: [],\n      addDate: tab.lastAccessed,\n      paths: [], // Tab Session Manager doesn't have folders\n    })),\n  );\n}\n\nfunction parseMymindBookmarkFile(textContent: string): ParsedBookmark[] {\n  const zMymindRecordSchema = z.object({\n    id: z.string(),\n    type: z.string(),\n    title: z.string(),\n    url: z.string(),\n    content: z.string(),\n    note: z.string(),\n    tags: z.string(),\n    created: z.string(),\n  });\n\n  const zMymindExportSchema = z.array(zMymindRecordSchema);\n\n  const records = parse(textContent, {\n    columns: true,\n    skip_empty_lines: true,\n  });\n\n  const parsed = zMymindExportSchema.safeParse(records);\n  if (!parsed.success) {\n    throw new Error(\n      `The uploaded CSV file contains an invalid mymind bookmark file: ${parsed.error.toString()}`,\n    );\n  }\n\n  return parsed.data.map((record) => {\n    // Determine content type based on presence of URL and content fields\n    let content: ParsedBookmark[\"content\"];\n    if (record.url && record.url.trim().length > 0) {\n      content = { type: BookmarkTypes.LINK as const, url: record.url.trim() };\n    } else if (record.content && record.content.trim().length > 0) {\n      content = {\n        type: BookmarkTypes.TEXT as const,\n        text: record.content.trim(),\n      };\n    }\n\n    // Parse tags from comma-separated string\n    const tags =\n      record.tags && record.tags.trim().length > 0\n        ? record.tags.split(\",\").map((tag) => tag.trim())\n        : [];\n\n    // Parse created date to timestamp (in seconds)\n    const addDate = record.created\n      ? new Date(record.created).getTime() / 1000\n      : undefined;\n\n    return {\n      title: record.title || \"\",\n      content,\n      tags,\n      addDate,\n      notes:\n        record.note && record.note.trim().length > 0 ? record.note : undefined,\n      paths: [], // mymind doesn't have folder structure\n    };\n  });\n}\n\nfunction parseInstapaperBookmarkFile(textContent: string): ParsedBookmark[] {\n  const zInstapaperRecordScheme = z.object({\n    URL: z.string(),\n    Title: z.string(),\n    Selection: z.string(),\n    Folder: z.string(),\n    Timestamp: z.string(),\n    Tags: z.string(),\n  });\n\n  const zInstapaperExportScheme = z.array(zInstapaperRecordScheme);\n\n  const record = parse(textContent, {\n    columns: true,\n    skip_empty_lines: true,\n  });\n\n  const parsed = zInstapaperExportScheme.safeParse(record);\n\n  if (!parsed.success) {\n    throw new Error(\n      `CSV file contains an invalid instapaper bookmark file: ${parsed.error.toString()}`,\n    );\n  }\n\n  return parsed.data.map((record) => {\n    let content: ParsedBookmark[\"content\"];\n    if (record.URL && record.URL.trim().length > 0) {\n      content = { type: BookmarkTypes.LINK as const, url: record.URL.trim() };\n    } else if (record.Selection && record.Selection.trim().length > 0) {\n      content = {\n        type: BookmarkTypes.TEXT as const,\n        text: record.Selection.trim(),\n      };\n    }\n\n    const addDate = parseInt(record.Timestamp);\n\n    let tags: string[] = [];\n    try {\n      const parsedTags = JSON.parse(record.Tags);\n      if (Array.isArray(parsedTags)) {\n        tags = parsedTags.map((tag) => tag.toString().trim());\n      }\n    } catch {\n      tags = [];\n    }\n\n    return {\n      title: record.Title || \"\",\n      content,\n      addDate,\n      tags,\n      paths: [], // TODO\n    };\n  });\n}\n\nfunction deduplicateBookmarks(bookmarks: ParsedBookmark[]): ParsedBookmark[] {\n  const deduplicatedBookmarksMap = new Map<string, ParsedBookmark>();\n  const textBookmarks: ParsedBookmark[] = [];\n\n  for (const bookmark of bookmarks) {\n    if (bookmark.content?.type === BookmarkTypes.LINK) {\n      const url = bookmark.content.url;\n      if (deduplicatedBookmarksMap.has(url)) {\n        const existing = deduplicatedBookmarksMap.get(url)!;\n        // Merge tags\n        existing.tags = [...new Set([...existing.tags, ...bookmark.tags])];\n        // Merge paths\n        existing.paths = [...existing.paths, ...bookmark.paths];\n        if (existing.listExternalIds || bookmark.listExternalIds) {\n          existing.listExternalIds = [\n            ...new Set([\n              ...(existing.listExternalIds ?? []),\n              ...(bookmark.listExternalIds ?? []),\n            ]),\n          ];\n        }\n        const existingDate = existing.addDate ?? Infinity;\n        const newDate = bookmark.addDate ?? Infinity;\n        if (newDate < existingDate) {\n          existing.addDate = bookmark.addDate;\n        }\n        // Append notes if both exist\n        if (existing.notes && bookmark.notes) {\n          existing.notes = `${existing.notes}\\n---\\n${bookmark.notes}`;\n        } else if (bookmark.notes) {\n          existing.notes = bookmark.notes;\n        }\n        // For archived status, prefer archived if either is archived\n        if (bookmark.archived === true) {\n          existing.archived = true;\n        }\n        // Title: keep existing one for simplicity\n      } else {\n        deduplicatedBookmarksMap.set(url, bookmark);\n      }\n    } else {\n      // Keep text bookmarks as they are (no URL to dedupe on)\n      textBookmarks.push(bookmark);\n    }\n  }\n\n  return [...deduplicatedBookmarksMap.values(), ...textBookmarks];\n}\n\nexport function parseImportFile(\n  source: ImportSource,\n  textContent: string,\n): ParsedImportFile {\n  if (source === \"karakeep\") {\n    const parsed = parseKarakeepBookmarkFile(textContent);\n    return {\n      bookmarks: deduplicateBookmarks(parsed.bookmarks),\n      lists: parsed.lists,\n    };\n  }\n\n  let result: ParsedBookmark[];\n  switch (source) {\n    case \"html\":\n      result = parseNetscapeBookmarkFile(textContent);\n      break;\n    case \"pocket\":\n      result = parsePocketBookmarkFile(textContent);\n      break;\n    case \"matter\":\n      result = parseMatterBookmarkFile(textContent);\n      break;\n    case \"omnivore\":\n      result = parseOmnivoreBookmarkFile(textContent);\n      break;\n    case \"linkwarden\":\n      result = parseLinkwardenBookmarkFile(textContent);\n      break;\n    case \"tab-session-manager\":\n      result = parseTabSessionManagerStateFile(textContent);\n      break;\n    case \"mymind\":\n      result = parseMymindBookmarkFile(textContent);\n      break;\n    case \"instapaper\":\n      result = parseInstapaperBookmarkFile(textContent);\n      break;\n  }\n  return { bookmarks: deduplicateBookmarks(result), lists: [] };\n}\n"
  },
  {
    "path": "packages/shared/index.ts",
    "content": "export {};\n"
  },
  {
    "path": "packages/shared/inference.ts",
    "content": "import { Ollama } from \"ollama\";\nimport OpenAI from \"openai\";\nimport { zodResponseFormat } from \"openai/helpers/zod\";\nimport * as undici from \"undici\";\nimport { z } from \"zod\";\nimport { zodToJsonSchema } from \"zod-to-json-schema\";\n\nimport serverConfig from \"./config\";\nimport { customFetch } from \"./customFetch\";\nimport logger from \"./logger\";\n\nexport interface InferenceResponse {\n  response: string;\n  totalTokens: number | undefined;\n}\n\nexport interface EmbeddingResponse {\n  embeddings: number[][];\n}\n\nexport interface InferenceOptions {\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  schema: z.ZodSchema<any> | null;\n  abortSignal?: AbortSignal;\n}\n\nconst defaultInferenceOptions: InferenceOptions = {\n  schema: null,\n};\n\nexport interface InferenceClient {\n  inferFromText(\n    prompt: string,\n    opts: Partial<InferenceOptions>,\n  ): Promise<InferenceResponse>;\n  inferFromImage(\n    prompt: string,\n    contentType: string,\n    image: string,\n    opts: Partial<InferenceOptions>,\n  ): Promise<InferenceResponse>;\n  generateEmbeddingFromText(inputs: string[]): Promise<EmbeddingResponse>;\n}\n\nconst mapInferenceOutputSchema = <\n  T,\n  S extends typeof serverConfig.inference.outputSchema,\n>(\n  opts: Record<S, T>,\n  type: S,\n): T => {\n  return opts[type];\n};\n\nexport interface OpenAIInferenceConfig {\n  apiKey: string;\n  baseURL?: string;\n  proxyUrl?: string;\n  serviceTier?: typeof serverConfig.inference.openAIServiceTier;\n  textModel: string;\n  imageModel: string;\n  contextLength: number;\n  maxOutputTokens: number;\n  useMaxCompletionTokens: boolean;\n  outputSchema: \"structured\" | \"json\" | \"plain\";\n}\n\nexport class InferenceClientFactory {\n  static build(): InferenceClient | null {\n    if (serverConfig.inference.openAIApiKey) {\n      return OpenAIInferenceClient.fromConfig();\n    }\n\n    if (serverConfig.inference.ollamaBaseUrl) {\n      return OllamaInferenceClient.fromConfig();\n    }\n    return null;\n  }\n}\n\nexport class OpenAIInferenceClient implements InferenceClient {\n  openAI: OpenAI;\n  private config: OpenAIInferenceConfig;\n\n  constructor(config: OpenAIInferenceConfig) {\n    this.config = config;\n\n    const fetchOptions = config.proxyUrl\n      ? {\n          dispatcher: new undici.ProxyAgent(config.proxyUrl),\n        }\n      : undefined;\n\n    this.openAI = new OpenAI({\n      apiKey: config.apiKey,\n      baseURL: config.baseURL,\n      ...(fetchOptions ? { fetchOptions } : {}),\n      defaultHeaders: {\n        \"X-Title\": \"Karakeep\",\n        \"HTTP-Referer\": \"https://karakeep.app\",\n      },\n    });\n  }\n\n  static fromConfig(): OpenAIInferenceClient {\n    return new OpenAIInferenceClient({\n      apiKey: serverConfig.inference.openAIApiKey!,\n      baseURL: serverConfig.inference.openAIBaseUrl,\n      proxyUrl: serverConfig.inference.openAIProxyUrl,\n      serviceTier: serverConfig.inference.openAIServiceTier,\n      textModel: serverConfig.inference.textModel,\n      imageModel: serverConfig.inference.imageModel,\n      contextLength: serverConfig.inference.contextLength,\n      maxOutputTokens: serverConfig.inference.maxOutputTokens,\n      useMaxCompletionTokens: serverConfig.inference.useMaxCompletionTokens,\n      outputSchema: serverConfig.inference.outputSchema,\n    });\n  }\n\n  async inferFromText(\n    prompt: string,\n    _opts: Partial<InferenceOptions>,\n  ): Promise<InferenceResponse> {\n    const optsWithDefaults: InferenceOptions = {\n      ...defaultInferenceOptions,\n      ..._opts,\n    };\n    const chatCompletion = await this.openAI.chat.completions.create(\n      {\n        messages: [{ role: \"user\", content: prompt }],\n        model: this.config.textModel,\n        ...(this.config.serviceTier\n          ? { service_tier: this.config.serviceTier }\n          : {}),\n        ...(this.config.useMaxCompletionTokens\n          ? { max_completion_tokens: this.config.maxOutputTokens }\n          : { max_tokens: this.config.maxOutputTokens }),\n        response_format: mapInferenceOutputSchema(\n          {\n            structured: optsWithDefaults.schema\n              ? zodResponseFormat(optsWithDefaults.schema, \"schema\")\n              : undefined,\n            json: { type: \"json_object\" },\n            plain: undefined,\n          },\n          this.config.outputSchema,\n        ),\n      },\n      {\n        signal: optsWithDefaults.abortSignal,\n      },\n    );\n\n    const response = chatCompletion.choices[0].message.content;\n    if (!response) {\n      throw new Error(`Got no message content from OpenAI`);\n    }\n    return { response, totalTokens: chatCompletion.usage?.total_tokens };\n  }\n\n  async inferFromImage(\n    prompt: string,\n    contentType: string,\n    image: string,\n    _opts: Partial<InferenceOptions>,\n  ): Promise<InferenceResponse> {\n    const optsWithDefaults: InferenceOptions = {\n      ...defaultInferenceOptions,\n      ..._opts,\n    };\n    const chatCompletion = await this.openAI.chat.completions.create(\n      {\n        model: this.config.imageModel,\n        ...(this.config.serviceTier\n          ? { service_tier: this.config.serviceTier }\n          : {}),\n        ...(this.config.useMaxCompletionTokens\n          ? { max_completion_tokens: this.config.maxOutputTokens }\n          : { max_tokens: this.config.maxOutputTokens }),\n        response_format: mapInferenceOutputSchema(\n          {\n            structured: optsWithDefaults.schema\n              ? zodResponseFormat(optsWithDefaults.schema, \"schema\")\n              : undefined,\n            json: { type: \"json_object\" },\n            plain: undefined,\n          },\n          this.config.outputSchema,\n        ),\n        messages: [\n          {\n            role: \"user\",\n            content: [\n              { type: \"text\", text: prompt },\n              {\n                type: \"image_url\",\n                image_url: {\n                  url: `data:${contentType};base64,${image}`,\n                  detail: \"low\",\n                },\n              },\n            ],\n          },\n        ],\n      },\n      {\n        signal: optsWithDefaults.abortSignal,\n      },\n    );\n\n    const response = chatCompletion.choices[0].message.content;\n    if (!response) {\n      throw new Error(`Got no message content from OpenAI`);\n    }\n    return { response, totalTokens: chatCompletion.usage?.total_tokens };\n  }\n\n  async generateEmbeddingFromText(\n    inputs: string[],\n  ): Promise<EmbeddingResponse> {\n    const model = serverConfig.embedding.textModel;\n    const embedResponse = await this.openAI.embeddings.create({\n      model: model,\n      input: inputs,\n    });\n    const embedding2D: number[][] = embedResponse.data.map(\n      (embedding: OpenAI.Embedding) => embedding.embedding,\n    );\n    return { embeddings: embedding2D };\n  }\n}\n\nexport interface OllamaInferenceConfig {\n  baseUrl: string;\n  textModel: string;\n  imageModel: string;\n  contextLength: number;\n  maxOutputTokens: number;\n  keepAlive?: string;\n  outputSchema: \"structured\" | \"json\" | \"plain\";\n}\n\nclass OllamaInferenceClient implements InferenceClient {\n  ollama: Ollama;\n  private config: OllamaInferenceConfig;\n\n  constructor(config: OllamaInferenceConfig) {\n    this.config = config;\n    this.ollama = new Ollama({\n      host: config.baseUrl,\n      fetch: customFetch, // Use the custom fetch with configurable timeout\n    });\n  }\n\n  static fromConfig(): OllamaInferenceClient {\n    return new OllamaInferenceClient({\n      baseUrl: serverConfig.inference.ollamaBaseUrl!,\n      textModel: serverConfig.inference.textModel,\n      imageModel: serverConfig.inference.imageModel,\n      contextLength: serverConfig.inference.contextLength,\n      maxOutputTokens: serverConfig.inference.maxOutputTokens,\n      keepAlive: serverConfig.inference.ollamaKeepAlive,\n      outputSchema: serverConfig.inference.outputSchema,\n    });\n  }\n\n  async runModel(\n    model: string,\n    prompt: string,\n    _opts: InferenceOptions,\n    image?: string,\n  ) {\n    const optsWithDefaults: InferenceOptions = {\n      ...defaultInferenceOptions,\n      ..._opts,\n    };\n\n    let newAbortSignal = undefined;\n    if (optsWithDefaults.abortSignal) {\n      newAbortSignal = AbortSignal.any([optsWithDefaults.abortSignal]);\n      newAbortSignal.onabort = () => {\n        this.ollama.abort();\n      };\n    }\n    const chatCompletion = await this.ollama.generate({\n      model: model,\n      format: mapInferenceOutputSchema(\n        {\n          structured: optsWithDefaults.schema\n            ? zodToJsonSchema(optsWithDefaults.schema)\n            : undefined,\n          json: \"json\",\n          plain: undefined,\n        },\n        this.config.outputSchema,\n      ),\n      stream: true,\n      keep_alive: this.config.keepAlive,\n      options: {\n        num_ctx: this.config.contextLength,\n        num_predict: this.config.maxOutputTokens,\n      },\n      prompt: prompt,\n      images: image ? [image] : undefined,\n    });\n\n    let totalTokens = 0;\n    let response = \"\";\n    try {\n      for await (const part of chatCompletion) {\n        response += part.response;\n        if (!isNaN(part.eval_count)) {\n          totalTokens += part.eval_count;\n        }\n        if (!isNaN(part.prompt_eval_count)) {\n          totalTokens += part.prompt_eval_count;\n        }\n      }\n    } catch (e) {\n      if (e instanceof Error && e.name === \"AbortError\") {\n        throw e;\n      }\n      // There seem to be some bug in ollama where you can get some successful response, but still throw an error.\n      // Using stream + accumulating the response so far is a workaround.\n      // https://github.com/ollama/ollama-js/issues/72\n      totalTokens = NaN;\n      logger.warn(\n        `Got an exception from ollama, will still attempt to deserialize the response we got so far: ${e}`,\n      );\n    } finally {\n      if (newAbortSignal) {\n        newAbortSignal.onabort = null;\n      }\n    }\n\n    return { response, totalTokens };\n  }\n\n  async inferFromText(\n    prompt: string,\n    _opts: Partial<InferenceOptions>,\n  ): Promise<InferenceResponse> {\n    const optsWithDefaults: InferenceOptions = {\n      ...defaultInferenceOptions,\n      ..._opts,\n    };\n    return await this.runModel(\n      this.config.textModel,\n      prompt,\n      optsWithDefaults,\n      undefined,\n    );\n  }\n\n  async inferFromImage(\n    prompt: string,\n    _contentType: string,\n    image: string,\n    _opts: Partial<InferenceOptions>,\n  ): Promise<InferenceResponse> {\n    const optsWithDefaults: InferenceOptions = {\n      ...defaultInferenceOptions,\n      ..._opts,\n    };\n    return await this.runModel(\n      this.config.imageModel,\n      prompt,\n      optsWithDefaults,\n      image,\n    );\n  }\n\n  async generateEmbeddingFromText(\n    inputs: string[],\n  ): Promise<EmbeddingResponse> {\n    const embedding = await this.ollama.embed({\n      model: serverConfig.embedding.textModel,\n      input: inputs,\n      // Truncate the input to fit into the model's max token limit,\n      // in the future we want to add a way to split the input into multiple parts.\n      truncate: true,\n    });\n    return { embeddings: embedding.embeddings };\n  }\n}\n"
  },
  {
    "path": "packages/shared/langs.ts",
    "content": "export const langNameMappings: Record<string, string> = {\n  ar: \"Arabic\",\n  zh: \"Simplified Chinese\",\n  zhtw: \"Traditional Chinese\",\n  cs: \"Czech\",\n  hr: \"Croatian\",\n  da: \"Danish\",\n  nl: \"Dutch\",\n  el: \"Greek\",\n  en: \"English\",\n  en_US: \"English (US)\",\n  fi: \"Finnish\",\n  fr: \"French\",\n  ga: \"Irish\",\n  gl: \"Galician\",\n  de: \"German\",\n  hu: \"Hungarian\",\n  it: \"Italian\",\n  ja: \"Japanese\",\n  ko: \"Korean\",\n  nb_NO: \"Norwegian Bokmål\",\n  pl: \"Polish\",\n  pt: \"Portuguese\",\n  pt_BR: \"Portuguese (Brazil)\",\n  ru: \"Russian\",\n  sk: \"Slovak\",\n  sl: \"Slovenian\",\n  es: \"Spanish\",\n  sv: \"Swedish\",\n  tr: \"Turkish\",\n  uk: \"Ukrainian\",\n  vi: \"Vietnamese\",\n};\n\nexport const supportedLangs = Object.keys(langNameMappings);\n"
  },
  {
    "path": "packages/shared/logger.ts",
    "content": "import winston from \"winston\";\n\nimport serverConfig from \"./config\";\n\nconst logger = winston.createLogger({\n  level: serverConfig.logLevel,\n  format: winston.format.combine(\n    winston.format.timestamp(),\n    ...(serverConfig.logNoColor ? [] : [winston.format.colorize()]),\n    winston.format.printf(\n      (info) => `${info.timestamp} ${info.level}: ${info.message}`,\n    ),\n  ),\n  transports: [new winston.transports.Console()],\n});\n\nexport function throttledLogger(periodMs: number) {\n  let lastLogTime = 0;\n\n  return (level: string, message: string) => {\n    const now = Date.now();\n    if (now - lastLogTime >= periodMs) {\n      lastLogTime = now;\n      logger.log(level, message);\n    }\n  };\n}\n\nexport default logger;\n"
  },
  {
    "path": "packages/shared/package.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/package.json\",\n  \"name\": \"@karakeep/shared\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"@aws-sdk/client-s3\": \"^3.842.0\",\n    \"glob\": \"^11.0.0\",\n    \"html-to-text\": \"^9.0.5\",\n    \"js-tiktoken\": \"^1.0.20\",\n    \"nodemailer\": \"^7.0.4\",\n    \"ollama\": \"^0.5.14\",\n    \"openai\": \"^4.86.1\",\n    \"typescript-parsec\": \"^0.3.4\",\n    \"winston\": \"^3.11.0\",\n    \"zod\": \"^3.24.2\",\n    \"zod-to-json-schema\": \"^3.24.3\"\n  },\n  \"devDependencies\": {\n    \"@karakeep/tsconfig\": \"workspace:^0.1.0\",\n    \"@types/html-to-text\": \"^9.0.4\",\n    \"@types/nodemailer\": \"^6.4.17\",\n    \"vite-tsconfig-paths\": \"^4.3.1\",\n    \"vitest\": \"^3.2.4\"\n  },\n  \"scripts\": {\n    \"typecheck\": \"tsc --noEmit\",\n    \"format\": \"oxfmt --check .\",\n    \"format:fix\": \"oxfmt .\",\n    \"lint\": \"oxlint .\",\n    \"lint:fix\": \"oxlint . --fix\",\n    \"test\": \"vitest\"\n  },\n  \"main\": \"index.ts\"\n}\n"
  },
  {
    "path": "packages/shared/plugins.ts",
    "content": "// Implementation inspired from Outline\n\nimport type { QueueClient } from \"./queueing\";\nimport type { RateLimitClient } from \"./ratelimiting\";\nimport logger from \"./logger\";\nimport { SearchIndexClient } from \"./search\";\n\nexport enum PluginType {\n  Search = \"search\",\n  Queue = \"queue\",\n  RateLimit = \"ratelimit\",\n}\n\ninterface PluginTypeMap {\n  [PluginType.Search]: SearchIndexClient;\n  [PluginType.Queue]: QueueClient;\n  [PluginType.RateLimit]: RateLimitClient;\n}\n\nexport interface TPlugin<T extends PluginType> {\n  type: T;\n  name: string;\n  provider: PluginProvider<PluginTypeMap[T]>;\n}\n\nexport interface PluginProvider<T> {\n  getClient(): Promise<T | null>;\n}\n\n// Preserve the key-dependent value type: for K, store TPlugin<K>[]\ntype ProviderMap = { [K in PluginType]: TPlugin<K>[] };\n\nexport class PluginManager {\n  private static providers: ProviderMap = {\n    [PluginType.Search]: [],\n    [PluginType.Queue]: [],\n    [PluginType.RateLimit]: [],\n  };\n\n  static register<T extends PluginType>(plugin: TPlugin<T>): void {\n    PluginManager.providers[plugin.type].push(plugin);\n  }\n\n  static async getClient<T extends PluginType>(\n    type: T,\n  ): Promise<PluginTypeMap[T] | null> {\n    const providers: TPlugin<T>[] = PluginManager.providers[type];\n    if (providers.length === 0) {\n      return null;\n    }\n    return await providers[providers.length - 1]!.provider.getClient();\n  }\n\n  static isRegistered<T extends PluginType>(type: T): boolean {\n    return PluginManager.providers[type].length > 0;\n  }\n\n  static getPluginName<T extends PluginType>(type: T): string | null {\n    const providers: TPlugin<T>[] = PluginManager.providers[type];\n    if (providers.length === 0) {\n      return null;\n    }\n    return providers[providers.length - 1]!.name;\n  }\n\n  static logAllPlugins() {\n    logger.info(\"Plugins (Last one wins):\");\n    for (const type of Object.values(PluginType)) {\n      logger.info(`  ${type}:`);\n      const plugins = PluginManager.providers[type];\n      if (!plugins) {\n        logger.info(\"    - None\");\n        continue;\n      }\n      for (const plugin of plugins) {\n        logger.info(`    - ${plugin.name}`);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/shared/prompts.server.ts",
    "content": "import type { Tiktoken } from \"js-tiktoken\";\n\nimport type { ZTagStyle } from \"./types/users\";\nimport { constructSummaryPrompt, constructTextTaggingPrompt } from \"./prompts\";\n\nlet encoding: Tiktoken | null = null;\n\n/**\n * Lazy load the encoding to avoid loading the tiktoken data into memory\n * until it's actually needed\n */\nasync function getEncodingInstance(): Promise<Tiktoken> {\n  if (!encoding) {\n    // Dynamic import to lazy load the tiktoken module\n    const { getEncoding } = await import(\"js-tiktoken\");\n    encoding = getEncoding(\"o200k_base\");\n  }\n  return encoding;\n}\n\nasync function calculateNumTokens(text: string): Promise<number> {\n  const enc = await getEncodingInstance();\n  return enc.encode(text).length;\n}\n\nasync function truncateContent(\n  content: string,\n  length: number,\n): Promise<string> {\n  const enc = await getEncodingInstance();\n  const tokens = enc.encode(content);\n  if (tokens.length <= length) {\n    return content;\n  }\n  const truncatedTokens = tokens.slice(0, length);\n  return enc.decode(truncatedTokens);\n}\n\n/**\n * Remove duplicate whitespaces to avoid tokenization issues\n */\nfunction preprocessContent(content: string) {\n  return content.replace(/(\\s){10,}/g, \"$1\");\n}\n\nexport async function buildTextPrompt(\n  lang: string,\n  customPrompts: string[],\n  content: string,\n  contextLength: number,\n  tagStyle: ZTagStyle,\n  curatedTags?: string[],\n): Promise<string> {\n  content = preprocessContent(content);\n  const promptTemplate = constructTextTaggingPrompt(\n    lang,\n    customPrompts,\n    \"\",\n    tagStyle,\n    curatedTags,\n  );\n  const promptSize = await calculateNumTokens(promptTemplate);\n  const available = Math.max(0, contextLength - promptSize);\n  const truncatedContent =\n    available === 0 ? \"\" : await truncateContent(content, available);\n  return constructTextTaggingPrompt(\n    lang,\n    customPrompts,\n    truncatedContent,\n    tagStyle,\n    curatedTags,\n  );\n}\n\nexport async function buildSummaryPrompt(\n  lang: string,\n  customPrompts: string[],\n  content: string,\n  contextLength: number,\n): Promise<string> {\n  content = preprocessContent(content);\n  const promptTemplate = constructSummaryPrompt(lang, customPrompts, \"\");\n  const promptSize = await calculateNumTokens(promptTemplate);\n  const available = Math.max(0, contextLength - promptSize);\n  const truncatedContent =\n    available === 0 ? \"\" : await truncateContent(content, available);\n  return constructSummaryPrompt(lang, customPrompts, truncatedContent);\n}\n"
  },
  {
    "path": "packages/shared/prompts.ts",
    "content": "import type { ZTagStyle } from \"./types/users\";\nimport { getCuratedTagsPrompt, getTagStylePrompt } from \"./utils/tag\";\n\n/**\n * Remove duplicate whitespaces to avoid tokenization issues\n */\nfunction preprocessContent(content: string) {\n  return content.replace(/(\\s){10,}/g, \"$1\");\n}\n\nexport function buildImagePrompt(\n  lang: string,\n  customPrompts: string[],\n  tagStyle: ZTagStyle,\n  curatedTags?: string[],\n) {\n  const tagStyleInstruction = getTagStylePrompt(tagStyle);\n  const curatedInstruction = getCuratedTagsPrompt(curatedTags);\n\n  return `\nYou are an expert whose responsibility is to help with automatic text tagging for a read-it-later/bookmarking app.\nAnalyze the attached image and suggest relevant tags that describe its key themes, topics, and main ideas. The rules are:\n- Aim for a variety of tags, including broad categories, specific keywords, and potential sub-genres.\n- The tags must be in ${lang}.\n- If the tag is not generic enough, don't include it.\n- Aim for 10-15 tags.\n- If there are no good tags, don't emit any.\n${curatedInstruction}\n${tagStyleInstruction}\n${customPrompts && customPrompts.map((p) => `- ${p}`).join(\"\\n\")}\nYou must respond in valid JSON with the key \"tags\" and the value is list of tags. Don't wrap the response in a markdown code.`;\n}\n\n/**\n * Construct tagging prompt for text content\n */\nexport function constructTextTaggingPrompt(\n  lang: string,\n  customPrompts: string[],\n  content: string,\n  tagStyle: ZTagStyle,\n  curatedTags?: string[],\n): string {\n  const tagStyleInstruction = getTagStylePrompt(tagStyle);\n  const curatedInstruction = getCuratedTagsPrompt(curatedTags);\n\n  return `\nYou are an expert whose responsibility is to help with automatic tagging for a read-it-later/bookmarking app.\nAnalyze the TEXT_CONTENT below and suggest relevant tags that describe its key themes, topics, and main ideas. The rules are:\n- Aim for a variety of tags, including broad categories, specific keywords, and potential sub-genres.\n- The tags must be in ${lang}.\n- If the tag is not generic enough, don't include it.\n- Do NOT generate tags related to:\n    - An error page (404, 403, blocked, not found, dns errors)\n    - Boilerplate content (cookie consent, login walls, GDPR notices)\n- Aim for 3-5 tags.\n- If there are no good tags, leave the array empty.\n${curatedInstruction}\n${tagStyleInstruction}\n${customPrompts && customPrompts.map((p) => `- ${p}`).join(\"\\n\")}\n\n<TEXT_CONTENT>\n${content}\n</TEXT_CONTENT>\nYou must respond in JSON with the key \"tags\" and the value is an array of string tags.`;\n}\n\n/**\n * Construct summary prompt\n */\nexport function constructSummaryPrompt(\n  lang: string,\n  customPrompts: string[],\n  content: string,\n): string {\n  return `\nSummarize the following content responding ONLY with the summary. You MUST follow the following rules:\n- Summary must be in 3-4 sentences.\n- The summary must be in ${lang}.\n${customPrompts && customPrompts.map((p) => `- ${p}`).join(\"\\n\")}\n    ${content}`;\n}\n\n/**\n * Build text tagging prompt without truncation (for previews/UI)\n */\nexport function buildTextPromptUntruncated(\n  lang: string,\n  customPrompts: string[],\n  content: string,\n  tagStyle: ZTagStyle,\n  curatedTags?: string[],\n): string {\n  return constructTextTaggingPrompt(\n    lang,\n    customPrompts,\n    preprocessContent(content),\n    tagStyle,\n    curatedTags,\n  );\n}\n\n/**\n * Build summary prompt without truncation (for previews/UI)\n */\nexport function buildSummaryPromptUntruncated(\n  lang: string,\n  customPrompts: string[],\n  content: string,\n): string {\n  return constructSummaryPrompt(\n    lang,\n    customPrompts,\n    preprocessContent(content),\n  );\n}\n\n/**\n * Build OCR prompt for extracting text from images using LLM\n */\nexport function buildOCRPrompt(): string {\n  return `You are an OCR (Optical Character Recognition) expert. Your task is to extract ALL text from this image.\n\nRules:\n- Extract every piece of text visible in the image, including titles, body text, captions, labels, watermarks, and any other textual content.\n- Preserve the original structure and formatting as much as possible (e.g., paragraphs, lists, headings).\n- If text appears in multiple columns, read from left to right, top to bottom.\n- If text is partially obscured or unclear, make your best attempt and indicate uncertainty with [unclear] if needed.\n- Do not add any commentary, explanations, or descriptions of non-text elements.\n- If there is no text in the image, respond with an empty string.\n- Output ONLY the extracted text, nothing else.`;\n}\n"
  },
  {
    "path": "packages/shared/queueing.ts",
    "content": "import { ZodType } from \"zod\";\n\nimport { PluginManager, PluginType } from \"./plugins\";\n\n/**\n * Special error that indicates a job should be retried after a delay\n * without counting against the retry attempts limit.\n * Useful for handling rate limiting scenarios.\n */\nexport class QueueRetryAfterError extends Error {\n  constructor(\n    message: string,\n    public readonly delayMs: number,\n  ) {\n    super(message);\n    this.name = \"QueueRetryAfterError\";\n  }\n}\n\nexport interface EnqueueOptions {\n  idempotencyKey?: string;\n  priority?: number;\n  delayMs?: number;\n  groupId?: string;\n}\n\nexport interface QueueOptions {\n  defaultJobArgs: {\n    numRetries: number;\n  };\n  keepFailedJobs: boolean;\n}\n\nexport interface DequeuedJob<T> {\n  id: string;\n  data: T;\n  priority: number;\n  runNumber: number;\n  abortSignal: AbortSignal;\n}\n\nexport interface DequeuedJobError<T> {\n  id: string;\n  data?: T;\n  priority: number;\n  error: Error;\n  runNumber: number;\n  numRetriesLeft: number;\n}\n\nexport interface RunnerFuncs<T, R = void> {\n  run: (job: DequeuedJob<T>) => Promise<R>;\n  onComplete?: (job: DequeuedJob<T>, result: R) => Promise<void>;\n  onError?: (job: DequeuedJobError<T>) => Promise<void>;\n}\n\nexport interface RunnerOptions<T> {\n  pollIntervalMs?: number;\n  timeoutSecs: number;\n  concurrency: number;\n  validator?: ZodType<T>;\n}\n\nexport interface Queue<T> {\n  opts: QueueOptions;\n  ensureInit(): Promise<void>;\n  name(): string;\n  enqueue(payload: T, options?: EnqueueOptions): Promise<string | undefined>;\n  stats(): Promise<{\n    pending: number;\n    pending_retry: number;\n    running: number;\n    failed: number;\n  }>;\n  cancelAllNonRunning?(): Promise<number>;\n}\n\nexport interface Runner<_T> {\n  run(): Promise<void>;\n  stop(): void;\n  runUntilEmpty?(): Promise<void>;\n}\n\nexport interface QueueClient {\n  prepare(): Promise<void>;\n  start(): Promise<void>;\n  createQueue<T>(name: string, options: QueueOptions): Queue<T>;\n  createRunner<T, R = void>(\n    queue: Queue<T>,\n    funcs: RunnerFuncs<T, R>,\n    opts: RunnerOptions<T>,\n  ): Runner<T>;\n  shutdown?(): Promise<void>;\n}\n\nexport async function getQueueClient(): Promise<QueueClient> {\n  const client = await PluginManager.getClient(PluginType.Queue);\n  if (!client) {\n    throw new Error(\"Failed to get queue client\");\n  }\n  return client;\n}\n"
  },
  {
    "path": "packages/shared/ratelimiting.ts",
    "content": "import { PluginManager, PluginType } from \"./plugins\";\n\nexport interface RateLimitConfig {\n  name: string;\n  windowMs: number;\n  maxRequests: number;\n}\n\nexport type RateLimitResult =\n  | { allowed: true }\n  | { allowed: false; resetInSeconds: number };\n\nexport interface RateLimitClient {\n  /**\n   * Check if a request should be allowed based on rate limiting rules\n   * @param config Rate limit configuration\n   * @param key Unique rate limiting key (e.g., \"ip:127.0.0.1:path:/api/v1\")\n   * @returns Result indicating if the request is allowed and reset time if not\n   */\n  checkRateLimit(\n    config: RateLimitConfig,\n    key: string,\n  ): RateLimitResult | Promise<RateLimitResult>;\n\n  /**\n   * Reset rate limit for a specific key\n   * @param config Rate limit configuration\n   * @param key Unique rate limiting key\n   */\n  reset(config: RateLimitConfig, key: string): void | Promise<void>;\n\n  /**\n   * Clear all rate limit entries\n   */\n  clear(): void | Promise<void>;\n}\n\nexport async function getRateLimitClient(): Promise<RateLimitClient | null> {\n  return PluginManager.getClient(PluginType.RateLimit);\n}\n"
  },
  {
    "path": "packages/shared/search.ts",
    "content": "import { z } from \"zod\";\n\nimport { PluginManager, PluginType } from \"./plugins\";\n\nexport const zBookmarkSearchDocument = z.object({\n  id: z.string(),\n  userId: z.string(),\n  url: z.string().nullish(),\n  title: z.string().nullish(),\n  linkTitle: z.string().nullish(),\n  description: z.string().nullish(),\n  content: z.string().nullish(),\n  metadata: z.string().nullish(),\n  fileName: z.string().nullish(),\n  createdAt: z.string().nullish(),\n  note: z.string().nullish(),\n  summary: z.string().nullish(),\n  tags: z.array(z.string()).default([]),\n  publisher: z.string().nullish(),\n  author: z.string().nullish(),\n  datePublished: z.date().nullish(),\n  dateModified: z.date().nullish(),\n});\n\nexport type BookmarkSearchDocument = z.infer<typeof zBookmarkSearchDocument>;\n\nexport type SortOrder = \"asc\" | \"desc\";\nexport type SortableAttributes = \"createdAt\";\n\nexport type FilterableAttributes = \"userId\" | \"id\";\nexport type FilterQuery =\n  | {\n      type: \"eq\";\n      field: FilterableAttributes;\n      value: string;\n    }\n  | {\n      type: \"in\";\n      field: FilterableAttributes;\n      values: string[];\n    };\n\nexport interface SearchResult {\n  id: string;\n  score?: number;\n}\n\nexport interface SearchOptions {\n  query: string;\n  // Diffeernt filters are ANDed together\n  filter?: FilterQuery[];\n  limit?: number;\n  offset?: number;\n  sort?: { field: SortableAttributes; order: SortOrder }[];\n}\n\nexport interface SearchResponse {\n  hits: SearchResult[];\n  totalHits: number;\n  processingTimeMs: number;\n}\n\nexport interface IndexingOptions {\n  /**\n   * Whether to batch requests. Defaults to true.\n   * Set to false to bypass batching for improved reliability (e.g., on retries).\n   */\n  batch?: boolean;\n}\n\nexport interface SearchIndexClient {\n  addDocuments(\n    documents: BookmarkSearchDocument[],\n    options?: IndexingOptions,\n  ): Promise<void>;\n  deleteDocuments(ids: string[], options?: IndexingOptions): Promise<void>;\n  search(options: SearchOptions): Promise<SearchResponse>;\n  clearIndex(): Promise<void>;\n}\n\nexport async function getSearchClient(): Promise<SearchIndexClient | null> {\n  return PluginManager.getClient(PluginType.Search);\n}\n"
  },
  {
    "path": "packages/shared/searchQueryParser.test.ts",
    "content": "import { describe, expect, test } from \"vitest\";\n\nimport { parseSearchQuery } from \"./searchQueryParser\";\nimport { BookmarkTypes } from \"./types/bookmarks\";\n\ndescribe(\"Search Query Parser\", () => {\n  test(\"simple is queries\", () => {\n    expect(parseSearchQuery(\"is:archived\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"archived\",\n        archived: true,\n      },\n    });\n    expect(parseSearchQuery(\"-is:archived\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"archived\",\n        archived: false,\n      },\n    });\n    expect(parseSearchQuery(\"is:fav\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"favourited\",\n        favourited: true,\n      },\n    });\n    expect(parseSearchQuery(\"-is:fav\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"favourited\",\n        favourited: false,\n      },\n    });\n    expect(parseSearchQuery(\"is:tagged\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"tagged\",\n        tagged: true,\n      },\n    });\n    expect(parseSearchQuery(\"-is:tagged\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"tagged\",\n        tagged: false,\n      },\n    });\n    expect(parseSearchQuery(\"is:inlist\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"inlist\",\n        inList: true,\n      },\n    });\n    expect(parseSearchQuery(\"-is:inlist\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"inlist\",\n        inList: false,\n      },\n    });\n    expect(parseSearchQuery(\"is:link\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"type\",\n        typeName: BookmarkTypes.LINK,\n        inverse: false,\n      },\n    });\n    expect(parseSearchQuery(\"-is:link\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"type\",\n        typeName: BookmarkTypes.LINK,\n        inverse: true,\n      },\n    });\n    expect(parseSearchQuery(\"is:text\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"type\",\n        typeName: BookmarkTypes.TEXT,\n        inverse: false,\n      },\n    });\n    expect(parseSearchQuery(\"-is:text\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"type\",\n        typeName: BookmarkTypes.TEXT,\n        inverse: true,\n      },\n    });\n    expect(parseSearchQuery(\"is:media\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"type\",\n        typeName: BookmarkTypes.ASSET,\n        inverse: false,\n      },\n    });\n    expect(parseSearchQuery(\"-is:media\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"type\",\n        typeName: BookmarkTypes.ASSET,\n        inverse: true,\n      },\n    });\n    expect(parseSearchQuery(\"is:broken\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"brokenLinks\",\n        brokenLinks: true,\n      },\n    });\n    expect(parseSearchQuery(\"-is:broken\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"brokenLinks\",\n        brokenLinks: false,\n      },\n    });\n  });\n\n  test(\"simple string queries\", () => {\n    expect(parseSearchQuery(\"url:https://example.com\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"url\",\n        url: \"https://example.com\",\n        inverse: false,\n      },\n    });\n    expect(parseSearchQuery(\"-url:https://example.com\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"url\",\n        url: \"https://example.com\",\n        inverse: true,\n      },\n    });\n    expect(parseSearchQuery('url:\"https://example.com\"')).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"url\",\n        url: \"https://example.com\",\n        inverse: false,\n      },\n    });\n    expect(parseSearchQuery('-url:\"https://example.com\"')).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"url\",\n        url: \"https://example.com\",\n        inverse: true,\n      },\n    });\n    expect(parseSearchQuery(\"title:example\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"title\",\n        title: \"example\",\n        inverse: false,\n      },\n    });\n    expect(parseSearchQuery(\"-title:example\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"title\",\n        title: \"example\",\n        inverse: true,\n      },\n    });\n    expect(parseSearchQuery('title:\"my title\"')).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"title\",\n        title: \"my title\",\n        inverse: false,\n      },\n    });\n    expect(parseSearchQuery('-title:\"my title\"')).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"title\",\n        title: \"my title\",\n        inverse: true,\n      },\n    });\n    expect(parseSearchQuery(\"#my-tag\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"tagName\",\n        tagName: \"my-tag\",\n        inverse: false,\n      },\n    });\n    expect(parseSearchQuery(\"-#my-tag\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"tagName\",\n        tagName: \"my-tag\",\n        inverse: true,\n      },\n    });\n    expect(parseSearchQuery('#\"my tag\"')).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"tagName\",\n        tagName: \"my tag\",\n        inverse: false,\n      },\n    });\n    expect(parseSearchQuery('-#\"my tag\"')).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"tagName\",\n        tagName: \"my tag\",\n        inverse: true,\n      },\n    });\n    // Tags starting with qualifiers should be treated correctly\n    expect(parseSearchQuery(\"#android\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"tagName\",\n        tagName: \"android\",\n        inverse: false,\n      },\n    });\n    expect(parseSearchQuery(\"list:my-list\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"listName\",\n        listName: \"my-list\",\n        inverse: false,\n      },\n    });\n    expect(parseSearchQuery(\"-list:my-list\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"listName\",\n        listName: \"my-list\",\n        inverse: true,\n      },\n    });\n    expect(parseSearchQuery('list:\"my list\"')).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"listName\",\n        listName: \"my list\",\n        inverse: false,\n      },\n    });\n    expect(parseSearchQuery('-list:\"my list\"')).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"listName\",\n        listName: \"my list\",\n        inverse: true,\n      },\n    });\n    expect(parseSearchQuery(\"feed:my-feed\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"rssFeedName\",\n        feedName: \"my-feed\",\n        inverse: false,\n      },\n    });\n    expect(parseSearchQuery(\"-feed:my-feed\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"rssFeedName\",\n        feedName: \"my-feed\",\n        inverse: true,\n      },\n    });\n    expect(parseSearchQuery('feed:\"my feed\"')).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"rssFeedName\",\n        feedName: \"my feed\",\n        inverse: false,\n      },\n    });\n    expect(parseSearchQuery('-feed:\"my feed\"')).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"rssFeedName\",\n        feedName: \"my feed\",\n        inverse: true,\n      },\n    });\n    expect(parseSearchQuery(\"source:rss\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"source\",\n        source: \"rss\",\n        inverse: false,\n      },\n    });\n    expect(parseSearchQuery(\"-source:rss\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"source\",\n        source: \"rss\",\n        inverse: true,\n      },\n    });\n    expect(parseSearchQuery(\"source:web\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"source\",\n        source: \"web\",\n        inverse: false,\n      },\n    });\n    expect(parseSearchQuery(\"-source:web\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"source\",\n        source: \"web\",\n        inverse: true,\n      },\n    });\n  });\n  test(\"! negation alias for -\", () => {\n    // ! should work exactly like - for negation\n    expect(parseSearchQuery(\"!is:archived\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"archived\",\n        archived: false,\n      },\n    });\n    expect(parseSearchQuery(\"!is:fav\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"favourited\",\n        favourited: false,\n      },\n    });\n    expect(parseSearchQuery(\"!#my-tag\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"tagName\",\n        tagName: \"my-tag\",\n        inverse: true,\n      },\n    });\n    expect(parseSearchQuery(\"!tag:my-tag\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"tagName\",\n        tagName: \"my-tag\",\n        inverse: true,\n      },\n    });\n    expect(parseSearchQuery(\"!url:example.com\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"url\",\n        url: \"example.com\",\n        inverse: true,\n      },\n    });\n    expect(parseSearchQuery(\"!list:my-list\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"listName\",\n        listName: \"my-list\",\n        inverse: true,\n      },\n    });\n    expect(parseSearchQuery(\"!is:link\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"type\",\n        typeName: BookmarkTypes.LINK,\n        inverse: true,\n      },\n    });\n    // Combined with complex queries\n    expect(parseSearchQuery(\"is:fav !is:archived\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"and\",\n        matchers: [\n          {\n            type: \"favourited\",\n            favourited: true,\n          },\n          {\n            type: \"archived\",\n            archived: false,\n          },\n        ],\n      },\n    });\n  });\n\n  test(\"tag: qualifier alias for #\", () => {\n    // tag: should work exactly like #\n    expect(parseSearchQuery(\"tag:my-tag\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"tagName\",\n        tagName: \"my-tag\",\n        inverse: false,\n      },\n    });\n    expect(parseSearchQuery(\"-tag:my-tag\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"tagName\",\n        tagName: \"my-tag\",\n        inverse: true,\n      },\n    });\n    expect(parseSearchQuery('tag:\"my tag\"')).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"tagName\",\n        tagName: \"my tag\",\n        inverse: false,\n      },\n    });\n    expect(parseSearchQuery('-tag:\"my tag\"')).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"tagName\",\n        tagName: \"my tag\",\n        inverse: true,\n      },\n    });\n    // Tags starting with qualifiers should be treated correctly\n    expect(parseSearchQuery(\"tag:android\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"tagName\",\n        tagName: \"android\",\n        inverse: false,\n      },\n    });\n  });\n\n  test(\"date queries\", () => {\n    expect(parseSearchQuery(\"after:2023-10-12\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"dateAfter\",\n        dateAfter: new Date(\"2023-10-12\"),\n        inverse: false,\n      },\n    });\n    expect(parseSearchQuery(\"-after:2023-10-12\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"dateAfter\",\n        dateAfter: new Date(\"2023-10-12\"),\n        inverse: true,\n      },\n    });\n    expect(parseSearchQuery(\"before:2023-10-12\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"dateBefore\",\n        dateBefore: new Date(\"2023-10-12\"),\n        inverse: false,\n      },\n    });\n    expect(parseSearchQuery(\"-before:2023-10-12\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"dateBefore\",\n        dateBefore: new Date(\"2023-10-12\"),\n        inverse: true,\n      },\n    });\n  });\n  test(\"age queries\", () => {\n    expect(parseSearchQuery(\"age:<3d\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"age\",\n        relativeDate: {\n          direction: \"newer\",\n          amount: 3,\n          unit: \"day\",\n        },\n      },\n    });\n    expect(parseSearchQuery(\"age:>2y\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"age\",\n        relativeDate: {\n          direction: \"older\",\n          amount: 2,\n          unit: \"year\",\n        },\n      },\n    });\n  });\n\n  test(\"complex queries\", () => {\n    expect(parseSearchQuery(\"is:fav -is:archived\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"and\",\n        matchers: [\n          {\n            type: \"favourited\",\n            favourited: true,\n          },\n          {\n            type: \"archived\",\n            archived: false,\n          },\n        ],\n      },\n    });\n\n    expect(parseSearchQuery(\"(is:fav is:archived) #my-tag\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"and\",\n        matchers: [\n          {\n            type: \"favourited\",\n            favourited: true,\n          },\n          {\n            type: \"archived\",\n            archived: true,\n          },\n          {\n            type: \"tagName\",\n            tagName: \"my-tag\",\n            inverse: false,\n          },\n        ],\n      },\n    });\n\n    expect(parseSearchQuery(\"(is:fav is:archived) or (#my-tag)\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"or\",\n        matchers: [\n          {\n            type: \"and\",\n            matchers: [\n              {\n                type: \"favourited\",\n                favourited: true,\n              },\n              {\n                type: \"archived\",\n                archived: true,\n              },\n            ],\n          },\n          {\n            type: \"tagName\",\n            tagName: \"my-tag\",\n            inverse: false,\n          },\n        ],\n      },\n    });\n\n    expect(parseSearchQuery(\"(is:fav or is:archived) and #my-tag\")).toEqual({\n      result: \"full\",\n      text: \"\",\n      matcher: {\n        type: \"and\",\n        matchers: [\n          {\n            type: \"or\",\n            matchers: [\n              {\n                type: \"favourited\",\n                favourited: true,\n              },\n              {\n                type: \"archived\",\n                archived: true,\n              },\n            ],\n          },\n          {\n            type: \"tagName\",\n            tagName: \"my-tag\",\n            inverse: false,\n          },\n        ],\n      },\n    });\n  });\n  test(\"pure text\", () => {\n    expect(parseSearchQuery(\"hello\")).toEqual({\n      result: \"full\",\n      text: \"hello\",\n      matcher: undefined,\n    });\n    expect(parseSearchQuery(\"hello world\")).toEqual({\n      result: \"full\",\n      text: \"hello world\",\n      matcher: undefined,\n    });\n  });\n\n  test(\"text interlived with matchers\", () => {\n    expect(\n      parseSearchQuery(\n        \"hello is:fav world is:archived mixed world #my-tag test\",\n      ),\n    ).toEqual({\n      result: \"full\",\n      text: \"hello world mixed world test\",\n      matcher: {\n        type: \"and\",\n        matchers: [\n          {\n            type: \"favourited\",\n            favourited: true,\n          },\n          {\n            type: \"archived\",\n            archived: true,\n          },\n          {\n            type: \"tagName\",\n            tagName: \"my-tag\",\n            inverse: false,\n          },\n        ],\n      },\n    });\n  });\n\n  test(\"unknown qualifiers are emitted as pure text\", () => {\n    expect(parseSearchQuery(\"is:fav is:helloworld\")).toEqual({\n      result: \"full\",\n      text: \"is:helloworld\",\n      matcher: {\n        type: \"favourited\",\n        favourited: true,\n      },\n    });\n  });\n\n  test(\"partial results\", () => {\n    expect(parseSearchQuery(\"(is:archived) or \")).toEqual({\n      result: \"partial\",\n      text: \"or\",\n      matcher: {\n        type: \"archived\",\n        archived: true,\n      },\n    });\n    expect(parseSearchQuery(\"is:fav is: ( random\")).toEqual({\n      result: \"partial\",\n      text: \"is: ( random\",\n      matcher: {\n        type: \"favourited\",\n        favourited: true,\n      },\n    });\n  });\n});\n"
  },
  {
    "path": "packages/shared/searchQueryParser.ts",
    "content": "import {\n  alt,\n  alt_sc,\n  apply,\n  kleft,\n  kmid,\n  kright,\n  lrec_sc,\n  opt,\n  rule,\n  seq,\n  str,\n  tok,\n  Token,\n  TokenPosition,\n} from \"typescript-parsec\";\nimport { z } from \"zod\";\n\nimport { BookmarkTypes, zBookmarkSourceSchema } from \"./types/bookmarks\";\nimport { Matcher } from \"./types/search\";\nimport { parseRelativeDate } from \"./utils/relativeDateUtils\";\n\nenum TokenType {\n  And = \"AND\",\n  Or = \"OR\",\n\n  Qualifier = \"QUALIFIER\",\n  Ident = \"IDENT\",\n  StringLiteral = \"STRING_LITERAL\",\n\n  LParen = \"LPAREN\",\n  RParen = \"RPAREN\",\n  Space = \"SPACE\",\n  Hash = \"HASH\",\n  Minus = \"MINUS\",\n  Exclamation = \"EXCLAMATION\",\n}\n\n// Rules are in order of priority\nconst lexerRules: [RegExp, TokenType][] = [\n  [/^\\s+and/i, TokenType.And],\n  [/^\\s+or/i, TokenType.Or],\n\n  [/^#/, TokenType.Hash],\n  [\n    /^(is|url|list|after|before|age|feed|title|tag|source):/,\n    TokenType.Qualifier,\n  ],\n\n  [/^\"([^\"]+)\"/, TokenType.StringLiteral],\n\n  [/^\\(/, TokenType.LParen],\n  [/^\\)/, TokenType.RParen],\n  [/^\\s+/, TokenType.Space],\n  [/^-/, TokenType.Minus],\n  [/^!/, TokenType.Exclamation],\n\n  // This needs to be last as it matches a lot of stuff\n  [/^[^ )(]+/, TokenType.Ident],\n] as const;\n\nclass LexerToken implements Token<TokenType> {\n  private constructor(\n    private readonly input: string,\n    public kind: TokenType,\n    public text: string,\n    public pos: TokenPosition,\n  ) {}\n\n  public static from(input: string): Token<TokenType> | undefined {\n    const tok = new LexerToken(\n      input,\n      /* Doesn't matter */ TokenType.Ident,\n      \"\",\n      {\n        index: 0,\n        rowBegin: 1,\n        rowEnd: 1,\n        columnBegin: 0,\n        columnEnd: 0,\n      },\n    );\n    return tok.next;\n  }\n\n  public get next(): Token<TokenType> | undefined {\n    if (!this.input.length) {\n      return undefined;\n    }\n\n    for (const [regex, tokenType] of lexerRules) {\n      const matchRes = regex.exec(this.input);\n      if (!matchRes) {\n        continue;\n      }\n      const match = matchRes[0];\n      return new LexerToken(this.input.slice(match.length), tokenType, match, {\n        index: this.pos.index + match.length,\n        columnBegin: this.pos.index + 1,\n        columnEnd: this.pos.index + 1 + match.length,\n        // Our strings are always only one line\n        rowBegin: 1,\n        rowEnd: 1,\n      });\n    }\n    // No match\n    throw new Error(\n      `Failed to tokenize the token at position ${this.pos.index}: ${this.input[0]}`,\n    );\n  }\n}\n\nexport interface TextAndMatcher {\n  text: string;\n  matcher?: Matcher;\n}\n\nconst MATCHER = rule<TokenType, TextAndMatcher>();\nconst EXP = rule<TokenType, TextAndMatcher>();\n\nMATCHER.setPattern(\n  alt_sc(\n    apply(\n      seq(\n        opt(alt(str(\"-\"), str(\"!\"))),\n        kright(str(\"is:\"), tok(TokenType.Ident)),\n      ),\n      ([minus, ident]) => {\n        switch (ident.text) {\n          case \"fav\":\n            return {\n              text: \"\",\n              matcher: { type: \"favourited\", favourited: !minus },\n            };\n          case \"archived\":\n            return {\n              text: \"\",\n              matcher: { type: \"archived\", archived: !minus },\n            };\n          case \"tagged\":\n            return {\n              text: \"\",\n              matcher: { type: \"tagged\", tagged: !minus },\n            };\n          case \"inlist\":\n            return {\n              text: \"\",\n              matcher: { type: \"inlist\", inList: !minus },\n            };\n          case \"link\":\n            return {\n              text: \"\",\n              matcher: {\n                type: \"type\",\n                typeName: BookmarkTypes.LINK,\n                inverse: !!minus,\n              },\n            };\n          case \"text\":\n            return {\n              text: \"\",\n              matcher: {\n                type: \"type\",\n                typeName: BookmarkTypes.TEXT,\n                inverse: !!minus,\n              },\n            };\n          case \"media\":\n            return {\n              text: \"\",\n              matcher: {\n                type: \"type\",\n                typeName: BookmarkTypes.ASSET,\n                inverse: !!minus,\n              },\n            };\n          case \"broken\":\n            return {\n              text: \"\",\n              matcher: { type: \"brokenLinks\", brokenLinks: !minus },\n            };\n          default:\n            // If the token is not known, emit it as pure text\n            return {\n              text: `${minus?.text ?? \"\"}is:${ident.text}`,\n              matcher: undefined,\n            };\n        }\n      },\n    ),\n    apply(\n      seq(\n        opt(alt(str(\"-\"), str(\"!\"))),\n        alt(tok(TokenType.Qualifier), tok(TokenType.Hash)),\n        alt(\n          apply(tok(TokenType.Ident), (tok) => {\n            return tok.text;\n          }),\n          apply(tok(TokenType.StringLiteral), (tok) => {\n            return tok.text.slice(1, -1);\n          }),\n        ),\n      ),\n      ([minus, qualifier, ident]) => {\n        switch (qualifier.text) {\n          case \"url:\":\n            return {\n              text: \"\",\n              matcher: { type: \"url\", url: ident, inverse: !!minus },\n            };\n          case \"title:\":\n            return {\n              text: \"\",\n              matcher: { type: \"title\", title: ident, inverse: !!minus },\n            };\n          case \"#\":\n          case \"tag:\":\n            return {\n              text: \"\",\n              matcher: { type: \"tagName\", tagName: ident, inverse: !!minus },\n            };\n          case \"list:\":\n            return {\n              text: \"\",\n              matcher: { type: \"listName\", listName: ident, inverse: !!minus },\n            };\n          case \"feed:\":\n            return {\n              text: \"\",\n              matcher: {\n                type: \"rssFeedName\",\n                feedName: ident,\n                inverse: !!minus,\n              },\n            };\n          case \"source:\": {\n            const parsed = zBookmarkSourceSchema.safeParse(ident);\n            if (!parsed.success) {\n              return {\n                text: (minus?.text ?? \"\") + qualifier.text + ident,\n                matcher: undefined,\n              };\n            }\n            return {\n              text: \"\",\n              matcher: {\n                type: \"source\",\n                source: parsed.data,\n                inverse: !!minus,\n              },\n            };\n          }\n          case \"after:\":\n            try {\n              return {\n                text: \"\",\n                matcher: {\n                  type: \"dateAfter\",\n                  dateAfter: z.coerce.date().parse(ident),\n                  inverse: !!minus,\n                },\n              };\n            } catch {\n              return {\n                // If parsing the date fails, emit it as pure text\n                text: (minus?.text ?? \"\") + qualifier.text + ident,\n                matcher: undefined,\n              };\n            }\n          case \"before:\":\n            try {\n              return {\n                text: \"\",\n                matcher: {\n                  type: \"dateBefore\",\n                  dateBefore: z.coerce.date().parse(ident),\n                  inverse: !!minus,\n                },\n              };\n            } catch {\n              return {\n                // If parsing the date fails, emit it as pure text\n                text: (minus?.text ?? \"\") + qualifier.text + ident,\n                matcher: undefined,\n              };\n            }\n          case \"age:\":\n            try {\n              const { direction, amount, unit } = parseRelativeDate(ident);\n              return {\n                text: \"\",\n                matcher: {\n                  type: \"age\",\n                  relativeDate: { direction, amount, unit },\n                },\n              };\n            } catch {\n              return {\n                // If parsing the relative time fails, emit it as pure text\n                text: (minus?.text ?? \"\") + qualifier.text + ident,\n                matcher: undefined,\n              };\n            }\n          default:\n            // If the token is not known, emit it as pure text\n            return {\n              text: (minus?.text ?? \"\") + qualifier.text + ident,\n              matcher: undefined,\n            };\n        }\n      },\n    ),\n    // Ident or an incomlete qualifier\n    apply(alt(tok(TokenType.Ident), tok(TokenType.Qualifier)), (toks) => {\n      return {\n        text: toks.text,\n        matcher: undefined,\n      };\n    }),\n    kmid(tok(TokenType.LParen), EXP, tok(TokenType.RParen)),\n  ),\n);\n\nEXP.setPattern(\n  lrec_sc(\n    MATCHER,\n    seq(\n      alt(\n        tok(TokenType.Space),\n        kleft(tok(TokenType.And), tok(TokenType.Space)),\n        kleft(tok(TokenType.Or), tok(TokenType.Space)),\n      ),\n      MATCHER,\n    ),\n    (toks, next) => {\n      switch (next[0].kind) {\n        case TokenType.Space:\n        case TokenType.And:\n          return {\n            text: [toks.text, next[1].text].join(\" \").trim(),\n            matcher:\n              !!toks.matcher || !!next[1].matcher\n                ? {\n                    type: \"and\",\n                    matchers: [toks.matcher, next[1].matcher].filter(\n                      (a) => !!a,\n                    ),\n                  }\n                : undefined,\n          };\n        case TokenType.Or:\n          return {\n            text: [toks.text, next[1].text].join(\" \").trim(),\n            matcher:\n              !!toks.matcher || !!next[1].matcher\n                ? {\n                    type: \"or\",\n                    matchers: [toks.matcher, next[1].matcher].filter(\n                      (a) => !!a,\n                    ),\n                  }\n                : undefined,\n          };\n      }\n    },\n  ),\n);\n\nfunction flattenAndsAndOrs(matcher: Matcher): Matcher {\n  switch (matcher.type) {\n    case \"and\":\n    case \"or\": {\n      if (matcher.matchers.length == 1) {\n        return flattenAndsAndOrs(matcher.matchers[0]);\n      }\n      const flattened: Matcher[] = [];\n      for (let m of matcher.matchers) {\n        // If inside the matcher is another matcher of the same type, flatten it\n        m = flattenAndsAndOrs(m);\n        if (m.type == matcher.type) {\n          flattened.push(...m.matchers);\n        } else {\n          flattened.push(m);\n        }\n      }\n      matcher.matchers = flattened;\n      return matcher;\n    }\n    default:\n      return matcher;\n  }\n}\n\nexport function _parseAndPrintTokens(query: string) {\n  console.log(`PARSING: ${query}`);\n  let tok = LexerToken.from(query);\n  do {\n    console.log(tok?.kind, tok?.text);\n    tok = tok?.next;\n  } while (tok);\n  console.log(\"DONE\");\n}\n\nfunction consumeTokenStream(token: Token<TokenType>) {\n  let str = \"\";\n  let tok: Token<TokenType> | undefined = token;\n  do {\n    str += tok.text;\n    tok = tok.next;\n  } while (tok);\n  return str;\n}\n\nexport function parseSearchQuery(\n  query: string,\n): TextAndMatcher & { result: \"full\" | \"partial\" | \"invalid\" } {\n  // _parseAndPrintTokens(query); // Uncomment to debug tokenization\n  const parsed = EXP.parse(LexerToken.from(query.trim()));\n  if (!parsed.successful || parsed.candidates.length != 1) {\n    // If the query is not valid, return the whole query as pure text\n    return {\n      text: query,\n      result: \"invalid\",\n    };\n  }\n\n  const parseCandidate = parsed.candidates[0];\n  if (parseCandidate.result.matcher) {\n    parseCandidate.result.matcher = flattenAndsAndOrs(\n      parseCandidate.result.matcher,\n    );\n  }\n  if (parseCandidate.nextToken) {\n    // Parser failed to consume the whole query. This usually happen\n    // when the user is still typing the query. Return the partial\n    // result and the remaining query as pure text\n    return {\n      text: (\n        parseCandidate.result.text +\n        consumeTokenStream(parseCandidate.nextToken)\n      ).trim(),\n      matcher: parseCandidate.result.matcher,\n      result: \"partial\",\n    };\n  }\n\n  return {\n    text: parseCandidate.result.text,\n    matcher: parseCandidate.result.matcher,\n    result: \"full\",\n  };\n}\n"
  },
  {
    "path": "packages/shared/signedTokens.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\nimport { z } from \"zod\";\n\nimport {\n  createSignedToken,\n  getAlignedExpiry,\n  SignedTokenPayload,\n  verifySignedToken,\n} from \"./signedTokens\";\n\nconst SECRET = \"secret\";\n\ndescribe(\"getAlignedExpiry\", () => {\n  it(\"should align to next interval when within grace period\", () => {\n    const now = new Date(\"2023-01-01T12:29:30Z\"); // 30 seconds before next interval\n    const expiry = getAlignedExpiry(1800, 60, now); // 30min interval, 60s grace\n    expect(expiry).toBe(new Date(\"2023-01-01T13:00:00Z\").getTime());\n  });\n\n  it(\"should align to current interval when outside grace period\", () => {\n    const now = new Date(\"2023-01-01T12:10:01Z\");\n    const expiry = getAlignedExpiry(1800, 60, now); // 30min interval, 60s grace\n    expect(expiry).toBe(new Date(\"2023-01-01T12:30:00Z\").getTime());\n  });\n\n  it(\"should handle exact interval boundary\", () => {\n    const now = new Date(\"2023-01-01T12:00:00Z\");\n    const expiry = getAlignedExpiry(1800, 60, now);\n    expect(expiry).toBe(new Date(\"2023-01-01T12:30:00Z\").getTime());\n  });\n});\n\ndescribe(\"signed tokens\", () => {\n  const testSchema = z\n    .object({\n      id: z.string(),\n      name: z.string(),\n    })\n    .strict();\n\n  const testPayload = {\n    id: \"123\",\n    name: \"John\",\n  };\n\n  it(\"should create and verify valid token\", () => {\n    const token = createSignedToken(testPayload, SECRET);\n    const verified = verifySignedToken(token, SECRET, testSchema);\n    expect(verified).toEqual(testPayload);\n  });\n\n  it(\"should return null for expired token\", () => {\n    const token = createSignedToken(testPayload, SECRET, Date.now() - 1000);\n    const verified = verifySignedToken(token, SECRET, testSchema);\n    expect(verified).toBeNull();\n  });\n\n  it(\"should return null for invalid signature\", () => {\n    const token = createSignedToken(testPayload, SECRET);\n    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n    const decoded: SignedTokenPayload = JSON.parse(\n      Buffer.from(token, \"base64\").toString(),\n    );\n    decoded.signature = \"tampered\" + decoded.signature;\n    const tampered = Buffer.from(JSON.stringify(decoded)).toString(\"base64\");\n    const verified = verifySignedToken(tampered, SECRET, testSchema);\n    expect(verified).toBeNull();\n  });\n\n  it(\"should return null if payload doesn't match schema\", () => {\n    const invalidPayload = { ...testPayload, extra: \"field\" };\n    const token = createSignedToken(invalidPayload, SECRET);\n    const verified = verifySignedToken(token, SECRET, testSchema);\n    expect(verified).toBeNull();\n  });\n\n  it(\"should fail with different signing secrets\", () => {\n    const token = createSignedToken(testPayload, \"ONE SECRET\");\n    const verified = verifySignedToken(token, \"ANOTHER SECRET\", testSchema);\n    expect(verified).toBeNull();\n  });\n});\n"
  },
  {
    "path": "packages/shared/signedTokens.ts",
    "content": "import crypto from \"node:crypto\";\nimport { z } from \"zod\";\n\nconst zTokenPayload = z.object({\n  payload: z.unknown(),\n  expiresAt: z.number(),\n});\n\nconst zSignedTokenPayload = z.object({\n  payload: zTokenPayload,\n  signature: z.string(),\n});\n\n/**\n * Returns the expiry date aligned to the specified interval.\n * If the time left until the next interval is less than the grace period,\n * it skips to the following interval.\n *\n * @param now - The current date and time (defaults to new Date()).\n * @param intervalSeconds - The interval in seconds (e.g., 1800 for 30 mins).\n * @param gracePeriodSeconds - The grace period in seconds.\n * @returns The calculated expiry Date.\n */\nexport function getAlignedExpiry(\n  intervalSeconds: number,\n  gracePeriodSeconds: number,\n  now: Date = new Date(),\n): number {\n  const ms = now.getTime();\n  const intervalMs = intervalSeconds * 1000;\n\n  // Find the next interval\n  const nextIntervalTime =\n    Math.floor(ms / intervalMs) * intervalMs + intervalMs;\n\n  // Time left until the next interval\n  const timeLeft = nextIntervalTime - ms;\n\n  // Decide which interval to use\n  const finalIntervalTime =\n    timeLeft < gracePeriodSeconds * 1000\n      ? nextIntervalTime + intervalMs\n      : nextIntervalTime;\n\n  return finalIntervalTime;\n}\n\nexport type SignedTokenPayload = z.infer<typeof zSignedTokenPayload>;\n\nexport function createSignedToken(\n  payload: unknown,\n  secret: string,\n  expiryEpoch?: number,\n): string {\n  const expiresAt = expiryEpoch ?? Date.now() + 5 * 60 * 1000; // 5 minutes from now\n\n  const toBeSigned: z.infer<typeof zTokenPayload> = {\n    payload,\n    expiresAt,\n  };\n\n  const payloadString = JSON.stringify(toBeSigned);\n  const signature = crypto\n    .createHmac(\"sha256\", secret)\n    .update(payloadString)\n    .digest(\"hex\");\n\n  const tokenData: z.infer<typeof zSignedTokenPayload> = {\n    payload: toBeSigned,\n    signature,\n  };\n\n  return Buffer.from(JSON.stringify(tokenData)).toString(\"base64\");\n}\n\nexport function verifySignedToken<T>(\n  token: string,\n  secret: string,\n  schema: z.ZodSchema<T>,\n): T | null {\n  try {\n    const tokenData = zSignedTokenPayload.parse(\n      JSON.parse(Buffer.from(token, \"base64\").toString()),\n    );\n    const { payload, signature } = tokenData;\n\n    // Verify signature\n    const expectedSignature = crypto\n      .createHmac(\"sha256\", secret)\n      .update(JSON.stringify(payload))\n      .digest(\"hex\");\n\n    if (signature !== expectedSignature) {\n      return null;\n    }\n    // Check expiry\n    if (Date.now() > payload.expiresAt) {\n      return null;\n    }\n\n    return schema.parse(payload.payload);\n  } catch {\n    return null;\n  }\n}\n"
  },
  {
    "path": "packages/shared/storageQuota.ts",
    "content": "/**\n * A token that proves storage quota has been checked and approved.\n * This class cannot be instantiated directly - it can only be created\n * by the checkStorageQuota function.\n */\nexport class QuotaApproved {\n  private constructor(\n    public readonly userId: string,\n    public readonly approvedSize: number,\n  ) {}\n\n  /**\n   * Internal method to create a QuotaApproved token.\n   * This should only be called by checkStorageQuota.\n   */\n  static _create(userId: string, approvedSize: number): QuotaApproved {\n    return new QuotaApproved(userId, approvedSize);\n  }\n}\n"
  },
  {
    "path": "packages/shared/trpc.ts",
    "content": "/**\n * Maximum URL length for tRPC httpBatchLink requests.\n *\n * The web app uses a higher limit to support bulk operations (e.g.\n * drag-and-drop bookmark import from the browser) that produce large\n * batched request URLs. The trade-off is that users behind a reverse\n * proxy with strict default header limits may hit 431 errors on very\n * large batches.\n *\n * The mobile app and browser extension use a lower limit because they\n * don't need bulk operations and their requests always travel through\n * the user's server, which typically sits behind a reverse proxy.\n * nginx — the most common reverse proxy for self-hosted deployments —\n * defaults `large_client_header_buffers` to 4 × 8 KB, meaning a single\n * request line (including the URL) must fit within ~8 KB. 4,000\n * characters stays safely under that limit after accounting for the\n * method, HTTP version, and any URL-encoding overhead.\n *\n * Also see:\n * * https://github.com/karakeep-app/karakeep/issues/281\n * * https://github.com/karakeep-app/karakeep/issues/1619\n * * https://nginx.org/en/docs/http/ngx_http_core_module.html#large_client_header_buffers\n */\nexport const TRPC_MAX_URL_LENGTH_INTERNAL = 14000;\nexport const TRPC_MAX_URL_LENGTH_EXTERNAL = 4000;\n"
  },
  {
    "path": "packages/shared/tryCatch.ts",
    "content": "// Types for the result object with discriminated union\ninterface Success<T> {\n  data: T;\n  error: null;\n}\n\ninterface Failure<E> {\n  data: null;\n  error: E;\n}\n\ntype Result<T, E = Error> = Success<T> | Failure<E>;\n\n// Main wrapper function\nexport async function tryCatch<T, E = Error>(\n  promise: Promise<T>,\n): Promise<Result<T, E>> {\n  try {\n    const data = await promise;\n    return { data, error: null };\n  } catch (error) {\n    return { data: null, error: error as E };\n  }\n}\n"
  },
  {
    "path": "packages/shared/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@karakeep/tsconfig/node.json\",\n  \"include\": [\"**/*.ts\"],\n  \"exclude\": [\"node_modules\"],\n  \"compilerOptions\": {\n    \"tsBuildInfoFile\": \"node_modules/.cache/tsbuildinfo.json\"\n  },\n}\n"
  },
  {
    "path": "packages/shared/types/admin.ts",
    "content": "import { z } from \"zod\";\n\nimport { PASSWORD_MAX_LENGTH, zSignUpSchema } from \"./users\";\n\nexport const zRoleSchema = z.object({\n  role: z.enum([\"user\", \"admin\"]),\n});\n\nexport const zAdminCreateUserSchema = zSignUpSchema.and(zRoleSchema);\n\nexport const updateUserSchema = z.object({\n  userId: z.string(),\n  role: z.enum([\"user\", \"admin\"]).optional(),\n  bookmarkQuota: z.number().int().min(0).nullable().optional(),\n  storageQuota: z.number().int().min(0).nullable().optional(),\n  browserCrawlingEnabled: z.boolean().nullable().optional(),\n});\n\nexport const resetPasswordSchema = z\n  .object({\n    userId: z.string(),\n    newPassword: z.string().min(8).max(PASSWORD_MAX_LENGTH),\n    newPasswordConfirm: z.string(),\n  })\n  .refine((data) => data.newPassword === data.newPasswordConfirm, {\n    message: \"Passwords don't match\",\n    path: [\"newPasswordConfirm\"],\n  });\n"
  },
  {
    "path": "packages/shared/types/assets.ts",
    "content": "import { z } from \"zod\";\n\nexport const zAssetSignedTokenSchema = z.object({\n  assetId: z.string(),\n  userId: z.string(),\n});\n"
  },
  {
    "path": "packages/shared/types/backups.ts",
    "content": "import { z } from \"zod\";\n\nexport const zBackupSchema = z.object({\n  id: z.string(),\n  userId: z.string(),\n  assetId: z.string().nullable(),\n  createdAt: z.date(),\n  size: z.number(),\n  bookmarkCount: z.number(),\n  status: z.enum([\"pending\", \"success\", \"failure\"]),\n  errorMessage: z.string().nullable().optional(),\n});\n\nexport type ZBackup = z.infer<typeof zBackupSchema>;\n"
  },
  {
    "path": "packages/shared/types/bookmarks.ts",
    "content": "import { z } from \"zod\";\n\nimport { zCursorV2 } from \"./pagination\";\nimport { zAttachedByEnumSchema, zBookmarkTagSchema } from \"./tags\";\n\nexport const MAX_BOOKMARK_TITLE_LENGTH = 1000;\n\nexport const enum BookmarkTypes {\n  LINK = \"link\",\n  TEXT = \"text\",\n  ASSET = \"asset\",\n  UNKNOWN = \"unknown\",\n}\n\nexport const zSortOrder = z.enum([\"asc\", \"desc\", \"relevance\"]);\nexport type ZSortOrder = z.infer<typeof zSortOrder>;\n\nexport const zAssetTypesSchema = z.enum([\n  \"linkHtmlContent\",\n  \"screenshot\",\n  \"pdf\",\n  \"assetScreenshot\",\n  \"bannerImage\",\n  \"fullPageArchive\",\n  \"video\",\n  \"bookmarkAsset\",\n  \"precrawledArchive\",\n  \"userUploaded\",\n  \"avatar\",\n  \"unknown\",\n]);\nexport type ZAssetType = z.infer<typeof zAssetTypesSchema>;\n\nexport const zAssetSchema = z.object({\n  id: z.string(),\n  assetType: zAssetTypesSchema,\n  fileName: z.string().nullish(),\n});\n\nexport const zBookmarkedLinkSchema = z.object({\n  type: z.literal(BookmarkTypes.LINK),\n  url: z.string(),\n  title: z.string().nullish(),\n  description: z.string().nullish(),\n  imageUrl: z.string().nullish(),\n  imageAssetId: z.string().nullish(),\n  screenshotAssetId: z.string().nullish(),\n  pdfAssetId: z.string().nullish(),\n  fullPageArchiveAssetId: z.string().nullish(),\n  precrawledArchiveAssetId: z.string().nullish(),\n  videoAssetId: z.string().nullish(),\n  favicon: z.string().nullish(),\n  htmlContent: z.string().nullish(),\n  contentAssetId: z.string().nullish(),\n  crawledAt: z.date().nullish(),\n  crawlStatus: z.enum([\"success\", \"failure\", \"pending\"]).nullish(),\n  author: z.string().nullish(),\n  publisher: z.string().nullish(),\n  datePublished: z.date().nullish(),\n  dateModified: z.date().nullish(),\n});\nexport type ZBookmarkedLink = z.infer<typeof zBookmarkedLinkSchema>;\n\nexport const zBookmarkedTextSchema = z.object({\n  type: z.literal(BookmarkTypes.TEXT),\n  text: z.string(),\n  sourceUrl: z.string().nullish(),\n});\nexport type ZBookmarkedText = z.infer<typeof zBookmarkedTextSchema>;\n\nexport const zBookmarkedAssetSchema = z.object({\n  type: z.literal(BookmarkTypes.ASSET),\n  assetType: z.enum([\"image\", \"pdf\"]),\n  assetId: z.string(),\n  fileName: z.string().nullish(),\n  sourceUrl: z.string().nullish(),\n  size: z.number().nullish(),\n  content: z.string().nullish(),\n});\nexport type ZBookmarkedAsset = z.infer<typeof zBookmarkedAssetSchema>;\n\nexport const zBookmarkContentSchema = z.discriminatedUnion(\"type\", [\n  zBookmarkedLinkSchema,\n  zBookmarkedTextSchema,\n  zBookmarkedAssetSchema,\n  z.object({ type: z.literal(BookmarkTypes.UNKNOWN) }),\n]);\nexport type ZBookmarkContent = z.infer<typeof zBookmarkContentSchema>;\n\nexport const zBookmarkSourceSchema = z.enum([\n  \"api\",\n  \"web\",\n  \"cli\",\n  \"mobile\",\n  \"extension\",\n  \"singlefile\",\n  \"rss\",\n  \"import\",\n]);\nexport type ZBookmarkSource = z.infer<typeof zBookmarkSourceSchema>;\n\nexport const zBareBookmarkSchema = z.object({\n  id: z.string(),\n  createdAt: z.date(),\n  modifiedAt: z.date().nullable(),\n  title: z.string().nullish(),\n  archived: z.boolean(),\n  favourited: z.boolean(),\n  taggingStatus: z.enum([\"success\", \"failure\", \"pending\"]).nullable(),\n  summarizationStatus: z.enum([\"success\", \"failure\", \"pending\"]).nullable(),\n  note: z.string().nullish(),\n  summary: z.string().nullish(),\n  source: zBookmarkSourceSchema.nullish(),\n  userId: z.string(),\n});\n\nexport type ZBareBookmark = z.infer<typeof zBareBookmarkSchema>;\n\nexport const zBookmarkSchema = zBareBookmarkSchema.merge(\n  z.object({\n    tags: z.array(zBookmarkTagSchema),\n    content: zBookmarkContentSchema,\n    assets: z.array(zAssetSchema),\n  }),\n);\nexport type ZBookmark = z.infer<typeof zBookmarkSchema>;\n\nconst zBookmarkTypeLinkSchema = zBareBookmarkSchema.merge(\n  z.object({\n    tags: z.array(zBookmarkTagSchema),\n    content: zBookmarkedLinkSchema,\n    assets: z.array(zAssetSchema),\n  }),\n);\nexport type ZBookmarkTypeLink = z.infer<typeof zBookmarkTypeLinkSchema>;\n\nconst zBookmarkTypeTextSchema = zBareBookmarkSchema.merge(\n  z.object({\n    tags: z.array(zBookmarkTagSchema),\n    content: zBookmarkedTextSchema,\n    assets: z.array(zAssetSchema),\n  }),\n);\nexport type ZBookmarkTypeText = z.infer<typeof zBookmarkTypeTextSchema>;\n\nconst zBookmarkTypeAssetSchema = zBareBookmarkSchema.merge(\n  z.object({\n    tags: z.array(zBookmarkTagSchema),\n    content: zBookmarkedAssetSchema,\n    assets: z.array(zAssetSchema),\n  }),\n);\nexport type ZBookmarkTypeAsset = z.infer<typeof zBookmarkTypeAssetSchema>;\n\n// POST /v1/bookmarks\nexport const zNewBookmarkRequestSchema = z\n  .object({\n    title: z.string().max(MAX_BOOKMARK_TITLE_LENGTH).nullish(),\n    archived: z.boolean().optional(),\n    favourited: z.boolean().optional(),\n    note: z.string().optional(),\n    summary: z.string().optional(),\n    createdAt: z.coerce.date().optional(),\n    // A mechanism to prioritize crawling of bookmarks depending on whether\n    // they were created by a user interaction or by a bulk import.\n    crawlPriority: z.enum([\"low\", \"normal\"]).optional(),\n    // Deprecated\n    importSessionId: z.string().optional(),\n    source: zBookmarkSourceSchema.optional(),\n  })\n  .and(\n    z.discriminatedUnion(\"type\", [\n      z.object({\n        type: z.literal(BookmarkTypes.LINK),\n        url: z.string().url(),\n        precrawledArchiveId: z.string().optional(),\n      }),\n      z.object({\n        type: z.literal(BookmarkTypes.TEXT),\n        text: z.string(),\n        sourceUrl: z.string().optional(),\n      }),\n      z.object({\n        type: z.literal(BookmarkTypes.ASSET),\n        assetType: z.enum([\"image\", \"pdf\"]),\n        assetId: z.string(),\n        fileName: z.string().optional(),\n        sourceUrl: z.string().optional(),\n      }),\n    ]),\n  );\nexport type ZNewBookmarkRequest = z.infer<typeof zNewBookmarkRequestSchema>;\n\n// GET /v1/bookmarks\n\nexport const DEFAULT_NUM_BOOKMARKS_PER_PAGE = 20;\nexport const MAX_NUM_BOOKMARKS_PER_PAGE = 100;\n\nexport const zGetBookmarksRequestSchema = z.object({\n  ids: z.array(z.string()).optional(),\n  archived: z.boolean().optional(),\n  favourited: z.boolean().optional(),\n  tagId: z.string().optional(),\n  listId: z.string().optional(),\n  rssFeedId: z.string().optional(),\n  limit: z.number().max(MAX_NUM_BOOKMARKS_PER_PAGE).optional(),\n  cursor: zCursorV2.nullish(),\n  // TODO: This was done for backward comptability. At this point, all clients should be settings this to true.\n  // The value is currently not being used, but keeping it so that client can still set it to true for older\n  // servers.\n  useCursorV2: z.boolean().optional(),\n  sortOrder: zSortOrder.exclude([\"relevance\"]).optional().default(\"desc\"),\n  includeContent: z.boolean().optional().default(false),\n});\nexport type ZGetBookmarksRequest = z.infer<typeof zGetBookmarksRequestSchema>;\n\nexport const zGetBookmarksResponseSchema = z.object({\n  bookmarks: z.array(zBookmarkSchema),\n  nextCursor: zCursorV2.nullable(),\n});\nexport type ZGetBookmarksResponse = z.infer<typeof zGetBookmarksResponseSchema>;\n\n// PATCH /v1/bookmarks/[bookmarkId]\nexport const zUpdateBookmarksRequestSchema = z.object({\n  bookmarkId: z.string(),\n  archived: z.boolean().optional(),\n  favourited: z.boolean().optional(),\n  summary: z.string().nullish(),\n  note: z.string().optional(),\n  title: z.string().max(MAX_BOOKMARK_TITLE_LENGTH).nullish(),\n  createdAt: z.coerce.date().optional(),\n  // Link specific fields (optional)\n  url: z.string().url().optional(),\n  description: z.string().nullish(),\n  author: z.string().nullish(),\n  publisher: z.string().nullish(),\n  datePublished: z.coerce.date().nullish(),\n  dateModified: z.coerce.date().nullish(),\n\n  // Text specific fields (optional)\n  text: z.string().nullish(),\n\n  // Asset specific fields (optional)\n  assetContent: z.string().nullish(),\n});\nexport type ZUpdateBookmarksRequest = z.infer<\n  typeof zUpdateBookmarksRequestSchema\n>;\n\n// The schema that's used to for attachig/detaching tags\nexport const zManipulatedTagSchema = z\n  .object({\n    // At least one of the two must be set\n    tagId: z.string().optional(), // If the tag already exists and we know its id we should pass it\n    tagName: z.string().optional(),\n    attachedBy: zAttachedByEnumSchema.optional().default(\"human\"),\n  })\n  .refine((val) => !!val.tagId || !!val.tagName, {\n    message: \"You must provide either a tagId or a tagName\",\n    path: [\"tagId\", \"tagName\"],\n  });\n\nexport const zSearchBookmarksCursor = z.discriminatedUnion(\"ver\", [\n  z.object({\n    ver: z.literal(1),\n    offset: z.number(),\n  }),\n]);\nexport const zSearchBookmarksRequestSchema = z.object({\n  text: z.string(),\n  limit: z.number().max(MAX_NUM_BOOKMARKS_PER_PAGE).optional(),\n  cursor: zSearchBookmarksCursor.nullish(),\n  sortOrder: zSortOrder.optional().default(\"relevance\"),\n  includeContent: z.boolean().optional().default(false),\n});\n\nexport const zPublicBookmarkSchema = z.object({\n  id: z.string(),\n  createdAt: z.date(),\n  modifiedAt: z.date().nullable(),\n  title: z.string().nullish(),\n  tags: z.array(z.string()),\n  description: z.string().nullish(),\n  bannerImageUrl: z.string().nullable(),\n  content: z.discriminatedUnion(\"type\", [\n    z.object({\n      type: z.literal(BookmarkTypes.LINK),\n      url: z.string(),\n      author: z.string().nullish(),\n    }),\n    z.object({\n      type: z.literal(BookmarkTypes.TEXT),\n      text: z.string(),\n    }),\n    z.object({\n      type: z.literal(BookmarkTypes.ASSET),\n      assetType: z.enum([\"image\", \"pdf\"]),\n      assetId: z.string(),\n      assetUrl: z.string(),\n      fileName: z.string().nullish(),\n      sourceUrl: z.string().nullish(),\n    }),\n  ]),\n});\n\nexport type ZPublicBookmark = z.infer<typeof zPublicBookmarkSchema>;\n"
  },
  {
    "path": "packages/shared/types/config.ts",
    "content": "import { z } from \"zod\";\n\nexport const zClientConfigSchema = z.object({\n  publicUrl: z.string(),\n  publicApiUrl: z.string(),\n  demoMode: z\n    .object({\n      email: z.string().optional(),\n      password: z.string().optional(),\n    })\n    .optional(),\n  auth: z.object({\n    disableSignups: z.boolean(),\n    disablePasswordAuth: z.boolean(),\n  }),\n  turnstile: z\n    .object({\n      siteKey: z.string(),\n    })\n    .nullable(),\n  inference: z.object({\n    isConfigured: z.boolean(),\n    inferredTagLang: z.string(),\n    enableAutoTagging: z.boolean(),\n    enableAutoSummarization: z.boolean(),\n  }),\n  legal: z.object({\n    termsOfServiceUrl: z.string().optional(),\n    privacyPolicyUrl: z.string().optional(),\n  }),\n  serverVersion: z.string().optional(),\n  disableNewReleaseCheck: z.boolean(),\n});\n"
  },
  {
    "path": "packages/shared/types/feeds.ts",
    "content": "import { z } from \"zod\";\n\nconst MAX_FEED_URL_LENGTH = 2000;\nconst MAX_FEED_NAME_LENGTH = 100;\n\nexport const zAppliesToEnumSchema = z.enum([\"all\", \"text\", \"images\"]);\n\nexport const zFeedSchema = z.object({\n  id: z.string(),\n  name: z.string().min(1).max(MAX_FEED_NAME_LENGTH),\n  url: z.string().url(),\n  enabled: z.boolean(),\n  importTags: z.boolean(),\n  lastFetchedStatus: z.enum([\"success\", \"failure\", \"pending\"]).nullable(),\n  lastFetchedAt: z.date().nullable(),\n});\n\nexport type ZFeed = z.infer<typeof zFeedSchema>;\n\nexport const zNewFeedSchema = z.object({\n  name: z.string().min(1).max(MAX_FEED_NAME_LENGTH),\n  url: z.string().max(MAX_FEED_URL_LENGTH).url(),\n  enabled: z.boolean(),\n  importTags: z.boolean().optional().default(false),\n});\n\nexport const zUpdateFeedSchema = z.object({\n  feedId: z.string(),\n  name: z.string().min(1).max(MAX_FEED_NAME_LENGTH).optional(),\n  url: z.string().max(MAX_FEED_URL_LENGTH).url().optional(),\n  enabled: z.boolean().optional(),\n  importTags: z.boolean().optional(),\n});\n"
  },
  {
    "path": "packages/shared/types/highlights.ts",
    "content": "import { z } from \"zod\";\n\nimport { zCursorV2 } from \"./pagination\";\n\nexport const DEFAULT_NUM_HIGHLIGHTS_PER_PAGE = 20;\n\nconst zHighlightColorSchema = z.enum([\"yellow\", \"red\", \"green\", \"blue\"]);\nexport type ZHighlightColor = z.infer<typeof zHighlightColorSchema>;\nexport const SUPPORTED_HIGHLIGHT_COLORS = zHighlightColorSchema.options;\n\nconst zHighlightBaseSchema = z.object({\n  bookmarkId: z.string(),\n  startOffset: z.number(),\n  endOffset: z.number(),\n  color: zHighlightColorSchema.default(\"yellow\"),\n  text: z.string().nullable(),\n  note: z.string().nullable(),\n});\n\nexport const zHighlightSchema = zHighlightBaseSchema.merge(\n  z.object({\n    id: z.string(),\n    userId: z.string(),\n    createdAt: z.date(),\n  }),\n);\n\nexport type ZHighlight = z.infer<typeof zHighlightSchema>;\n\nexport const zNewHighlightSchema = zHighlightBaseSchema;\n\nexport const zUpdateHighlightSchema = z.object({\n  highlightId: z.string(),\n  color: zHighlightColorSchema.optional(),\n  note: z.string().nullable().optional(),\n});\n\nexport const zGetAllHighlightsResponseSchema = z.object({\n  highlights: z.array(zHighlightSchema),\n  nextCursor: zCursorV2.nullable(),\n});\nexport type ZGetAllHighlightsResponse = z.infer<\n  typeof zGetAllHighlightsResponseSchema\n>;\n"
  },
  {
    "path": "packages/shared/types/importSessions.ts",
    "content": "import { z } from \"zod\";\n\nexport const zImportSessionStatusSchema = z.enum([\n  \"staging\",\n  \"pending\",\n  \"running\",\n  \"paused\",\n  \"completed\",\n  \"failed\",\n]);\nexport type ZImportSessionStatus = z.infer<typeof zImportSessionStatusSchema>;\n\nexport const zImportSessionBookmarkStatusSchema = z.enum([\n  \"pending\",\n  \"processing\",\n  \"completed\",\n  \"failed\",\n]);\nexport type ZImportSessionBookmarkStatus = z.infer<\n  typeof zImportSessionBookmarkStatusSchema\n>;\n\nexport const zImportSessionSchema = z.object({\n  id: z.string(),\n  name: z.string(),\n  userId: z.string(),\n  message: z.string().nullable(),\n  rootListId: z.string().nullable(),\n  status: zImportSessionStatusSchema,\n  createdAt: z.date(),\n  modifiedAt: z.date().nullable(),\n});\nexport type ZImportSession = z.infer<typeof zImportSessionSchema>;\n\nexport const zImportSessionWithStatsSchema = zImportSessionSchema.extend({\n  totalBookmarks: z.number(),\n  completedBookmarks: z.number(),\n  failedBookmarks: z.number(),\n  pendingBookmarks: z.number(),\n  processingBookmarks: z.number(),\n});\nexport type ZImportSessionWithStats = z.infer<\n  typeof zImportSessionWithStatsSchema\n>;\n\nexport const zCreateImportSessionRequestSchema = z.object({\n  name: z.string().min(1).max(255),\n  rootListId: z.string().optional(),\n});\nexport type ZCreateImportSessionRequest = z.infer<\n  typeof zCreateImportSessionRequestSchema\n>;\n\nexport const zGetImportSessionStatsRequestSchema = z.object({\n  importSessionId: z.string(),\n});\nexport type ZGetImportSessionStatsRequest = z.infer<\n  typeof zGetImportSessionStatsRequestSchema\n>;\n\nexport const zListImportSessionsRequestSchema = z.object({});\nexport type ZListImportSessionsRequest = z.infer<\n  typeof zListImportSessionsRequestSchema\n>;\n\nexport const zListImportSessionsResponseSchema = z.object({\n  sessions: z.array(zImportSessionWithStatsSchema),\n});\nexport type ZListImportSessionsResponse = z.infer<\n  typeof zListImportSessionsResponseSchema\n>;\n\nexport const zDeleteImportSessionRequestSchema = z.object({\n  importSessionId: z.string(),\n});\nexport type ZDeleteImportSessionRequest = z.infer<\n  typeof zDeleteImportSessionRequestSchema\n>;\n"
  },
  {
    "path": "packages/shared/types/lists.ts",
    "content": "import { z } from \"zod\";\n\nimport { parseSearchQuery } from \"../searchQueryParser\";\n\nexport const MAX_LIST_NAME_LENGTH = 100;\nexport const MAX_LIST_DESCRIPTION_LENGTH = 500;\n\nexport const zNewBookmarkListSchema = z\n  .object({\n    name: z\n      .string()\n      .min(1, \"List name can't be empty\")\n      .max(\n        MAX_LIST_NAME_LENGTH,\n        `List name is at most ${MAX_LIST_NAME_LENGTH} chars`,\n      ),\n    description: z\n      .string()\n      .min(0, \"Description can be empty\")\n      .max(\n        MAX_LIST_DESCRIPTION_LENGTH,\n        `Description can have at most ${MAX_LIST_DESCRIPTION_LENGTH} chars`,\n      )\n      .optional(),\n    icon: z.string(),\n    type: z.enum([\"manual\", \"smart\"]).optional().default(\"manual\"),\n    query: z.string().min(1).optional(),\n    parentId: z.string().nullish(),\n  })\n  .refine((val) => val.type === \"smart\" || !val.query, {\n    message: \"Manual lists cannot have a query\",\n    path: [\"query\"],\n  })\n  .refine((val) => val.type === \"manual\" || val.query, {\n    message: \"Smart lists must have a query\",\n    path: [\"query\"],\n  })\n  .refine(\n    (val) => !val.query || parseSearchQuery(val.query).result === \"full\",\n    {\n      message: \"Smart search query is not valid\",\n      path: [\"query\"],\n    },\n  )\n  .refine((val) => !val.query || parseSearchQuery(val.query).text.length == 0, {\n    message:\n      \"Smart lists cannot have unqualified terms (aka full text search terms) in the query\",\n    path: [\"query\"],\n  });\n\nexport const zBookmarkListSchema = z.object({\n  id: z.string(),\n  name: z.string(),\n  description: z.string().nullish(),\n  icon: z.string(),\n  parentId: z.string().nullable(),\n  type: z.enum([\"manual\", \"smart\"]).default(\"manual\"),\n  query: z.string().nullish(),\n  public: z.boolean(),\n  hasCollaborators: z.boolean(),\n  userRole: z.enum([\"owner\", \"editor\", \"viewer\", \"public\"]),\n});\n\nexport type ZBookmarkList = z.infer<typeof zBookmarkListSchema>;\n\nexport const zEditBookmarkListSchema = z.object({\n  listId: z.string(),\n  name: z\n    .string()\n    .min(1, \"List name can't be empty\")\n    .max(\n      MAX_LIST_NAME_LENGTH,\n      `List name is at most ${MAX_LIST_NAME_LENGTH} chars`,\n    )\n    .optional(),\n  description: z\n    .string()\n    .min(0, \"Description can be empty\")\n    .max(\n      MAX_LIST_DESCRIPTION_LENGTH,\n      `Description can have at most ${MAX_LIST_DESCRIPTION_LENGTH} chars`,\n    )\n    .nullish(),\n  icon: z.string().optional(),\n  parentId: z.string().nullish(),\n  query: z.string().min(1).optional(),\n  public: z.boolean().optional(),\n});\n\nexport const zEditBookmarkListSchemaWithValidation = zEditBookmarkListSchema\n  .refine((val) => val.parentId != val.listId, {\n    message: \"List can't be its own parent\",\n    path: [\"parentId\"],\n  })\n  .refine(\n    (val) => !val.query || parseSearchQuery(val.query).result === \"full\",\n    {\n      message: \"Smart search query is not valid\",\n      path: [\"query\"],\n    },\n  )\n  .refine((val) => !val.query || parseSearchQuery(val.query).text.length == 0, {\n    message:\n      \"Smart lists cannot have unqualified terms (aka full text search terms) in the query\",\n    path: [\"query\"],\n  });\n\nexport const zMergeListSchema = z\n  .object({\n    sourceId: z.string(),\n    targetId: z.string(),\n    deleteSourceAfterMerge: z.boolean(),\n  })\n  .refine((val) => val.sourceId !== val.targetId, {\n    message: \"Cannot merge a list into itself\",\n    path: [\"targetId\"],\n  });\n\nexport type ZMergeList = z.infer<typeof zMergeListSchema>;\n"
  },
  {
    "path": "packages/shared/types/pagination.ts",
    "content": "import { z } from \"zod\";\n\nexport const zCursorV2 = z.object({\n  createdAt: z.date(),\n  id: z.string(),\n});\n\nexport type ZCursor = z.infer<typeof zCursorV2>;\n"
  },
  {
    "path": "packages/shared/types/prompts.ts",
    "content": "import { z } from \"zod\";\n\nconst MAX_PROMPT_TEXT_LENGTH = 500;\n\nexport const zAppliesToEnumSchema = z.enum([\n  \"all_tagging\",\n  \"text\",\n  \"images\",\n  \"summary\",\n]);\n\nexport const zPromptSchema = z.object({\n  id: z.string(),\n  text: z.string(),\n  enabled: z.boolean(),\n  appliesTo: zAppliesToEnumSchema,\n});\n\nexport type ZPrompt = z.infer<typeof zPromptSchema>;\n\nexport const zNewPromptSchema = z.object({\n  text: z.string().min(1).max(MAX_PROMPT_TEXT_LENGTH),\n  appliesTo: zAppliesToEnumSchema,\n});\n\nexport const zUpdatePromptSchema = z.object({\n  promptId: z.string(),\n  text: z.string().max(MAX_PROMPT_TEXT_LENGTH).optional(),\n  appliesTo: zAppliesToEnumSchema.optional(),\n  enabled: z.boolean().optional(),\n});\n"
  },
  {
    "path": "packages/shared/types/readers.ts",
    "content": "import { z } from \"zod\";\n\nimport { ZReaderFontFamily, zReaderFontFamilySchema } from \"./users\";\n\nexport const READER_DEFAULTS = {\n  fontSize: 18,\n  lineHeight: 1.6,\n  fontFamily: \"serif\" as const,\n} as const;\n\nexport const READER_FONT_FAMILIES: Record<ZReaderFontFamily, string> = {\n  serif: \"ui-serif, Georgia, Cambria, serif\",\n  sans: \"ui-sans-serif, system-ui, sans-serif\",\n  mono: \"ui-monospace, Menlo, Monaco, monospace\",\n} as const;\n\n// Setting constraints for UI controls\nexport const READER_SETTING_CONSTRAINTS = {\n  fontSize: { min: 12, max: 24, step: 1 },\n  lineHeight: { min: 1.2, max: 2.5, step: 0.1 },\n} as const;\n\n// Formatting functions for display\nexport function formatFontSize(value: number): string {\n  return `${value}px`;\n}\n\nexport function formatLineHeight(value: number): string {\n  return value.toFixed(1);\n}\n\nexport function formatFontFamily(\n  value: ZReaderFontFamily,\n  t?: (key: string) => string,\n): string {\n  if (t) {\n    return t(`settings.info.reader_settings.${value}`);\n  }\n  // Fallback labels when no translation function provided\n  switch (value) {\n    case \"serif\":\n      return \"Serif\";\n    case \"sans\":\n      return \"Sans Serif\";\n    case \"mono\":\n      return \"Monospace\";\n  }\n}\n\nexport const zReaderSettings = z.object({\n  fontSize: z.number().int().min(12).max(24),\n  lineHeight: z.number().min(1.2).max(2.5),\n  fontFamily: zReaderFontFamilySchema,\n});\n\nexport type ReaderSettings = z.infer<typeof zReaderSettings>;\n\nexport const zReaderSettingsPartial = zReaderSettings.partial();\nexport type ReaderSettingsPartial = z.infer<typeof zReaderSettingsPartial>;\n"
  },
  {
    "path": "packages/shared/types/rules.ts",
    "content": "import { RefinementCtx, z } from \"zod\";\n\nimport { zBookmarkSourceSchema } from \"./bookmarks\";\n\n// Events\nconst zBookmarkAddedEvent = z.object({\n  type: z.literal(\"bookmarkAdded\"),\n});\n\nconst zTagAddedEvent = z.object({\n  type: z.literal(\"tagAdded\"),\n  tagId: z.string(),\n});\n\nconst zTagRemovedEvent = z.object({\n  type: z.literal(\"tagRemoved\"),\n  tagId: z.string(),\n});\n\nconst zAddedToListEvent = z.object({\n  type: z.literal(\"addedToList\"),\n  listId: z.string(),\n});\n\nconst zRemovedFromListEvent = z.object({\n  type: z.literal(\"removedFromList\"),\n  listId: z.string(),\n});\n\nconst zFavouritedEvent = z.object({\n  type: z.literal(\"favourited\"),\n});\n\nconst zArchivedEvent = z.object({\n  type: z.literal(\"archived\"),\n});\n\nexport const zRuleEngineEventSchema = z.discriminatedUnion(\"type\", [\n  zBookmarkAddedEvent,\n  zTagAddedEvent,\n  zTagRemovedEvent,\n  zAddedToListEvent,\n  zRemovedFromListEvent,\n  zFavouritedEvent,\n  zArchivedEvent,\n]);\nexport type RuleEngineEvent = z.infer<typeof zRuleEngineEventSchema>;\n\n// Conditions\nconst zAlwaysTrueCondition = z.object({\n  type: z.literal(\"alwaysTrue\"),\n});\n\nconst zUrlContainsCondition = z.object({\n  type: z.literal(\"urlContains\"),\n  str: z.string(),\n});\n\nconst zUrlDoesNotContainCondition = z.object({\n  type: z.literal(\"urlDoesNotContain\"),\n  str: z.string(),\n});\n\nconst zTitleContainsCondition = z.object({\n  type: z.literal(\"titleContains\"),\n  str: z.string(),\n});\n\nconst zTitleDoesNotContainCondition = z.object({\n  type: z.literal(\"titleDoesNotContain\"),\n  str: z.string(),\n});\n\nconst zImportedFromFeedCondition = z.object({\n  type: z.literal(\"importedFromFeed\"),\n  feedId: z.string(),\n});\n\nconst zBookmarkTypeIsCondition = z.object({\n  type: z.literal(\"bookmarkTypeIs\"),\n  bookmarkType: z.enum([\"link\", \"text\", \"asset\"]),\n});\n\nconst zBookmarkSourceIsCondition = z.object({\n  type: z.literal(\"bookmarkSourceIs\"),\n  source: zBookmarkSourceSchema,\n});\n\nconst zHasTagCondition = z.object({\n  type: z.literal(\"hasTag\"),\n  tagId: z.string(),\n});\n\nconst zIsFavouritedCondition = z.object({\n  type: z.literal(\"isFavourited\"),\n});\n\nconst zIsArchivedCondition = z.object({\n  type: z.literal(\"isArchived\"),\n});\n\nconst nonRecursiveCondition = z.discriminatedUnion(\"type\", [\n  zAlwaysTrueCondition,\n  zUrlContainsCondition,\n  zUrlDoesNotContainCondition,\n  zTitleContainsCondition,\n  zTitleDoesNotContainCondition,\n  zImportedFromFeedCondition,\n  zBookmarkTypeIsCondition,\n  zBookmarkSourceIsCondition,\n  zHasTagCondition,\n  zIsFavouritedCondition,\n  zIsArchivedCondition,\n]);\n\ntype NonRecursiveCondition = z.infer<typeof nonRecursiveCondition>;\nexport type RuleEngineCondition =\n  | NonRecursiveCondition\n  | { type: \"and\"; conditions: RuleEngineCondition[] }\n  | { type: \"or\"; conditions: RuleEngineCondition[] };\n\nexport const zRuleEngineConditionSchema: z.ZodType<RuleEngineCondition> =\n  z.lazy(() =>\n    z.discriminatedUnion(\"type\", [\n      zAlwaysTrueCondition,\n      zUrlContainsCondition,\n      zUrlDoesNotContainCondition,\n      zTitleContainsCondition,\n      zTitleDoesNotContainCondition,\n      zImportedFromFeedCondition,\n      zBookmarkTypeIsCondition,\n      zBookmarkSourceIsCondition,\n      zHasTagCondition,\n      zIsFavouritedCondition,\n      zIsArchivedCondition,\n      z.object({\n        type: z.literal(\"and\"),\n        conditions: z.array(zRuleEngineConditionSchema),\n      }),\n      z.object({\n        type: z.literal(\"or\"),\n        conditions: z.array(zRuleEngineConditionSchema),\n      }),\n    ]),\n  );\n\n// Actions\nconst zAddTagAction = z.object({\n  type: z.literal(\"addTag\"),\n  tagId: z.string(),\n});\n\nconst zRemoveTagAction = z.object({\n  type: z.literal(\"removeTag\"),\n  tagId: z.string(),\n});\n\nconst zAddToListAction = z.object({\n  type: z.literal(\"addToList\"),\n  listId: z.string(),\n});\n\nconst zRemoveFromListAction = z.object({\n  type: z.literal(\"removeFromList\"),\n  listId: z.string(),\n});\n\nconst zDownloadFullPageArchiveAction = z.object({\n  type: z.literal(\"downloadFullPageArchive\"),\n});\n\nconst zFavouriteBookmarkAction = z.object({\n  type: z.literal(\"favouriteBookmark\"),\n});\n\nconst zArchiveBookmarkAction = z.object({\n  type: z.literal(\"archiveBookmark\"),\n});\n\nexport const zRuleEngineActionSchema = z.discriminatedUnion(\"type\", [\n  zAddTagAction,\n  zRemoveTagAction,\n  zAddToListAction,\n  zRemoveFromListAction,\n  zDownloadFullPageArchiveAction,\n  zFavouriteBookmarkAction,\n  zArchiveBookmarkAction,\n]);\nexport type RuleEngineAction = z.infer<typeof zRuleEngineActionSchema>;\n\nexport const zRuleEngineRuleSchema = z.object({\n  id: z.string(),\n  name: z.string().min(1),\n  description: z.string().nullable(),\n  enabled: z.boolean(),\n  event: zRuleEngineEventSchema,\n  condition: zRuleEngineConditionSchema,\n  actions: z.array(zRuleEngineActionSchema),\n});\nexport type RuleEngineRule = z.infer<typeof zRuleEngineRuleSchema>;\n\nconst ruleValidaitorFn = (\n  r: Omit<RuleEngineRule, \"id\">,\n  ctx: RefinementCtx,\n) => {\n  const validateEvent = (event: RuleEngineEvent) => {\n    switch (event.type) {\n      case \"bookmarkAdded\":\n      case \"favourited\":\n      case \"archived\":\n        return true;\n      case \"tagAdded\":\n      case \"tagRemoved\":\n        if (event.tagId.length == 0) {\n          ctx.addIssue({\n            code: \"custom\",\n            message: \"You must specify a tag for this event type\",\n            path: [\"event\", \"tagId\"],\n          });\n          return false;\n        }\n        return true;\n      case \"addedToList\":\n      case \"removedFromList\":\n        if (event.listId.length == 0) {\n          ctx.addIssue({\n            code: \"custom\",\n            message: \"You must specify a list for this event type\",\n            path: [\"event\", \"listId\"],\n          });\n          return false;\n        }\n        return true;\n      default: {\n        const _exhaustiveCheck: never = event;\n        return false;\n      }\n    }\n  };\n\n  const validateCondition = (\n    condition: RuleEngineCondition,\n    depth: number,\n  ): boolean => {\n    if (depth > 10) {\n      ctx.addIssue({\n        code: \"custom\",\n        message:\n          \"Rule conditions are too complex. Maximum allowed depth is 10.\",\n      });\n      return false;\n    }\n    switch (condition.type) {\n      case \"alwaysTrue\":\n      case \"bookmarkTypeIs\":\n      case \"bookmarkSourceIs\":\n      case \"isFavourited\":\n      case \"isArchived\":\n        return true;\n      case \"urlContains\":\n      case \"urlDoesNotContain\":\n        if (condition.str.length == 0) {\n          ctx.addIssue({\n            code: \"custom\",\n            message: \"You must specify a URL for this condition type\",\n            path: [\"condition\", \"str\"],\n          });\n          return false;\n        }\n        return true;\n      case \"titleContains\":\n      case \"titleDoesNotContain\":\n        if (condition.str.length == 0) {\n          ctx.addIssue({\n            code: \"custom\",\n            message: \"You must specify a title for this condition type\",\n            path: [\"condition\", \"str\"],\n          });\n          return false;\n        }\n        return true;\n      case \"hasTag\":\n        if (condition.tagId.length == 0) {\n          ctx.addIssue({\n            code: \"custom\",\n            message: \"You must specify a tag for this condition type\",\n            path: [\"condition\", \"tagId\"],\n          });\n          return false;\n        }\n        return true;\n      case \"importedFromFeed\":\n        if (condition.feedId.length == 0) {\n          ctx.addIssue({\n            code: \"custom\",\n            message: \"You must specify a feed for this condition type\",\n            path: [\"condition\", \"feedId\"],\n          });\n          return false;\n        }\n        return true;\n      case \"and\":\n      case \"or\":\n        if (condition.conditions.length == 0) {\n          ctx.addIssue({\n            code: \"custom\",\n            message:\n              \"You must specify at least one condition for this condition type\",\n            path: [\"condition\"],\n          });\n          return false;\n        }\n        return condition.conditions.every((c) =>\n          validateCondition(c, depth + 1),\n        );\n      default: {\n        const _exhaustiveCheck: never = condition;\n        return false;\n      }\n    }\n  };\n  const validateAction = (action: RuleEngineAction): boolean => {\n    switch (action.type) {\n      case \"addTag\":\n      case \"removeTag\":\n        if (action.tagId.length == 0) {\n          ctx.addIssue({\n            code: \"custom\",\n            message: \"You must specify a tag for this action type\",\n            path: [\"actions\", \"tagId\"],\n          });\n          return false;\n        }\n        return true;\n      case \"addToList\":\n      case \"removeFromList\":\n        if (action.listId.length == 0) {\n          ctx.addIssue({\n            code: \"custom\",\n            message: \"You must specify a list for this action type\",\n            path: [\"actions\", \"listId\"],\n          });\n          return false;\n        }\n        return true;\n      case \"downloadFullPageArchive\":\n      case \"favouriteBookmark\":\n      case \"archiveBookmark\":\n        return true;\n      default: {\n        const _exhaustiveCheck: never = action;\n        return false;\n      }\n    }\n  };\n  validateEvent(r.event);\n  validateCondition(r.condition, 0);\n  if (r.actions.length == 0) {\n    ctx.addIssue({\n      code: \"custom\",\n      message: \"You must specify at least one action\",\n      path: [\"actions\"],\n    });\n    return false;\n  }\n  r.actions.every((a) => validateAction(a));\n};\n\nexport const zNewRuleEngineRuleSchema = zRuleEngineRuleSchema\n  .omit({\n    id: true,\n  })\n  .superRefine(ruleValidaitorFn);\n\nexport const zUpdateRuleEngineRuleSchema =\n  zRuleEngineRuleSchema.superRefine(ruleValidaitorFn);\n"
  },
  {
    "path": "packages/shared/types/search.ts",
    "content": "import { z } from \"zod\";\n\nimport { BookmarkTypes, zBookmarkSourceSchema } from \"./bookmarks\";\n\nconst zTagNameMatcher = z.object({\n  type: z.literal(\"tagName\"),\n  tagName: z.string(),\n  inverse: z.boolean(),\n});\n\nconst zListNameMatcher = z.object({\n  type: z.literal(\"listName\"),\n  listName: z.string(),\n  inverse: z.boolean(),\n});\n\nconst zRssFeedNameMatcher = z.object({\n  type: z.literal(\"rssFeedName\"),\n  feedName: z.string(),\n  inverse: z.boolean(),\n});\n\nconst zArchivedMatcher = z.object({\n  type: z.literal(\"archived\"),\n  archived: z.boolean(),\n});\n\nconst zUrlMatcher = z.object({\n  type: z.literal(\"url\"),\n  url: z.string(),\n  inverse: z.boolean(),\n});\n\nconst zTitleMatcher = z.object({\n  type: z.literal(\"title\"),\n  title: z.string(),\n  inverse: z.boolean(),\n});\n\nconst zFavouritedMatcher = z.object({\n  type: z.literal(\"favourited\"),\n  favourited: z.boolean(),\n});\n\nconst zDateAfterMatcher = z.object({\n  type: z.literal(\"dateAfter\"),\n  dateAfter: z.date(),\n  inverse: z.boolean(),\n});\n\nconst zDateBeforeMatcher = z.object({\n  type: z.literal(\"dateBefore\"),\n  dateBefore: z.date(),\n  inverse: z.boolean(),\n});\n\nconst zAgeMatcher = z.object({\n  type: z.literal(\"age\"),\n  relativeDate: z.object({\n    direction: z.enum([\"newer\", \"older\"]),\n    amount: z.number(),\n    unit: z.enum([\"day\", \"week\", \"month\", \"year\"]),\n  }),\n});\n\nconst zIsTaggedMatcher = z.object({\n  type: z.literal(\"tagged\"),\n  tagged: z.boolean(),\n});\n\nconst zIsInListMatcher = z.object({\n  type: z.literal(\"inlist\"),\n  inList: z.boolean(),\n});\n\nconst zTypeMatcher = z.object({\n  type: z.literal(\"type\"),\n  typeName: z.enum([\n    BookmarkTypes.LINK,\n    BookmarkTypes.TEXT,\n    BookmarkTypes.ASSET,\n  ]),\n  inverse: z.boolean(),\n});\n\nconst zBrokenLinksMatcher = z.object({\n  type: z.literal(\"brokenLinks\"),\n  brokenLinks: z.boolean(),\n});\n\nconst zSourceMatcher = z.object({\n  type: z.literal(\"source\"),\n  source: zBookmarkSourceSchema,\n  inverse: z.boolean(),\n});\n\nconst zNonRecursiveMatcher = z.union([\n  zTagNameMatcher,\n  zListNameMatcher,\n  zArchivedMatcher,\n  zUrlMatcher,\n  zTitleMatcher,\n  zFavouritedMatcher,\n  zDateAfterMatcher,\n  zDateBeforeMatcher,\n  zAgeMatcher,\n  zIsTaggedMatcher,\n  zIsInListMatcher,\n  zTypeMatcher,\n  zRssFeedNameMatcher,\n  zBrokenLinksMatcher,\n  zSourceMatcher,\n]);\n\ntype NonRecursiveMatcher = z.infer<typeof zNonRecursiveMatcher>;\nexport type Matcher =\n  | NonRecursiveMatcher\n  | { type: \"and\"; matchers: Matcher[] }\n  | { type: \"or\"; matchers: Matcher[] };\n\nexport const zMatcherSchema: z.ZodType<Matcher> = z.lazy(() => {\n  return z.discriminatedUnion(\"type\", [\n    zTagNameMatcher,\n    zListNameMatcher,\n    zArchivedMatcher,\n    zUrlMatcher,\n    zTitleMatcher,\n    zFavouritedMatcher,\n    zDateAfterMatcher,\n    zDateBeforeMatcher,\n    zAgeMatcher,\n    zIsTaggedMatcher,\n    zIsInListMatcher,\n    zTypeMatcher,\n    zRssFeedNameMatcher,\n    zBrokenLinksMatcher,\n    zSourceMatcher,\n    z.object({\n      type: z.literal(\"and\"),\n      matchers: z.array(zMatcherSchema),\n    }),\n    z.object({\n      type: z.literal(\"or\"),\n      matchers: z.array(zMatcherSchema),\n    }),\n  ]);\n});\n"
  },
  {
    "path": "packages/shared/types/tags.ts",
    "content": "import { z } from \"zod\";\n\nimport { normalizeTagName } from \"../utils/tag\";\n\nexport const MAX_NUM_TAGS_PER_PAGE = 1000;\n\nconst zTagNameSchemaWithValidation = z\n  .string()\n  .transform((s) => normalizeTagName(s).trim())\n  .pipe(z.string().min(1));\n\nexport const zCreateTagRequestSchema = z.object({\n  name: zTagNameSchemaWithValidation,\n});\n\nexport const zAttachedByEnumSchema = z.enum([\"ai\", \"human\"]);\nexport type ZAttachedByEnum = z.infer<typeof zAttachedByEnumSchema>;\nexport const zBookmarkTagSchema = z.object({\n  id: z.string(),\n  name: z.string(),\n  attachedBy: zAttachedByEnumSchema,\n});\nexport type ZBookmarkTags = z.infer<typeof zBookmarkTagSchema>;\n\nexport const zGetTagResponseSchema = z.object({\n  id: z.string(),\n  name: z.string(),\n  numBookmarks: z.number(),\n  numBookmarksByAttachedType: z.record(zAttachedByEnumSchema, z.number()),\n});\nexport type ZGetTagResponse = z.infer<typeof zGetTagResponseSchema>;\n\nexport const zUpdateTagRequestSchema = z.object({\n  tagId: z.string(),\n  name: zTagNameSchemaWithValidation.optional(),\n});\n\nexport const zTagBasicSchema = z.object({\n  id: z.string(),\n  name: z.string(),\n});\nexport type ZTagBasic = z.infer<typeof zTagBasicSchema>;\n\nexport const zTagCursorSchema = z.object({\n  page: z.number().int().min(0),\n});\n\nexport const zTagListRequestSchema = z.object({\n  nameContains: z.string().optional(),\n  ids: z.array(z.string()).optional(),\n  attachedBy: z.enum([...zAttachedByEnumSchema.options, \"none\"]).optional(),\n  sortBy: z.enum([\"name\", \"usage\", \"relevance\"]).optional().default(\"usage\"),\n  cursor: zTagCursorSchema.nullish().default({ page: 0 }),\n  // TODO: Remove the optional to enforce a limit after the next release\n  limit: z.number().int().min(1).max(MAX_NUM_TAGS_PER_PAGE).optional(),\n});\n\nexport const zTagListValidatedRequestSchema = zTagListRequestSchema.refine(\n  (val) => val.sortBy != \"relevance\" || val.nameContains !== undefined,\n  {\n    message: \"Relevance sorting requires a nameContains filter\",\n    path: [\"sortBy\"],\n  },\n);\n\nexport const zTagListResponseSchema = z.object({\n  tags: z.array(zGetTagResponseSchema),\n  nextCursor: zTagCursorSchema.nullish(),\n});\nexport type ZTagListResponse = z.infer<typeof zTagListResponseSchema>;\n\n// API Types\n\nexport const zTagListQueryParamsSchema = z.object({\n  nameContains: zTagListRequestSchema.shape.nameContains,\n  sort: zTagListRequestSchema.shape.sortBy,\n  attachedBy: zTagListRequestSchema.shape.attachedBy,\n  cursor: z\n    .string()\n    .transform((val, ctx) => {\n      try {\n        return JSON.parse(Buffer.from(val, \"base64url\").toString(\"utf8\"));\n      } catch {\n        ctx.addIssue({\n          code: \"custom\",\n          message: \"Invalid cursor\",\n        });\n        return z.NEVER;\n      }\n    })\n    .optional()\n    .pipe(zTagListRequestSchema.shape.cursor),\n  limit: z.coerce.number().optional(),\n});\n\nexport const zTagListApiResultSchema = z.object({\n  tags: zTagListResponseSchema.shape.tags,\n  nextCursor: z.string().nullish(),\n});\n"
  },
  {
    "path": "packages/shared/types/uploads.ts",
    "content": "import { z } from \"zod\";\n\nexport const zUploadErrorSchema = z.object({\n  error: z.string(),\n});\n\nexport type ZUploadError = z.infer<typeof zUploadErrorSchema>;\n\nexport const zUploadResponseSchema = z.object({\n  assetId: z.string(),\n  contentType: z.string(),\n  size: z.number(),\n  fileName: z.string(),\n});\n\nexport type ZUploadResponse = z.infer<typeof zUploadResponseSchema>;\n"
  },
  {
    "path": "packages/shared/types/users.ts",
    "content": "import { z } from \"zod\";\n\nimport { zBookmarkSourceSchema } from \"./bookmarks\";\n\nexport const PASSWORD_MIN_LENGTH = 8;\nexport const PASSWORD_MAX_LENGTH = 100;\n\nexport const zTagStyleSchema = z.enum([\n  \"lowercase-hyphens\",\n  \"lowercase-spaces\",\n  \"lowercase-underscores\",\n  \"titlecase-spaces\",\n  \"titlecase-hyphens\",\n  \"camelCase\",\n  \"as-generated\",\n]);\nexport type ZTagStyle = z.infer<typeof zTagStyleSchema>;\n\nexport const zSignUpSchema = z\n  .object({\n    name: z.string().min(1, { message: \"Name can't be empty\" }),\n    email: z.string().email(),\n    password: z.string().min(PASSWORD_MIN_LENGTH).max(PASSWORD_MAX_LENGTH),\n    confirmPassword: z.string(),\n    turnstileToken: z.string().optional(),\n  })\n  .refine((data) => data.password === data.confirmPassword, {\n    message: \"Passwords don't match\",\n    path: [\"confirmPassword\"],\n  });\n\nexport const zResetPasswordSchema = z.object({\n  token: z.string(),\n  newPassword: z.string().min(PASSWORD_MIN_LENGTH).max(PASSWORD_MAX_LENGTH),\n});\n\nexport const zChangePasswordSchema = z\n  .object({\n    currentPassword: z.string(),\n    newPassword: z.string().min(8).max(PASSWORD_MAX_LENGTH),\n    newPasswordConfirm: z.string(),\n  })\n  .refine((data) => data.newPassword === data.newPasswordConfirm, {\n    message: \"Passwords don't match\",\n    path: [\"newPasswordConfirm\"],\n  });\n\nexport const zWhoAmIResponseSchema = z.object({\n  id: z.string(),\n  name: z.string().nullish(),\n  email: z.string().nullish(),\n  image: z.string().nullish(),\n  localUser: z.boolean(),\n});\n\nexport const zUserStatsResponseSchema = z.object({\n  numBookmarks: z.number(),\n  numFavorites: z.number(),\n  numArchived: z.number(),\n  numTags: z.number(),\n  numLists: z.number(),\n  numHighlights: z.number(),\n  bookmarksByType: z.object({\n    link: z.number(),\n    text: z.number(),\n    asset: z.number(),\n  }),\n  topDomains: z\n    .array(\n      z.object({\n        domain: z.string(),\n        count: z.number(),\n      }),\n    )\n    .max(10),\n  totalAssetSize: z.number(),\n  assetsByType: z.array(\n    z.object({\n      type: z.string(),\n      count: z.number(),\n      totalSize: z.number(),\n    }),\n  ),\n  bookmarkingActivity: z.object({\n    thisWeek: z.number(),\n    thisMonth: z.number(),\n    thisYear: z.number(),\n    byHour: z.array(\n      z.object({\n        hour: z.number(),\n        count: z.number(),\n      }),\n    ),\n    byDayOfWeek: z.array(\n      z.object({\n        day: z.number(),\n        count: z.number(),\n      }),\n    ),\n  }),\n  tagUsage: z\n    .array(\n      z.object({\n        name: z.string(),\n        count: z.number(),\n      }),\n    )\n    .max(10),\n  bookmarksBySource: z.array(\n    z.object({\n      source: zBookmarkSourceSchema.nullable(),\n      count: z.number(),\n    }),\n  ),\n});\n\nexport const zWrappedStatsResponseSchema = z.object({\n  year: z.number(),\n  totalBookmarks: z.number(),\n  totalFavorites: z.number(),\n  totalArchived: z.number(),\n  totalHighlights: z.number(),\n  totalTags: z.number(),\n  totalLists: z.number(),\n\n  firstBookmark: z\n    .object({\n      id: z.string(),\n      title: z.string().nullable(),\n      createdAt: z.date(),\n      type: z.enum([\"link\", \"text\", \"asset\"]),\n    })\n    .nullable(),\n\n  mostActiveDay: z\n    .object({\n      date: z.string(),\n      count: z.number(),\n    })\n    .nullable(),\n\n  topDomains: z\n    .array(\n      z.object({\n        domain: z.string(),\n        count: z.number(),\n      }),\n    )\n    .max(5),\n\n  topTags: z\n    .array(\n      z.object({\n        name: z.string(),\n        count: z.number(),\n      }),\n    )\n    .max(5),\n\n  bookmarksByType: z.object({\n    link: z.number(),\n    text: z.number(),\n    asset: z.number(),\n  }),\n\n  bookmarksBySource: z.array(\n    z.object({\n      source: zBookmarkSourceSchema.nullable(),\n      count: z.number(),\n    }),\n  ),\n\n  monthlyActivity: z.array(\n    z.object({\n      month: z.number(),\n      count: z.number(),\n    }),\n  ),\n\n  peakHour: z.number(),\n  peakDayOfWeek: z.number(),\n});\n\nexport const zReaderFontFamilySchema = z.enum([\"serif\", \"sans\", \"mono\"]);\nexport type ZReaderFontFamily = z.infer<typeof zReaderFontFamilySchema>;\n\nexport const zUserSettingsSchema = z.object({\n  bookmarkClickAction: z.enum([\n    \"open_original_link\",\n    \"expand_bookmark_preview\",\n  ]),\n  archiveDisplayBehaviour: z.enum([\"show\", \"hide\"]),\n  timezone: z.string(),\n  backupsEnabled: z.boolean(),\n  backupsFrequency: z.enum([\"daily\", \"weekly\"]),\n  backupsRetentionDays: z.number().int().min(1).max(365),\n  // Reader settings (nullable = opt-in, null means use client default)\n  readerFontSize: z.number().int().min(12).max(24).nullable(),\n  readerLineHeight: z.number().min(1.2).max(2.5).nullable(),\n  readerFontFamily: zReaderFontFamilySchema.nullable(),\n  // AI settings (nullable = opt-in, null means use server default)\n  autoTaggingEnabled: z.boolean().nullable(),\n  autoSummarizationEnabled: z.boolean().nullable(),\n  tagStyle: zTagStyleSchema,\n  curatedTagIds: z.array(z.string()).nullable(),\n  inferredTagLang: z.string().nullable(),\n});\n\nexport type ZUserSettings = z.infer<typeof zUserSettingsSchema>;\n\nexport const zUpdateUserSettingsSchema = zUserSettingsSchema.partial().pick({\n  bookmarkClickAction: true,\n  archiveDisplayBehaviour: true,\n  timezone: true,\n  backupsEnabled: true,\n  backupsFrequency: true,\n  backupsRetentionDays: true,\n  readerFontSize: true,\n  readerLineHeight: true,\n  readerFontFamily: true,\n  autoTaggingEnabled: true,\n  autoSummarizationEnabled: true,\n  tagStyle: true,\n  curatedTagIds: true,\n  inferredTagLang: true,\n});\n\nexport const zUpdateBackupSettingsSchema = zUpdateUserSettingsSchema.pick({\n  backupsEnabled: true,\n  backupsFrequency: true,\n  backupsRetentionDays: true,\n});\n"
  },
  {
    "path": "packages/shared/types/webhooks.ts",
    "content": "import { z } from \"zod\";\n\nconst MAX_WEBHOOK_URL_LENGTH = 500;\nconst MAX_WEBHOOK_TOKEN_LENGTH = 100;\n\nexport const zWebhookEventSchema = z.enum([\n  \"created\",\n  \"edited\",\n  \"crawled\",\n  \"ai tagged\",\n  \"deleted\",\n]);\nexport type ZWebhookEvent = z.infer<typeof zWebhookEventSchema>;\n\nexport const zWebhookSchema = z.object({\n  id: z.string(),\n  url: z.string().url(),\n  events: z.array(zWebhookEventSchema),\n  hasToken: z.boolean(),\n  createdAt: z.date(),\n});\n\nexport type ZWebhook = z.infer<typeof zWebhookSchema>;\n\nexport const zNewWebhookSchema = z.object({\n  url: z.string().max(MAX_WEBHOOK_URL_LENGTH).url(),\n  events: z.array(zWebhookEventSchema).min(1),\n  token: z.string().max(MAX_WEBHOOK_TOKEN_LENGTH).optional(),\n});\n\nexport const zUpdateWebhookSchema = z.object({\n  webhookId: z.string(),\n  url: z.string().max(MAX_WEBHOOK_URL_LENGTH).url().optional(),\n  events: z.array(zWebhookEventSchema).min(1).optional(),\n  token: z.string().max(MAX_WEBHOOK_TOKEN_LENGTH).nullish(),\n});\n"
  },
  {
    "path": "packages/shared/utils/assetUtils.ts",
    "content": "export function getAssetUrl(assetId: string) {\n  return `/api/assets/${assetId}`;\n}\n"
  },
  {
    "path": "packages/shared/utils/bookmarkUtils.ts",
    "content": "import { BookmarkTypes, ZBookmark, ZBookmarkedLink } from \"../types/bookmarks\";\nimport { getAssetUrl } from \"./assetUtils\";\n\nexport function getBookmarkLinkAssetIdOrUrl(bookmark: ZBookmarkedLink) {\n  if (bookmark.imageAssetId) {\n    return { assetId: bookmark.imageAssetId, localAsset: true as const };\n  }\n  if (bookmark.screenshotAssetId) {\n    return { assetId: bookmark.screenshotAssetId, localAsset: true as const };\n  }\n  return bookmark.imageUrl\n    ? { url: bookmark.imageUrl, localAsset: false as const }\n    : null;\n}\n\nexport function getBookmarkLinkImageUrl(bookmark: ZBookmarkedLink) {\n  const assetOrUrl = getBookmarkLinkAssetIdOrUrl(bookmark);\n  if (!assetOrUrl) {\n    return null;\n  }\n  if (!assetOrUrl.localAsset) {\n    return assetOrUrl;\n  }\n  return {\n    url: getAssetUrl(assetOrUrl.assetId),\n    localAsset: true,\n  };\n}\n\nexport function isBookmarkStillCrawling(bookmark: ZBookmark) {\n  if (bookmark.content.type != BookmarkTypes.LINK) {\n    return false;\n  }\n  if (bookmark.content.crawlStatus) {\n    return bookmark.content.crawlStatus === \"pending\";\n  }\n  return !bookmark.content.crawledAt;\n}\n\nexport function isBookmarkStillTagging(bookmark: ZBookmark) {\n  return bookmark.taggingStatus == \"pending\";\n}\n\nexport function isBookmarkStillSummarizing(bookmark: ZBookmark) {\n  return bookmark.summarizationStatus == \"pending\";\n}\n\nexport function isBookmarkStillLoading(bookmark: ZBookmark) {\n  return (\n    isBookmarkStillTagging(bookmark) ||\n    isBookmarkStillCrawling(bookmark) ||\n    isBookmarkStillSummarizing(bookmark)\n  );\n}\n\nexport function getBookmarkRefreshInterval(\n  bookmark: ZBookmark,\n): number | false {\n  if (!isBookmarkStillLoading(bookmark)) {\n    return false;\n  }\n\n  // For the first 30 seconds, we'll refresh the bookmark every second\n  if (Date.now().valueOf() - bookmark.createdAt.valueOf() < 30 * 1000) {\n    return 1000;\n  }\n\n  // Then, we'll refresh it every 10 seconds after than for 10mins\n  if (Date.now().valueOf() - bookmark.createdAt.valueOf() < 10 * 60 * 1000) {\n    return 10_000;\n  }\n\n  // Then, we'll refresh it every minute after than for 6hrs\n  if (\n    Date.now().valueOf() - bookmark.createdAt.valueOf() <\n    6 * 60 * 60 * 1000\n  ) {\n    return 60_000;\n  }\n\n  // Then we'll stop refreshing it\n  return false;\n}\n\nexport function getSourceUrl(bookmark: ZBookmark) {\n  if (bookmark.content.type === BookmarkTypes.LINK) {\n    return bookmark.content.url;\n  }\n  if (bookmark.content.type === BookmarkTypes.ASSET) {\n    return bookmark.content.sourceUrl ?? null;\n  }\n  if (bookmark.content.type === BookmarkTypes.TEXT) {\n    return bookmark.content.sourceUrl ?? null;\n  }\n  return null;\n}\n\nexport function getBookmarkTitle(bookmark: ZBookmark) {\n  let title: string | null = null;\n  switch (bookmark.content.type) {\n    case BookmarkTypes.LINK:\n      title = bookmark.content.title ?? bookmark.content.url;\n      break;\n    case BookmarkTypes.TEXT:\n      title = null;\n      break;\n    case BookmarkTypes.ASSET:\n      title = bookmark.content.fileName ?? null;\n      break;\n  }\n\n  return bookmark.title ? bookmark.title : title;\n}\n"
  },
  {
    "path": "packages/shared/utils/htmlUtils.ts",
    "content": "import { compile } from \"html-to-text\";\n\nconst compiledConvert = compile({\n  selectors: [{ selector: \"img\", format: \"skip\" }],\n});\n\n/**\n * Converts HTML content to plain text\n */\nexport function htmlToPlainText(htmlContent: string): string {\n  if (!htmlContent) {\n    return \"\";\n  }\n\n  // TODO, we probably should also remove singlefile inline images from the content\n  return compiledConvert(htmlContent);\n}\n"
  },
  {
    "path": "packages/shared/utils/listUtils.ts",
    "content": "import { ZBookmarkList } from \"../types/lists\";\n\nexport interface ZBookmarkListTreeNode {\n  item: ZBookmarkList;\n  children: ZBookmarkListTreeNode[];\n}\n\nexport type ZBookmarkListRoot = Record<string, ZBookmarkListTreeNode>;\n\nexport function listsToTree(lists: ZBookmarkList[]) {\n  const idToList = lists.reduce<Record<string, ZBookmarkList>>((acc, list) => {\n    acc[list.id] = list;\n    return acc;\n  }, {});\n\n  const root: ZBookmarkListRoot = {};\n\n  // Prepare all refs\n  const refIdx = lists.reduce<Record<string, ZBookmarkListTreeNode>>(\n    (acc, l) => {\n      acc[l.id] = {\n        item: l,\n        children: [],\n      };\n      return acc;\n    },\n    {},\n  );\n\n  // Build the tree\n  lists.forEach((list) => {\n    const node = refIdx[list.id];\n    if (list.parentId) {\n      refIdx[list.parentId].children.push(node);\n    } else {\n      root[list.id] = node;\n    }\n  });\n\n  const allPaths: ZBookmarkList[][] = [];\n  const dfs = (node: ZBookmarkListTreeNode, path: ZBookmarkList[]) => {\n    const list = idToList[node.item.id];\n    const newPath = [...path, list];\n    allPaths.push(newPath);\n    node.children.forEach((child) => {\n      dfs(child, newPath);\n    });\n  };\n\n  Object.values(root).forEach((node) => {\n    dfs(node, []);\n  });\n\n  return {\n    allPaths,\n    root,\n    getPathById: (id: string) =>\n      allPaths.find((path) => path[path.length - 1].id === id),\n  };\n}\n"
  },
  {
    "path": "packages/shared/utils/reading-progress-dom.test.ts",
    "content": "import { describe, expect, test } from \"vitest\";\n\nimport { normalizeText, normalizeTextLength } from \"./reading-progress-dom\";\n\ndescribe(\"normalizeText\", () => {\n  test(\"collapses multiple spaces to single space\", () => {\n    expect(normalizeText(\"hello    world\")).toBe(\"hello world\");\n  });\n\n  test(\"collapses newlines and tabs to single space\", () => {\n    expect(normalizeText(\"hello\\n\\nworld\")).toBe(\"hello world\");\n    expect(normalizeText(\"hello\\t\\tworld\")).toBe(\"hello world\");\n    expect(normalizeText(\"hello\\r\\nworld\")).toBe(\"hello world\");\n  });\n\n  test(\"trims leading and trailing whitespace\", () => {\n    expect(normalizeText(\"  hello world  \")).toBe(\"hello world\");\n    expect(normalizeText(\"\\n\\nhello world\\n\\n\")).toBe(\"hello world\");\n  });\n\n  test(\"handles empty string\", () => {\n    expect(normalizeText(\"\")).toBe(\"\");\n  });\n\n  test(\"handles whitespace-only string\", () => {\n    expect(normalizeText(\"   \")).toBe(\"\");\n    expect(normalizeText(\"\\n\\t\\r\")).toBe(\"\");\n  });\n\n  test(\"handles text with no extra whitespace\", () => {\n    expect(normalizeText(\"hello world\")).toBe(\"hello world\");\n  });\n\n  test(\"handles mixed whitespace types\", () => {\n    expect(normalizeText(\"hello  \\n\\t  world\")).toBe(\"hello world\");\n  });\n});\n\ndescribe(\"normalizeTextLength\", () => {\n  test(\"returns length of normalized text\", () => {\n    expect(normalizeTextLength(\"hello world\")).toBe(11);\n  });\n\n  test(\"returns normalized length for text with extra whitespace\", () => {\n    // \"hello    world\" normalizes to \"hello world\" (11 chars)\n    expect(normalizeTextLength(\"hello    world\")).toBe(11);\n  });\n\n  test(\"returns 0 for empty string\", () => {\n    expect(normalizeTextLength(\"\")).toBe(0);\n  });\n\n  test(\"returns 0 for whitespace-only string\", () => {\n    expect(normalizeTextLength(\"   \\n\\t\")).toBe(0);\n  });\n});\n"
  },
  {
    "path": "packages/shared/utils/reading-progress-dom.ts",
    "content": "/**\n * Reading Progress Utilities\n *\n * Functions for reading position tracking in web contexts.\n * Includes text normalization, position calculation, scroll restoration,\n * and Radix ScrollArea detection.\n */\n\n/**\n * Reading position data including offset, anchor text for verification, and percentage.\n */\nexport interface ReadingPosition {\n  offset: number;\n  anchor: string;\n  percent: number;\n}\n\nconst PARAGRAPH_SELECTORS = [\n  \"p\",\n  \"h1\",\n  \"h2\",\n  \"h3\",\n  \"h4\",\n  \"h5\",\n  \"h6\",\n  \"li\",\n  \"blockquote\",\n];\n\nconst PARAGRAPH_SELECTOR_STRING = PARAGRAPH_SELECTORS.join(\", \");\n\n/**\n * Maximum length of anchor text extracted from paragraphs.\n * Used for position verification when restoring reading progress.\n */\nexport const ANCHOR_TEXT_MAX_LENGTH = 50;\n\n/** Threshold in pixels for detecting \"scrolled to bottom\" */\nconst SCROLL_BOTTOM_THRESHOLD = 5;\n\n/** Minimum interval between scroll position updates (milliseconds) */\nexport const SCROLL_THROTTLE_MS = 150;\n\n/**\n * Scroll position info for determining if user is at bottom of content.\n */\ninterface ScrollInfo {\n  scrollTop: number;\n  scrollHeight: number;\n  clientHeight: number;\n}\n\n/**\n * Normalizes text by collapsing all whitespace to single spaces and trimming.\n * This ensures consistent character counting regardless of HTML formatting.\n */\nexport function normalizeText(text: string): string {\n  return text\n    .replace(/[\\n\\r\\t]+/g, \" \")\n    .replace(/\\s+/g, \" \")\n    .trim();\n}\n\n/**\n * Returns the normalized length of text for consistent offset calculation.\n */\nexport function normalizeTextLength(text: string): number {\n  return normalizeText(text).length;\n}\n\n/**\n * Check if element is visible (not hidden by CSS display:none).\n * Uses bounding rect - hidden elements have 0 dimensions.\n * We use this instead of offsetParent because dialogs use position:fixed,\n * which makes offsetParent null even for visible elements.\n */\nexport function isElementVisible(element: HTMLElement | null): boolean {\n  if (!element) return false;\n  const rect = element.getBoundingClientRect();\n  return rect.width > 0 && rect.height > 0;\n}\n\n/**\n * Finds the nearest scrollable ancestor of an element.\n * Handles both standard overflow-based scrolling, Radix ScrollArea components,\n * and window-level scrolling (falls back to document.documentElement).\n */\nexport function findScrollableParent(element: HTMLElement): HTMLElement {\n  let current: HTMLElement | null = element.parentElement;\n\n  while (current) {\n    const style = getComputedStyle(current);\n    const overflowY = style.overflowY;\n    const isOverflowScrollable = overflowY === \"auto\" || overflowY === \"scroll\";\n    // Check for Radix ScrollArea viewport (uses data attribute for scrolling)\n    const isRadixViewport = current.hasAttribute(\n      \"data-radix-scroll-area-viewport\",\n    );\n\n    const isCandidate = isOverflowScrollable || isRadixViewport;\n    const hasScrollContent = current.scrollHeight > current.clientHeight;\n\n    if (isCandidate && hasScrollContent) {\n      return current;\n    }\n    current = current.parentElement;\n  }\n\n  // Fall back to document.documentElement for window-level scrolling\n  return document.documentElement;\n}\n\n/**\n * Calculates the text offset of the paragraph at the top of the viewport.\n * Finds the paragraph whose top edge is at or near the top of the visible area.\n * Returns offset, anchor text for position verification, and percentage through the document.\n *\n * Handles nested scrolling with Radix ScrollArea detection.\n * Returns 100% when scrolled to the bottom of the document.\n */\nexport function getReadingPosition(\n  container: HTMLElement,\n): ReadingPosition | null {\n  // Find the scrollable parent to get the correct viewport reference\n  const scrollParent = findScrollableParent(container);\n  const isWindowScroll = scrollParent === document.documentElement;\n\n  // Build scroll info for 100% detection\n  const scrollInfo: ScrollInfo = {\n    scrollTop: isWindowScroll ? window.scrollY : scrollParent.scrollTop,\n    scrollHeight: isWindowScroll\n      ? document.body.scrollHeight\n      : scrollParent.scrollHeight,\n    clientHeight: isWindowScroll\n      ? window.innerHeight\n      : scrollParent.clientHeight,\n  };\n\n  // For window-level scrolling, viewport top is 0; for container scrolling, use container's top\n  const viewportTop = isWindowScroll\n    ? 0\n    : scrollParent.getBoundingClientRect().top;\n\n  return getReadingPositionWithViewport(container, viewportTop, scrollInfo);\n}\n\n/**\n * Calculates the text offset of the paragraph at the top of the viewport.\n * Takes explicit viewport top position and optional scroll info for bottom detection.\n */\nfunction getReadingPositionWithViewport(\n  container: HTMLElement,\n  viewportTop: number,\n  scrollInfo?: ScrollInfo,\n): ReadingPosition | null {\n  const paragraphs = Array.from(\n    container.querySelectorAll(PARAGRAPH_SELECTOR_STRING),\n  );\n  if (paragraphs.length === 0) return null;\n\n  // Calculate total length for percentage calculation\n  const totalLength = normalizeTextLength(container.textContent ?? \"\");\n\n  // Check if scrolled to bottom - return 100% immediately\n  if (scrollInfo) {\n    const { scrollTop, scrollHeight, clientHeight } = scrollInfo;\n    const isAtBottom =\n      scrollTop + clientHeight >= scrollHeight - SCROLL_BOTTOM_THRESHOLD;\n\n    if (isAtBottom) {\n      // Find the last paragraph for anchor text\n      const lastParagraph = paragraphs[paragraphs.length - 1];\n      const anchor = lastParagraph\n        ? normalizeText(lastParagraph.textContent ?? \"\").slice(\n            0,\n            ANCHOR_TEXT_MAX_LENGTH,\n          )\n        : \"\";\n\n      return { offset: totalLength, anchor, percent: 100 };\n    }\n  }\n\n  // Find the paragraph at the top of the viewport\n  let topParagraph: Element | null = null;\n\n  for (const paragraph of paragraphs) {\n    const rect = paragraph.getBoundingClientRect();\n\n    // If this paragraph's top is at or below the viewport top, it's our target\n    if (rect.top >= viewportTop) {\n      topParagraph = paragraph;\n      break;\n    }\n\n    // If this paragraph spans the viewport top (started above, ends below), use it\n    if (rect.top < viewportTop && rect.bottom > viewportTop) {\n      topParagraph = paragraph;\n      break;\n    }\n  }\n\n  if (!topParagraph) return null;\n\n  // Extract anchor text for position verification\n  const anchor = normalizeText(topParagraph.textContent ?? \"\").slice(\n    0,\n    ANCHOR_TEXT_MAX_LENGTH,\n  );\n\n  // Calculate the text offset of this paragraph using TreeWalker\n  const walker = document.createTreeWalker(\n    container,\n    NodeFilter.SHOW_TEXT,\n    null,\n  );\n\n  let offset = 0;\n  let node: Node | null;\n  while ((node = walker.nextNode())) {\n    if (topParagraph.contains(node)) {\n      // Found the start of our target paragraph\n      // Calculate percentage (clamped to 0-100)\n      const percent =\n        totalLength > 0\n          ? Math.min(100, Math.max(0, Math.round((offset / totalLength) * 100)))\n          : 0;\n      return { offset, anchor, percent };\n    }\n    offset += normalizeTextLength(node.textContent ?? \"\");\n  }\n\n  // topParagraph has no text nodes (empty or contains only non-text elements)\n  return null;\n}\n\n/**\n * Scrolls to the position in the content corresponding to the given text offset.\n * Uses anchor text for verification when available, falling back to offset-based lookup.\n */\nexport function scrollToReadingPosition(\n  container: HTMLElement,\n  offset: number,\n  behavior: ScrollBehavior = \"smooth\",\n  anchor?: string | null,\n): boolean {\n  if (offset <= 0) return false;\n\n  // Strategy 1: Try to find paragraph by anchor text (most reliable)\n  if (anchor) {\n    const paragraphs = Array.from(\n      container.querySelectorAll(PARAGRAPH_SELECTOR_STRING),\n    );\n\n    let fuzzyMatch: Element | null = null;\n    const anchorPrefix = anchor.slice(0, 20);\n\n    for (const paragraph of paragraphs) {\n      const paragraphAnchor = normalizeText(paragraph.textContent ?? \"\").slice(\n        0,\n        ANCHOR_TEXT_MAX_LENGTH,\n      );\n\n      // Exact match - immediate return\n      if (paragraphAnchor === anchor) {\n        paragraph.scrollIntoView({ behavior, block: \"start\" });\n        return true;\n      }\n\n      // Track first fuzzy match for fallback (first 20 chars match)\n      if (\n        !fuzzyMatch &&\n        anchor.length >= 20 &&\n        paragraphAnchor.startsWith(anchorPrefix)\n      ) {\n        fuzzyMatch = paragraph;\n      }\n    }\n\n    if (fuzzyMatch) {\n      fuzzyMatch.scrollIntoView({ behavior, block: \"start\" });\n      return true;\n    }\n  }\n\n  // Strategy 2: Fall back to offset-based lookup\n  const walker = document.createTreeWalker(\n    container,\n    NodeFilter.SHOW_TEXT,\n    null,\n  );\n\n  let currentOffset = 0;\n  let node: Node | null;\n  while ((node = walker.nextNode())) {\n    const textContent = node.textContent ?? \"\";\n    // Use normalized length for consistent offset calculation\n    const nodeLength = normalizeTextLength(textContent);\n\n    // Skip nodes with no meaningful content (whitespace-only nodes normalize to length 0)\n    if (nodeLength === 0) {\n      continue;\n    }\n\n    // Check if we've passed the target offset\n    if (currentOffset + nodeLength >= offset) {\n      // Found the text node containing our offset\n      // Find the enclosing paragraph element\n      let targetElement: HTMLElement | null = node.parentElement;\n      while (targetElement && targetElement !== container) {\n        const tagName = targetElement.tagName.toLowerCase();\n        if (PARAGRAPH_SELECTORS.includes(tagName)) {\n          break;\n        }\n        targetElement = targetElement.parentElement;\n      }\n\n      // Use the text node's parent if no paragraph found\n      if (!targetElement || targetElement === container) {\n        targetElement = node.parentElement;\n      }\n\n      if (targetElement) {\n        targetElement.scrollIntoView({ behavior, block: \"start\" });\n        return true;\n      }\n      break;\n    }\n\n    currentOffset += nodeLength;\n  }\n\n  return false;\n}\n"
  },
  {
    "path": "packages/shared/utils/redirectUrl.test.ts",
    "content": "import { describe, expect, it } from \"vitest\";\n\nimport { isMobileAppRedirect, validateRedirectUrl } from \"./redirectUrl\";\n\ndescribe(\"validateRedirectUrl\", () => {\n  it(\"should return undefined for null input\", () => {\n    expect(validateRedirectUrl(null)).toBe(undefined);\n  });\n\n  it(\"should return undefined for undefined input\", () => {\n    expect(validateRedirectUrl(undefined)).toBe(undefined);\n  });\n\n  it(\"should return undefined for empty string\", () => {\n    expect(validateRedirectUrl(\"\")).toBe(undefined);\n  });\n\n  it(\"should allow relative paths starting with '/'\", () => {\n    expect(validateRedirectUrl(\"/\")).toBe(\"/\");\n    expect(validateRedirectUrl(\"/dashboard\")).toBe(\"/dashboard\");\n    expect(validateRedirectUrl(\"/settings/profile\")).toBe(\"/settings/profile\");\n    expect(validateRedirectUrl(\"/path?query=value\")).toBe(\"/path?query=value\");\n    expect(validateRedirectUrl(\"/path#hash\")).toBe(\"/path#hash\");\n  });\n\n  it(\"should reject protocol-relative URLs (//)\", () => {\n    expect(validateRedirectUrl(\"//evil.com\")).toBe(undefined);\n    expect(validateRedirectUrl(\"//evil.com/path\")).toBe(undefined);\n  });\n\n  it(\"should allow karakeep:// scheme for mobile app\", () => {\n    expect(validateRedirectUrl(\"karakeep://\")).toBe(\"karakeep://\");\n    expect(validateRedirectUrl(\"karakeep://callback\")).toBe(\n      \"karakeep://callback\",\n    );\n    expect(validateRedirectUrl(\"karakeep://callback/path\")).toBe(\n      \"karakeep://callback/path\",\n    );\n    expect(validateRedirectUrl(\"karakeep://callback?param=value\")).toBe(\n      \"karakeep://callback?param=value\",\n    );\n  });\n\n  it(\"should reject http:// scheme\", () => {\n    expect(validateRedirectUrl(\"http://example.com\")).toBe(undefined);\n    expect(validateRedirectUrl(\"http://localhost:3000\")).toBe(undefined);\n  });\n\n  it(\"should reject https:// scheme\", () => {\n    expect(validateRedirectUrl(\"https://example.com\")).toBe(undefined);\n    expect(validateRedirectUrl(\"https://evil.com/phishing\")).toBe(undefined);\n  });\n\n  it(\"should reject javascript: scheme\", () => {\n    expect(validateRedirectUrl(\"javascript:alert(1)\")).toBe(undefined);\n  });\n\n  it(\"should reject data: scheme\", () => {\n    expect(\n      validateRedirectUrl(\"data:text/html,<script>alert(1)</script>\"),\n    ).toBe(undefined);\n  });\n\n  it(\"should reject other custom schemes\", () => {\n    expect(validateRedirectUrl(\"file:///etc/passwd\")).toBe(undefined);\n    expect(validateRedirectUrl(\"ftp://example.com\")).toBe(undefined);\n    expect(validateRedirectUrl(\"mailto:test@example.com\")).toBe(undefined);\n  });\n\n  it(\"should reject paths not starting with /\", () => {\n    expect(validateRedirectUrl(\"dashboard\")).toBe(undefined);\n    expect(validateRedirectUrl(\"path/to/page\")).toBe(undefined);\n  });\n});\n\ndescribe(\"isMobileAppRedirect\", () => {\n  it(\"should return true for karakeep:// URLs\", () => {\n    expect(isMobileAppRedirect(\"karakeep://\")).toBe(true);\n    expect(isMobileAppRedirect(\"karakeep://callback\")).toBe(true);\n    expect(isMobileAppRedirect(\"karakeep://callback/path\")).toBe(true);\n  });\n\n  it(\"should return false for other URLs\", () => {\n    expect(isMobileAppRedirect(\"/\")).toBe(false);\n    expect(isMobileAppRedirect(\"/dashboard\")).toBe(false);\n    expect(isMobileAppRedirect(\"https://example.com\")).toBe(false);\n    expect(isMobileAppRedirect(\"http://localhost\")).toBe(false);\n  });\n});\n"
  },
  {
    "path": "packages/shared/utils/redirectUrl.ts",
    "content": "/**\n * Validates a redirect URL to prevent open redirect attacks.\n * Only allows:\n * - Relative paths starting with \"/\" (but not \"//\" to prevent protocol-relative URLs)\n * - The karakeep:// scheme for the mobile app\n *\n * @returns The validated URL if valid, otherwise undefined.\n */\nexport function validateRedirectUrl(\n  url: string | null | undefined,\n): string | undefined {\n  if (!url) {\n    return undefined;\n  }\n\n  // Allow relative paths starting with \"/\" but not \"//\" (protocol-relative URLs)\n  if (url.startsWith(\"/\") && !url.startsWith(\"//\")) {\n    return url;\n  }\n\n  // Allow karakeep:// scheme for mobile app deep links\n  if (url.startsWith(\"karakeep://\")) {\n    return url;\n  }\n\n  // Reject all other schemes (http, https, javascript, data, etc.)\n  return undefined;\n}\n\n/**\n * Checks if the redirect URL is a mobile app deep link.\n */\nexport function isMobileAppRedirect(url: string): boolean {\n  return url.startsWith(\"karakeep://\");\n}\n"
  },
  {
    "path": "packages/shared/utils/relativeDateUtils.ts",
    "content": "interface RelativeDate {\n  direction: \"newer\" | \"older\";\n  amount: number;\n  unit: \"day\" | \"week\" | \"month\" | \"year\";\n}\n\nconst parseRelativeDate = (date: string): RelativeDate => {\n  const match = /^([<>])(\\d+)([dwmy])$/.exec(date);\n  if (!match) {\n    throw new Error(`Invalid relative date format: ${date}`);\n  }\n  const direction = match[1] === \"<\" ? \"newer\" : \"older\";\n  const amount = parseInt(match[2], 10);\n  const unit = {\n    d: \"day\",\n    w: \"week\",\n    m: \"month\",\n    y: \"year\",\n  }[match[3]] as \"day\" | \"week\" | \"month\" | \"year\";\n  return { direction, amount, unit };\n};\n\nconst toAbsoluteDate = (relativeDate: RelativeDate): Date => {\n  const date = new Date();\n  switch (relativeDate.unit) {\n    case \"day\":\n      date.setDate(date.getDate() - relativeDate.amount);\n      break;\n    case \"week\":\n      date.setDate(date.getDate() - relativeDate.amount * 7);\n      break;\n    case \"month\":\n      date.setMonth(date.getMonth() - relativeDate.amount);\n      break;\n    case \"year\":\n      date.setFullYear(date.getFullYear() - relativeDate.amount);\n      break;\n  }\n  return date;\n};\n\nexport { parseRelativeDate, toAbsoluteDate };\n"
  },
  {
    "path": "packages/shared/utils/switch.ts",
    "content": "export function switchCase<T extends string | number, R>(\n  value: T,\n  cases: Record<T, R>,\n) {\n  return cases[value];\n}\n"
  },
  {
    "path": "packages/shared/utils/tag.ts",
    "content": "import type { ZTagStyle } from \"../types/users\";\n\n/**\n * Ensures exactly ONE leading #\n */\nexport function normalizeTagName(raw: string): string {\n  return raw.trim().replace(/^#+/, \"\"); // strip every leading #\n}\n\nexport type TagStyle = ZTagStyle;\n\nexport function getTagStylePrompt(style: TagStyle): string {\n  switch (style) {\n    case \"lowercase-hyphens\":\n      return \"- Use lowercase letters with hyphens between words (e.g., 'machine-learning', 'web-development')\";\n    case \"lowercase-spaces\":\n      return \"- Use lowercase letters with spaces between words (e.g., 'machine learning', 'web development')\";\n    case \"lowercase-underscores\":\n      return \"- Use lowercase letters with underscores between words (e.g., 'machine_learning', 'web_development')\";\n    case \"titlecase-spaces\":\n      return \"- Use title case with spaces between words (e.g., 'Machine Learning', 'Web Development')\";\n    case \"titlecase-hyphens\":\n      return \"- Use title case with hyphens between words (e.g., 'Machine-Learning', 'Web-Development')\";\n    case \"camelCase\":\n      return \"- Use camelCase format (e.g., 'machineLearning', 'webDevelopment')\";\n    case \"as-generated\":\n    default:\n      return \"\";\n  }\n}\n\nexport function getCuratedTagsPrompt(curatedTags?: string[]): string {\n  if (curatedTags && curatedTags.length > 0) {\n    return `- ONLY use tags from this predefined list: [${curatedTags.join(\", \")}]. Do not create any new tags outside this list. If no tags fit, don't emit any.`;\n  }\n  return \"\";\n}\n"
  },
  {
    "path": "packages/shared/vitest.config.ts",
    "content": "/// <reference types=\"vitest\" />\n\nimport tsconfigPaths from \"vite-tsconfig-paths\";\nimport { defineConfig } from \"vitest/config\";\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  plugins: [tsconfigPaths()],\n  test: {\n    alias: {\n      \"@/*\": \"./*\",\n    },\n  },\n});\n"
  },
  {
    "path": "packages/shared-react/.oxlintrc.json",
    "content": "{\n  \"$schema\": \"../../node_modules/oxlint/configuration_schema.json\",\n  \"extends\": [\n    \"../../tooling/oxlint/oxlint-base.json\",\n    \"../../tooling/oxlint/oxlint-react.json\"\n  ],\n  \"env\": {\n    \"builtin\": true,\n    \"commonjs\": true,\n    \"browser\": true,\n    \"es2022\": true,\n    \"node\": true\n  },\n  \"globals\": {\n    \"React\": \"writeable\"\n  },\n  \"settings\": {\n    \"react\": {\n      \"version\": \"19\"\n    }\n  },\n  \"ignorePatterns\": [\n    \"**/*.config.js\",\n    \"**/*.config.cjs\",\n    \"**/.eslintrc.cjs\",\n    \".next\",\n    \"dist\",\n    \"build\",\n    \"pnpm-lock.yaml\"\n  ]\n}\n"
  },
  {
    "path": "packages/shared-react/components/BookmarkHtmlHighlighter.tsx",
    "content": "import React, {\n  forwardRef,\n  useEffect,\n  useImperativeHandle,\n  useRef,\n  useState,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { PopoverAnchor } from \"@radix-ui/react-popover\";\nimport { Check, Trash2 } from \"lucide-react\";\n\nimport {\n  SUPPORTED_HIGHLIGHT_COLORS,\n  ZHighlightColor,\n} from \"@karakeep/shared/types/highlights\";\n\nimport { HIGHLIGHT_COLOR_MAP } from \"./highlights\";\nimport { Button } from \"./ui/button\";\nimport { Popover, PopoverContent } from \"./ui/popover\";\nimport { Textarea } from \"./ui/textarea\";\n\ninterface HighlightFormProps {\n  position: { x: number; y: number } | null;\n  selectedHighlight: Highlight | null;\n  onClose: () => void;\n  onSave: (color: ZHighlightColor, note: string | null) => void;\n  onDelete?: () => void;\n  isMobile: boolean;\n}\n\nconst HighlightForm: React.FC<HighlightFormProps> = ({\n  position,\n  selectedHighlight,\n  onClose,\n  onSave,\n  onDelete,\n  isMobile,\n}) => {\n  const [selectedColor, setSelectedColor] = useState<ZHighlightColor>(\n    selectedHighlight?.color || \"yellow\",\n  );\n  const [noteText, setNoteText] = useState(selectedHighlight?.note || \"\");\n\n  // Update state when selectedHighlight changes\n  useEffect(() => {\n    setSelectedColor(selectedHighlight?.color || \"yellow\");\n    setNoteText(selectedHighlight?.note || \"\");\n  }, [selectedHighlight]);\n\n  const handleSave = () => {\n    onSave(selectedColor, noteText || null);\n  };\n\n  return (\n    <Popover\n      open={position !== null}\n      onOpenChange={(val) => {\n        if (!val) {\n          onClose();\n        }\n      }}\n    >\n      <PopoverAnchor\n        className=\"fixed\"\n        style={{\n          left: position?.x,\n          top: position?.y,\n        }}\n      />\n      <PopoverContent\n        side={isMobile ? \"bottom\" : \"top\"}\n        className=\"w-80 space-y-3 p-3\"\n        onOpenAutoFocus={(e) => e.preventDefault()}\n      >\n        <div>\n          <label className=\"mb-2 block text-sm font-medium\">Color</label>\n          <div className=\"flex items-center gap-1\">\n            {SUPPORTED_HIGHLIGHT_COLORS.map((color) => (\n              <Button\n                size=\"none\"\n                key={color}\n                onClick={() => setSelectedColor(color)}\n                variant=\"none\"\n                className={cn(\n                  `size-8 rounded-full hover:border focus-visible:ring-0`,\n                  HIGHLIGHT_COLOR_MAP.bg[color],\n                )}\n              >\n                {selectedColor === color && (\n                  <Check className=\"size-5 text-gray-600\" />\n                )}\n              </Button>\n            ))}\n          </div>\n        </div>\n        <div>\n          <label className=\"mb-2 block text-sm font-medium\">Note</label>\n          <Textarea\n            placeholder=\"Add a note (optional)...\"\n            value={noteText}\n            onChange={(e) => setNoteText(e.target.value)}\n            className=\"min-h-[80px] text-sm\"\n          />\n        </div>\n        <div className=\"flex items-center justify-between gap-2\">\n          <div className=\"flex gap-2\">\n            <Button onClick={handleSave} size=\"sm\">\n              Save\n            </Button>\n            <Button onClick={onClose} variant=\"outline\" size=\"sm\">\n              Cancel\n            </Button>\n          </div>\n          {selectedHighlight && onDelete && (\n            <Button\n              size=\"sm\"\n              onClick={onDelete}\n              variant=\"ghost\"\n              title=\"Delete highlight\"\n            >\n              <Trash2 className=\"size-4 text-destructive\" />\n            </Button>\n          )}\n        </div>\n      </PopoverContent>\n    </Popover>\n  );\n};\n\nexport interface Highlight {\n  id: string;\n  startOffset: number;\n  endOffset: number;\n  color: ZHighlightColor;\n  text: string | null;\n  note?: string | null;\n}\n\ninterface HTMLHighlighterProps {\n  htmlContent: string;\n  style?: React.CSSProperties;\n  className?: string;\n  highlights?: Highlight[];\n  readOnly?: boolean;\n  onHighlight?: (highlight: Highlight) => void;\n  onUpdateHighlight?: (highlight: Highlight) => void;\n  onDeleteHighlight?: (highlight: Highlight) => void;\n}\n\nconst BookmarkHTMLHighlighter = forwardRef<\n  HTMLDivElement,\n  HTMLHighlighterProps\n>(function BookmarkHTMLHighlighter(\n  {\n    htmlContent,\n    className,\n    style,\n    highlights = [],\n    readOnly = false,\n    onHighlight,\n    onUpdateHighlight,\n    onDeleteHighlight,\n  },\n  ref,\n) {\n  const contentRef = useRef<HTMLDivElement>(null);\n\n  // Expose the content div ref to parent components\n  useImperativeHandle(ref, () => contentRef.current!, []);\n\n  const [menuPosition, setMenuPosition] = useState<{\n    x: number;\n    y: number;\n  } | null>(null);\n  const [pendingHighlight, setPendingHighlight] = useState<Highlight | null>(\n    null,\n  );\n  const [selectedHighlight, setSelectedHighlight] = useState<Highlight | null>(\n    null,\n  );\n  const isMobile = useState(\n    () =>\n      typeof window !== \"undefined\" &&\n      window.matchMedia(\"(pointer: coarse)\").matches,\n  )[0];\n\n  // Apply existing highlights when component mounts or highlights change\n  useEffect(() => {\n    if (!contentRef.current) return;\n\n    // Clear existing highlights first\n    const existingHighlights = contentRef.current.querySelectorAll(\n      \"span[data-highlight]\",\n    );\n    existingHighlights.forEach((el) => {\n      const parent = el.parentNode;\n      if (parent) {\n        while (el.firstChild) {\n          parent.insertBefore(el.firstChild, el);\n        }\n        parent.removeChild(el);\n      }\n    });\n\n    // Apply all highlights\n    highlights.forEach((highlight) => {\n      applyHighlightByOffset(highlight);\n    });\n  });\n\n  // Re-apply the selection when the pending range changes\n  useEffect(() => {\n    if (!pendingHighlight) {\n      return;\n    }\n    if (!contentRef.current) {\n      return;\n    }\n    const ranges = getRangeFromHighlight(pendingHighlight);\n    if (!ranges) {\n      return;\n    }\n    const newRange = document.createRange();\n    newRange.setStart(ranges[0].node, ranges[0].start);\n    newRange.setEnd(\n      ranges[ranges.length - 1].node,\n      ranges[ranges.length - 1].end,\n    );\n    window.getSelection()?.removeAllRanges();\n    window.getSelection()?.addRange(newRange);\n  }, [pendingHighlight, contentRef]);\n\n  const handlePointerUp = (e: React.PointerEvent) => {\n    if (readOnly) {\n      return;\n    }\n\n    const selection = window.getSelection();\n\n    // Check if we clicked on an existing highlight\n    const target = e.target as HTMLElement;\n    if (target.dataset.highlight) {\n      const highlightId = target.dataset.highlightId;\n      if (highlightId && highlights) {\n        const highlight = highlights.find((h) => h.id === highlightId);\n        if (!highlight) {\n          return;\n        }\n        setSelectedHighlight(highlight);\n        setMenuPosition({\n          x: e.clientX,\n          y: e.clientY,\n        });\n        return;\n      }\n    }\n\n    if (!selection || selection.isCollapsed || !contentRef.current) {\n      return;\n    }\n\n    const range = selection.getRangeAt(0);\n\n    // Only process selections within our component\n    if (!contentRef.current.contains(range.commonAncestorContainer)) {\n      return;\n    }\n\n    // Position the menu based on device type\n    const rect = range.getBoundingClientRect();\n    setMenuPosition({\n      x: rect.left + rect.width / 2, // Center the menu horizontally\n      y: isMobile ? rect.bottom : rect.top, // Position below on mobile, above otherwise\n    });\n\n    // Store the highlight for later use\n    setPendingHighlight(createHighlightFromRange(range, \"yellow\"));\n  };\n\n  const handleSave = (color: ZHighlightColor, note: string | null) => {\n    if (pendingHighlight) {\n      pendingHighlight.color = color;\n      pendingHighlight.note = note;\n      onHighlight?.(pendingHighlight);\n    } else if (selectedHighlight) {\n      selectedHighlight.color = color;\n      selectedHighlight.note = note;\n      onUpdateHighlight?.(selectedHighlight);\n    }\n    closeForm();\n  };\n\n  const closeForm = () => {\n    setMenuPosition(null);\n    setPendingHighlight(null);\n    setSelectedHighlight(null);\n    window.getSelection()?.removeAllRanges();\n  };\n\n  const handleDelete = () => {\n    if (selectedHighlight && onDeleteHighlight) {\n      onDeleteHighlight(selectedHighlight);\n      closeForm();\n    }\n  };\n\n  const getTextNodeOffset = (node: Node): number => {\n    let offset = 0;\n    const walker = document.createTreeWalker(\n      contentRef.current!,\n      NodeFilter.SHOW_TEXT,\n      null,\n    );\n\n    while (walker.nextNode()) {\n      if (walker.currentNode === node) {\n        return offset;\n      }\n      offset += walker.currentNode.textContent?.length ?? 0;\n    }\n    return -1;\n  };\n\n  const createHighlightFromRange = (\n    range: Range,\n    color: ZHighlightColor,\n  ): Highlight | null => {\n    if (!contentRef.current) return null;\n\n    const startOffset =\n      getTextNodeOffset(range.startContainer) + range.startOffset;\n    const endOffset = getTextNodeOffset(range.endContainer) + range.endOffset;\n\n    if (startOffset === -1 || endOffset === -1) return null;\n\n    const highlight: Highlight = {\n      id: \"NOT_SET\",\n      startOffset,\n      endOffset,\n      color,\n      text: range.toString(),\n    };\n\n    applyHighlightByOffset(highlight);\n    return highlight;\n  };\n\n  const getRangeFromHighlight = (highlight: Highlight) => {\n    if (!contentRef.current) return;\n\n    let currentOffset = 0;\n    const walker = document.createTreeWalker(\n      contentRef.current,\n      NodeFilter.SHOW_TEXT,\n      null,\n    );\n\n    const ranges: { node: Text; start: number; end: number }[] = [];\n\n    // Find all text nodes that need highlighting\n    let node: Text | null;\n    while ((node = walker.nextNode() as Text)) {\n      const nodeLength = node.length;\n      const nodeStart = currentOffset;\n      const nodeEnd = nodeStart + nodeLength;\n\n      if (nodeStart < highlight.endOffset && nodeEnd > highlight.startOffset) {\n        ranges.push({\n          node,\n          start: Math.max(0, highlight.startOffset - nodeStart),\n          end: Math.min(nodeLength, highlight.endOffset - nodeStart),\n        });\n      }\n\n      currentOffset += nodeLength;\n    }\n    return ranges;\n  };\n\n  const applyHighlightByOffset = (highlight: Highlight) => {\n    const ranges = getRangeFromHighlight(highlight);\n    if (!ranges) {\n      return;\n    }\n    // Apply highlights to found ranges\n    ranges.forEach(({ node, start, end }) => {\n      if (start > 0) {\n        node.splitText(start);\n        node = node.nextSibling as Text;\n        end -= start;\n      }\n      if (end < node.length) {\n        node.splitText(end);\n      }\n\n      const span = document.createElement(\"span\");\n      span.classList.add(HIGHLIGHT_COLOR_MAP.bg[highlight.color]);\n      span.classList.add(\"text-gray-600\");\n      span.dataset.highlight = \"true\";\n      span.dataset.highlightId = highlight.id;\n      node.parentNode?.insertBefore(span, node);\n      span.appendChild(node);\n    });\n  };\n\n  return (\n    <div>\n      <div\n        role=\"presentation\"\n        ref={contentRef}\n        dangerouslySetInnerHTML={{ __html: htmlContent }}\n        onPointerUp={handlePointerUp}\n        className={cn(\n          \"prose prose-neutral max-w-none break-words dark:prose-invert [&_code]:break-all [&_img]:h-auto [&_img]:max-w-full [&_pre]:overflow-x-auto [&_table]:block [&_table]:overflow-x-auto\",\n          className,\n        )}\n        style={style}\n      />\n      <HighlightForm\n        position={menuPosition}\n        selectedHighlight={selectedHighlight || pendingHighlight}\n        onClose={closeForm}\n        onSave={handleSave}\n        onDelete={selectedHighlight ? handleDelete : undefined}\n        isMobile={isMobile}\n      />\n    </div>\n  );\n});\n\nexport default BookmarkHTMLHighlighter;\n"
  },
  {
    "path": "packages/shared-react/components/ScrollProgressTracker.tsx",
    "content": "import React, {\n  forwardRef,\n  useEffect,\n  useImperativeHandle,\n  useRef,\n  useState,\n} from \"react\";\n\nimport type { ReadingPosition } from \"@karakeep/shared/utils/reading-progress-dom\";\nimport {\n  findScrollableParent,\n  getReadingPosition,\n  SCROLL_THROTTLE_MS,\n  scrollToReadingPosition,\n} from \"@karakeep/shared/utils/reading-progress-dom\";\n\n/** Delay after the last scroll event before reporting position (milliseconds) */\nconst IDLE_SAVE_DELAY_MS = 5000;\n\n/** Delay after the last scroll event before hiding the progress bar (milliseconds) */\nconst PROGRESS_BAR_HIDE_DELAY_MS = 2000;\n\ninterface ScrollProgressTrackerProps {\n  /** Called lazily on intent signals (idle, visibility change, beforeunload, unmount) — use for persisting position */\n  onSavePosition?: (position: ReadingPosition) => void;\n  /** Called on every throttled scroll — use for responsive UI (banner dismissal, etc.) */\n  onScrollPositionChange?: (position: ReadingPosition) => void;\n  /** When set to true, scrolls to the saved reading position */\n  restorePosition?: boolean;\n  readingProgressOffset?: number | null;\n  readingProgressAnchor?: string | null;\n  /** Show a Medium-style reading progress bar at the top */\n  showProgressBar?: boolean;\n  /** Custom styles for the progress bar container (e.g. positioning overrides) */\n  progressBarStyle?: React.CSSProperties;\n  children: React.ReactNode;\n}\n\n/**\n * Wraps content and tracks scroll progress, reporting position changes\n * lazily (idle after scrolling, visibility change, beforeunload, unmount).\n * Can also restore a previously saved reading position.\n */\nconst ScrollProgressTracker = forwardRef<\n  HTMLDivElement,\n  ScrollProgressTrackerProps\n>(function ScrollProgressTracker(\n  {\n    onSavePosition,\n    onScrollPositionChange,\n    restorePosition,\n    readingProgressOffset,\n    readingProgressAnchor,\n    showProgressBar,\n    progressBarStyle,\n    children,\n  },\n  ref,\n) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  useImperativeHandle(ref, () => containerRef.current!, []);\n  const [scrollPercent, setScrollPercent] = useState(0);\n  const [progressBarVisible, setProgressBarVisible] = useState(false);\n  const latestPositionRef = useRef<ReadingPosition | null>(null);\n\n  const onSavePositionRef = useRef(onSavePosition);\n  const onScrollPositionChangeRef = useRef(onScrollPositionChange);\n  useEffect(() => {\n    onSavePositionRef.current = onSavePosition;\n    onScrollPositionChangeRef.current = onScrollPositionChange;\n  });\n\n  // Restore reading position when triggered\n  const hasRestoredRef = useRef(false);\n  useEffect(() => {\n    if (\n      !restorePosition ||\n      hasRestoredRef.current ||\n      !readingProgressOffset ||\n      readingProgressOffset <= 0\n    )\n      return;\n\n    hasRestoredRef.current = true;\n    const rafId = requestAnimationFrame(() => {\n      const container = containerRef.current;\n      if (!container) return;\n\n      scrollToReadingPosition(\n        container,\n        readingProgressOffset,\n        \"smooth\",\n        readingProgressAnchor,\n      );\n    });\n\n    return () => cancelAnimationFrame(rafId);\n  }, [restorePosition, readingProgressOffset, readingProgressAnchor]);\n\n  // Scroll tracking — updates the progress bar on every scroll,\n  // but only reports position lazily via an idle timer.\n  useEffect(() => {\n    const container = containerRef.current;\n    if (!container) return;\n\n    let lastScrollTime = 0;\n    let idleTimerId: ReturnType<typeof setTimeout> | null = null;\n    let hideBarTimerId: ReturnType<typeof setTimeout> | null = null;\n    let trailingTimerId: ReturnType<typeof setTimeout> | null = null;\n\n    const reportLatestPosition = () => {\n      const pos = latestPositionRef.current;\n      if (pos && pos.offset > 0 && onSavePositionRef.current) {\n        onSavePositionRef.current(pos);\n      }\n    };\n\n    const processScroll = () => {\n      lastScrollTime = Date.now();\n\n      const position = getReadingPosition(container);\n      if (position) {\n        setScrollPercent(position.percent);\n        latestPositionRef.current = position;\n        if (onScrollPositionChangeRef.current) {\n          onScrollPositionChangeRef.current(position);\n        }\n      }\n\n      // Show progress bar on scroll, hide after idle\n      setProgressBarVisible(true);\n      if (hideBarTimerId) clearTimeout(hideBarTimerId);\n      hideBarTimerId = setTimeout(\n        () => setProgressBarVisible(false),\n        PROGRESS_BAR_HIDE_DELAY_MS,\n      );\n\n      // Reset idle timer — report position after scrolling stops\n      if (idleTimerId) clearTimeout(idleTimerId);\n      idleTimerId = setTimeout(reportLatestPosition, IDLE_SAVE_DELAY_MS);\n    };\n\n    const handleScroll = () => {\n      const now = Date.now();\n      if (now - lastScrollTime < SCROLL_THROTTLE_MS) {\n        // Schedule a trailing call so the last scroll event is never lost\n        if (!trailingTimerId) {\n          trailingTimerId = setTimeout(() => {\n            trailingTimerId = null;\n            processScroll();\n          }, SCROLL_THROTTLE_MS);\n        }\n        return;\n      }\n      processScroll();\n    };\n\n    const scrollParent = findScrollableParent(container);\n    const isWindowScroll = scrollParent === document.documentElement;\n    const target: HTMLElement | Window = isWindowScroll ? window : scrollParent;\n\n    target.addEventListener(\"scroll\", handleScroll, { passive: true });\n\n    return () => {\n      target.removeEventListener(\"scroll\", handleScroll);\n      if (idleTimerId) clearTimeout(idleTimerId);\n      if (hideBarTimerId) clearTimeout(hideBarTimerId);\n      if (trailingTimerId) clearTimeout(trailingTimerId);\n    };\n  }, []);\n\n  // Report position on visibility change, beforeunload, and unmount\n  useEffect(() => {\n    const reportPosition = () => {\n      const pos = latestPositionRef.current;\n      if (pos && pos.offset > 0 && onSavePositionRef.current) {\n        onSavePositionRef.current(pos);\n      }\n    };\n\n    const handleVisibilityChange = () => {\n      if (document.visibilityState === \"hidden\") {\n        reportPosition();\n      }\n    };\n\n    document.addEventListener(\"visibilitychange\", handleVisibilityChange);\n    window.addEventListener(\"beforeunload\", reportPosition);\n\n    return () => {\n      reportPosition();\n      document.removeEventListener(\"visibilitychange\", handleVisibilityChange);\n      window.removeEventListener(\"beforeunload\", reportPosition);\n    };\n  }, []);\n\n  return (\n    <div ref={containerRef}>\n      {showProgressBar && (\n        <div\n          style={{\n            position: \"sticky\",\n            top: 0,\n            left: 0,\n            right: 0,\n            height: 3,\n            zIndex: 50,\n            backgroundColor: \"transparent\",\n            opacity: progressBarVisible ? 1 : 0,\n            transition: \"opacity 300ms ease-out\",\n            ...progressBarStyle,\n          }}\n        >\n          <div\n            style={{\n              height: \"100%\",\n              width: `${scrollPercent}%`,\n              backgroundColor: \"rgb(249, 115, 22)\",\n              transition: \"width 150ms ease-out\",\n            }}\n          />\n        </div>\n      )}\n      {children}\n    </div>\n  );\n});\n\nexport default ScrollProgressTracker;\n"
  },
  {
    "path": "packages/shared-react/components/highlights.ts",
    "content": "// Tailwind requires the color to be complete strings (can't be dynamic), so we have to list all the strings here manually.\nexport const HIGHLIGHT_COLOR_MAP = {\n  bg: {\n    red: \"bg-red-200\",\n    green: \"bg-green-200\",\n    blue: \"bg-blue-200\",\n    yellow: \"bg-yellow-200\",\n  } as const,\n  [\"border-l\"]: {\n    red: \"border-l-red-200\",\n    green: \"border-l-green-200\",\n    blue: \"border-l-blue-200\",\n    yellow: \"border-l-yellow-200\",\n  } as const,\n};\n"
  },
  {
    "path": "packages/shared-react/components/ui/button.tsx",
    "content": "import type { VariantProps } from \"class-variance-authority\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva } from \"class-variance-authority\";\n\nconst buttonVariants = cva(\n  \"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50\",\n  {\n    variants: {\n      variant: {\n        none: \"\",\n        default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n        destructive:\n          \"bg-destructive text-destructive-foreground hover:bg-destructive/90\",\n        destructiveOutline:\n          \"border border-destructive bg-transparent text-destructive hover:bg-destructive/90 hover:text-destructive-foreground\",\n        outline:\n          \"border border-input bg-background hover:bg-accent hover:text-accent-foreground\",\n        secondary:\n          \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n        ghost:\n          \"hover:bg-accent hover:text-accent-foreground focus-visible:ring-0 focus-visible:ring-offset-0\",\n        ghostDestructive:\n          \"text-destructive hover:bg-destructive/10 hover:text-destructive focus-visible:ring-0 focus-visible:ring-offset-0\",\n        border: \"border border-input hover:bg-accent\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        none: \"\",\n        default: \"h-10 px-4 py-2\",\n        sm: \"h-9 rounded-md px-3\",\n        lg: \"h-11 rounded-md px-8\",\n        icon: \"size-10\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nexport interface ButtonProps\n  extends\n    React.ButtonHTMLAttributes<HTMLButtonElement>,\n    VariantProps<typeof buttonVariants> {\n  asChild?: boolean;\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n  ({ className, variant, size, asChild = false, ...props }, ref) => {\n    const Comp = asChild ? Slot : \"button\";\n    return (\n      <Comp\n        className={cn(buttonVariants({ variant, size, className }))}\n        ref={ref}\n        {...props}\n      />\n    );\n  },\n);\nButton.displayName = \"Button\";\n\nexport { Button, buttonVariants };\n"
  },
  {
    "path": "packages/shared-react/components/ui/popover.tsx",
    "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\";\n\nconst Popover = PopoverPrimitive.Root;\n\nconst PopoverTrigger = PopoverPrimitive.Trigger;\n\nconst PopoverContent = React.forwardRef<\n  React.ElementRef<typeof PopoverPrimitive.Content>,\n  React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>\n>(({ className, align = \"center\", sideOffset = 4, ...props }, ref) => (\n  <PopoverPrimitive.Portal>\n    <PopoverPrimitive.Content\n      ref={ref}\n      align={align}\n      sideOffset={sideOffset}\n      className={cn(\n        \"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2\",\n        className,\n      )}\n      {...props}\n    />\n  </PopoverPrimitive.Portal>\n));\nPopoverContent.displayName = PopoverPrimitive.Content.displayName;\n\nexport { Popover, PopoverTrigger, PopoverContent };\n"
  },
  {
    "path": "packages/shared-react/components/ui/textarea.tsx",
    "content": "import * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>;\n\nconst Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(\n  ({ className, ...props }, ref) => {\n    return (\n      <textarea\n        className={cn(\n          \"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50\",\n          className,\n        )}\n        ref={ref}\n        {...props}\n      />\n    );\n  },\n);\nTextarea.displayName = \"Textarea\";\n\nexport { Textarea };\n"
  },
  {
    "path": "packages/shared-react/hooks/assets.ts",
    "content": "import { useMutation, useQueryClient } from \"@tanstack/react-query\";\n\nimport { useTRPC } from \"../trpc\";\n\ntype TRPCApi = ReturnType<typeof useTRPC>;\n\nexport function useAttachBookmarkAsset(\n  opts?: Parameters<TRPCApi[\"assets\"][\"attachAsset\"][\"mutationOptions\"]>[0],\n) {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n  return useMutation(\n    api.assets.attachAsset.mutationOptions({\n      ...opts,\n      onSuccess: (res, req, meta, context) => {\n        queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter());\n        queryClient.invalidateQueries(\n          api.bookmarks.searchBookmarks.pathFilter(),\n        );\n        queryClient.invalidateQueries(\n          api.bookmarks.getBookmark.queryFilter({ bookmarkId: req.bookmarkId }),\n        );\n        queryClient.invalidateQueries(api.assets.list.pathFilter());\n        return opts?.onSuccess?.(res, req, meta, context);\n      },\n    }),\n  );\n}\n\nexport function useReplaceBookmarkAsset(\n  opts?: Parameters<TRPCApi[\"assets\"][\"replaceAsset\"][\"mutationOptions\"]>[0],\n) {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n  return useMutation(\n    api.assets.replaceAsset.mutationOptions({\n      ...opts,\n      onSuccess: (res, req, meta, context) => {\n        queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter());\n        queryClient.invalidateQueries(\n          api.bookmarks.searchBookmarks.pathFilter(),\n        );\n        queryClient.invalidateQueries(\n          api.bookmarks.getBookmark.queryFilter({ bookmarkId: req.bookmarkId }),\n        );\n        queryClient.invalidateQueries(api.assets.list.pathFilter());\n        return opts?.onSuccess?.(res, req, meta, context);\n      },\n    }),\n  );\n}\n\nexport function useDetachBookmarkAsset(\n  opts?: Parameters<TRPCApi[\"assets\"][\"detachAsset\"][\"mutationOptions\"]>[0],\n) {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n  return useMutation(\n    api.assets.detachAsset.mutationOptions({\n      ...opts,\n      onSuccess: (res, req, meta, context) => {\n        queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter());\n        queryClient.invalidateQueries(\n          api.bookmarks.searchBookmarks.pathFilter(),\n        );\n        queryClient.invalidateQueries(\n          api.bookmarks.getBookmark.queryFilter({ bookmarkId: req.bookmarkId }),\n        );\n        queryClient.invalidateQueries(api.assets.list.pathFilter());\n        return opts?.onSuccess?.(res, req, meta, context);\n      },\n    }),\n  );\n}\n"
  },
  {
    "path": "packages/shared-react/hooks/bookmark-grid-context.tsx",
    "content": "\"use client\";\n\nimport { createContext, useContext } from \"react\";\n\nimport type { ZGetBookmarksRequest } from \"@karakeep/shared/types/bookmarks\";\n\nexport const BookmarkGridContext = createContext<\n  ZGetBookmarksRequest | undefined\n>(undefined);\n\nexport function BookmarkGridContextProvider({\n  query,\n  children,\n}: {\n  query: ZGetBookmarksRequest;\n  children: React.ReactNode;\n}) {\n  return (\n    <BookmarkGridContext.Provider value={query}>\n      {children}\n    </BookmarkGridContext.Provider>\n  );\n}\n\nexport function useBookmarkGridContext() {\n  return useContext(BookmarkGridContext);\n}\n"
  },
  {
    "path": "packages/shared-react/hooks/bookmark-list-context.tsx",
    "content": "\"use client\";\n\nimport { createContext, useContext } from \"react\";\n\nimport { ZBookmarkList } from \"@karakeep/shared/types/lists\";\n\nexport const BookmarkListContext = createContext<ZBookmarkList | undefined>(\n  undefined,\n);\n\nexport function BookmarkListContextProvider({\n  list,\n  children,\n}: {\n  list: ZBookmarkList;\n  children: React.ReactNode;\n}) {\n  return (\n    <BookmarkListContext.Provider value={list}>\n      {children}\n    </BookmarkListContext.Provider>\n  );\n}\n\nexport function useBookmarkListContext() {\n  return useContext(BookmarkListContext);\n}\n"
  },
  {
    "path": "packages/shared-react/hooks/bookmarks.ts",
    "content": "import { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\n\nimport { getBookmarkRefreshInterval } from \"@karakeep/shared/utils/bookmarkUtils\";\n\nimport { useTRPC } from \"../trpc\";\nimport { useBookmarkGridContext } from \"./bookmark-grid-context\";\nimport { useAddBookmarkToList } from \"./lists\";\n\ntype TRPCApi = ReturnType<typeof useTRPC>;\n\nexport function useAutoRefreshingBookmarkQuery(\n  input: Parameters<TRPCApi[\"bookmarks\"][\"getBookmark\"][\"queryOptions\"]>[0],\n) {\n  const api = useTRPC();\n  return useQuery(\n    api.bookmarks.getBookmark.queryOptions(input, {\n      refetchInterval: (query) => {\n        const data = query.state.data;\n        if (!data) {\n          return false;\n        }\n        return getBookmarkRefreshInterval(data);\n      },\n    }),\n  );\n}\n\nexport function useCreateBookmark(\n  opts?: Parameters<\n    TRPCApi[\"bookmarks\"][\"createBookmark\"][\"mutationOptions\"]\n  >[0],\n) {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n  return useMutation(\n    api.bookmarks.createBookmark.mutationOptions({\n      ...opts,\n      onSuccess: (res, req, meta, context) => {\n        queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter());\n        queryClient.invalidateQueries(\n          api.bookmarks.searchBookmarks.pathFilter(),\n        );\n        queryClient.invalidateQueries(api.lists.stats.pathFilter());\n        return opts?.onSuccess?.(res, req, meta, context);\n      },\n    }),\n  );\n}\n\nexport function useCreateBookmarkWithPostHook(\n  opts?: Parameters<\n    TRPCApi[\"bookmarks\"][\"createBookmark\"][\"mutationOptions\"]\n  >[0],\n) {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n  const postCreationCB = useBookmarkPostCreationHook();\n  return useMutation(\n    api.bookmarks.createBookmark.mutationOptions({\n      ...opts,\n      onSuccess: async (res, req, meta, context) => {\n        queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter());\n        queryClient.invalidateQueries(\n          api.bookmarks.searchBookmarks.pathFilter(),\n        );\n        await postCreationCB(res.id);\n        return opts?.onSuccess?.(res, req, meta, context);\n      },\n    }),\n  );\n}\n\nexport function useDeleteBookmark(\n  opts?: Parameters<\n    TRPCApi[\"bookmarks\"][\"deleteBookmark\"][\"mutationOptions\"]\n  >[0],\n) {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n  return useMutation(\n    api.bookmarks.deleteBookmark.mutationOptions({\n      ...opts,\n      onSuccess: (res, req, meta, context) => {\n        queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter());\n        queryClient.invalidateQueries(\n          api.bookmarks.searchBookmarks.pathFilter(),\n        );\n        queryClient.removeQueries(\n          api.bookmarks.getBookmark.queryFilter({ bookmarkId: req.bookmarkId }),\n        );\n        queryClient.invalidateQueries(api.lists.stats.pathFilter());\n        return opts?.onSuccess?.(res, req, meta, context);\n      },\n    }),\n  );\n}\n\nexport function useUpdateBookmark(\n  opts?: Parameters<\n    TRPCApi[\"bookmarks\"][\"updateBookmark\"][\"mutationOptions\"]\n  >[0],\n) {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n  return useMutation(\n    api.bookmarks.updateBookmark.mutationOptions({\n      ...opts,\n      onSuccess: (res, req, meta, context) => {\n        queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter());\n        queryClient.invalidateQueries(\n          api.bookmarks.searchBookmarks.pathFilter(),\n        );\n        queryClient.invalidateQueries(\n          api.bookmarks.getBookmark.queryFilter({ bookmarkId: req.bookmarkId }),\n        );\n        queryClient.invalidateQueries(api.lists.stats.pathFilter());\n        return opts?.onSuccess?.(res, req, meta, context);\n      },\n    }),\n  );\n}\n\nexport function useSummarizeBookmark(\n  opts?: Parameters<\n    TRPCApi[\"bookmarks\"][\"summarizeBookmark\"][\"mutationOptions\"]\n  >[0],\n) {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n  return useMutation(\n    api.bookmarks.summarizeBookmark.mutationOptions({\n      ...opts,\n      onSuccess: (res, req, meta, context) => {\n        queryClient.invalidateQueries(api.bookmarks.getBookmarks.pathFilter());\n        queryClient.invalidateQueries(\n          api.bookmarks.searchBookmarks.pathFilter(),\n        );\n        queryClient.invalidateQueries(\n          api.bookmarks.getBookmark.queryFilter({ bookmarkId: req.bookmarkId }),\n        );\n        return opts?.onSuccess?.(res, req, meta, context);\n      },\n    }),\n  );\n}\n\nexport function useRecrawlBookmark(\n  opts?: Parameters<\n    TRPCApi[\"bookmarks\"][\"recrawlBookmark\"][\"mutationOptions\"]\n  >[0],\n) {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n  return useMutation(\n    api.bookmarks.recrawlBookmark.mutationOptions({\n      ...opts,\n      onSuccess: (res, req, meta, context) => {\n        queryClient.invalidateQueries(\n          api.bookmarks.getBookmark.queryFilter({ bookmarkId: req.bookmarkId }),\n        );\n        return opts?.onSuccess?.(res, req, meta, context);\n      },\n    }),\n  );\n}\n\nexport function useUpdateBookmarkTags(\n  opts?: Parameters<TRPCApi[\"bookmarks\"][\"updateTags\"][\"mutationOptions\"]>[0],\n) {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n  return useMutation(\n    api.bookmarks.updateTags.mutationOptions({\n      ...opts,\n      onSuccess: (res, req, meta, context) => {\n        queryClient.invalidateQueries(\n          api.bookmarks.getBookmark.queryFilter({ bookmarkId: req.bookmarkId }),\n        );\n\n        [...res.attached, ...res.detached].forEach((id) => {\n          queryClient.invalidateQueries(\n            api.tags.get.queryFilter({ tagId: id }),\n          );\n          queryClient.invalidateQueries(\n            api.bookmarks.getBookmarks.queryFilter({ tagId: id }),\n          );\n        });\n        queryClient.invalidateQueries(api.tags.list.pathFilter());\n        queryClient.invalidateQueries(api.lists.stats.pathFilter());\n        return opts?.onSuccess?.(res, req, meta, context);\n      },\n    }),\n  );\n}\n\n/**\n * Checks the grid query context to know if we need to augment the bookmark post creation to fit the grid context\n */\nexport function useBookmarkPostCreationHook() {\n  const gridQueryCtx = useBookmarkGridContext();\n  const { mutateAsync: updateBookmark } = useUpdateBookmark();\n  const { mutateAsync: addToList } = useAddBookmarkToList();\n  const { mutateAsync: updateTags } = useUpdateBookmarkTags();\n\n  return async (bookmarkId: string) => {\n    if (!gridQueryCtx) {\n      return;\n    }\n\n    const promises = [];\n    if (gridQueryCtx.favourited ?? gridQueryCtx.archived) {\n      promises.push(\n        updateBookmark({\n          bookmarkId,\n          favourited: gridQueryCtx.favourited,\n          archived: gridQueryCtx.archived,\n        }),\n      );\n    }\n\n    if (gridQueryCtx.listId) {\n      promises.push(\n        addToList({\n          bookmarkId,\n          listId: gridQueryCtx.listId,\n        }),\n      );\n    }\n\n    if (gridQueryCtx.tagId) {\n      promises.push(\n        updateTags({\n          bookmarkId,\n          attach: [{ tagId: gridQueryCtx.tagId }],\n          detach: [],\n        }),\n      );\n    }\n\n    return Promise.all(promises);\n  };\n}\n"
  },
  {
    "path": "packages/shared-react/hooks/highlights.ts",
    "content": "import { useMutation, useQueryClient } from \"@tanstack/react-query\";\n\nimport { useTRPC } from \"../trpc\";\n\ntype TRPCApi = ReturnType<typeof useTRPC>;\n\nexport function useCreateHighlight(\n  opts?: Parameters<TRPCApi[\"highlights\"][\"create\"][\"mutationOptions\"]>[0],\n) {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n  return useMutation(\n    api.highlights.create.mutationOptions({\n      ...opts,\n      onSuccess: (res, req, meta, context) => {\n        queryClient.invalidateQueries(\n          api.highlights.getForBookmark.queryFilter({\n            bookmarkId: req.bookmarkId,\n          }),\n        );\n        queryClient.invalidateQueries(api.highlights.getAll.pathFilter());\n        return opts?.onSuccess?.(res, req, meta, context);\n      },\n    }),\n  );\n}\n\nexport function useUpdateHighlight(\n  opts?: Parameters<TRPCApi[\"highlights\"][\"update\"][\"mutationOptions\"]>[0],\n) {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n  return useMutation(\n    api.highlights.update.mutationOptions({\n      ...opts,\n      onSuccess: (res, req, meta, context) => {\n        queryClient.invalidateQueries(\n          api.highlights.getForBookmark.queryFilter({\n            bookmarkId: res.bookmarkId,\n          }),\n        );\n        queryClient.invalidateQueries(api.highlights.getAll.pathFilter());\n        return opts?.onSuccess?.(res, req, meta, context);\n      },\n    }),\n  );\n}\n\nexport function useDeleteHighlight(\n  opts?: Parameters<TRPCApi[\"highlights\"][\"delete\"][\"mutationOptions\"]>[0],\n) {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n  return useMutation(\n    api.highlights.delete.mutationOptions({\n      ...opts,\n      onSuccess: (res, req, meta, context) => {\n        queryClient.invalidateQueries(\n          api.highlights.getForBookmark.queryFilter({\n            bookmarkId: res.bookmarkId,\n          }),\n        );\n        queryClient.invalidateQueries(api.highlights.getAll.pathFilter());\n        return opts?.onSuccess?.(res, req, meta, context);\n      },\n    }),\n  );\n}\n"
  },
  {
    "path": "packages/shared-react/hooks/lists.ts",
    "content": "import { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\n\nimport { ZBookmarkList } from \"@karakeep/shared/types/lists\";\nimport {\n  listsToTree,\n  ZBookmarkListRoot,\n} from \"@karakeep/shared/utils/listUtils\";\n\nimport { useTRPC } from \"../trpc\";\n\ntype TRPCApi = ReturnType<typeof useTRPC>;\n\nexport function useCreateBookmarkList(\n  opts?: Parameters<TRPCApi[\"lists\"][\"create\"][\"mutationOptions\"]>[0],\n) {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n  return useMutation(\n    api.lists.create.mutationOptions({\n      ...opts,\n      onSuccess: (res, req, meta, context) => {\n        queryClient.invalidateQueries(api.lists.list.pathFilter());\n        return opts?.onSuccess?.(res, req, meta, context);\n      },\n    }),\n  );\n}\n\nexport function useEditBookmarkList(\n  opts?: Parameters<TRPCApi[\"lists\"][\"edit\"][\"mutationOptions\"]>[0],\n) {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n  return useMutation(\n    api.lists.edit.mutationOptions({\n      ...opts,\n      onSuccess: (res, req, meta, context) => {\n        queryClient.invalidateQueries(api.lists.list.pathFilter());\n        queryClient.invalidateQueries(\n          api.lists.get.queryFilter({ listId: req.listId }),\n        );\n        if (res.type === \"smart\") {\n          queryClient.invalidateQueries(\n            api.bookmarks.getBookmarks.queryFilter({ listId: req.listId }),\n          );\n        }\n        return opts?.onSuccess?.(res, req, meta, context);\n      },\n    }),\n  );\n}\n\nexport function useMergeLists(\n  opts?: Parameters<TRPCApi[\"lists\"][\"merge\"][\"mutationOptions\"]>[0],\n) {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n  return useMutation(\n    api.lists.merge.mutationOptions({\n      ...opts,\n      onSuccess: (res, req, meta, context) => {\n        queryClient.invalidateQueries(api.lists.list.pathFilter());\n        queryClient.invalidateQueries(\n          api.bookmarks.getBookmarks.queryFilter({ listId: req.targetId }),\n        );\n        queryClient.invalidateQueries(api.lists.stats.pathFilter());\n        return opts?.onSuccess?.(res, req, meta, context);\n      },\n    }),\n  );\n}\n\nexport function useAddBookmarkToList(\n  opts?: Parameters<TRPCApi[\"lists\"][\"addToList\"][\"mutationOptions\"]>[0],\n) {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n  return useMutation(\n    api.lists.addToList.mutationOptions({\n      ...opts,\n      onSuccess: (res, req, meta, context) => {\n        queryClient.invalidateQueries(\n          api.bookmarks.getBookmarks.queryFilter({ listId: req.listId }),\n        );\n        queryClient.invalidateQueries(\n          api.lists.getListsOfBookmark.queryFilter({\n            bookmarkId: req.bookmarkId,\n          }),\n        );\n        queryClient.invalidateQueries(api.lists.stats.pathFilter());\n        return opts?.onSuccess?.(res, req, meta, context);\n      },\n    }),\n  );\n}\n\nexport function useRemoveBookmarkFromList(\n  opts?: Parameters<TRPCApi[\"lists\"][\"removeFromList\"][\"mutationOptions\"]>[0],\n) {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n  return useMutation(\n    api.lists.removeFromList.mutationOptions({\n      ...opts,\n      onSuccess: (res, req, meta, context) => {\n        queryClient.invalidateQueries(\n          api.bookmarks.getBookmarks.queryFilter({ listId: req.listId }),\n        );\n        queryClient.invalidateQueries(\n          api.lists.getListsOfBookmark.queryFilter({\n            bookmarkId: req.bookmarkId,\n          }),\n        );\n        queryClient.invalidateQueries(api.lists.stats.pathFilter());\n        return opts?.onSuccess?.(res, req, meta, context);\n      },\n    }),\n  );\n}\n\nexport function useDeleteBookmarkList(\n  opts?: Parameters<TRPCApi[\"lists\"][\"delete\"][\"mutationOptions\"]>[0],\n) {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n  return useMutation(\n    api.lists.delete.mutationOptions({\n      ...opts,\n      onSuccess: (res, req, meta, context) => {\n        queryClient.invalidateQueries(api.lists.list.pathFilter());\n        queryClient.removeQueries(\n          api.lists.get.queryFilter({ listId: req.listId }),\n        );\n        return opts?.onSuccess?.(res, req, meta, context);\n      },\n    }),\n  );\n}\n\nexport function useBookmarkLists(\n  input?: Parameters<TRPCApi[\"lists\"][\"list\"][\"queryOptions\"]>[0],\n  opts?: Parameters<TRPCApi[\"lists\"][\"list\"][\"queryOptions\"]>[1],\n) {\n  const api = useTRPC();\n  return useQuery(\n    api.lists.list.queryOptions(input, {\n      ...opts,\n      select: (data) => {\n        return { data: data.lists, ...listsToTree(data.lists) };\n      },\n    }),\n  );\n}\n\nexport function augmentBookmarkListsWithInitialData(\n  data:\n    | {\n        data: ZBookmarkList[];\n        root: ZBookmarkListRoot;\n        allPaths: ZBookmarkList[][];\n        getPathById: (id: string) => ZBookmarkList[] | undefined;\n      }\n    | undefined,\n  initialData: ZBookmarkList[],\n) {\n  if (data) {\n    return data;\n  }\n  return { data: initialData, ...listsToTree(initialData) };\n}\n"
  },
  {
    "path": "packages/shared-react/hooks/reader-settings.tsx",
    "content": "\"use client\";\n\nimport {\n  createContext,\n  ReactNode,\n  useCallback,\n  useContext,\n  useEffect,\n  useMemo,\n  useState,\n} from \"react\";\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\n\nimport {\n  READER_DEFAULTS,\n  ReaderSettings,\n  ReaderSettingsPartial,\n} from \"@karakeep/shared/types/readers\";\n\nimport { useTRPC } from \"../trpc\";\n\nexport interface UseReaderSettingsOptions {\n  /**\n   * Get local overrides (device-specific settings stored locally)\n   */\n  getLocalOverrides: () => ReaderSettingsPartial;\n  /**\n   * Save local overrides to local storage\n   */\n  saveLocalOverrides: (overrides: ReaderSettingsPartial) => void;\n  /**\n   * Optional session overrides (for live preview in web).\n   * If provided, these take highest precedence.\n   */\n  sessionOverrides?: ReaderSettingsPartial;\n  /**\n   * Callback when session overrides should be cleared (after successful server save)\n   */\n  onClearSessionOverrides?: () => void;\n}\n\nexport function useReaderSettings(options: UseReaderSettingsOptions) {\n  const api = useTRPC();\n  const {\n    getLocalOverrides,\n    saveLocalOverrides,\n    sessionOverrides = {},\n    onClearSessionOverrides,\n  } = options;\n\n  const [localOverrides, setLocalOverrides] = useState<ReaderSettingsPartial>(\n    {},\n  );\n  const [pendingServerSave, setPendingServerSave] =\n    useState<ReaderSettings | null>(null);\n\n  const { data: serverSettings } = useQuery(api.users.settings.queryOptions());\n  const queryClient = useQueryClient();\n\n  // Load local overrides on mount\n  useEffect(() => {\n    setLocalOverrides(getLocalOverrides());\n  }, [getLocalOverrides]);\n\n  // Clear pending state when server settings match what we saved\n  useEffect(() => {\n    if (pendingServerSave && serverSettings) {\n      const serverMatches =\n        serverSettings.readerFontSize === pendingServerSave.fontSize &&\n        // Tolerate minor float normalization differences for lineHeight\n        Math.abs(\n          (serverSettings.readerLineHeight ?? 0) - pendingServerSave.lineHeight,\n        ) < 1e-6 &&\n        serverSettings.readerFontFamily === pendingServerSave.fontFamily;\n      if (serverMatches) {\n        setPendingServerSave(null);\n      }\n    }\n  }, [serverSettings, pendingServerSave]);\n\n  const { mutate: updateServerSettings, isPending: isSaving } = useMutation(\n    api.users.updateSettings.mutationOptions({\n      onSettled: async () => {\n        await queryClient.refetchQueries(api.users.settings.pathFilter());\n      },\n    }),\n  );\n\n  // Separate mutation for saving defaults (clears local overrides on success)\n  const { mutate: saveServerSettings, isPending: isSavingDefaults } =\n    useMutation(\n      api.users.updateSettings.mutationOptions({\n        onSuccess: () => {\n          // Clear local and session overrides after successful server save\n          setLocalOverrides({});\n          saveLocalOverrides({});\n          onClearSessionOverrides?.();\n        },\n        onError: () => {\n          // Clear pending state so we don't show values that failed to persist\n          setPendingServerSave(null);\n        },\n        onSettled: async () => {\n          await queryClient.refetchQueries(api.users.settings.pathFilter());\n        },\n      }),\n    );\n\n  // Compute effective settings with precedence: session → local → pendingSave → server → default\n  const settings: ReaderSettings = useMemo(\n    () => ({\n      fontSize:\n        sessionOverrides.fontSize ??\n        localOverrides.fontSize ??\n        pendingServerSave?.fontSize ??\n        serverSettings?.readerFontSize ??\n        READER_DEFAULTS.fontSize,\n      lineHeight:\n        sessionOverrides.lineHeight ??\n        localOverrides.lineHeight ??\n        pendingServerSave?.lineHeight ??\n        serverSettings?.readerLineHeight ??\n        READER_DEFAULTS.lineHeight,\n      fontFamily:\n        sessionOverrides.fontFamily ??\n        localOverrides.fontFamily ??\n        pendingServerSave?.fontFamily ??\n        serverSettings?.readerFontFamily ??\n        READER_DEFAULTS.fontFamily,\n    }),\n    [sessionOverrides, localOverrides, pendingServerSave, serverSettings],\n  );\n\n  // Get the server setting values (for UI indicators)\n  const serverDefaults: ReaderSettingsPartial = useMemo(\n    () => ({\n      fontSize: serverSettings?.readerFontSize ?? undefined,\n      lineHeight: serverSettings?.readerLineHeight ?? undefined,\n      fontFamily: serverSettings?.readerFontFamily ?? undefined,\n    }),\n    [serverSettings],\n  );\n\n  // Update local override (per-device, immediate)\n  const updateLocal = useCallback(\n    (updates: ReaderSettingsPartial) => {\n      setLocalOverrides((prev) => {\n        const newOverrides = { ...prev, ...updates };\n        saveLocalOverrides(newOverrides);\n        return newOverrides;\n      });\n    },\n    [saveLocalOverrides],\n  );\n\n  // Clear a specific local override\n  const clearLocal = useCallback(\n    (key: keyof ReaderSettings) => {\n      setLocalOverrides((prev) => {\n        const { [key]: _, ...rest } = prev;\n        saveLocalOverrides(rest);\n        return rest;\n      });\n    },\n    [saveLocalOverrides],\n  );\n\n  // Clear all local overrides\n  const clearAllLocal = useCallback(() => {\n    setLocalOverrides({});\n    saveLocalOverrides({});\n  }, [saveLocalOverrides]);\n\n  // Save current effective settings as server default (syncs across devices)\n  const saveAsDefault = useCallback(\n    (settingsToSave?: ReaderSettingsPartial) => {\n      const toSave: ReaderSettings = {\n        fontSize: settingsToSave?.fontSize ?? settings.fontSize,\n        lineHeight: settingsToSave?.lineHeight ?? settings.lineHeight,\n        fontFamily: settingsToSave?.fontFamily ?? settings.fontFamily,\n      };\n      // Set pending state to prevent flicker while server syncs\n      setPendingServerSave(toSave);\n      saveServerSettings({\n        readerFontSize: toSave.fontSize,\n        readerLineHeight: toSave.lineHeight,\n        readerFontFamily: toSave.fontFamily,\n      });\n    },\n    [settings, saveServerSettings],\n  );\n\n  // Clear a specific server default (set to null)\n  const clearDefault = useCallback(\n    (key: keyof ReaderSettings) => {\n      const serverKeyMap = {\n        fontSize: \"readerFontSize\",\n        lineHeight: \"readerLineHeight\",\n        fontFamily: \"readerFontFamily\",\n      } as const;\n      updateServerSettings({ [serverKeyMap[key]]: null });\n    },\n    [updateServerSettings],\n  );\n\n  // Clear all server defaults\n  const clearAllDefaults = useCallback(() => {\n    updateServerSettings({\n      readerFontSize: null,\n      readerLineHeight: null,\n      readerFontFamily: null,\n    });\n  }, [updateServerSettings]);\n\n  // Check if there are any local overrides\n  const hasLocalOverrides = Object.keys(localOverrides).length > 0;\n\n  // Check if there are any server defaults\n  const hasServerDefaults =\n    serverSettings?.readerFontSize != null ||\n    serverSettings?.readerLineHeight != null ||\n    serverSettings?.readerFontFamily != null;\n\n  return {\n    // Current effective settings (what should be displayed)\n    settings,\n\n    // Raw values for UI indicators\n    localOverrides,\n    serverDefaults,\n\n    // Status flags\n    hasLocalOverrides,\n    hasServerDefaults,\n    isSaving: isSaving || isSavingDefaults,\n\n    // Internal state setters (for web's context-based approach)\n    setLocalOverrides,\n\n    // Actions\n    updateLocal,\n    clearLocal,\n    clearAllLocal,\n    saveAsDefault,\n    clearDefault,\n    clearAllDefaults,\n  };\n}\n\n// Context for sharing reader settings state across components\nexport type ReaderSettingsContextValue = ReturnType<typeof useReaderSettings>;\n\nconst ReaderSettingsContext = createContext<ReaderSettingsContextValue | null>(\n  null,\n);\n\nexport interface ReaderSettingsProviderProps extends UseReaderSettingsOptions {\n  children: ReactNode;\n}\n\n/**\n * Provider that creates a single instance of reader settings state\n * and shares it across all child components.\n */\nexport function ReaderSettingsProvider({\n  children,\n  ...options\n}: ReaderSettingsProviderProps) {\n  const value = useReaderSettings(options);\n\n  return (\n    <ReaderSettingsContext.Provider value={value}>\n      {children}\n    </ReaderSettingsContext.Provider>\n  );\n}\n\n/**\n * Hook to access shared reader settings from context.\n * Must be used within a ReaderSettingsProvider.\n */\nexport function useReaderSettingsContext() {\n  const context = useContext(ReaderSettingsContext);\n  if (!context) {\n    throw new Error(\n      \"useReaderSettingsContext must be used within a ReaderSettingsProvider\",\n    );\n  }\n  return context;\n}\n"
  },
  {
    "path": "packages/shared-react/hooks/reading-progress.ts",
    "content": "import { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\n\nimport type { ReadingPosition } from \"@karakeep/shared/utils/reading-progress-dom\";\n\nimport { useTRPC } from \"../trpc\";\n\ninterface UseReadingProgressOptions {\n  bookmarkId: string;\n}\n\n/**\n * Unified reading progress hook for web and mobile.\n *\n * Handles:\n * - Fetching reading progress via its own tRPC query\n * - Capturing initial reading position (stable across query re-fetches)\n * - \"Continue reading\" banner state and auto-dismiss on scroll past 15%\n * - Lazy saving via onSavePosition (idle, visibility change, unmount)\n * - Deduplication of save calls by offset\n *\n * Pass the returned `onSavePosition` and `onScrollPositionChange` to ScrollProgressTracker.\n */\nexport function useReadingProgress({ bookmarkId }: UseReadingProgressOptions) {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n\n  const { data: progressData } = useQuery(\n    api.bookmarks.getReadingProgress.queryOptions({ bookmarkId }),\n  );\n\n  const readingProgressOffset = progressData?.readingProgressOffset;\n  const readingProgressAnchor = progressData?.readingProgressAnchor;\n  const readingProgressPercent = progressData?.readingProgressPercent;\n\n  // Capture initial reading progress on first load — stays stable across re-fetches\n  const initialProgressRef = useRef<{\n    offset: number | null;\n    anchor: string | null;\n    percent: number | null;\n  } | null>(null);\n  const previousBookmarkIdRef = useRef<string | null>(null);\n  const lastSavedOffset = useRef<number | null>(null);\n\n  if (previousBookmarkIdRef.current !== bookmarkId) {\n    previousBookmarkIdRef.current = bookmarkId;\n    initialProgressRef.current = null;\n    lastSavedOffset.current = null;\n  }\n\n  // Only capture once data has loaded (offset transitions from undefined to a value)\n  if (!initialProgressRef.current && readingProgressOffset !== undefined) {\n    initialProgressRef.current = {\n      offset: readingProgressOffset ?? null,\n      anchor: readingProgressAnchor ?? null,\n      percent: readingProgressPercent ?? null,\n    };\n  }\n\n  if (\n    lastSavedOffset.current === null &&\n    initialProgressRef.current?.offset != null\n  ) {\n    lastSavedOffset.current = initialProgressRef.current.offset;\n  }\n\n  const initialOffset = initialProgressRef.current?.offset ?? null;\n  const initialAnchor = initialProgressRef.current?.anchor ?? null;\n  const initialPercent = initialProgressRef.current?.percent ?? null;\n\n  // Banner state\n  const [bannerDismissed, setBannerDismissed] = useState(false);\n  const [restoreRequested, setRestoreRequested] = useState(false);\n  const showBanner =\n    !!initialOffset &&\n    initialOffset > 0 &&\n    initialPercent != null &&\n    initialPercent >= 10 &&\n    initialPercent < 100 &&\n    !bannerDismissed;\n\n  const bannerVisibleRef = useRef(false);\n  bannerVisibleRef.current = showBanner;\n\n  useEffect(() => {\n    setBannerDismissed(false);\n    setRestoreRequested(false);\n  }, [bookmarkId]);\n\n  // Save mutation\n  const { mutate: updateProgress } = useMutation(\n    api.bookmarks.updateReadingProgress.mutationOptions({\n      onSuccess: () => {\n        queryClient.invalidateQueries(\n          api.bookmarks.getReadingProgress.pathFilter(),\n        );\n      },\n    }),\n  );\n\n  // Lazy save — called by ScrollProgressTracker on idle/visibility/beforeunload/unmount\n  const onSavePosition = useCallback(\n    (position: ReadingPosition) => {\n      if (bannerVisibleRef.current) return;\n      if (lastSavedOffset.current === position.offset) return;\n      lastSavedOffset.current = position.offset;\n      updateProgress({\n        bookmarkId,\n        readingProgressOffset: position.offset,\n        readingProgressAnchor: position.anchor,\n        readingProgressPercent: position.percent,\n      });\n    },\n    [bookmarkId, updateProgress],\n  );\n\n  // Responsive — called on every throttled scroll for banner dismissal\n  const onScrollPositionChange = useCallback((position: ReadingPosition) => {\n    if (bannerVisibleRef.current && position.percent > 15) {\n      setBannerDismissed(true);\n    }\n  }, []);\n\n  const onContinue = useCallback(() => {\n    setRestoreRequested(true);\n    setBannerDismissed(true);\n  }, []);\n\n  const onDismiss = useCallback(() => {\n    setBannerDismissed(true);\n  }, []);\n\n  return {\n    // Banner\n    showBanner,\n    bannerPercent: initialPercent,\n    onContinue,\n    onDismiss,\n    // ScrollProgressTracker props\n    restorePosition: restoreRequested,\n    readingProgressOffset: initialOffset,\n    readingProgressAnchor: initialAnchor,\n    onSavePosition,\n    onScrollPositionChange,\n  };\n}\n"
  },
  {
    "path": "packages/shared-react/hooks/rules.ts",
    "content": "import { useMutation, useQueryClient } from \"@tanstack/react-query\";\n\nimport { useTRPC } from \"../trpc\";\n\ntype TRPCApi = ReturnType<typeof useTRPC>;\n\nexport function useCreateRule(\n  opts?: Parameters<TRPCApi[\"rules\"][\"create\"][\"mutationOptions\"]>[0],\n) {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n  return useMutation(\n    api.rules.create.mutationOptions({\n      ...opts,\n      onSuccess: (res, req, meta, context) => {\n        queryClient.invalidateQueries(api.rules.list.pathFilter());\n        return opts?.onSuccess?.(res, req, meta, context);\n      },\n    }),\n  );\n}\n\nexport function useUpdateRule(\n  opts?: Parameters<TRPCApi[\"rules\"][\"update\"][\"mutationOptions\"]>[0],\n) {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n  return useMutation(\n    api.rules.update.mutationOptions({\n      ...opts,\n      onSuccess: (res, req, meta, context) => {\n        queryClient.invalidateQueries(api.rules.list.pathFilter());\n        return opts?.onSuccess?.(res, req, meta, context);\n      },\n    }),\n  );\n}\n\nexport function useDeleteRule(\n  opts?: Parameters<TRPCApi[\"rules\"][\"delete\"][\"mutationOptions\"]>[0],\n) {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n  return useMutation(\n    api.rules.delete.mutationOptions({\n      ...opts,\n      onSuccess: (res, req, meta, context) => {\n        queryClient.invalidateQueries(api.rules.list.pathFilter());\n        return opts?.onSuccess?.(res, req, meta, context);\n      },\n    }),\n  );\n}\n"
  },
  {
    "path": "packages/shared-react/hooks/search-history.ts",
    "content": "import { useCallback, useEffect, useMemo, useState } from \"react\";\nimport { z } from \"zod\";\n\nconst searchHistorySchema = z.array(z.string());\n\nconst BOOKMARK_SEARCH_HISTORY_KEY = \"karakeep_search_history\";\nconst MAX_STORED_ITEMS = 50;\n\nclass SearchHistoryUtil {\n  constructor(\n    private storage: {\n      getItem(key: string): Promise<string | null> | string | null;\n      setItem(key: string, value: string): Promise<void> | void;\n      removeItem(key: string): Promise<void> | void;\n    },\n  ) {}\n\n  async getSearchHistory(): Promise<string[]> {\n    try {\n      const rawHistory = await this.storage.getItem(\n        BOOKMARK_SEARCH_HISTORY_KEY,\n      );\n      if (rawHistory) {\n        const parsed = JSON.parse(rawHistory) as unknown;\n        const result = searchHistorySchema.safeParse(parsed);\n        if (result.success) {\n          return result.data;\n        }\n      }\n      return [];\n    } catch (error) {\n      console.error(\"Failed to parse search history:\", error);\n      return [];\n    }\n  }\n\n  async addSearchTermToHistory(term: string): Promise<void> {\n    if (!term || term.trim().length === 0) {\n      return;\n    }\n    try {\n      const currentHistory = await this.getSearchHistory();\n      const filteredHistory = currentHistory.filter(\n        (item) => item.toLowerCase() !== term.toLowerCase(),\n      );\n      const newHistory = [term, ...filteredHistory];\n      const finalHistory = newHistory.slice(0, MAX_STORED_ITEMS);\n      await this.storage.setItem(\n        BOOKMARK_SEARCH_HISTORY_KEY,\n        JSON.stringify(finalHistory),\n      );\n    } catch (error) {\n      console.error(\"Failed to save search history:\", error);\n    }\n  }\n\n  async clearSearchHistory(): Promise<void> {\n    try {\n      await this.storage.removeItem(BOOKMARK_SEARCH_HISTORY_KEY);\n    } catch (error) {\n      console.error(\"Failed to clear search history:\", error);\n    }\n  }\n}\n\nexport function useSearchHistory(adapter: {\n  getItem(key: string): Promise<string | null> | string | null;\n  setItem(key: string, value: string): Promise<void> | void;\n  removeItem(key: string): Promise<void> | void;\n}) {\n  const [history, setHistory] = useState<string[]>([]);\n  const searchHistoryUtil = useMemo(() => new SearchHistoryUtil(adapter), []);\n\n  const loadHistory = useCallback(async () => {\n    const storedHistory = await searchHistoryUtil.getSearchHistory();\n    setHistory(storedHistory);\n  }, [searchHistoryUtil]);\n\n  useEffect(() => {\n    loadHistory();\n  }, [loadHistory]);\n\n  const addTerm = useCallback(\n    async (term: string) => {\n      await searchHistoryUtil.addSearchTermToHistory(term);\n      await loadHistory();\n    },\n    [searchHistoryUtil, loadHistory],\n  );\n\n  const clearHistory = useCallback(async () => {\n    await searchHistoryUtil.clearSearchHistory();\n    setHistory([]);\n  }, [searchHistoryUtil]);\n\n  return {\n    history,\n    addTerm,\n    clearHistory,\n    refreshHistory: loadHistory,\n  };\n}\n"
  },
  {
    "path": "packages/shared-react/hooks/tags.ts",
    "content": "import {\n  keepPreviousData,\n  useInfiniteQuery,\n  useMutation,\n  useQuery,\n  useQueryClient,\n} from \"@tanstack/react-query\";\n\nimport { ZTagListResponse } from \"@karakeep/shared/types/tags\";\n\nimport { useTRPC } from \"../trpc\";\n\ntype TRPCApi = ReturnType<typeof useTRPC>;\n\nexport function usePaginatedSearchTags(\n  input: Parameters<TRPCApi[\"tags\"][\"list\"][\"infiniteQueryOptions\"]>[0],\n) {\n  const api = useTRPC();\n  return useInfiniteQuery({\n    ...api.tags.list.infiniteQueryOptions(input, {\n      placeholderData: keepPreviousData,\n      getNextPageParam: (lastPage) => lastPage.nextCursor,\n      gcTime: 60_000,\n    }),\n    select: (data) => ({\n      tags: data.pages.flatMap((page) => page.tags),\n    }),\n  });\n}\n\nexport function useTagAutocomplete<T = ZTagListResponse>(opts: {\n  nameContains: string;\n  select?: (data: ZTagListResponse) => T;\n  enabled?: boolean;\n}) {\n  const api = useTRPC();\n  return useQuery({\n    ...api.tags.list.queryOptions(\n      {\n        nameContains: opts.nameContains,\n        limit: 50,\n        sortBy: opts.nameContains ? \"relevance\" : \"usage\",\n      },\n      {\n        placeholderData: keepPreviousData,\n        gcTime: opts.nameContains?.length > 0 ? 60_000 : 3_600_000,\n        enabled: opts.enabled,\n      },\n    ),\n    select: opts.select,\n  });\n}\n\nexport function useCreateTag(\n  opts?: Parameters<TRPCApi[\"tags\"][\"create\"][\"mutationOptions\"]>[0],\n) {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n\n  return useMutation(\n    api.tags.create.mutationOptions({\n      ...opts,\n      onSuccess: (res, req, meta, context) => {\n        queryClient.invalidateQueries(api.tags.list.pathFilter());\n        return opts?.onSuccess?.(res, req, meta, context);\n      },\n    }),\n  );\n}\n\nexport function useUpdateTag(\n  opts?: Parameters<TRPCApi[\"tags\"][\"update\"][\"mutationOptions\"]>[0],\n) {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n\n  return useMutation(\n    api.tags.update.mutationOptions({\n      ...opts,\n      onSuccess: (res, req, meta, context) => {\n        queryClient.invalidateQueries(api.tags.list.pathFilter());\n        queryClient.invalidateQueries(\n          api.tags.get.queryFilter({ tagId: res.id }),\n        );\n        queryClient.invalidateQueries(\n          api.bookmarks.getBookmarks.queryFilter({ tagId: res.id }),\n        );\n\n        // TODO: Maybe we can only look at the cache and invalidate only affected bookmarks\n        queryClient.invalidateQueries(api.bookmarks.getBookmark.pathFilter());\n        return opts?.onSuccess?.(res, req, meta, context);\n      },\n    }),\n  );\n}\n\nexport function useMergeTag(\n  opts?: Parameters<TRPCApi[\"tags\"][\"merge\"][\"mutationOptions\"]>[0],\n) {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n\n  return useMutation(\n    api.tags.merge.mutationOptions({\n      ...opts,\n      onSuccess: (res, req, meta, context) => {\n        queryClient.invalidateQueries(api.tags.list.pathFilter());\n        [res.mergedIntoTagId, ...res.deletedTags].forEach((tagId) => {\n          queryClient.invalidateQueries(api.tags.get.queryFilter({ tagId }));\n          queryClient.invalidateQueries(\n            api.bookmarks.getBookmarks.queryFilter({ tagId }),\n          );\n        });\n        // TODO: Maybe we can only look at the cache and invalidate only affected bookmarks\n        queryClient.invalidateQueries(api.bookmarks.getBookmark.pathFilter());\n        return opts?.onSuccess?.(res, req, meta, context);\n      },\n    }),\n  );\n}\n\nexport function useDeleteTag(\n  opts?: Parameters<TRPCApi[\"tags\"][\"delete\"][\"mutationOptions\"]>[0],\n) {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n\n  return useMutation(\n    api.tags.delete.mutationOptions({\n      ...opts,\n      onSuccess: (res, req, meta, context) => {\n        queryClient.invalidateQueries(api.tags.list.pathFilter());\n        queryClient.invalidateQueries(api.bookmarks.getBookmark.pathFilter());\n        return opts?.onSuccess?.(res, req, meta, context);\n      },\n    }),\n  );\n}\n\nexport function useDeleteUnusedTags(\n  opts?: Parameters<TRPCApi[\"tags\"][\"deleteUnused\"][\"mutationOptions\"]>[0],\n) {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n\n  return useMutation(\n    api.tags.deleteUnused.mutationOptions({\n      ...opts,\n      onSuccess: (res, req, meta, context) => {\n        queryClient.invalidateQueries(api.tags.list.pathFilter());\n        return opts?.onSuccess?.(res, req, meta, context);\n      },\n    }),\n  );\n}\n"
  },
  {
    "path": "packages/shared-react/hooks/use-debounce.ts",
    "content": "import React from \"react\";\n\nexport function useDebounce<T>(value: T, delayMs: number): T {\n  const [debouncedValue, setDebouncedValue] = React.useState<T>(value);\n\n  React.useEffect(() => {\n    const handler = setTimeout(() => {\n      setDebouncedValue(value);\n    }, delayMs);\n\n    return () => {\n      clearTimeout(handler);\n    };\n  }, [value, delayMs]);\n\n  return debouncedValue;\n}\n"
  },
  {
    "path": "packages/shared-react/hooks/users.ts",
    "content": "import { useMutation, useQuery, useQueryClient } from \"@tanstack/react-query\";\n\nimport { useTRPC } from \"../trpc\";\n\ntype TRPCApi = ReturnType<typeof useTRPC>;\n\nexport function useUpdateUserSettings(\n  opts?: Parameters<TRPCApi[\"users\"][\"updateSettings\"][\"mutationOptions\"]>[0],\n) {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n  return useMutation(\n    api.users.updateSettings.mutationOptions({\n      ...opts,\n      onSuccess: (res, req, meta, context) => {\n        queryClient.invalidateQueries(api.users.settings.pathFilter());\n        return opts?.onSuccess?.(res, req, meta, context);\n      },\n    }),\n  );\n}\n\nexport function useUpdateUserAvatar(\n  opts?: Parameters<TRPCApi[\"users\"][\"updateAvatar\"][\"mutationOptions\"]>[0],\n) {\n  const api = useTRPC();\n  const queryClient = useQueryClient();\n  return useMutation(\n    api.users.updateAvatar.mutationOptions({\n      ...opts,\n      onSuccess: (res, req, meta, context) => {\n        queryClient.invalidateQueries(api.users.whoami.pathFilter());\n        return opts?.onSuccess?.(res, req, meta, context);\n      },\n    }),\n  );\n}\n\nexport function useDeleteAccount(\n  opts?: Parameters<TRPCApi[\"users\"][\"deleteAccount\"][\"mutationOptions\"]>[0],\n) {\n  const api = useTRPC();\n  return useMutation(api.users.deleteAccount.mutationOptions(opts));\n}\n\nexport function useWhoAmI() {\n  const api = useTRPC();\n  return useQuery(api.users.whoami.queryOptions());\n}\n"
  },
  {
    "path": "packages/shared-react/lib/utils.ts",
    "content": "import type { ClassValue } from \"clsx\";\nimport { clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
  },
  {
    "path": "packages/shared-react/package.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/package.json\",\n  \"name\": \"@karakeep/shared-react\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"@karakeep/shared\": \"workspace:^0.1.0\",\n    \"@karakeep/trpc\": \"workspace:^0.1.0\",\n    \"@radix-ui/react-popover\": \"^1.1.14\",\n    \"@radix-ui/react-slot\": \"^1.2.4\",\n    \"@tanstack/react-query\": \"5.90.2\",\n    \"@trpc/client\": \"^11.9.0\",\n    \"@trpc/tanstack-react-query\": \"^11.9.0\",\n    \"class-variance-authority\": \"^0.7.0\",\n    \"clsx\": \"^2.1.0\",\n    \"lucide-react\": \"^0.501.0\",\n    \"superjson\": \"^2.2.1\",\n    \"tailwind-merge\": \"^2.2.1\"\n  },\n  \"devDependencies\": {\n    \"@karakeep/tsconfig\": \"workspace:^0.1.0\"\n  },\n  \"peerDependencies\": {\n    \"react\": \"^19.2.1\",\n    \"react-dom\": \"^19.2.1\",\n    \"react-native\": \"0.79.5\"\n  },\n  \"peerDependenciesMeta\": {\n    \"react-dom\": {\n      \"optional\": true\n    },\n    \"react-native\": {\n      \"optional\": true\n    }\n  },\n  \"scripts\": {\n    \"typecheck\": \"tsc --noEmit\",\n    \"format\": \"oxfmt --check .\",\n    \"format:fix\": \"oxfmt .\",\n    \"lint\": \"oxlint .\",\n    \"lint:fix\": \"oxlint . --fix\"\n  }\n}\n"
  },
  {
    "path": "packages/shared-react/providers/trpc-provider.tsx",
    "content": "import { useMemo } from \"react\";\nimport { QueryClient, QueryClientProvider } from \"@tanstack/react-query\";\nimport { createTRPCClient, httpBatchLink } from \"@trpc/client\";\nimport superjson from \"superjson\";\n\nimport type { AppRouter } from \"@karakeep/trpc/routers/_app\";\n\nimport { TRPC_MAX_URL_LENGTH_EXTERNAL, TRPCProvider } from \"../trpc\";\n\ninterface Settings {\n  apiKey?: string;\n  address: string;\n  customHeaders?: Record<string, string>;\n}\n\nlet browserQueryClient: QueryClient | undefined = undefined;\n\nfunction makeQueryClient() {\n  return new QueryClient({\n    defaultOptions: {\n      queries: {\n        staleTime: 60_000,\n      },\n    },\n  });\n}\n\nfunction getQueryClient() {\n  if (typeof window === \"undefined\") {\n    // Server: always make a new query client\n    return makeQueryClient();\n  } else {\n    // Browser: make a new query client if we don't already have one\n    // This is very important, so we don't re-make a new client if React\n    // suspends during the initial render. This may not be needed if we\n    // have a suspense boundary BELOW the creation of the query client\n    if (!browserQueryClient) browserQueryClient = makeQueryClient();\n    return browserQueryClient;\n  }\n}\n\nfunction getTRPCClient(settings: Settings) {\n  return createTRPCClient<AppRouter>({\n    links: [\n      httpBatchLink({\n        url: `${settings.address}/api/trpc`,\n        maxURLLength: TRPC_MAX_URL_LENGTH_EXTERNAL,\n        fetch: (url, options) => {\n          const controller = new AbortController();\n          const timeout = setTimeout(() => controller.abort(), 30_000);\n\n          // Forward any existing abort signal from tRPC / React Query\n          const externalSignal = options?.signal as AbortSignal | undefined;\n          let onAbort: (() => void) | undefined;\n          if (externalSignal) {\n            if (externalSignal.aborted) {\n              controller.abort(externalSignal.reason);\n            } else {\n              onAbort = () => controller.abort(externalSignal.reason);\n              externalSignal.addEventListener(\"abort\", onAbort);\n            }\n          }\n\n          return fetch(url, {\n            ...options,\n            signal: controller.signal,\n          }).then(\n            (response) => {\n              clearTimeout(timeout);\n              if (onAbort)\n                externalSignal!.removeEventListener(\"abort\", onAbort);\n              return response;\n            },\n            (error) => {\n              clearTimeout(timeout);\n              if (onAbort)\n                externalSignal!.removeEventListener(\"abort\", onAbort);\n              throw error;\n            },\n          );\n        },\n        headers() {\n          return {\n            Authorization: settings.apiKey\n              ? `Bearer ${settings.apiKey}`\n              : undefined,\n            ...settings.customHeaders,\n          };\n        },\n        transformer: superjson,\n      }),\n    ],\n  });\n}\n\nexport function TRPCSettingsProvider({\n  settings,\n  children,\n}: {\n  settings: Settings;\n  children: React.ReactNode;\n}) {\n  const queryClient = getQueryClient();\n  const trpcClient = useMemo(() => getTRPCClient(settings), [settings]);\n\n  return (\n    <QueryClientProvider client={queryClient}>\n      <TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>\n        {children}\n      </TRPCProvider>\n    </QueryClientProvider>\n  );\n}\n"
  },
  {
    "path": "packages/shared-react/trpc.ts",
    "content": "\"use client\";\n\nimport { createTRPCContext } from \"@trpc/tanstack-react-query\";\n\nimport type { AppRouter } from \"@karakeep/trpc/routers/_app\";\n\nexport const { TRPCProvider, useTRPC } = createTRPCContext<AppRouter>();\n\nexport {\n  TRPC_MAX_URL_LENGTH_EXTERNAL,\n  TRPC_MAX_URL_LENGTH_INTERNAL,\n} from \"@karakeep/shared/trpc\";\n"
  },
  {
    "path": "packages/shared-react/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@karakeep/tsconfig/base.json\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"plugins\": [],\n    \"paths\": {\n      \"@/*\": [\"./*\"]\n    },\n    \"tsBuildInfoFile\": \"node_modules/.cache/tsbuildinfo.json\"\n  },\n  \"include\": [\"**/*.ts\", \"**/*.tsx\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "packages/shared-server/.oxlintrc.json",
    "content": "{\n  \"$schema\": \"../../node_modules/oxlint/configuration_schema.json\",\n  \"extends\": [\n    \"../../tooling/oxlint/oxlint-base.json\"\n  ],\n  \"env\": {\n    \"builtin\": true,\n    \"commonjs\": true\n  },\n  \"ignorePatterns\": [\n    \"**/*.config.js\",\n    \"**/*.config.cjs\",\n    \"**/.eslintrc.cjs\",\n    \"**/.next\",\n    \"**/dist\",\n    \"**/build\",\n    \"**/pnpm-lock.yaml\"\n  ]\n}\n"
  },
  {
    "path": "packages/shared-server/index.ts",
    "content": "export * from \"./src\";\n"
  },
  {
    "path": "packages/shared-server/package.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/package.json\",\n  \"name\": \"@karakeep/shared-server\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"dependencies\": {\n    \"@karakeep/db\": \"workspace:^0.1.0\",\n    \"@karakeep/plugins\": \"workspace:^0.1.0\",\n    \"@karakeep/shared\": \"workspace:^0.1.0\",\n    \"@opentelemetry/api\": \"^1.9.0\",\n    \"@opentelemetry/exporter-trace-otlp-http\": \"^0.208.0\",\n    \"@opentelemetry/resources\": \"^2.2.0\",\n    \"@opentelemetry/sdk-trace-base\": \"^2.2.0\",\n    \"@opentelemetry/sdk-trace-node\": \"^2.2.0\",\n    \"@opentelemetry/semantic-conventions\": \"^1.38.0\"\n  },\n  \"devDependencies\": {\n    \"@karakeep/tsconfig\": \"workspace:^0.1.0\"\n  },\n  \"scripts\": {\n    \"typecheck\": \"tsc --noEmit\",\n    \"format\": \"oxfmt --check .\",\n    \"format:fix\": \"oxfmt .\",\n    \"lint\": \"oxlint .\",\n    \"lint:fix\": \"oxlint . --fix\",\n    \"test\": \"vitest\"\n  },\n  \"main\": \"index.ts\",\n  \"exports\": {\n    \".\": \"./index.ts\"\n  }\n}\n"
  },
  {
    "path": "packages/shared-server/src/index.ts",
    "content": "export { loadAllPlugins } from \"./plugins\";\nexport { QuotaService, StorageQuotaError } from \"./services/quotaService\";\nexport * from \"./queues\";\nexport * from \"./tracing\";\n"
  },
  {
    "path": "packages/shared-server/src/plugins.ts",
    "content": "import { PluginManager } from \"@karakeep/shared/plugins\";\n\nlet pluginsLoaded = false;\nexport async function loadAllPlugins() {\n  if (pluginsLoaded) {\n    return;\n  }\n  // Load plugins here. Order of plugin loading matter.\n  // Queue provider(s)\n  await import(\"@karakeep/plugins/queue-liteque\");\n  await import(\"@karakeep/plugins/queue-restate\");\n  await import(\"@karakeep/plugins/search-meilisearch\");\n  // Rate limiters (order matters - last one wins)\n  await import(\"@karakeep/plugins/ratelimit-memory\");\n  await import(\"@karakeep/plugins/ratelimit-redis\");\n  PluginManager.logAllPlugins();\n  pluginsLoaded = true;\n}\n"
  },
  {
    "path": "packages/shared-server/src/queues.ts",
    "content": "import { z } from \"zod\";\n\nimport {\n  EnqueueOptions,\n  getQueueClient,\n  Queue,\n  QueueClient,\n  QueueOptions,\n} from \"@karakeep/shared/queueing\";\nimport { zRuleEngineEventSchema } from \"@karakeep/shared/types/rules\";\n\nimport { loadAllPlugins } from \".\";\n\nexport enum QueuePriority {\n  Low = 50,\n  Default = 0,\n}\n\n// Lazy client initialization - plugins are loaded on first access\n// We cache the promise to ensure only one initialization happens even with concurrent calls\nlet clientPromise: Promise<QueueClient> | null = null;\n\nfunction getClient(): Promise<QueueClient> {\n  if (!clientPromise) {\n    clientPromise = (async () => {\n      await loadAllPlugins();\n      return await getQueueClient();\n    })();\n  }\n  return clientPromise;\n}\n\n/**\n * Creates a deferred queue that initializes lazily on first use.\n * This allows the module to be imported without requiring plugins to be loaded.\n */\nfunction createDeferredQueue<T>(name: string, options: QueueOptions): Queue<T> {\n  // Cache the promise to ensure only one queue is created even with concurrent calls\n  let queuePromise: Promise<Queue<T>> | null = null;\n\n  const ensureQueue = (): Promise<Queue<T>> => {\n    if (!queuePromise) {\n      queuePromise = (async () => {\n        const client = await getClient();\n        return client.createQueue<T>(name, options);\n      })();\n    }\n    return queuePromise;\n  };\n\n  return {\n    opts: options,\n    name: () => name,\n    ensureInit: async () => {\n      await ensureQueue();\n    },\n    async enqueue(payload: T, opts?: EnqueueOptions) {\n      return (await ensureQueue()).enqueue(payload, opts);\n    },\n    async stats() {\n      return (await ensureQueue()).stats();\n    },\n    async cancelAllNonRunning() {\n      const q = await ensureQueue();\n      return q.cancelAllNonRunning?.() ?? 0;\n    },\n  };\n}\n\nexport async function prepareQueue() {\n  const client = await getClient();\n  await client.prepare();\n}\n\nexport async function startQueue() {\n  const client = await getClient();\n  await client.start();\n}\n\n// Link Crawler\nexport const zCrawlLinkRequestSchema = z.object({\n  bookmarkId: z.string(),\n  runInference: z.boolean().optional(),\n  archiveFullPage: z.boolean().optional().default(false),\n  storePdf: z.boolean().optional().default(false),\n});\nexport type ZCrawlLinkRequest = z.input<typeof zCrawlLinkRequestSchema>;\n\nexport const LinkCrawlerQueue = createDeferredQueue<ZCrawlLinkRequest>(\n  \"link_crawler_queue\",\n  {\n    defaultJobArgs: {\n      numRetries: 5,\n    },\n    keepFailedJobs: false,\n  },\n);\n\n// Separate queue for low priority link crawling (e.g. imports)\n// This prevents low priority crawling from impacting the parallelism of the main queue\nexport const LowPriorityCrawlerQueue = createDeferredQueue<ZCrawlLinkRequest>(\n  \"low_priority_crawler_queue\",\n  {\n    defaultJobArgs: {\n      numRetries: 5,\n    },\n    keepFailedJobs: false,\n  },\n);\n\n// Inference Worker\nexport const zOpenAIRequestSchema = z.object({\n  bookmarkId: z.string(),\n  type: z.enum([\"summarize\", \"tag\"]).default(\"tag\"),\n});\nexport type ZOpenAIRequest = z.infer<typeof zOpenAIRequestSchema>;\n\nexport const OpenAIQueue = createDeferredQueue<ZOpenAIRequest>(\"openai_queue\", {\n  defaultJobArgs: {\n    numRetries: 3,\n  },\n  keepFailedJobs: false,\n});\n\n// Search Indexing Worker\nexport const zSearchIndexingRequestSchema = z.object({\n  bookmarkId: z.string(),\n  type: z.enum([\"index\", \"delete\"]),\n});\nexport type ZSearchIndexingRequest = z.infer<\n  typeof zSearchIndexingRequestSchema\n>;\nexport const SearchIndexingQueue = createDeferredQueue<ZSearchIndexingRequest>(\n  \"searching_indexing\",\n  {\n    defaultJobArgs: {\n      numRetries: 5,\n    },\n    keepFailedJobs: false,\n  },\n);\n\n// Admin maintenance worker\nexport const zTidyAssetsRequestSchema = z.object({\n  cleanDanglingAssets: z.boolean().optional().default(false),\n  syncAssetMetadata: z.boolean().optional().default(false),\n});\nexport type ZTidyAssetsRequest = z.infer<typeof zTidyAssetsRequestSchema>;\n\nexport const zAdminMaintenanceTaskSchema = z.discriminatedUnion(\"type\", [\n  z.object({\n    type: z.literal(\"tidy_assets\"),\n    args: zTidyAssetsRequestSchema,\n  }),\n  z.object({\n    type: z.literal(\"migrate_large_link_html\"),\n  }),\n]);\n\nexport type ZAdminMaintenanceTask = z.infer<typeof zAdminMaintenanceTaskSchema>;\nexport type ZAdminMaintenanceTaskType = ZAdminMaintenanceTask[\"type\"];\nexport type ZAdminMaintenanceTidyAssetsTask = Extract<\n  ZAdminMaintenanceTask,\n  { type: \"tidy_assets\" }\n>;\nexport type ZAdminMaintenanceMigrateLargeLinkHtmlTask = Extract<\n  ZAdminMaintenanceTask,\n  { type: \"migrate_large_link_html\" }\n>;\n\nexport const AdminMaintenanceQueue = createDeferredQueue<ZAdminMaintenanceTask>(\n  \"admin_maintenance_queue\",\n  {\n    defaultJobArgs: {\n      numRetries: 1,\n    },\n    keepFailedJobs: false,\n  },\n);\n\nexport async function triggerSearchReindex(\n  bookmarkId: string,\n  opts?: Omit<EnqueueOptions, \"idempotencyKey\">,\n) {\n  await SearchIndexingQueue.enqueue(\n    {\n      bookmarkId,\n      type: \"index\",\n    },\n    {\n      ...opts,\n      idempotencyKey: `index:${bookmarkId}`,\n    },\n  );\n}\n\nexport const zvideoRequestSchema = z.object({\n  bookmarkId: z.string(),\n  url: z.string(),\n});\nexport type ZVideoRequest = z.infer<typeof zvideoRequestSchema>;\n\nexport const VideoWorkerQueue = createDeferredQueue<ZVideoRequest>(\n  \"video_queue\",\n  {\n    defaultJobArgs: {\n      numRetries: 5,\n    },\n    keepFailedJobs: false,\n  },\n);\n\n// Feed Worker\nexport const zFeedRequestSchema = z.object({\n  feedId: z.string(),\n});\nexport type ZFeedRequestSchema = z.infer<typeof zFeedRequestSchema>;\n\nexport const FeedQueue = createDeferredQueue<ZFeedRequestSchema>(\"feed_queue\", {\n  defaultJobArgs: {\n    // One retry is enough for the feed queue given that it's periodic\n    numRetries: 1,\n  },\n  keepFailedJobs: false,\n});\n\n// Preprocess Assets\nexport const zAssetPreprocessingRequestSchema = z.object({\n  bookmarkId: z.string(),\n  fixMode: z.boolean().optional().default(false),\n});\nexport type AssetPreprocessingRequest = z.infer<\n  typeof zAssetPreprocessingRequestSchema\n>;\nexport const AssetPreprocessingQueue =\n  createDeferredQueue<AssetPreprocessingRequest>(\"asset_preprocessing_queue\", {\n    defaultJobArgs: {\n      numRetries: 2,\n    },\n    keepFailedJobs: false,\n  });\n\n// Webhook worker\nexport const zWebhookRequestSchema = z.object({\n  bookmarkId: z.string(),\n  operation: z.enum([\"crawled\", \"created\", \"edited\", \"ai tagged\", \"deleted\"]),\n  userId: z.string().optional(),\n});\nexport type ZWebhookRequest = z.infer<typeof zWebhookRequestSchema>;\nexport const WebhookQueue = createDeferredQueue<ZWebhookRequest>(\n  \"webhook_queue\",\n  {\n    defaultJobArgs: {\n      numRetries: 3,\n    },\n    keepFailedJobs: false,\n  },\n);\n\nexport async function triggerWebhook(\n  bookmarkId: string,\n  operation: ZWebhookRequest[\"operation\"],\n  userId?: string,\n  opts?: EnqueueOptions,\n) {\n  await WebhookQueue.enqueue(\n    {\n      bookmarkId,\n      userId,\n      operation,\n    },\n    opts,\n  );\n}\n\n// RuleEngine worker\nexport const zRuleEngineRequestSchema = z.object({\n  bookmarkId: z.string(),\n  events: z.array(zRuleEngineEventSchema),\n});\nexport type ZRuleEngineRequest = z.infer<typeof zRuleEngineRequestSchema>;\nexport const RuleEngineQueue = createDeferredQueue<ZRuleEngineRequest>(\n  \"rule_engine_queue\",\n  {\n    defaultJobArgs: {\n      numRetries: 1,\n    },\n    keepFailedJobs: false,\n  },\n);\n\nexport async function triggerRuleEngineOnEvent(\n  bookmarkId: string,\n  events: z.infer<typeof zRuleEngineEventSchema>[],\n  opts?: EnqueueOptions,\n) {\n  await RuleEngineQueue.enqueue(\n    {\n      events,\n      bookmarkId,\n    },\n    opts,\n  );\n}\n\n// Backup worker\nexport const zBackupRequestSchema = z.object({\n  userId: z.string(),\n  backupId: z.string().optional(),\n});\nexport type ZBackupRequest = z.infer<typeof zBackupRequestSchema>;\nexport const BackupQueue = createDeferredQueue<ZBackupRequest>(\"backup_queue\", {\n  defaultJobArgs: {\n    numRetries: 2,\n  },\n  keepFailedJobs: false,\n});\n"
  },
  {
    "path": "packages/shared-server/src/services/quotaService.ts",
    "content": "import { count, eq, sum } from \"drizzle-orm\";\n\nimport type { DB, KarakeepDBTransaction } from \"@karakeep/db\";\nimport { assets, bookmarks, users } from \"@karakeep/db/schema\";\nimport { QuotaApproved } from \"@karakeep/shared/storageQuota\";\n\nexport class StorageQuotaError extends Error {\n  constructor(\n    public readonly currentUsage: number,\n    public readonly quota: number,\n    public readonly requestedSize: number,\n  ) {\n    super(\n      `Storage quota exceeded. Current usage: ${Math.round(currentUsage / 1024 / 1024)}MB, Quota: ${Math.round(quota / 1024 / 1024)}MB, Requested: ${Math.round(requestedSize / 1024 / 1024)}MB`,\n    );\n    this.name = \"StorageQuotaError\";\n  }\n}\n\n// TODO: Change the API of this class to either return a boolean\n// or throw an exception on lack of quota because now, it's inconsistent.\nexport class QuotaService {\n  // TODO: Use quota approval tokens for bookmark creation when\n  // bookmark creation logic is in the model.\n  static async canCreateBookmark(\n    db: DB | KarakeepDBTransaction,\n    userId: string,\n  ) {\n    const user = await db.query.users.findFirst({\n      where: eq(users.id, userId),\n      columns: {\n        bookmarkQuota: true,\n      },\n    });\n\n    if (user?.bookmarkQuota !== null && user?.bookmarkQuota !== undefined) {\n      const currentBookmarkCount = await db\n        .select({ count: count() })\n        .from(bookmarks)\n        .where(eq(bookmarks.userId, userId));\n\n      if (currentBookmarkCount[0].count >= user.bookmarkQuota) {\n        return {\n          result: false,\n          error: `Bookmark quota exceeded. You can only have ${user.bookmarkQuota} bookmarks.`,\n        } as const;\n      }\n    }\n    return {\n      result: true,\n    } as const;\n  }\n\n  static async checkStorageQuota(\n    db: DB | KarakeepDBTransaction,\n    userId: string,\n    requestedSize: number,\n  ): Promise<QuotaApproved> {\n    const user = await db.query.users.findFirst({\n      where: eq(users.id, userId),\n      columns: {\n        storageQuota: true,\n      },\n    });\n\n    if (user?.storageQuota === null || user?.storageQuota === undefined) {\n      // No quota limit - approve the request\n      return QuotaApproved._create(userId, requestedSize);\n    }\n\n    const currentUsage = await this.getCurrentStorageUsage(db, userId);\n\n    if (currentUsage + requestedSize > user.storageQuota) {\n      throw new StorageQuotaError(\n        currentUsage,\n        user.storageQuota,\n        requestedSize,\n      );\n    }\n\n    // Quota check passed - return approval token\n    return QuotaApproved._create(userId, requestedSize);\n  }\n\n  static async getCurrentStorageUsage(\n    db: DB | KarakeepDBTransaction,\n    userId: string,\n  ): Promise<number> {\n    const currentUsageResult = await db\n      .select({ totalSize: sum(assets.size) })\n      .from(assets)\n      .where(eq(assets.userId, userId));\n\n    return Number(currentUsageResult[0]?.totalSize ?? 0);\n  }\n}\n"
  },
  {
    "path": "packages/shared-server/src/tracing.ts",
    "content": "import type { Context, Span, Tracer } from \"@opentelemetry/api\";\nimport {\n  context,\n  propagation,\n  SpanKind,\n  SpanStatusCode,\n  trace,\n} from \"@opentelemetry/api\";\nimport { OTLPTraceExporter } from \"@opentelemetry/exporter-trace-otlp-http\";\nimport { resourceFromAttributes } from \"@opentelemetry/resources\";\nimport {\n  BatchSpanProcessor,\n  ConsoleSpanExporter,\n  ParentBasedSampler,\n  SimpleSpanProcessor,\n  TraceIdRatioBasedSampler,\n} from \"@opentelemetry/sdk-trace-base\";\nimport { NodeTracerProvider } from \"@opentelemetry/sdk-trace-node\";\nimport {\n  ATTR_SERVICE_NAME,\n  ATTR_SERVICE_VERSION,\n} from \"@opentelemetry/semantic-conventions\";\n\nimport serverConfig from \"@karakeep/shared/config\";\nimport logger from \"@karakeep/shared/logger\";\n\nimport type { TracingAttributes } from \"./tracingTypes\";\n\nexport type { TracingAttributeKey, TracingAttributes } from \"./tracingTypes\";\n\nlet tracerProvider: NodeTracerProvider | null = null;\nlet isInitialized = false;\n\n/**\n * Initialize the OpenTelemetry tracing infrastructure.\n * Should be called once at application startup.\n */\nexport function initTracing(serviceSuffix?: string): void {\n  if (isInitialized) {\n    logger.debug(\"Tracing already initialized, skipping\");\n    return;\n  }\n\n  if (!serverConfig.tracing.enabled) {\n    logger.info(\"Tracing is disabled\");\n    isInitialized = true;\n    return;\n  }\n\n  const serviceName = serviceSuffix\n    ? `${serverConfig.tracing.serviceName}-${serviceSuffix}`\n    : serverConfig.tracing.serviceName;\n\n  logger.info(`Initializing OpenTelemetry tracing for service: ${serviceName}`);\n\n  const resource = resourceFromAttributes({\n    [ATTR_SERVICE_NAME]: serviceName,\n    [ATTR_SERVICE_VERSION]: serverConfig.serverVersion ?? \"unknown\",\n  });\n\n  // Configure span processors\n  const spanProcessors = [];\n\n  if (serverConfig.tracing.otlpEndpoint) {\n    // OTLP exporter (Jaeger, Zipkin, etc.)\n    const otlpExporter = new OTLPTraceExporter({\n      url: serverConfig.tracing.otlpEndpoint,\n    });\n    spanProcessors.push(new BatchSpanProcessor(otlpExporter));\n    logger.info(\n      `OTLP exporter configured: ${serverConfig.tracing.otlpEndpoint}`,\n    );\n  } else {\n    // Fallback to console exporter for development\n    spanProcessors.push(new SimpleSpanProcessor(new ConsoleSpanExporter()));\n    logger.info(\"Console span exporter configured (no OTLP endpoint set)\");\n  }\n\n  tracerProvider = new NodeTracerProvider({\n    resource,\n    sampler: new ParentBasedSampler({\n      root: new TraceIdRatioBasedSampler(serverConfig.tracing.sampleRate),\n    }),\n    spanProcessors,\n  });\n\n  // Register the provider globally\n  tracerProvider.register();\n\n  isInitialized = true;\n  logger.info(\"OpenTelemetry tracing initialized successfully\");\n}\n\n/**\n * Shutdown the tracing infrastructure gracefully.\n * Should be called on application shutdown.\n */\nexport async function shutdownTracing(): Promise<void> {\n  if (tracerProvider) {\n    await tracerProvider.shutdown();\n    logger.info(\"OpenTelemetry tracing shut down\");\n  }\n}\n\n/**\n * Get a tracer instance for creating spans.\n * @param name - The name of the tracer (typically the module/component name)\n */\nexport function getTracer(name: string): Tracer {\n  return trace.getTracer(name);\n}\n\n/**\n * Get the currently active span, if any.\n */\nexport function getActiveSpan(): Span | undefined {\n  return trace.getActiveSpan();\n}\n\n/**\n * Get the current trace context.\n */\nexport function getActiveContext(): Context {\n  return context.active();\n}\n\n/**\n * Execute a function within a new span.\n * Automatically handles error recording and span status.\n */\nexport async function withSpan<T>(\n  tracer: Tracer,\n  spanName: string,\n  options: {\n    kind?: SpanKind;\n    attributes?: TracingAttributes;\n  },\n  fn: (span: Span) => Promise<T>,\n): Promise<T> {\n  return tracer.startActiveSpan(\n    spanName,\n    {\n      kind: options.kind ?? SpanKind.INTERNAL,\n      attributes: options.attributes,\n    },\n    async (span) => {\n      try {\n        const result = await fn(span);\n        span.setStatus({ code: SpanStatusCode.OK });\n        return result;\n      } catch (error) {\n        span.setStatus({\n          code: SpanStatusCode.ERROR,\n          message: error instanceof Error ? error.message : String(error),\n        });\n        span.recordException(\n          error instanceof Error ? error : new Error(String(error)),\n        );\n        throw error;\n      } finally {\n        span.end();\n      }\n    },\n  );\n}\n\n/**\n * Execute a synchronous function within a new span.\n */\nexport function withSpanSync<T>(\n  tracer: Tracer,\n  spanName: string,\n  options: {\n    kind?: SpanKind;\n    attributes?: TracingAttributes;\n  },\n  fn: (span: Span) => T,\n): T {\n  const span = tracer.startSpan(spanName, {\n    kind: options.kind ?? SpanKind.INTERNAL,\n    attributes: options.attributes,\n  });\n\n  try {\n    const result = context.with(trace.setSpan(context.active(), span), () =>\n      fn(span),\n    );\n    span.setStatus({ code: SpanStatusCode.OK });\n    return result;\n  } catch (error) {\n    span.setStatus({\n      code: SpanStatusCode.ERROR,\n      message: error instanceof Error ? error.message : String(error),\n    });\n    span.recordException(\n      error instanceof Error ? error : new Error(String(error)),\n    );\n    throw error;\n  } finally {\n    span.end();\n  }\n}\n\n/**\n * Add an event to the current active span.\n */\nexport function addSpanEvent(\n  name: string,\n  attributes?: Record<string, string | number | boolean>,\n): void {\n  const span = getActiveSpan();\n  if (span) {\n    span.addEvent(name, attributes);\n  }\n}\n\n/**\n * Set attributes on the current active span.\n */\nexport function setSpanAttributes(attributes: TracingAttributes): void {\n  const span = getActiveSpan();\n  if (span) {\n    span.setAttributes(attributes);\n  }\n}\n\n/**\n * Record an error on the current active span.\n */\nexport function recordSpanError(error: Error): void {\n  const span = getActiveSpan();\n  if (span) {\n    span.recordException(error);\n    span.setStatus({\n      code: SpanStatusCode.ERROR,\n      message: error.message,\n    });\n  }\n}\n\n/**\n * Extract trace context from HTTP headers (for distributed tracing).\n */\nexport function extractTraceContext(\n  headers: Record<string, string | string[] | undefined>,\n): Context {\n  const normalizedHeaders: Record<string, string> = {};\n  for (const [key, value] of Object.entries(headers)) {\n    if (value) {\n      normalizedHeaders[key] = Array.isArray(value) ? value[0] : value;\n    }\n  }\n  return propagation.extract(context.active(), normalizedHeaders);\n}\n\n/**\n * Inject trace context into HTTP headers (for distributed tracing).\n */\nexport function injectTraceContext(\n  headers: Record<string, string>,\n): Record<string, string> {\n  propagation.inject(context.active(), headers);\n  return headers;\n}\n\n/**\n * Run a function within a specific context.\n */\nexport function runWithContext<T>(ctx: Context, fn: () => T): T {\n  return context.with(ctx, fn);\n}\n\n// Re-export commonly used types and constants\nexport { SpanKind, SpanStatusCode } from \"@opentelemetry/api\";\n"
  },
  {
    "path": "packages/shared-server/src/tracingTypes.ts",
    "content": "export type TracingAttributeKey =\n  // User attributes\n  | \"user.id\"\n  | \"user.role\"\n  | \"user.tier\"\n  // RPC attributes\n  | \"rpc.system\"\n  | \"rpc.method\"\n  | \"rpc.type\"\n  // Job attributes\n  | \"job.id\"\n  | \"job.priority\"\n  | \"job.runNumber\"\n  | \"job.groupId\"\n  // Bookmark attributes\n  | \"bookmark.id\"\n  | \"bookmark.url\"\n  | \"bookmark.domain\"\n  | \"bookmark.content.size\"\n  | \"bookmark.content.type\"\n  // Asset attributes\n  | \"asset.id\"\n  | \"asset.type\"\n  | \"asset.size\"\n  // Crawler-specific attributes\n  | \"crawler.forceStorePdf\"\n  | \"crawler.archiveFullPage\"\n  | \"crawler.hasPrecrawledArchive\"\n  | \"crawler.getContentType.statusCode\"\n  | \"crawler.contentType\"\n  | \"crawler.statusCode\"\n  | \"crawler.cleanup.hasPage\"\n  | \"crawler.cleanup.pageClosed\"\n  | \"crawler.cleanup.contextClosed\"\n  // Database attributes\n  | \"db.system\"\n  | \"db.statement\"\n  | \"db.operation\"\n  // Inference-specific attributes\n  | \"inference.tagging.numGeneratedTags\"\n  | \"inference.tagging.style\"\n  | \"inference.summary.size\"\n  | \"inference.lang\"\n  | \"inference.prompt.size\"\n  | \"inference.prompt.customCount\"\n  | \"inference.totalTokens\"\n  | \"inference.model\"\n  | \"inference.type\";\n\nexport type TracingAttributes = Partial<\n  Record<TracingAttributeKey, string | number | boolean>\n>;\n"
  },
  {
    "path": "packages/shared-server/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@karakeep/tsconfig/node.json\",\n  \"include\": [\"src/**/*.ts\"],\n  \"exclude\": [\"node_modules\"],\n  \"compilerOptions\": {\n    \"rootDir\": \"src/\",\n    \"tsBuildInfoFile\": \"node_modules/.cache/tsbuildinfo.json\"\n  },\n}\n"
  },
  {
    "path": "packages/trpc/.oxlintrc.json",
    "content": "{\n  \"$schema\": \"../../node_modules/oxlint/configuration_schema.json\",\n  \"extends\": [\n    \"../../tooling/oxlint/oxlint-base.json\"\n  ],\n  \"env\": {\n    \"builtin\": true,\n    \"commonjs\": true\n  },\n  \"ignorePatterns\": [\n    \"**/*.config.js\",\n    \"**/*.config.cjs\",\n    \"**/.eslintrc.cjs\",\n    \"**/.next\",\n    \"**/dist\",\n    \"**/build\",\n    \"**/pnpm-lock.yaml\"\n  ]\n}\n"
  },
  {
    "path": "packages/trpc/auth.ts",
    "content": "import { createHash, randomBytes } from \"crypto\";\nimport * as bcrypt from \"bcryptjs\";\nimport { and, eq } from \"drizzle-orm\";\n\nimport { apiKeys } from \"@karakeep/db/schema\";\nimport serverConfig from \"@karakeep/shared/config\";\n\nimport type { Context } from \"./index\";\n\nconst BCRYPT_SALT_ROUNDS = 10;\nconst API_KEY_PREFIX_V1 = \"ak1\";\nconst API_KEY_PREFIX_V2 = \"ak2\";\n\nfunction generateApiKeySecret() {\n  const secret = randomBytes(16).toString(\"hex\");\n  return {\n    keyId: randomBytes(10).toString(\"hex\"),\n    secret,\n    secretHash: createHash(\"sha256\").update(secret).digest(\"base64\"),\n  };\n}\n\nexport function generatePasswordSalt() {\n  return randomBytes(32).toString(\"hex\");\n}\n\nexport async function regenerateApiKey(\n  id: string,\n  userId: string,\n  database: Context[\"db\"],\n) {\n  const { keyId, secret, secretHash } = generateApiKeySecret();\n\n  const plain = `${API_KEY_PREFIX_V2}_${keyId}_${secret}`;\n\n  const res = await database\n    .update(apiKeys)\n    .set({\n      keyId: keyId,\n      keyHash: secretHash,\n    })\n    .where(and(eq(apiKeys.id, id), eq(apiKeys.userId, userId)));\n\n  if (res.changes == 0) {\n    throw new Error(\"Failed to regenerate API key\");\n  }\n  return plain;\n}\n\nexport async function generateApiKey(\n  name: string,\n  userId: string,\n  database: Context[\"db\"],\n) {\n  const { keyId, secret, secretHash } = generateApiKeySecret();\n\n  const plain = `${API_KEY_PREFIX_V2}_${keyId}_${secret}`;\n\n  const key = (\n    await database\n      .insert(apiKeys)\n      .values({\n        name: name,\n        userId: userId,\n        keyId,\n        keyHash: secretHash,\n      })\n      .returning()\n  )[0];\n\n  return {\n    id: key.id,\n    name: key.name,\n    createdAt: key.createdAt,\n    key: plain,\n  };\n}\n\nfunction parseApiKey(plain: string) {\n  const parts = plain.split(\"_\");\n  if (parts.length != 3) {\n    throw new Error(\n      `Malformd API key. API keys should have 3 segments, found ${parts.length} instead.`,\n    );\n  }\n  if (parts[0] !== API_KEY_PREFIX_V1 && parts[0] !== API_KEY_PREFIX_V2) {\n    throw new Error(`Malformd API key. Got unexpected key prefix.`);\n  }\n  return {\n    version: parts[0] == API_KEY_PREFIX_V1 ? (1 as const) : (2 as const),\n    keyId: parts[1],\n    keySecret: parts[2],\n  };\n}\n\nexport async function authenticateApiKey(key: string, database: Context[\"db\"]) {\n  const { version, keyId, keySecret } = parseApiKey(key);\n  const apiKey = await database.query.apiKeys.findFirst({\n    where: (k, { eq }) => eq(k.keyId, keyId),\n    with: {\n      user: true,\n    },\n  });\n\n  if (!apiKey) {\n    throw new Error(\"API key not found\");\n  }\n\n  const hash = apiKey.keyHash;\n\n  let validation = false;\n  switch (version) {\n    case 1:\n      validation = await bcrypt.compare(keySecret, hash);\n      break;\n    case 2:\n      validation =\n        createHash(\"sha256\").update(keySecret).digest(\"base64\") == hash;\n      break;\n    default:\n      throw new Error(\"Invalid API Key\");\n  }\n\n  if (!validation) {\n    throw new Error(\"Invalid API Key\");\n  }\n\n  // Update lastUsedAt with 10-minute throttle to avoid excessive DB writes\n  const tenMinutesAgo = new Date(Date.now() - 10 * 60 * 1000);\n  if (!apiKey.lastUsedAt || apiKey.lastUsedAt < tenMinutesAgo) {\n    // Fire and forget - don't await to avoid blocking the auth response\n    database\n      .update(apiKeys)\n      .set({ lastUsedAt: new Date() })\n      .where(eq(apiKeys.id, apiKey.id))\n      .catch((err) => {\n        console.error(\"Failed to update API key lastUsedAt:\", err);\n      });\n  }\n\n  return apiKey.user;\n}\n\nexport async function hashPassword(password: string, salt: string | null) {\n  return await bcrypt.hash(password + (salt ?? \"\"), BCRYPT_SALT_ROUNDS);\n}\n\nexport async function validatePassword(\n  email: string,\n  password: string,\n  database: Context[\"db\"],\n) {\n  if (serverConfig.auth.disablePasswordAuth) {\n    throw new Error(\"Password authentication is currently disabled\");\n  }\n  const user = await database.query.users.findFirst({\n    where: (u, { eq }) => eq(u.email, email),\n  });\n\n  if (!user) {\n    // Run a bcrypt comparison anyways to hide the fact of whether the user exists or not (protecting against timing attacks)\n    await bcrypt.compare(\n      password +\n        \"b6bfd1e907eb40462e73986f6cd628c036dc079b101186d36d53b824af3c9d2e\",\n      \"a-dummy-password-that-should-never-match\",\n    );\n    throw new Error(\"User not found\");\n  }\n\n  if (!user.password) {\n    throw new Error(\"This user doesn't have a password defined\");\n  }\n\n  const validation = await bcrypt.compare(\n    password + (user.salt ?? \"\"),\n    user.password,\n  );\n  if (!validation) {\n    throw new Error(\"Wrong password\");\n  }\n\n  return user;\n}\n"
  },
  {
    "path": "packages/trpc/email.ts",
    "content": "import { createTransport } from \"nodemailer\";\n\nimport { getTracer, withSpan } from \"@karakeep/shared-server\";\nimport serverConfig from \"@karakeep/shared/config\";\n\nconst tracer = getTracer(\"@karakeep/trpc\");\n\nfunction buildTransporter() {\n  if (!serverConfig.email.smtp) {\n    throw new Error(\"SMTP is not configured\");\n  }\n  return createTransport({\n    host: serverConfig.email.smtp.host,\n    port: serverConfig.email.smtp.port,\n    secure: serverConfig.email.smtp.secure,\n    auth:\n      serverConfig.email.smtp.user && serverConfig.email.smtp.password\n        ? {\n            user: serverConfig.email.smtp.user,\n            pass: serverConfig.email.smtp.password,\n          }\n        : undefined,\n  });\n}\n\ntype Transporter = ReturnType<typeof buildTransporter>;\n\ntype Fn<Args extends unknown[] = unknown[]> = (\n  transport: Transporter,\n  ...args: Args\n) => Promise<void>;\n\ninterface TracingOptions {\n  silentFail?: boolean;\n}\n\nfunction withTracing<Args extends unknown[]>(\n  name: string,\n  fn: Fn<Args>,\n  options: TracingOptions = {},\n) {\n  return async (...args: Args): Promise<void> => {\n    if (options.silentFail && !serverConfig.email.smtp) {\n      return;\n    }\n    const transporter = buildTransporter();\n    await withSpan(tracer, name, {}, () => fn(transporter, ...args));\n  };\n}\n\nexport const sendVerificationEmail = withTracing(\n  \"sendVerificationEmail\",\n  async (\n    transporter: Transporter,\n    email: string,\n    name: string,\n    token: string,\n    redirectUrl?: string,\n  ) => {\n    let verificationUrl = `${serverConfig.publicUrl}/verify-email?token=${encodeURIComponent(token)}&email=${encodeURIComponent(email)}`;\n    if (redirectUrl) {\n      verificationUrl += `&redirectUrl=${encodeURIComponent(redirectUrl)}`;\n    }\n\n    const mailOptions = {\n      from: serverConfig.email.smtp!.from,\n      to: email,\n      subject: \"Verify your email address\",\n      html: `\n      <div style=\"font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;\">\n        <h2>Welcome to Karakeep, ${name}!</h2>\n        <p>Please verify your email address by clicking the link below:</p>\n        <p>\n          <a href=\"${verificationUrl}\" style=\"background-color: #007bff; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; display: inline-block;\">\n            Verify Email Address\n          </a>\n        </p>\n        <p>If the button doesn't work, you can copy and paste this link into your browser:</p>\n        <p><a href=\"${verificationUrl}\">${verificationUrl}</a></p>\n        <p>This link will expire in 24 hours.</p>\n        <p>If you didn't create an account with us, please ignore this email.</p>\n      </div>\n    `,\n      text: `\nWelcome to Karakeep, ${name}!\n\nPlease verify your email address by visiting this link:\n${verificationUrl}\n\nThis link will expire in 24 hours.\n\nIf you didn't create an account with us, please ignore this email.\n    `,\n    };\n\n    await transporter.sendMail(mailOptions);\n  },\n);\n\nexport const sendInviteEmail = withTracing(\n  \"sendInviteEmail\",\n  async (\n    transporter: Transporter,\n    email: string,\n    token: string,\n    inviterName: string,\n  ) => {\n    const inviteUrl = `${serverConfig.publicUrl}/invite/${encodeURIComponent(token)}`;\n\n    const mailOptions = {\n      from: serverConfig.email.smtp!.from,\n      to: email,\n      subject: \"You've been invited to join Karakeep\",\n      html: `\n      <div style=\"font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;\">\n        <h2>You've been invited to join Karakeep!</h2>\n        <p>${inviterName} has invited you to join Karakeep, the bookmark everything app.</p>\n        <p>Click the link below to accept your invitation and create your account:</p>\n        <p>\n          <a href=\"${inviteUrl}\" style=\"background-color: #007bff; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; display: inline-block;\">\n            Accept Invitation\n          </a>\n        </p>\n        <p>If the button doesn't work, you can copy and paste this link into your browser:</p>\n        <p><a href=\"${inviteUrl}\">${inviteUrl}</a></p>\n\n        <p>If you weren't expecting this invitation, you can safely ignore this email.</p>\n      </div>\n    `,\n      text: `\nYou've been invited to join Karakeep!\n\n${inviterName} has invited you to join Karakeep, a powerful bookmarking and content organization platform.\n\nAccept your invitation by visiting this link:\n${inviteUrl}\n\n\n\nIf you weren't expecting this invitation, you can safely ignore this email.\n    `,\n    };\n\n    await transporter.sendMail(mailOptions);\n  },\n);\n\nexport const sendPasswordResetEmail = withTracing(\n  \"sendPasswordResetEmail\",\n  async (\n    transporter: Transporter,\n    email: string,\n    name: string,\n    token: string,\n  ) => {\n    const resetUrl = `${serverConfig.publicUrl}/reset-password?token=${encodeURIComponent(token)}`;\n\n    const mailOptions = {\n      from: serverConfig.email.smtp!.from,\n      to: email,\n      subject: \"Reset your password\",\n      html: `\n      <div style=\"font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;\">\n        <h2>Password Reset Request</h2>\n        <p>Hi ${name},</p>\n        <p>You requested to reset your password for your Karakeep account. Click the link below to reset your password:</p>\n        <p>\n          <a href=\"${resetUrl}\" style=\"background-color: #007bff; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; display: inline-block;\">\n            Reset Password\n          </a>\n        </p>\n        <p>If the button doesn't work, you can copy and paste this link into your browser:</p>\n        <p><a href=\"${resetUrl}\">${resetUrl}</a></p>\n        <p>This link will expire in 1 hour.</p>\n        <p>If you didn't request a password reset, please ignore this email. Your password will remain unchanged.</p>\n      </div>\n    `,\n      text: `\nHi ${name},\n\nYou requested to reset your password for your Karakeep account. Visit this link to reset your password:\n${resetUrl}\n\nThis link will expire in 1 hour.\n\nIf you didn't request a password reset, please ignore this email. Your password will remain unchanged.\n    `,\n    };\n\n    await transporter.sendMail(mailOptions);\n  },\n);\n\nexport const sendListInvitationEmail = withTracing(\n  \"sendListInvitationEmail\",\n  async (\n    transporter: Transporter,\n    email: string,\n    inviterName: string,\n    listName: string,\n    listId: string,\n  ) => {\n    const inviteUrl = `${serverConfig.publicUrl}/dashboard/lists?pendingInvitation=${encodeURIComponent(listId)}`;\n\n    const mailOptions = {\n      from: serverConfig.email.smtp!.from,\n      to: email,\n      subject: `${inviterName} invited you to collaborate on \"${listName}\"`,\n      html: `\n      <div style=\"font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;\">\n        <h2>You've been invited to collaborate on a list!</h2>\n        <p>${inviterName} has invited you to collaborate on the list <strong>\"${listName}\"</strong> in Karakeep.</p>\n        <p>Click the link below to view and accept or decline the invitation:</p>\n        <p>\n          <a href=\"${inviteUrl}\" style=\"background-color: #007bff; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; display: inline-block;\">\n            View Invitation\n          </a>\n        </p>\n        <p>If the button doesn't work, you can copy and paste this link into your browser:</p>\n        <p><a href=\"${inviteUrl}\">${inviteUrl}</a></p>\n        <p>You can accept or decline this invitation from your Karakeep dashboard.</p>\n        <p>If you weren't expecting this invitation, you can safely ignore this email or decline it in your dashboard.</p>\n      </div>\n    `,\n      text: `\nYou've been invited to collaborate on a list!\n\n${inviterName} has invited you to collaborate on the list \"${listName}\" in Karakeep.\n\nView your invitation by visiting this link:\n${inviteUrl}\n\nYou can accept or decline this invitation from your Karakeep dashboard.\n\nIf you weren't expecting this invitation, you can safely ignore this email or decline it in your dashboard.\n    `,\n    };\n\n    await transporter.sendMail(mailOptions);\n  },\n  { silentFail: true },\n);\n"
  },
  {
    "path": "packages/trpc/index.ts",
    "content": "import { initTRPC, TRPCError } from \"@trpc/server\";\nimport superjson from \"superjson\";\nimport { ZodError } from \"zod\";\n\nimport type { db } from \"@karakeep/db\";\nimport serverConfig from \"@karakeep/shared/config\";\n\nimport { createRateLimitMiddleware } from \"./lib/rateLimit\";\nimport { createTracingMiddleware } from \"./lib/tracing\";\nimport {\n  apiErrorsTotalCounter,\n  apiRequestDurationSummary,\n  apiRequestsTotalCounter,\n} from \"./stats\";\n\ninterface User {\n  id: string;\n  name?: string | null | undefined;\n  email?: string | null | undefined;\n  role: \"admin\" | \"user\" | null;\n}\n\nexport interface Context {\n  user: User | null;\n  db: typeof db;\n  req: {\n    ip: string | null;\n  };\n}\n\nexport interface AuthedContext {\n  user: User;\n  db: typeof db;\n  req: {\n    ip: string | null;\n  };\n}\n\n// Avoid exporting the entire t-object\n// since it's not very descriptive.\n// For instance, the use of a t variable\n// is common in i18n libraries.\nconst t = initTRPC.context<Context>().create({\n  transformer: superjson,\n  errorFormatter(opts) {\n    const { shape, error } = opts;\n    apiErrorsTotalCounter.inc({\n      type: opts.type,\n      path: opts.path,\n      code: error.code,\n    });\n    return {\n      ...shape,\n      message:\n        error.code === \"INTERNAL_SERVER_ERROR\" &&\n        process.env.NODE_ENV === \"production\"\n          ? \"Internal server error\"\n          : shape.message,\n      data: {\n        ...shape.data,\n        zodError:\n          error.code === \"BAD_REQUEST\" && error.cause instanceof ZodError\n            ? error.cause.flatten()\n            : null,\n      },\n    };\n  },\n});\nexport const createCallerFactory = t.createCallerFactory;\n// Base router and procedure helpers\nexport const router = t.router;\nexport const procedure = t.procedure\n  .use(function isDemoMode(opts) {\n    if (serverConfig.demoMode && opts.type == \"mutation\") {\n      throw new TRPCError({\n        message: \"Mutations are not allowed in demo mode\",\n        code: \"FORBIDDEN\",\n      });\n    }\n    return opts.next();\n  })\n  .use(async (opts) => {\n    const end = apiRequestDurationSummary.startTimer({\n      path: opts.path,\n      type: opts.type,\n    });\n    const res = await opts.next();\n    apiRequestsTotalCounter.inc({\n      type: opts.type,\n      path: opts.path,\n      is_error: res.ok ? 0 : 1,\n    });\n    end();\n    return res;\n  })\n  // OpenTelemetry tracing middleware\n  .use(createTracingMiddleware());\n\n// Default public procedure rate limiting\nexport const publicProcedure = procedure.use(\n  createRateLimitMiddleware({\n    name: \"globalPublic\",\n    windowMs: 60 * 1000,\n    maxRequests: 1000,\n  }),\n);\n\nexport const authedProcedure = procedure\n  // Default authed procedure rate limiting\n  .use(\n    createRateLimitMiddleware({\n      name: \"globalAuthed\",\n      windowMs: 60 * 1000,\n      maxRequests: 3000,\n    }),\n  )\n  .use(function isAuthed(opts) {\n    const user = opts.ctx.user;\n\n    if (!user?.id) {\n      throw new TRPCError({ code: \"UNAUTHORIZED\" });\n    }\n\n    return opts.next({\n      ctx: {\n        user,\n      },\n    });\n  });\n\nexport const adminProcedure = authedProcedure.use(function isAdmin(opts) {\n  const user = opts.ctx.user;\n  if (user.role != \"admin\") {\n    throw new TRPCError({ code: \"FORBIDDEN\" });\n  }\n  return opts.next(opts);\n});\n\n// Export the rate limiting middleware for use in routers\nexport { createRateLimitMiddleware } from \"./lib/rateLimit\";\n"
  },
  {
    "path": "packages/trpc/lib/__tests__/ruleEngine.test.ts",
    "content": "import { and, eq } from \"drizzle-orm\";\nimport { afterEach, beforeEach, describe, expect, it, vi } from \"vitest\";\n\nimport { getInMemoryDB } from \"@karakeep/db/drizzle\";\nimport {\n  bookmarkLinks,\n  bookmarkLists,\n  bookmarks,\n  bookmarksInLists,\n  bookmarkTags,\n  rssFeedImportsTable,\n  rssFeedsTable,\n  ruleEngineActionsTable as ruleActions,\n  ruleEngineRulesTable as rules,\n  tagsOnBookmarks,\n  users,\n} from \"@karakeep/db/schema\";\nimport { LinkCrawlerQueue } from \"@karakeep/shared-server\";\nimport { BookmarkTypes } from \"@karakeep/shared/types/bookmarks\";\nimport {\n  RuleEngineAction,\n  RuleEngineCondition,\n  RuleEngineEvent,\n  RuleEngineRule,\n} from \"@karakeep/shared/types/rules\";\n\nimport { AuthedContext } from \"../..\";\nimport { TestDB } from \"../../testUtils\";\nimport { RuleEngine } from \"../ruleEngine\";\n\n// Mock the queue\nvi.mock(\"@karakeep/shared-server\", () => ({\n  LinkCrawlerQueue: {\n    enqueue: vi.fn(),\n  },\n  triggerRuleEngineOnEvent: vi.fn(),\n}));\n\ndescribe(\"RuleEngine\", () => {\n  let db: TestDB;\n  let ctx: AuthedContext;\n  let userId: string;\n  let bookmarkId: string;\n  let linkBookmarkId: string;\n  let _textBookmarkId: string;\n  let tagId1: string;\n  let tagId2: string;\n  let feedId1: string;\n  let listId1: string;\n\n  // Helper to seed a rule\n  const seedRule = async (\n    ruleData: Omit<RuleEngineRule, \"id\"> & { userId: string },\n  ): Promise<string> => {\n    const [insertedRule] = await db\n      .insert(rules)\n      .values({\n        userId: ruleData.userId,\n        name: ruleData.name,\n        description: ruleData.description,\n        enabled: ruleData.enabled,\n        event: JSON.stringify(ruleData.event),\n        condition: JSON.stringify(ruleData.condition),\n      })\n      .returning({ id: rules.id });\n\n    await db.insert(ruleActions).values(\n      ruleData.actions.map((action) => ({\n        ruleId: insertedRule.id,\n        action: JSON.stringify(action),\n        userId: ruleData.userId,\n      })),\n    );\n    return insertedRule.id;\n  };\n\n  beforeEach(async () => {\n    vi.resetAllMocks();\n    db = getInMemoryDB(/* runMigrations */ true);\n\n    // Seed User\n    [userId] = (\n      await db\n        .insert(users)\n        .values({ name: \"Test User\", email: \"test@test.com\" })\n        .returning({ id: users.id })\n    ).map((u) => u.id);\n\n    ctx = {\n      user: { id: userId, role: \"user\" },\n      db: db, // Cast needed because TestDB might have extra test methods\n      req: { ip: null },\n    };\n\n    // Seed Tags\n    [tagId1, tagId2] = (\n      await db\n        .insert(bookmarkTags)\n        .values([\n          { name: \"Tag1\", userId },\n          { name: \"Tag2\", userId },\n        ])\n        .returning({ id: bookmarkTags.id })\n    ).map((t) => t.id);\n\n    // Seed Feed\n    [feedId1] = (\n      await db\n        .insert(rssFeedsTable)\n        .values({ name: \"Feed1\", userId, url: \"https://example.com/feed1\" })\n        .returning({ id: rssFeedsTable.id })\n    ).map((f) => f.id);\n\n    // Seed List\n    [listId1] = (\n      await db\n        .insert(bookmarkLists)\n        .values({ name: \"List1\", userId, type: \"manual\", icon: \"📚\" })\n        .returning({ id: bookmarkLists.id })\n    ).map((l) => l.id);\n\n    // Seed Bookmarks\n    [linkBookmarkId] = (\n      await db\n        .insert(bookmarks)\n        .values({\n          userId,\n          type: BookmarkTypes.LINK,\n          title: \"Example Bookmark Title\",\n          favourited: false,\n          archived: false,\n          source: \"rss\",\n        })\n        .returning({ id: bookmarks.id })\n    ).map((b) => b.id);\n    await db.insert(bookmarkLinks).values({\n      id: linkBookmarkId,\n      url: \"https://example.com/test\",\n    });\n    await db.insert(tagsOnBookmarks).values({\n      bookmarkId: linkBookmarkId,\n      tagId: tagId1,\n      attachedBy: \"human\",\n    });\n    await db.insert(rssFeedImportsTable).values({\n      bookmarkId: linkBookmarkId,\n      rssFeedId: feedId1,\n      entryId: \"entry-id\",\n    });\n\n    [_textBookmarkId] = (\n      await db\n        .insert(bookmarks)\n        .values({\n          userId,\n          type: BookmarkTypes.TEXT,\n          favourited: true,\n          archived: false,\n        })\n        .returning({ id: bookmarks.id })\n    ).map((b) => b.id);\n\n    bookmarkId = linkBookmarkId; // Default bookmark for most tests\n  });\n\n  afterEach(() => {\n    vi.restoreAllMocks();\n  });\n\n  describe(\"RuleEngine.forBookmark static method\", () => {\n    it(\"should initialize RuleEngine successfully for an existing bookmark\", async () => {\n      const engine = await RuleEngine.forBookmark(ctx, bookmarkId);\n      expect(engine).toBeInstanceOf(RuleEngine);\n    });\n\n    it(\"should return null if bookmark is not found\", async () => {\n      const engine = await RuleEngine.forBookmark(ctx, \"nonexistent-bookmark\");\n      expect(engine).toBeNull();\n    });\n\n    it(\"should load rules associated with the bookmark's user\", async () => {\n      const ruleId = await seedRule({\n        userId,\n        name: \"Test Rule\",\n        description: \"\",\n        enabled: true,\n        event: { type: \"bookmarkAdded\" },\n        condition: { type: \"urlContains\", str: \"example\" },\n        actions: [{ type: \"addTag\", tagId: tagId2 }],\n      });\n\n      const engine = (await RuleEngine.forBookmark(ctx, bookmarkId))!;\n      // @ts-expect-error Accessing private property for test verification\n      expect(engine.rules).toHaveLength(1);\n      // @ts-expect-error Accessing private property for test verification\n      expect(engine.rules[0].id).toBe(ruleId);\n    });\n  });\n\n  describe(\"doesBookmarkMatchConditions\", () => {\n    let engine: RuleEngine;\n\n    beforeEach(async () => {\n      engine = (await RuleEngine.forBookmark(ctx, bookmarkId))!;\n    });\n\n    it(\"should return true for urlContains condition\", () => {\n      const condition: RuleEngineCondition = {\n        type: \"urlContains\",\n        str: \"example.com\",\n      };\n      expect(engine.doesBookmarkMatchConditions(condition)).toBe(true);\n    });\n\n    it(\"should return false for urlContains condition mismatch\", () => {\n      const condition: RuleEngineCondition = {\n        type: \"urlContains\",\n        str: \"nonexistent\",\n      };\n      expect(engine.doesBookmarkMatchConditions(condition)).toBe(false);\n    });\n\n    it(\"should return false for urlDoesNotContain condition when URL contains string\", () => {\n      const condition: RuleEngineCondition = {\n        type: \"urlDoesNotContain\",\n        str: \"example.com\",\n      };\n      expect(engine.doesBookmarkMatchConditions(condition)).toBe(false);\n    });\n\n    it(\"should return true for urlDoesNotContain condition when URL does not contain string\", () => {\n      const condition: RuleEngineCondition = {\n        type: \"urlDoesNotContain\",\n        str: \"nonexistent\",\n      };\n      expect(engine.doesBookmarkMatchConditions(condition)).toBe(true);\n    });\n\n    it(\"should return true for titleContains condition\", () => {\n      const condition: RuleEngineCondition = {\n        type: \"titleContains\",\n        str: \"Example\",\n      };\n      expect(engine.doesBookmarkMatchConditions(condition)).toBe(true);\n    });\n\n    it(\"should return false for titleContains condition mismatch\", () => {\n      const condition: RuleEngineCondition = {\n        type: \"titleContains\",\n        str: \"nonexistent\",\n      };\n      expect(engine.doesBookmarkMatchConditions(condition)).toBe(false);\n    });\n\n    it(\"should return false for titleDoesNotContain condition when title contains string\", () => {\n      const condition: RuleEngineCondition = {\n        type: \"titleDoesNotContain\",\n        str: \"Example\",\n      };\n      expect(engine.doesBookmarkMatchConditions(condition)).toBe(false);\n    });\n\n    it(\"should return true for titleDoesNotContain condition when title does not contain string\", () => {\n      const condition: RuleEngineCondition = {\n        type: \"titleDoesNotContain\",\n        str: \"nonexistent\",\n      };\n      expect(engine.doesBookmarkMatchConditions(condition)).toBe(true);\n    });\n\n    it(\"should return true for importedFromFeed condition\", () => {\n      const condition: RuleEngineCondition = {\n        type: \"importedFromFeed\",\n        feedId: feedId1,\n      };\n      expect(engine.doesBookmarkMatchConditions(condition)).toBe(true);\n    });\n\n    it(\"should return false for importedFromFeed condition mismatch\", () => {\n      const condition: RuleEngineCondition = {\n        type: \"importedFromFeed\",\n        feedId: \"other-feed\",\n      };\n      expect(engine.doesBookmarkMatchConditions(condition)).toBe(false);\n    });\n\n    it(\"should return true for bookmarkTypeIs condition (link)\", () => {\n      const condition: RuleEngineCondition = {\n        type: \"bookmarkTypeIs\",\n        bookmarkType: BookmarkTypes.LINK,\n      };\n      expect(engine.doesBookmarkMatchConditions(condition)).toBe(true);\n    });\n\n    it(\"should return false for bookmarkTypeIs condition mismatch\", () => {\n      const condition: RuleEngineCondition = {\n        type: \"bookmarkTypeIs\",\n        bookmarkType: BookmarkTypes.TEXT,\n      };\n      expect(engine.doesBookmarkMatchConditions(condition)).toBe(false);\n    });\n\n    it(\"should return true for bookmarkSourceIs condition\", () => {\n      const condition: RuleEngineCondition = {\n        type: \"bookmarkSourceIs\",\n        source: \"rss\",\n      };\n      expect(engine.doesBookmarkMatchConditions(condition)).toBe(true);\n    });\n\n    it(\"should return false for bookmarkSourceIs condition mismatch\", () => {\n      const condition: RuleEngineCondition = {\n        type: \"bookmarkSourceIs\",\n        source: \"web\",\n      };\n      expect(engine.doesBookmarkMatchConditions(condition)).toBe(false);\n    });\n\n    it(\"should return true for hasTag condition\", () => {\n      const condition: RuleEngineCondition = { type: \"hasTag\", tagId: tagId1 };\n      expect(engine.doesBookmarkMatchConditions(condition)).toBe(true);\n    });\n\n    it(\"should return false for hasTag condition mismatch\", () => {\n      const condition: RuleEngineCondition = { type: \"hasTag\", tagId: tagId2 };\n      expect(engine.doesBookmarkMatchConditions(condition)).toBe(false);\n    });\n\n    it(\"should return false for isFavourited condition (default)\", () => {\n      const condition: RuleEngineCondition = { type: \"isFavourited\" };\n      expect(engine.doesBookmarkMatchConditions(condition)).toBe(false);\n    });\n\n    it(\"should return true for isFavourited condition when favourited\", async () => {\n      await db\n        .update(bookmarks)\n        .set({ favourited: true })\n        .where(eq(bookmarks.id, bookmarkId));\n      const updatedEngine = (await RuleEngine.forBookmark(ctx, bookmarkId))!;\n      const condition: RuleEngineCondition = { type: \"isFavourited\" };\n      expect(updatedEngine.doesBookmarkMatchConditions(condition)).toBe(true);\n    });\n\n    it(\"should return false for isArchived condition (default)\", () => {\n      const condition: RuleEngineCondition = { type: \"isArchived\" };\n      expect(engine.doesBookmarkMatchConditions(condition)).toBe(false);\n    });\n\n    it(\"should return true for isArchived condition when archived\", async () => {\n      await db\n        .update(bookmarks)\n        .set({ archived: true })\n        .where(eq(bookmarks.id, bookmarkId));\n      const updatedEngine = (await RuleEngine.forBookmark(ctx, bookmarkId))!;\n      const condition: RuleEngineCondition = { type: \"isArchived\" };\n      expect(updatedEngine.doesBookmarkMatchConditions(condition)).toBe(true);\n    });\n\n    it(\"should handle and condition (true)\", () => {\n      const condition: RuleEngineCondition = {\n        type: \"and\",\n        conditions: [\n          { type: \"urlContains\", str: \"example\" },\n          { type: \"hasTag\", tagId: tagId1 },\n        ],\n      };\n      expect(engine.doesBookmarkMatchConditions(condition)).toBe(true);\n    });\n\n    it(\"should handle and condition (false)\", () => {\n      const condition: RuleEngineCondition = {\n        type: \"and\",\n        conditions: [\n          { type: \"urlContains\", str: \"example\" },\n          { type: \"hasTag\", tagId: tagId2 }, // This one is false\n        ],\n      };\n      expect(engine.doesBookmarkMatchConditions(condition)).toBe(false);\n    });\n\n    it(\"should handle or condition (true)\", () => {\n      const condition: RuleEngineCondition = {\n        type: \"or\",\n        conditions: [\n          { type: \"urlContains\", str: \"nonexistent\" }, // false\n          { type: \"hasTag\", tagId: tagId1 }, // true\n        ],\n      };\n      expect(engine.doesBookmarkMatchConditions(condition)).toBe(true);\n    });\n\n    it(\"should handle or condition (false)\", () => {\n      const condition: RuleEngineCondition = {\n        type: \"or\",\n        conditions: [\n          { type: \"urlContains\", str: \"nonexistent\" }, // false\n          { type: \"hasTag\", tagId: tagId2 }, // false\n        ],\n      };\n      expect(engine.doesBookmarkMatchConditions(condition)).toBe(false);\n    });\n  });\n\n  describe(\"evaluateRule\", () => {\n    let ruleId: string;\n    let engine: RuleEngine;\n    let testRule: RuleEngineRule;\n\n    beforeEach(async () => {\n      const tmp = {\n        id: \"\", // Will be set after seeding\n        userId,\n        name: \"Evaluate Rule Test\",\n        description: \"\",\n        enabled: true,\n        event: { type: \"bookmarkAdded\" },\n        condition: { type: \"urlContains\", str: \"example\" },\n        actions: [{ type: \"addTag\", tagId: tagId2 }],\n      } as Omit<RuleEngineRule, \"id\"> & { userId: string };\n      ruleId = await seedRule(tmp);\n      testRule = { ...tmp, id: ruleId };\n      engine = (await RuleEngine.forBookmark(ctx, bookmarkId))!;\n    });\n\n    it(\"should evaluate rule successfully when event and conditions match\", async () => {\n      const event: RuleEngineEvent = { type: \"bookmarkAdded\" };\n      const results = await engine.evaluateRule(testRule, event);\n      expect(results).toEqual([\n        { type: \"success\", ruleId: ruleId, message: `Added tag ${tagId2}` },\n      ]);\n      // Verify action was performed\n      const tags = await db.query.tagsOnBookmarks.findMany({\n        where: eq(tagsOnBookmarks.bookmarkId, bookmarkId),\n      });\n      expect(tags.map((t) => t.tagId)).toContain(tagId2);\n    });\n\n    it(\"should return empty array if rule is disabled\", async () => {\n      await db\n        .update(rules)\n        .set({ enabled: false })\n        .where(eq(rules.id, ruleId));\n      const disabledRule = { ...testRule, enabled: false };\n      const event: RuleEngineEvent = { type: \"bookmarkAdded\" };\n      const results = await engine.evaluateRule(disabledRule, event);\n      expect(results).toEqual([]);\n    });\n\n    it(\"should return empty array if event does not match\", async () => {\n      const event: RuleEngineEvent = { type: \"favourited\" };\n      const results = await engine.evaluateRule(testRule, event);\n      expect(results).toEqual([]);\n    });\n\n    it(\"should return empty array if condition does not match\", async () => {\n      const nonMatchingRule: RuleEngineRule = {\n        ...testRule,\n        condition: { type: \"urlContains\", str: \"nonexistent\" },\n      };\n      await db\n        .update(rules)\n        .set({ condition: JSON.stringify(nonMatchingRule.condition) })\n        .where(eq(rules.id, ruleId));\n\n      const event: RuleEngineEvent = { type: \"bookmarkAdded\" };\n      const results = await engine.evaluateRule(nonMatchingRule, event);\n      expect(results).toEqual([]);\n    });\n\n    it(\"should return failure result if action fails\", async () => {\n      // Mock addBookmark to throw an error\n      const listAddBookmarkSpy = vi\n        .spyOn(RuleEngine.prototype, \"executeAction\")\n        .mockImplementation(async (action: RuleEngineAction) => {\n          if (action.type === \"addToList\") {\n            throw new Error(\"Failed to add to list\");\n          }\n          // Call original for other actions if needed, though not strictly necessary here\n          return Promise.resolve(`Action ${action.type} executed`);\n        });\n\n      const ruleWithFailingAction = {\n        ...testRule,\n        actions: [{ type: \"addToList\", listId: \"invalid-list\" } as const],\n      };\n      await db.delete(ruleActions).where(eq(ruleActions.ruleId, ruleId)); // Clear old actions\n      await db.insert(ruleActions).values({\n        ruleId: ruleId,\n        action: JSON.stringify(ruleWithFailingAction.actions[0]),\n        userId,\n      });\n\n      const event: RuleEngineEvent = { type: \"bookmarkAdded\" };\n      const results = await engine.evaluateRule(ruleWithFailingAction, event);\n\n      expect(results).toEqual([\n        {\n          type: \"failure\",\n          ruleId: ruleId,\n          message: \"Failed to add to list\",\n        },\n      ]);\n      listAddBookmarkSpy.mockRestore();\n    });\n  });\n\n  describe(\"executeAction\", () => {\n    let engine: RuleEngine;\n\n    beforeEach(async () => {\n      engine = (await RuleEngine.forBookmark(ctx, bookmarkId))!;\n    });\n\n    it(\"should execute addTag action\", async () => {\n      const action: RuleEngineAction = { type: \"addTag\", tagId: tagId2 };\n      const result = await engine.executeAction(action);\n      expect(result).toBe(`Added tag ${tagId2}`);\n      const tagLink = await db.query.tagsOnBookmarks.findFirst({\n        where: and(\n          eq(tagsOnBookmarks.bookmarkId, bookmarkId),\n          eq(tagsOnBookmarks.tagId, tagId2),\n        ),\n      });\n      expect(tagLink).toBeDefined();\n    });\n\n    it(\"should execute removeTag action\", async () => {\n      // Ensure tag exists first\n      expect(\n        await db.query.tagsOnBookmarks.findFirst({\n          where: and(\n            eq(tagsOnBookmarks.bookmarkId, bookmarkId),\n            eq(tagsOnBookmarks.tagId, tagId1),\n          ),\n        }),\n      ).toBeDefined();\n\n      const action: RuleEngineAction = { type: \"removeTag\", tagId: tagId1 };\n      const result = await engine.executeAction(action);\n      expect(result).toBe(`Removed tag ${tagId1}`);\n      const tagLink = await db.query.tagsOnBookmarks.findFirst({\n        where: and(\n          eq(tagsOnBookmarks.bookmarkId, bookmarkId),\n          eq(tagsOnBookmarks.tagId, tagId1),\n        ),\n      });\n      expect(tagLink).toBeUndefined();\n    });\n\n    it(\"should execute addToList action\", async () => {\n      const action: RuleEngineAction = { type: \"addToList\", listId: listId1 };\n      const result = await engine.executeAction(action);\n      expect(result).toBe(`Added to list ${listId1}`);\n      const listLink = await db.query.bookmarksInLists.findFirst({\n        where: and(\n          eq(bookmarksInLists.bookmarkId, bookmarkId),\n          eq(bookmarksInLists.listId, listId1),\n        ),\n      });\n      expect(listLink).toBeDefined();\n    });\n\n    it(\"should execute removeFromList action\", async () => {\n      // Add to list first\n      await db\n        .insert(bookmarksInLists)\n        .values({ bookmarkId: bookmarkId, listId: listId1 });\n      expect(\n        await db.query.bookmarksInLists.findFirst({\n          where: and(\n            eq(bookmarksInLists.bookmarkId, bookmarkId),\n            eq(bookmarksInLists.listId, listId1),\n          ),\n        }),\n      ).toBeDefined();\n\n      const action: RuleEngineAction = {\n        type: \"removeFromList\",\n        listId: listId1,\n      };\n      const result = await engine.executeAction(action);\n      expect(result).toBe(`Removed from list ${listId1}`);\n      const listLink = await db.query.bookmarksInLists.findFirst({\n        where: and(\n          eq(bookmarksInLists.bookmarkId, bookmarkId),\n          eq(bookmarksInLists.listId, listId1),\n        ),\n      });\n      expect(listLink).toBeUndefined();\n    });\n\n    it(\"should execute downloadFullPageArchive action\", async () => {\n      const action: RuleEngineAction = { type: \"downloadFullPageArchive\" };\n      const result = await engine.executeAction(action);\n      expect(result).toBe(`Enqueued full page archive`);\n      expect(LinkCrawlerQueue.enqueue).toHaveBeenCalledWith(\n        {\n          bookmarkId: bookmarkId,\n          archiveFullPage: true,\n          runInference: false,\n        },\n        {\n          groupId: userId,\n        },\n      );\n    });\n\n    it(\"should execute favouriteBookmark action\", async () => {\n      let bm = await db.query.bookmarks.findFirst({\n        where: eq(bookmarks.id, bookmarkId),\n      });\n      expect(bm?.favourited).toBe(false);\n\n      const action: RuleEngineAction = { type: \"favouriteBookmark\" };\n      const result = await engine.executeAction(action);\n      expect(result).toBe(`Marked as favourited`);\n\n      bm = await db.query.bookmarks.findFirst({\n        where: eq(bookmarks.id, bookmarkId),\n      });\n      expect(bm?.favourited).toBe(true);\n    });\n\n    it(\"should execute archiveBookmark action\", async () => {\n      let bm = await db.query.bookmarks.findFirst({\n        where: eq(bookmarks.id, bookmarkId),\n      });\n      expect(bm?.archived).toBe(false);\n\n      const action: RuleEngineAction = { type: \"archiveBookmark\" };\n      const result = await engine.executeAction(action);\n      expect(result).toBe(`Marked as archived`);\n\n      bm = await db.query.bookmarks.findFirst({\n        where: eq(bookmarks.id, bookmarkId),\n      });\n      expect(bm?.archived).toBe(true);\n    });\n  });\n\n  describe(\"onEvent\", () => {\n    let ruleMatchId: string;\n    let _ruleNoMatchConditionId: string;\n    let _ruleNoMatchEventId: string;\n    let _ruleDisabledId: string;\n    let engine: RuleEngine;\n\n    beforeEach(async () => {\n      // Rule that should match and execute\n      ruleMatchId = await seedRule({\n        userId,\n        name: \"Match Rule\",\n        description: \"\",\n        enabled: true,\n        event: { type: \"bookmarkAdded\" },\n        condition: { type: \"urlContains\", str: \"example\" },\n        actions: [{ type: \"addTag\", tagId: tagId2 }],\n      });\n\n      // Rule with non-matching condition\n      _ruleNoMatchConditionId = await seedRule({\n        userId,\n        name: \"No Match Condition Rule\",\n        description: \"\",\n        enabled: true,\n        event: { type: \"bookmarkAdded\" },\n        condition: { type: \"urlContains\", str: \"nonexistent\" },\n        actions: [{ type: \"favouriteBookmark\" }],\n      });\n\n      // Rule with non-matching event\n      _ruleNoMatchEventId = await seedRule({\n        userId,\n        name: \"No Match Event Rule\",\n        description: \"\",\n        enabled: true,\n        event: { type: \"favourited\" }, // Must match rule event type\n        condition: { type: \"urlContains\", str: \"example\" },\n        actions: [{ type: \"archiveBookmark\" }],\n      });\n\n      // Disabled rule\n      _ruleDisabledId = await seedRule({\n        userId,\n        name: \"Disabled Rule\",\n        description: \"\",\n        enabled: false, // Disabled\n        event: { type: \"bookmarkAdded\" },\n        condition: { type: \"urlContains\", str: \"example\" },\n        actions: [{ type: \"addToList\", listId: listId1 }],\n      });\n\n      engine = (await RuleEngine.forBookmark(ctx, bookmarkId))!;\n    });\n\n    it(\"should process event and return only results for matching, enabled rules\", async () => {\n      const event: RuleEngineEvent = { type: \"bookmarkAdded\" };\n      const results = await engine.onEvent(event);\n\n      expect(results).toHaveLength(1); // Only ruleMatchId should produce a result\n      expect(results[0]).toEqual({\n        type: \"success\",\n        ruleId: ruleMatchId,\n        message: `Added tag ${tagId2}`,\n      });\n\n      // Verify only the action from the matching rule was executed\n      const tags = await db.query.tagsOnBookmarks.findMany({\n        where: eq(tagsOnBookmarks.bookmarkId, bookmarkId),\n      });\n      expect(tags.map((t) => t.tagId)).toContain(tagId2); // Tag added by ruleMatchId\n\n      const bm = await db.query.bookmarks.findFirst({\n        where: eq(bookmarks.id, bookmarkId),\n      });\n      expect(bm?.favourited).toBe(false); // Action from ruleNoMatchConditionId not executed\n      expect(bm?.archived).toBe(false); // Action from ruleNoMatchEventId not executed\n\n      const listLink = await db.query.bookmarksInLists.findFirst({\n        where: and(\n          eq(bookmarksInLists.bookmarkId, bookmarkId),\n          eq(bookmarksInLists.listId, listId1),\n        ),\n      });\n      expect(listLink).toBeUndefined(); // Action from ruleDisabledId not executed\n    });\n\n    it(\"should return empty array if no rules match the event\", async () => {\n      const event: RuleEngineEvent = { type: \"tagAdded\", tagId: \"some-tag\" }; // Event that matches no rules\n      const results = await engine.onEvent(event);\n      expect(results).toEqual([]);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/trpc/lib/__tests__/search.test.ts",
    "content": "import { beforeEach, describe, expect, it } from \"vitest\";\n\nimport { getInMemoryDB } from \"@karakeep/db/drizzle\";\nimport {\n  bookmarkAssets,\n  bookmarkLinks,\n  bookmarkLists,\n  bookmarks,\n  bookmarksInLists,\n  bookmarkTags,\n  bookmarkTexts,\n  rssFeedImportsTable,\n  rssFeedsTable,\n  tagsOnBookmarks,\n  users,\n} from \"@karakeep/db/schema\";\nimport { BookmarkTypes } from \"@karakeep/shared/types/bookmarks\";\nimport { Matcher } from \"@karakeep/shared/types/search\";\n\nimport { AuthedContext } from \"../..\";\nimport { getBookmarkIdsFromMatcher } from \"../search\";\n\nlet mockCtx: AuthedContext;\nlet testUserId: string;\n\nbeforeEach(async () => {\n  const db = getInMemoryDB(true);\n  testUserId = \"test-user\";\n\n  await db.insert(users).values([\n    {\n      id: testUserId,\n      name: \"Test User\",\n      email: \"test@example.com\",\n      role: \"user\",\n    },\n  ]);\n\n  // Setup test data\n  await db.insert(bookmarks).values([\n    {\n      id: \"b1\",\n      type: BookmarkTypes.LINK,\n      userId: testUserId,\n      archived: false,\n      favourited: false,\n      createdAt: new Date(\"2024-01-01\"),\n      title: null,\n    },\n    {\n      id: \"b2\",\n      type: BookmarkTypes.LINK,\n      userId: testUserId,\n      archived: true,\n      favourited: true,\n      createdAt: new Date(\"2024-01-02\"),\n      title: \"example domain page\",\n    },\n    {\n      id: \"b3\",\n      type: BookmarkTypes.TEXT,\n      userId: testUserId,\n      archived: true,\n      favourited: false,\n      createdAt: new Date(\"2024-01-03\"),\n      title: \"third bookmark\",\n    },\n    {\n      id: \"b4\",\n      type: BookmarkTypes.LINK,\n      userId: testUserId,\n      archived: false,\n      favourited: true,\n      createdAt: new Date(\"2024-01-04\"),\n      title: \"another example page\",\n    },\n    {\n      id: \"b5\",\n      type: BookmarkTypes.TEXT,\n      userId: testUserId,\n      archived: false,\n      favourited: false,\n      createdAt: new Date(\"2024-01-05\"),\n      title: \"fifth text\",\n    },\n    {\n      id: \"b6\",\n      type: BookmarkTypes.ASSET,\n      userId: testUserId,\n      archived: true,\n      favourited: false,\n      createdAt: new Date(\"2024-01-06\"),\n      title: \"example asset\",\n    },\n  ]);\n\n  await db.insert(bookmarkLinks).values([\n    { id: \"b1\", url: \"https://example.com/page1\", title: \"example link\" },\n    { id: \"b2\", url: \"https://test.com/page2\" },\n    { id: \"b4\", url: \"https://example.com/page3\" },\n  ]);\n\n  await db.insert(bookmarkTexts).values([\n    {\n      id: \"b3\",\n      text: \"This is a test bookmark\",\n      sourceUrl: \"https://example.com/page1\",\n    },\n    {\n      id: \"b5\",\n      text: \"Another text bookmark\",\n      sourceUrl: null,\n    },\n  ]);\n\n  await db.insert(bookmarkAssets).values([\n    {\n      id: \"b6\",\n      assetType: \"image\",\n      fileName: \"test.png\",\n      assetId: \"asset-id\",\n      sourceUrl: \"https://example.com/image.png\",\n    },\n  ]);\n\n  await db.insert(bookmarkTags).values([\n    { id: \"t1\", userId: testUserId, name: \"tag1\" },\n    { id: \"t2\", userId: testUserId, name: \"tag2\" },\n    { id: \"t3\", userId: testUserId, name: \"important\" },\n    { id: \"t4\", userId: testUserId, name: \"work\" },\n  ]);\n\n  await db.insert(tagsOnBookmarks).values([\n    { bookmarkId: \"b1\", tagId: \"t1\", attachedBy: \"ai\" },\n    { bookmarkId: \"b2\", tagId: \"t2\", attachedBy: \"ai\" },\n    { bookmarkId: \"b4\", tagId: \"t3\", attachedBy: \"human\" },\n    { bookmarkId: \"b5\", tagId: \"t4\", attachedBy: \"human\" },\n    { bookmarkId: \"b6\", tagId: \"t3\", attachedBy: \"ai\" },\n  ]);\n\n  await db.insert(bookmarkLists).values([\n    { id: \"l1\", userId: testUserId, name: \"list1\", icon: \"🚀\", type: \"manual\" },\n    { id: \"l2\", userId: testUserId, name: \"list2\", icon: \"🚀\", type: \"manual\" },\n    {\n      id: \"l3\",\n      userId: testUserId,\n      name: \"favorites\",\n      icon: \"⭐\",\n      type: \"manual\",\n    },\n    { id: \"l4\", userId: testUserId, name: \"work\", icon: \"💼\", type: \"manual\" },\n  ]);\n\n  await db.insert(bookmarksInLists).values([\n    { bookmarkId: \"b1\", listId: \"l1\" },\n    { bookmarkId: \"b2\", listId: \"l2\" },\n    { bookmarkId: \"b4\", listId: \"l3\" },\n    { bookmarkId: \"b5\", listId: \"l4\" },\n    { bookmarkId: \"b6\", listId: \"l1\" },\n  ]);\n\n  await db.insert(rssFeedsTable).values([\n    { id: \"f1\", userId: testUserId, name: \"feed1\", url: \"url1\" },\n    { id: \"f2\", userId: testUserId, name: \"feed2\", url: \"url2\" },\n  ]);\n\n  await db.insert(rssFeedImportsTable).values([\n    {\n      id: \"imp1\",\n      entryId: \"entry1\",\n      rssFeedId: \"f1\",\n      bookmarkId: \"b1\",\n    },\n    {\n      id: \"imp2\",\n      entryId: \"entry2\",\n      rssFeedId: \"f2\",\n      bookmarkId: \"b3\",\n    },\n    {\n      id: \"imp3\",\n      entryId: \"entry3\",\n      rssFeedId: \"f1\",\n      bookmarkId: \"b5\",\n    },\n  ]);\n\n  mockCtx = {\n    db,\n    user: {\n      id: testUserId,\n      name: \"Test User\",\n      email: \"test@example.com\",\n      role: \"user\",\n    },\n    req: {\n      ip: \"127.0.0.1\",\n    },\n  };\n});\n\ndescribe(\"getBookmarkIdsFromMatcher\", () => {\n  it(\"should handle tagName matcher\", async () => {\n    const matcher: Matcher = {\n      type: \"tagName\",\n      tagName: \"tag1\",\n      inverse: false,\n    };\n    const result = await getBookmarkIdsFromMatcher(mockCtx, matcher);\n    expect(result).toEqual([\"b1\"]);\n  });\n\n  it(\"should handle tagName matcher with inverse=true\", async () => {\n    const matcher: Matcher = {\n      type: \"tagName\",\n      tagName: \"tag1\",\n      inverse: true,\n    };\n    const result = await getBookmarkIdsFromMatcher(mockCtx, matcher);\n    expect(result.sort()).toEqual([\"b2\", \"b3\", \"b4\", \"b5\", \"b6\"]);\n  });\n\n  it(\"should handle listName matcher\", async () => {\n    const matcher: Matcher = {\n      type: \"listName\",\n      listName: \"list1\",\n      inverse: false,\n    };\n    const result = await getBookmarkIdsFromMatcher(mockCtx, matcher);\n    expect(result).toEqual([\"b1\", \"b6\"]);\n  });\n\n  it(\"should handle listName matcher with inverse=true\", async () => {\n    const matcher: Matcher = {\n      type: \"listName\",\n      listName: \"list1\",\n      inverse: true,\n    };\n    const result = await getBookmarkIdsFromMatcher(mockCtx, matcher);\n    expect(result.sort()).toEqual([\"b2\", \"b3\", \"b4\", \"b5\"]);\n  });\n\n  it(\"should handle listName matcher when multiple lists share the same name\", async () => {\n    await mockCtx.db.insert(bookmarkLists).values({\n      id: \"l5\",\n      userId: testUserId,\n      name: \"list1\",\n      icon: \"🚀\",\n      type: \"manual\",\n    });\n    await mockCtx.db.insert(bookmarksInLists).values({\n      bookmarkId: \"b2\",\n      listId: \"l5\",\n    });\n\n    const matcher: Matcher = {\n      type: \"listName\",\n      listName: \"list1\",\n      inverse: false,\n    };\n    const result = await getBookmarkIdsFromMatcher(mockCtx, matcher);\n    expect(result.sort()).toEqual([\"b1\", \"b2\", \"b6\"]);\n  });\n\n  it(\"should handle inverse listName matcher when multiple lists share the same name\", async () => {\n    await mockCtx.db.insert(bookmarkLists).values({\n      id: \"l5\",\n      userId: testUserId,\n      name: \"list1\",\n      icon: \"🚀\",\n      type: \"manual\",\n    });\n    await mockCtx.db.insert(bookmarksInLists).values({\n      bookmarkId: \"b2\",\n      listId: \"l5\",\n    });\n\n    const matcher: Matcher = {\n      type: \"listName\",\n      listName: \"list1\",\n      inverse: true,\n    };\n    const result = await getBookmarkIdsFromMatcher(mockCtx, matcher);\n    expect(result.sort()).toEqual([\"b3\", \"b4\", \"b5\"]);\n  });\n\n  it(\"should return empty when inverse listName references a missing list\", async () => {\n    const matcher: Matcher = {\n      type: \"listName\",\n      listName: \"does-not-exist\",\n      inverse: true,\n    };\n\n    const result = await getBookmarkIdsFromMatcher(mockCtx, matcher);\n    expect(result).toEqual([]);\n  });\n\n  it(\"should handle archived matcher\", async () => {\n    const matcher: Matcher = { type: \"archived\", archived: true };\n    const result = await getBookmarkIdsFromMatcher(mockCtx, matcher);\n    expect(result).toEqual([\"b2\", \"b3\", \"b6\"]);\n  });\n\n  it(\"should handle archived matcher archived=false\", async () => {\n    const matcher: Matcher = { type: \"archived\", archived: false };\n    const result = await getBookmarkIdsFromMatcher(mockCtx, matcher);\n    expect(result).toEqual([\"b1\", \"b4\", \"b5\"]);\n  });\n\n  it(\"should handle favourited matcher\", async () => {\n    const matcher: Matcher = { type: \"favourited\", favourited: true };\n    const result = await getBookmarkIdsFromMatcher(mockCtx, matcher);\n    expect(result).toEqual([\"b2\", \"b4\"]);\n  });\n\n  it(\"should handle favourited matcher favourited=false\", async () => {\n    const matcher: Matcher = { type: \"favourited\", favourited: false };\n    const result = await getBookmarkIdsFromMatcher(mockCtx, matcher);\n    expect(result).toEqual([\"b1\", \"b3\", \"b5\", \"b6\"]);\n  });\n\n  it(\"should handle url matcher\", async () => {\n    const matcher: Matcher = {\n      type: \"url\",\n      url: \"example.com\",\n      inverse: false,\n    };\n    const result = await getBookmarkIdsFromMatcher(mockCtx, matcher);\n    expect(result).toEqual([\"b1\", \"b4\", \"b6\"]);\n  });\n\n  it(\"should handle url matcher with inverse=true\", async () => {\n    const matcher: Matcher = {\n      type: \"url\",\n      url: \"example.com\",\n      inverse: true,\n    };\n    const result = await getBookmarkIdsFromMatcher(mockCtx, matcher);\n    // Not that only bookmarks of type link are returned\n    expect(result.sort()).toEqual([\"b2\"]);\n  });\n\n  it(\"should handle title matcher\", async () => {\n    const matcher: Matcher = {\n      type: \"title\",\n      title: \"example\",\n      inverse: false,\n    };\n    const result = await getBookmarkIdsFromMatcher(mockCtx, matcher);\n    expect(result.sort()).toEqual([\"b1\", \"b2\", \"b4\", \"b6\"]);\n  });\n\n  it(\"should handle title matcher with inverse=true\", async () => {\n    const matcher: Matcher = {\n      type: \"title\",\n      title: \"example\",\n      inverse: true,\n    };\n    const result = await getBookmarkIdsFromMatcher(mockCtx, matcher);\n    expect(result.sort()).toEqual([\"b3\", \"b5\"]);\n  });\n\n  it(\"should handle dateAfter matcher\", async () => {\n    const matcher: Matcher = {\n      type: \"dateAfter\",\n      dateAfter: new Date(\"2024-01-02\"),\n      inverse: false,\n    };\n    const result = await getBookmarkIdsFromMatcher(mockCtx, matcher);\n    expect(result).toEqual([\"b2\", \"b3\", \"b4\", \"b5\", \"b6\"]);\n  });\n\n  it(\"should handle dateAfter matcher with inverse=true\", async () => {\n    const matcher: Matcher = {\n      type: \"dateAfter\",\n      dateAfter: new Date(\"2024-01-02\"),\n      inverse: true,\n    };\n    const result = await getBookmarkIdsFromMatcher(mockCtx, matcher);\n    expect(result).toEqual([\"b1\"]);\n  });\n\n  it(\"should handle dateBefore matcher\", async () => {\n    const matcher: Matcher = {\n      type: \"dateBefore\",\n      dateBefore: new Date(\"2024-01-02\"),\n      inverse: false,\n    };\n    const result = await getBookmarkIdsFromMatcher(mockCtx, matcher);\n    expect(result).toEqual([\"b1\", \"b2\"]);\n  });\n\n  it(\"should handle type matcher\", async () => {\n    expect(\n      await getBookmarkIdsFromMatcher(mockCtx, {\n        type: \"type\",\n        typeName: BookmarkTypes.LINK,\n        inverse: false,\n      }),\n    ).toEqual([\"b1\", \"b2\", \"b4\"]);\n    expect(\n      await getBookmarkIdsFromMatcher(mockCtx, {\n        type: \"type\",\n        typeName: BookmarkTypes.TEXT,\n        inverse: false,\n      }),\n    ).toEqual([\"b3\", \"b5\"]);\n    expect(\n      await getBookmarkIdsFromMatcher(mockCtx, {\n        type: \"type\",\n        typeName: BookmarkTypes.ASSET,\n        inverse: false,\n      }),\n    ).toEqual([\"b6\"]);\n  });\n\n  it(\"should handle type matcher with inverse=true\", async () => {\n    expect(\n      await getBookmarkIdsFromMatcher(mockCtx, {\n        type: \"type\",\n        typeName: BookmarkTypes.LINK,\n        inverse: true,\n      }),\n    ).toEqual([\"b3\", \"b5\", \"b6\"]);\n    expect(\n      await getBookmarkIdsFromMatcher(mockCtx, {\n        type: \"type\",\n        typeName: BookmarkTypes.TEXT,\n        inverse: true,\n      }),\n    ).toEqual([\"b1\", \"b2\", \"b4\", \"b6\"]);\n    expect(\n      await getBookmarkIdsFromMatcher(mockCtx, {\n        type: \"type\",\n        typeName: BookmarkTypes.ASSET,\n        inverse: true,\n      }),\n    ).toEqual([\"b1\", \"b2\", \"b3\", \"b4\", \"b5\"]);\n  });\n\n  it(\"should handle dateBefore matcher with inverse=true\", async () => {\n    const matcher: Matcher = {\n      type: \"dateBefore\",\n      dateBefore: new Date(\"2024-01-02\"),\n      inverse: true,\n    };\n    const result = await getBookmarkIdsFromMatcher(mockCtx, matcher);\n    expect(result.sort()).toEqual([\"b3\", \"b4\", \"b5\", \"b6\"]);\n  });\n\n  it(\"should handle AND matcher\", async () => {\n    const matcher: Matcher = {\n      type: \"and\",\n      matchers: [\n        { type: \"archived\", archived: true },\n        { type: \"favourited\", favourited: true },\n      ],\n    };\n    const result = await getBookmarkIdsFromMatcher(mockCtx, matcher);\n    expect(result).toEqual([\"b2\"]);\n  });\n\n  it(\"should handle OR matcher #1\", async () => {\n    const matcher: Matcher = {\n      type: \"or\",\n      matchers: [\n        { type: \"archived\", archived: true },\n        { type: \"favourited\", favourited: true },\n      ],\n    };\n    const result = await getBookmarkIdsFromMatcher(mockCtx, matcher);\n    expect(result.sort()).toEqual([\"b2\", \"b3\", \"b4\", \"b6\"]);\n  });\n\n  it(\"should handle OR matcher #2\", async () => {\n    const matcher: Matcher = {\n      type: \"or\",\n      matchers: [\n        { type: \"listName\", listName: \"favorites\", inverse: false },\n        { type: \"tagName\", tagName: \"work\", inverse: false },\n      ],\n    };\n    const result = await getBookmarkIdsFromMatcher(mockCtx, matcher);\n    expect(result).toEqual([\"b4\", \"b5\"]);\n  });\n\n  it(\"should handle nested complex matchers\", async () => {\n    const matcher: Matcher = {\n      type: \"and\",\n      matchers: [\n        {\n          type: \"or\",\n          matchers: [\n            { type: \"listName\", listName: \"favorites\", inverse: false },\n            { type: \"tagName\", tagName: \"work\", inverse: false },\n          ],\n        },\n        {\n          type: \"or\",\n          matchers: [\n            { type: \"archived\", archived: true },\n            { type: \"favourited\", favourited: true },\n          ],\n        },\n      ],\n    };\n    const result = await getBookmarkIdsFromMatcher(mockCtx, matcher);\n    expect(result).toEqual([\"b4\"]);\n  });\n\n  it(\"should handle tagged matcher\", async () => {\n    const matcher: Matcher = { type: \"tagged\", tagged: true };\n    const result = await getBookmarkIdsFromMatcher(mockCtx, matcher);\n    expect(result.sort()).toEqual([\"b1\", \"b2\", \"b4\", \"b5\", \"b6\"]);\n  });\n\n  it(\"should handle tagged matcher with tagged=false\", async () => {\n    const matcher: Matcher = { type: \"tagged\", tagged: false };\n    const result = await getBookmarkIdsFromMatcher(mockCtx, matcher);\n    expect(result).toEqual([\"b3\"]);\n  });\n\n  it(\"should handle inlist matcher\", async () => {\n    const matcher: Matcher = { type: \"inlist\", inList: true };\n    const result = await getBookmarkIdsFromMatcher(mockCtx, matcher);\n    expect(result.sort()).toEqual([\"b1\", \"b2\", \"b4\", \"b5\", \"b6\"]);\n  });\n\n  it(\"should handle inlist matcher with inList=false\", async () => {\n    const matcher: Matcher = { type: \"inlist\", inList: false };\n    const result = await getBookmarkIdsFromMatcher(mockCtx, matcher);\n    expect(result).toEqual([\"b3\"]);\n  });\n\n  it(\"should handle rssFeedName matcher\", async () => {\n    const matcher: Matcher = {\n      type: \"rssFeedName\",\n      feedName: \"feed1\",\n      inverse: false,\n    };\n    const result = await getBookmarkIdsFromMatcher(mockCtx, matcher);\n    expect(result.sort()).toEqual([\"b1\", \"b5\"]);\n  });\n\n  it(\"should handle rssFeedName matcher with inverse=true\", async () => {\n    const matcher: Matcher = {\n      type: \"rssFeedName\",\n      feedName: \"feed1\",\n      inverse: true,\n    };\n    const result = await getBookmarkIdsFromMatcher(mockCtx, matcher);\n    expect(result.sort()).toEqual([\"b2\", \"b3\", \"b4\", \"b6\"]);\n  });\n\n  it(\"should throw error for unknown matcher type\", async () => {\n    const matcher = { type: \"unknown\" } as unknown as Matcher;\n    await expect(getBookmarkIdsFromMatcher(mockCtx, matcher)).rejects.toThrow(\n      \"Unknown matcher type\",\n    );\n  });\n});\n"
  },
  {
    "path": "packages/trpc/lib/attachments.ts",
    "content": "import { z } from \"zod\";\n\nimport { AssetTypes } from \"@karakeep/db/schema\";\nimport {\n  ZAssetType,\n  zAssetTypesSchema,\n} from \"@karakeep/shared/types/bookmarks\";\n\nexport function mapDBAssetTypeToUserType(assetType: AssetTypes): ZAssetType {\n  const map: Record<AssetTypes, z.infer<typeof zAssetTypesSchema>> = {\n    [AssetTypes.LINK_SCREENSHOT]: \"screenshot\",\n    [AssetTypes.LINK_PDF]: \"pdf\",\n    [AssetTypes.ASSET_SCREENSHOT]: \"assetScreenshot\",\n    [AssetTypes.LINK_FULL_PAGE_ARCHIVE]: \"fullPageArchive\",\n    [AssetTypes.LINK_PRECRAWLED_ARCHIVE]: \"precrawledArchive\",\n    [AssetTypes.LINK_BANNER_IMAGE]: \"bannerImage\",\n    [AssetTypes.LINK_VIDEO]: \"video\",\n    [AssetTypes.LINK_HTML_CONTENT]: \"linkHtmlContent\",\n    [AssetTypes.BOOKMARK_ASSET]: \"bookmarkAsset\",\n    [AssetTypes.USER_UPLOADED]: \"userUploaded\",\n    [AssetTypes.AVATAR]: \"avatar\",\n    [AssetTypes.BACKUP]: \"unknown\", // Backups are not displayed as regular assets\n    [AssetTypes.UNKNOWN]: \"bannerImage\",\n  };\n  return map[assetType];\n}\n\nexport function mapSchemaAssetTypeToDB(\n  assetType: z.infer<typeof zAssetTypesSchema>,\n): AssetTypes {\n  const map: Record<ZAssetType, AssetTypes> = {\n    screenshot: AssetTypes.LINK_SCREENSHOT,\n    pdf: AssetTypes.LINK_PDF,\n    assetScreenshot: AssetTypes.ASSET_SCREENSHOT,\n    fullPageArchive: AssetTypes.LINK_FULL_PAGE_ARCHIVE,\n    precrawledArchive: AssetTypes.LINK_PRECRAWLED_ARCHIVE,\n    bannerImage: AssetTypes.LINK_BANNER_IMAGE,\n    video: AssetTypes.LINK_VIDEO,\n    bookmarkAsset: AssetTypes.BOOKMARK_ASSET,\n    linkHtmlContent: AssetTypes.LINK_HTML_CONTENT,\n    userUploaded: AssetTypes.USER_UPLOADED,\n    avatar: AssetTypes.AVATAR,\n    unknown: AssetTypes.UNKNOWN,\n  };\n  return map[assetType];\n}\n\nexport function humanFriendlyNameForAssertType(type: ZAssetType) {\n  const map: Record<ZAssetType, string> = {\n    screenshot: \"Screenshot\",\n    pdf: \"PDF\",\n    assetScreenshot: \"Asset Screenshot\",\n    fullPageArchive: \"Full Page Archive\",\n    precrawledArchive: \"Precrawled Archive\",\n    bannerImage: \"Banner Image\",\n    video: \"Video\",\n    bookmarkAsset: \"Bookmark Asset\",\n    linkHtmlContent: \"HTML Content\",\n    userUploaded: \"User Uploaded File\",\n    avatar: \"Avatar\",\n    unknown: \"Unknown\",\n  };\n  return map[type];\n}\n\nexport function isAllowedToAttachAsset(type: ZAssetType) {\n  const map: Record<ZAssetType, boolean> = {\n    screenshot: true,\n    pdf: true,\n    assetScreenshot: true,\n    fullPageArchive: false,\n    precrawledArchive: true,\n    bannerImage: true,\n    video: true,\n    bookmarkAsset: false,\n    linkHtmlContent: false,\n    userUploaded: true,\n    avatar: false,\n    unknown: false,\n  };\n  return map[type];\n}\n\nexport function isAllowedToDetachAsset(type: ZAssetType) {\n  const map: Record<ZAssetType, boolean> = {\n    screenshot: true,\n    pdf: true,\n    assetScreenshot: true,\n    fullPageArchive: true,\n    precrawledArchive: true,\n    bannerImage: true,\n    video: true,\n    bookmarkAsset: false,\n    linkHtmlContent: false,\n    userUploaded: true,\n    avatar: false,\n    unknown: false,\n  };\n  return map[type];\n}\n"
  },
  {
    "path": "packages/trpc/lib/impersonate.ts",
    "content": "import { eq } from \"drizzle-orm\";\n\nimport { db } from \"@karakeep/db\";\nimport { users } from \"@karakeep/db/schema\";\n\nimport { AuthedContext } from \"..\";\n\nexport async function buildImpersonatingAuthedContext(\n  userId: string,\n): Promise<AuthedContext> {\n  const user = await db.query.users.findFirst({\n    where: eq(users.id, userId),\n  });\n  if (!user) {\n    throw new Error(\"User not found\");\n  }\n\n  return {\n    user: {\n      id: user.id,\n      name: user.name,\n      email: user.email,\n      role: user.role,\n    },\n    db,\n    req: {\n      ip: null,\n    },\n  };\n}\n"
  },
  {
    "path": "packages/trpc/lib/rateLimit.ts",
    "content": "import { TRPCError } from \"@trpc/server\";\n\nimport type { RateLimitConfig } from \"@karakeep/shared/ratelimiting\";\nimport serverConfig from \"@karakeep/shared/config\";\nimport { getRateLimitClient } from \"@karakeep/shared/ratelimiting\";\n\n/**\n * Create a tRPC middleware for rate limiting\n * @param config Rate limit configuration\n * @returns tRPC middleware function\n */\nexport function createRateLimitMiddleware<T>(config: RateLimitConfig) {\n  return async function rateLimitMiddleware(opts: {\n    path: string;\n    ctx: { req: { ip: string | null } };\n    next: () => Promise<T>;\n  }) {\n    if (!serverConfig.rateLimiting.enabled) {\n      return opts.next();\n    }\n\n    const ip = opts.ctx.req.ip;\n\n    if (!ip) {\n      return opts.next();\n    }\n\n    const client = await getRateLimitClient();\n\n    if (!client) {\n      // If no rate limit client is registered, allow the request\n      return opts.next();\n    }\n\n    // Build the rate limiting key from IP and path\n    const key = `${ip}:${opts.path}`;\n    const result = await client.checkRateLimit(config, key);\n\n    if (!result.allowed) {\n      throw new TRPCError({\n        code: \"TOO_MANY_REQUESTS\",\n        message: `Rate limit exceeded. Try again in ${result.resetInSeconds} seconds.`,\n      });\n    }\n\n    return opts.next();\n  };\n}\n"
  },
  {
    "path": "packages/trpc/lib/ruleEngine.ts",
    "content": "import deepEql from \"deep-equal\";\nimport { and, eq } from \"drizzle-orm\";\n\nimport { bookmarks, tagsOnBookmarks } from \"@karakeep/db/schema\";\nimport { LinkCrawlerQueue } from \"@karakeep/shared-server\";\nimport { BookmarkTypes } from \"@karakeep/shared/types/bookmarks\";\nimport {\n  RuleEngineAction,\n  RuleEngineCondition,\n  RuleEngineEvent,\n  RuleEngineRule,\n} from \"@karakeep/shared/types/rules\";\n\nimport { AuthedContext } from \"..\";\nimport { List } from \"../models/lists\";\nimport { RuleEngineRuleModel } from \"../models/rules\";\n\nasync function fetchBookmark(db: AuthedContext[\"db\"], bookmarkId: string) {\n  return await db.query.bookmarks.findFirst({\n    where: eq(bookmarks.id, bookmarkId),\n    with: {\n      link: {\n        columns: {\n          url: true,\n          title: true,\n        },\n      },\n      text: true,\n      asset: true,\n      tagsOnBookmarks: true,\n      rssFeeds: {\n        columns: {\n          rssFeedId: true,\n        },\n      },\n      user: {\n        columns: {},\n        with: {\n          rules: {\n            with: {\n              actions: true,\n            },\n          },\n        },\n      },\n    },\n  });\n}\n\ntype ReturnedBookmark = NonNullable<Awaited<ReturnType<typeof fetchBookmark>>>;\n\nexport interface RuleEngineEvaluationResult {\n  type: \"success\" | \"failure\";\n  ruleId: string;\n  message: string;\n}\n\nexport class RuleEngine {\n  private constructor(\n    private ctx: AuthedContext,\n    private bookmark: Omit<ReturnedBookmark, \"user\">,\n    private rules: RuleEngineRule[],\n  ) {}\n\n  private get bookmarkTitle(): string {\n    return (\n      this.bookmark.title ??\n      (this.bookmark.type === BookmarkTypes.LINK\n        ? this.bookmark.link?.title\n        : \"\") ??\n      \"\"\n    );\n  }\n\n  static async forBookmark(\n    ctx: AuthedContext,\n    bookmarkId: string,\n  ): Promise<RuleEngine | null> {\n    const [bookmark, rules] = await Promise.all([\n      fetchBookmark(ctx.db, bookmarkId),\n      RuleEngineRuleModel.getAll(ctx),\n    ]);\n    if (!bookmark) {\n      return null;\n    }\n    return new RuleEngine(\n      ctx,\n      bookmark,\n      rules.map((r) => r.rule),\n    );\n  }\n\n  doesBookmarkMatchConditions(condition: RuleEngineCondition): boolean {\n    switch (condition.type) {\n      case \"alwaysTrue\": {\n        return true;\n      }\n      case \"urlContains\": {\n        return (this.bookmark.link?.url ?? \"\").includes(condition.str);\n      }\n      case \"urlDoesNotContain\": {\n        return (\n          this.bookmark.type == BookmarkTypes.LINK &&\n          !(this.bookmark.link?.url ?? \"\").includes(condition.str)\n        );\n      }\n      case \"titleContains\": {\n        return this.bookmarkTitle.includes(condition.str);\n      }\n      case \"titleDoesNotContain\": {\n        return !this.bookmarkTitle.includes(condition.str);\n      }\n      case \"importedFromFeed\": {\n        return this.bookmark.rssFeeds.some(\n          (f) => f.rssFeedId === condition.feedId,\n        );\n      }\n      case \"bookmarkTypeIs\": {\n        return this.bookmark.type === condition.bookmarkType;\n      }\n      case \"bookmarkSourceIs\": {\n        return this.bookmark.source === condition.source;\n      }\n      case \"hasTag\": {\n        return this.bookmark.tagsOnBookmarks.some(\n          (t) => t.tagId === condition.tagId,\n        );\n      }\n      case \"isFavourited\": {\n        return this.bookmark.favourited;\n      }\n      case \"isArchived\": {\n        return this.bookmark.archived;\n      }\n      case \"and\": {\n        return condition.conditions.every((c) =>\n          this.doesBookmarkMatchConditions(c),\n        );\n      }\n      case \"or\": {\n        return condition.conditions.some((c) =>\n          this.doesBookmarkMatchConditions(c),\n        );\n      }\n      default: {\n        const _exhaustiveCheck: never = condition;\n        return false;\n      }\n    }\n  }\n\n  async evaluateRule(\n    rule: RuleEngineRule,\n    event: RuleEngineEvent,\n  ): Promise<RuleEngineEvaluationResult[]> {\n    if (!rule.enabled) {\n      return [];\n    }\n    if (!deepEql(rule.event, event, { strict: true })) {\n      return [];\n    }\n    if (!this.doesBookmarkMatchConditions(rule.condition)) {\n      return [];\n    }\n    const results = await Promise.allSettled(\n      rule.actions.map((action) => this.executeAction(action)),\n    );\n    return results.map((result) => {\n      if (result.status === \"fulfilled\") {\n        return {\n          type: \"success\",\n          ruleId: rule.id,\n          message: result.value,\n        };\n      } else {\n        return {\n          type: \"failure\",\n          ruleId: rule.id,\n          message: (result.reason as Error).message,\n        };\n      }\n    });\n  }\n\n  async executeAction(action: RuleEngineAction): Promise<string> {\n    switch (action.type) {\n      case \"addTag\": {\n        await this.ctx.db\n          .insert(tagsOnBookmarks)\n          .values([\n            {\n              attachedBy: \"human\",\n              bookmarkId: this.bookmark.id,\n              tagId: action.tagId,\n            },\n          ])\n          .onConflictDoNothing();\n        return `Added tag ${action.tagId}`;\n      }\n      case \"removeTag\": {\n        await this.ctx.db\n          .delete(tagsOnBookmarks)\n          .where(\n            and(\n              eq(tagsOnBookmarks.tagId, action.tagId),\n              eq(tagsOnBookmarks.bookmarkId, this.bookmark.id),\n            ),\n          );\n        return `Removed tag ${action.tagId}`;\n      }\n      case \"addToList\": {\n        const list = await List.fromId(this.ctx, action.listId);\n        await list.addBookmark(this.bookmark.id);\n        return `Added to list ${action.listId}`;\n      }\n      case \"removeFromList\": {\n        const list = await List.fromId(this.ctx, action.listId);\n        await list.removeBookmark(this.bookmark.id);\n        return `Removed from list ${action.listId}`;\n      }\n      case \"downloadFullPageArchive\": {\n        await LinkCrawlerQueue.enqueue(\n          {\n            bookmarkId: this.bookmark.id,\n            archiveFullPage: true,\n            runInference: false,\n          },\n          {\n            groupId: this.bookmark.userId,\n          },\n        );\n        return `Enqueued full page archive`;\n      }\n      case \"favouriteBookmark\": {\n        await this.ctx.db\n          .update(bookmarks)\n          .set({\n            favourited: true,\n          })\n          .where(eq(bookmarks.id, this.bookmark.id));\n        return `Marked as favourited`;\n      }\n      case \"archiveBookmark\": {\n        await this.ctx.db\n          .update(bookmarks)\n          .set({\n            archived: true,\n          })\n          .where(eq(bookmarks.id, this.bookmark.id));\n        return `Marked as archived`;\n      }\n      default: {\n        const _exhaustiveCheck: never = action;\n        return \"\";\n      }\n    }\n  }\n\n  async onEvent(event: RuleEngineEvent): Promise<RuleEngineEvaluationResult[]> {\n    const results = await Promise.all(\n      this.rules.map((rule) => this.evaluateRule(rule, event)),\n    );\n\n    return results.flat();\n  }\n}\n"
  },
  {
    "path": "packages/trpc/lib/search.ts",
    "content": "import {\n  and,\n  eq,\n  exists,\n  gt,\n  gte,\n  inArray,\n  isNotNull,\n  isNull,\n  like,\n  lt,\n  lte,\n  ne,\n  notExists,\n  notInArray,\n  notLike,\n  or,\n} from \"drizzle-orm\";\n\nimport {\n  bookmarkAssets,\n  bookmarkLinks,\n  bookmarkLists,\n  bookmarks,\n  bookmarksInLists,\n  bookmarkTags,\n  rssFeedImportsTable,\n  rssFeedsTable,\n  tagsOnBookmarks,\n} from \"@karakeep/db/schema\";\nimport { Matcher } from \"@karakeep/shared/types/search\";\nimport { toAbsoluteDate } from \"@karakeep/shared/utils/relativeDateUtils\";\n\nimport { AuthedContext } from \"..\";\n\ninterface BookmarkQueryReturnType {\n  id: string;\n}\n\nfunction intersect(\n  vals: BookmarkQueryReturnType[][],\n): BookmarkQueryReturnType[] {\n  if (!vals || vals.length === 0) {\n    return [];\n  }\n\n  if (vals.length === 1) {\n    return [...vals[0]];\n  }\n\n  const countMap = new Map<string, number>();\n  const map = new Map<string, BookmarkQueryReturnType>();\n\n  for (const arr of vals) {\n    for (const item of arr) {\n      countMap.set(item.id, (countMap.get(item.id) ?? 0) + 1);\n      map.set(item.id, item);\n    }\n  }\n\n  const result: BookmarkQueryReturnType[] = [];\n  for (const [id, count] of countMap) {\n    if (count === vals.length) {\n      result.push(map.get(id)!);\n    }\n  }\n\n  return result;\n}\n\nfunction union(vals: BookmarkQueryReturnType[][]): BookmarkQueryReturnType[] {\n  if (!vals || vals.length === 0) {\n    return [];\n  }\n\n  const uniqueIds = new Set<string>();\n  const map = new Map<string, BookmarkQueryReturnType>();\n  for (const arr of vals) {\n    for (const item of arr) {\n      uniqueIds.add(item.id);\n      map.set(item.id, item);\n    }\n  }\n\n  const result: BookmarkQueryReturnType[] = [];\n  for (const id of uniqueIds) {\n    result.push(map.get(id)!);\n  }\n\n  return result;\n}\n\nasync function getIds(\n  ctx: AuthedContext,\n  matcher: Matcher,\n  visitedListIds = new Set<string>(),\n): Promise<BookmarkQueryReturnType[]> {\n  const { db } = ctx;\n  const userId = ctx.user.id;\n\n  switch (matcher.type) {\n    case \"tagName\": {\n      const comp = matcher.inverse ? notExists : exists;\n      return db\n        .selectDistinct({ id: bookmarks.id })\n        .from(bookmarks)\n        .where(\n          and(\n            eq(bookmarks.userId, userId),\n            comp(\n              db\n                .select()\n                .from(tagsOnBookmarks)\n                .innerJoin(\n                  bookmarkTags,\n                  eq(tagsOnBookmarks.tagId, bookmarkTags.id),\n                )\n                .where(\n                  and(\n                    eq(tagsOnBookmarks.bookmarkId, bookmarks.id),\n                    eq(bookmarkTags.userId, userId),\n                    eq(bookmarkTags.name, matcher.tagName),\n                  ),\n                ),\n            ),\n          ),\n        );\n    }\n    case \"tagged\": {\n      const comp = matcher.tagged ? exists : notExists;\n      return db\n        .select({ id: bookmarks.id })\n        .from(bookmarks)\n        .where(\n          and(\n            eq(bookmarks.userId, userId),\n            comp(\n              db\n                .select()\n                .from(tagsOnBookmarks)\n                .where(and(eq(tagsOnBookmarks.bookmarkId, bookmarks.id))),\n            ),\n          ),\n        );\n    }\n    case \"listName\": {\n      // First, look up the list by name\n      const lists = await db.query.bookmarkLists.findMany({\n        where: and(\n          eq(bookmarkLists.userId, userId),\n          eq(bookmarkLists.name, matcher.listName),\n        ),\n      });\n\n      if (lists.length === 0) {\n        // No matching lists\n        return [];\n      }\n\n      // Use List model to resolve list membership (manual and smart)\n      // Import dynamically to avoid circular dependency\n      const { List } = await import(\"../models/lists\");\n      const listBookmarkIds = [\n        ...new Set(\n          (\n            await Promise.all(\n              lists.map(async (list) => {\n                const listModel = await List.fromId(ctx, list.id);\n                return await listModel.getBookmarkIds(visitedListIds);\n              }),\n            )\n          ).flat(),\n        ),\n      ];\n\n      if (listBookmarkIds.length === 0) {\n        if (matcher.inverse) {\n          return db\n            .selectDistinct({ id: bookmarks.id })\n            .from(bookmarks)\n            .where(eq(bookmarks.userId, userId));\n        }\n        return [];\n      }\n\n      return db\n        .selectDistinct({ id: bookmarks.id })\n        .from(bookmarks)\n        .where(\n          and(\n            eq(bookmarks.userId, userId),\n            matcher.inverse\n              ? notInArray(bookmarks.id, listBookmarkIds)\n              : inArray(bookmarks.id, listBookmarkIds),\n          ),\n        );\n    }\n    case \"inlist\": {\n      const comp = matcher.inList ? exists : notExists;\n      return db\n        .select({ id: bookmarks.id })\n        .from(bookmarks)\n        .where(\n          and(\n            eq(bookmarks.userId, userId),\n            comp(\n              db\n                .select()\n                .from(bookmarksInLists)\n                .where(and(eq(bookmarksInLists.bookmarkId, bookmarks.id))),\n            ),\n          ),\n        );\n    }\n    case \"rssFeedName\": {\n      const comp = matcher.inverse ? notExists : exists;\n      return db\n        .selectDistinct({ id: bookmarks.id })\n        .from(bookmarks)\n        .where(\n          and(\n            eq(bookmarks.userId, userId),\n            comp(\n              db\n                .select()\n                .from(rssFeedImportsTable)\n                .innerJoin(\n                  rssFeedsTable,\n                  eq(rssFeedImportsTable.rssFeedId, rssFeedsTable.id),\n                )\n                .where(\n                  and(\n                    eq(rssFeedImportsTable.bookmarkId, bookmarks.id),\n                    eq(rssFeedsTable.userId, userId),\n                    eq(rssFeedsTable.name, matcher.feedName),\n                  ),\n                ),\n            ),\n          ),\n        );\n    }\n    case \"archived\": {\n      return db\n        .select({ id: bookmarks.id })\n        .from(bookmarks)\n        .where(\n          and(\n            eq(bookmarks.userId, userId),\n            eq(bookmarks.archived, matcher.archived),\n          ),\n        );\n    }\n    case \"url\": {\n      const comp = matcher.inverse ? notLike : like;\n      return db\n        .select({ id: bookmarkLinks.id })\n        .from(bookmarkLinks)\n        .leftJoin(bookmarks, eq(bookmarks.id, bookmarkLinks.id))\n        .where(\n          and(\n            eq(bookmarks.userId, userId),\n            comp(bookmarkLinks.url, `%${matcher.url}%`),\n          ),\n        )\n        .union(\n          db\n            .select({ id: bookmarkAssets.id })\n            .from(bookmarkAssets)\n            .leftJoin(bookmarks, eq(bookmarks.id, bookmarkAssets.id))\n            .where(\n              and(\n                eq(bookmarks.userId, userId),\n                // When a user is asking for a link, the inverse matcher should match only assets with URLs.\n                isNotNull(bookmarkAssets.sourceUrl),\n                comp(bookmarkAssets.sourceUrl, `%${matcher.url}%`),\n              ),\n            ),\n        );\n    }\n    case \"title\": {\n      const comp = matcher.inverse ? notLike : like;\n      if (matcher.inverse) {\n        return db\n          .select({ id: bookmarks.id })\n          .from(bookmarks)\n          .leftJoin(bookmarkLinks, eq(bookmarks.id, bookmarkLinks.id))\n          .where(\n            and(\n              eq(bookmarks.userId, userId),\n              or(\n                isNull(bookmarks.title),\n                comp(bookmarks.title, `%${matcher.title}%`),\n              ),\n              or(\n                isNull(bookmarkLinks.title),\n                comp(bookmarkLinks.title, `%${matcher.title}%`),\n              ),\n            ),\n          );\n      }\n\n      return db\n        .select({ id: bookmarks.id })\n        .from(bookmarks)\n        .where(\n          and(\n            eq(bookmarks.userId, userId),\n            comp(bookmarks.title, `%${matcher.title}%`),\n          ),\n        )\n        .union(\n          db\n            .select({ id: bookmarkLinks.id })\n            .from(bookmarkLinks)\n            .leftJoin(bookmarks, eq(bookmarks.id, bookmarkLinks.id))\n            .where(\n              and(\n                eq(bookmarks.userId, userId),\n                comp(bookmarkLinks.title, `%${matcher.title}%`),\n              ),\n            ),\n        );\n    }\n    case \"favourited\": {\n      return db\n        .select({ id: bookmarks.id })\n        .from(bookmarks)\n        .where(\n          and(\n            eq(bookmarks.userId, userId),\n            eq(bookmarks.favourited, matcher.favourited),\n          ),\n        );\n    }\n    case \"dateAfter\": {\n      const comp = matcher.inverse ? lt : gte;\n      return db\n        .select({ id: bookmarks.id })\n        .from(bookmarks)\n        .where(\n          and(\n            eq(bookmarks.userId, userId),\n            comp(bookmarks.createdAt, matcher.dateAfter),\n          ),\n        );\n    }\n    case \"dateBefore\": {\n      const comp = matcher.inverse ? gt : lte;\n      return db\n        .select({ id: bookmarks.id })\n        .from(bookmarks)\n        .where(\n          and(\n            eq(bookmarks.userId, userId),\n            comp(bookmarks.createdAt, matcher.dateBefore),\n          ),\n        );\n    }\n    case \"age\": {\n      const comp = matcher.relativeDate.direction === \"newer\" ? gte : lt;\n      return db\n        .select({ id: bookmarks.id })\n        .from(bookmarks)\n        .where(\n          and(\n            eq(bookmarks.userId, userId),\n            comp(bookmarks.createdAt, toAbsoluteDate(matcher.relativeDate)),\n          ),\n        );\n    }\n    case \"type\": {\n      const comp = matcher.inverse ? ne : eq;\n      return db\n        .select({ id: bookmarks.id })\n        .from(bookmarks)\n        .where(\n          and(\n            eq(bookmarks.userId, userId),\n            comp(bookmarks.type, matcher.typeName),\n          ),\n        );\n    }\n    case \"brokenLinks\": {\n      // Only applies to bookmarks of type LINK\n      return db\n        .select({ id: bookmarkLinks.id })\n        .from(bookmarkLinks)\n        .leftJoin(bookmarks, eq(bookmarks.id, bookmarkLinks.id))\n        .where(\n          and(\n            eq(bookmarks.userId, userId),\n            matcher.brokenLinks\n              ? or(\n                  eq(bookmarkLinks.crawlStatus, \"failure\"),\n                  lt(bookmarkLinks.crawlStatusCode, 200),\n                  gt(bookmarkLinks.crawlStatusCode, 299),\n                )\n              : and(\n                  eq(bookmarkLinks.crawlStatus, \"success\"),\n                  gte(bookmarkLinks.crawlStatusCode, 200),\n                  lte(bookmarkLinks.crawlStatusCode, 299),\n                ),\n          ),\n        );\n    }\n    case \"source\": {\n      return db\n        .select({ id: bookmarks.id })\n        .from(bookmarks)\n        .where(\n          and(\n            eq(bookmarks.userId, userId),\n            matcher.inverse\n              ? or(\n                  ne(bookmarks.source, matcher.source),\n                  isNull(bookmarks.source),\n                )\n              : eq(bookmarks.source, matcher.source),\n          ),\n        );\n    }\n    case \"and\": {\n      const vals = await Promise.all(\n        matcher.matchers.map((m) => getIds(ctx, m, visitedListIds)),\n      );\n      return intersect(vals);\n    }\n    case \"or\": {\n      const vals = await Promise.all(\n        matcher.matchers.map((m) => getIds(ctx, m, visitedListIds)),\n      );\n      return union(vals);\n    }\n    default: {\n      const _exhaustiveCheck: never = matcher;\n      throw new Error(\"Unknown matcher type\");\n    }\n  }\n}\n\nexport async function getBookmarkIdsFromMatcher(\n  ctx: AuthedContext,\n  matcher: Matcher,\n  visitedListIds = new Set<string>(),\n): Promise<string[]> {\n  const results = await getIds(ctx, matcher, visitedListIds);\n  return results.map((r) => r.id);\n}\n"
  },
  {
    "path": "packages/trpc/lib/tracing.ts",
    "content": "import { SpanKind } from \"@opentelemetry/api\";\n\nimport {\n  getTracer,\n  setSpanAttributes,\n  withSpan,\n} from \"@karakeep/shared-server\";\nimport serverConfig from \"@karakeep/shared/config\";\n\nimport type { Context } from \"../index\";\n\nconst tracer = getTracer(\"@karakeep/trpc\");\n\n/**\n * tRPC middleware that creates a span for each procedure call.\n * This integrates OpenTelemetry tracing into the tRPC layer.\n */\nexport function createTracingMiddleware() {\n  return async function tracingMiddleware<T>(opts: {\n    ctx: Context;\n    type: \"query\" | \"mutation\" | \"subscription\";\n    path: string;\n    input: unknown;\n    next: () => Promise<T>;\n  }): Promise<T> {\n    // Skip if tracing is disabled\n    if (!serverConfig.tracing.enabled) {\n      return opts.next();\n    }\n\n    const spanName = `trpc.${opts.type}.${opts.path}`;\n\n    return withSpan(\n      tracer,\n      spanName,\n      {\n        kind: SpanKind.SERVER,\n        attributes: {\n          \"rpc.system\": \"trpc\",\n          \"rpc.method\": opts.path,\n          \"rpc.type\": opts.type,\n          \"user.id\": opts.ctx.user?.id ?? \"anonymous\",\n          \"user.role\": opts.ctx.user?.role ?? \"none\",\n        },\n      },\n      async () => {\n        return await opts.next();\n      },\n    );\n  };\n}\n\n/**\n * Helper to add tracing attributes within a tRPC procedure.\n * Use this to add custom attributes to the current span.\n */\nexport function addTracingAttributes(\n  attributes: Record<string, string | number | boolean>,\n): void {\n  if (serverConfig.tracing.enabled) {\n    setSpanAttributes(attributes);\n  }\n}\n"
  },
  {
    "path": "packages/trpc/lib/turnstile.ts",
    "content": "import { z } from \"zod\";\n\nimport serverConfig from \"@karakeep/shared/config\";\nimport logger from \"@karakeep/shared/logger\";\n\nconst TurnstileVerifyResponseSchema = z.object({\n  success: z.boolean(),\n  challenge_ts: z.string().optional(),\n  hostname: z.string().optional(),\n  \"error-codes\": z.array(z.string()).optional(),\n});\n\nexport async function verifyTurnstileToken(\n  token: string,\n  remoteIp?: string | null,\n) {\n  if (!serverConfig.auth.turnstile.enabled) {\n    return { success: true };\n  }\n\n  if (!token) {\n    return { success: false, \"error-codes\": [\"missing-input-response\"] };\n  }\n\n  const body = new URLSearchParams();\n  body.append(\"secret\", serverConfig.auth.turnstile.secretKey!);\n  body.append(\"response\", token);\n  if (remoteIp) {\n    body.append(\"remoteip\", remoteIp);\n  }\n\n  try {\n    const response = await fetch(\n      \"https://challenges.cloudflare.com/turnstile/v0/siteverify\",\n      {\n        method: \"POST\",\n        body,\n      },\n    );\n\n    if (!response.ok) {\n      logger.warn(\n        `[Turnstile] Verification request failed with status ${response.status}`,\n      );\n      return { success: false, \"error-codes\": [\"request-not-ok\"] };\n    }\n\n    const json = await response.json();\n    const parseResult = TurnstileVerifyResponseSchema.safeParse(json);\n\n    if (!parseResult.success) {\n      logger.warn(\"[Turnstile] Invalid response format\", {\n        error: parseResult.error,\n        remoteIp,\n      });\n      return { success: false, \"error-codes\": [\"invalid-response\"] };\n    }\n\n    const parsed = parseResult.data;\n    if (!parsed.success) {\n      logger.warn(\"[Turnstile] Verification failed\", {\n        errorCodes: parsed[\"error-codes\"],\n        remoteIp,\n      });\n    }\n    return parsed;\n  } catch (error) {\n    logger.warn(\"[Turnstile] Verification threw\", { error, remoteIp });\n    return { success: false, \"error-codes\": [\"internal-error\"] };\n  }\n}\n"
  },
  {
    "path": "packages/trpc/models/assets.ts",
    "content": "import { TRPCError } from \"@trpc/server\";\nimport { and, desc, eq, sql } from \"drizzle-orm\";\nimport { z } from \"zod\";\n\nimport { assets } from \"@karakeep/db/schema\";\nimport { deleteAsset } from \"@karakeep/shared/assetdb\";\nimport serverConfig from \"@karakeep/shared/config\";\nimport { createSignedToken } from \"@karakeep/shared/signedTokens\";\nimport { zAssetSignedTokenSchema } from \"@karakeep/shared/types/assets\";\nimport { zAssetTypesSchema } from \"@karakeep/shared/types/bookmarks\";\nimport { getAssetUrl } from \"@karakeep/shared/utils/assetUtils\";\n\nimport { AuthedContext } from \"..\";\nimport {\n  isAllowedToAttachAsset,\n  isAllowedToDetachAsset,\n  mapDBAssetTypeToUserType,\n  mapSchemaAssetTypeToDB,\n} from \"../lib/attachments\";\nimport { BareBookmark } from \"./bookmarks\";\n\nexport class Asset {\n  constructor(\n    protected ctx: AuthedContext,\n    public asset: typeof assets.$inferSelect,\n  ) {}\n\n  static async fromId(ctx: AuthedContext, id: string): Promise<Asset> {\n    const assetdb = await ctx.db.query.assets.findFirst({\n      where: eq(assets.id, id),\n    });\n\n    if (!assetdb) {\n      throw new TRPCError({\n        code: \"NOT_FOUND\",\n        message: \"Asset not found\",\n      });\n    }\n\n    const asset = new Asset(ctx, assetdb);\n\n    if (!(await asset.canUserView())) {\n      throw new TRPCError({\n        code: \"NOT_FOUND\",\n        message: \"Asset not found\",\n      });\n    }\n\n    return asset;\n  }\n\n  static async list(\n    ctx: AuthedContext,\n    input: {\n      limit: number;\n      cursor: number | null;\n    },\n  ) {\n    const page = input.cursor ?? 1;\n    const [results, totalCount] = await Promise.all([\n      ctx.db\n        .select()\n        .from(assets)\n        .where(eq(assets.userId, ctx.user.id))\n        .orderBy(desc(assets.size))\n        .limit(input.limit)\n        .offset((page - 1) * input.limit),\n      ctx.db\n        .select({ count: sql<number>`count(*)` })\n        .from(assets)\n        .where(eq(assets.userId, ctx.user.id)),\n    ]);\n\n    return {\n      assets: results.map((a) => ({\n        ...a,\n        assetType: mapDBAssetTypeToUserType(a.assetType),\n      })),\n      nextCursor: page * input.limit < totalCount[0].count ? page + 1 : null,\n      totalCount: totalCount[0].count,\n    };\n  }\n\n  static async attachAsset(\n    ctx: AuthedContext,\n    input: {\n      bookmarkId: string;\n      asset: {\n        id: string;\n        assetType: z.infer<typeof zAssetTypesSchema>;\n      };\n    },\n  ) {\n    const [asset] = await Promise.all([\n      Asset.fromId(ctx, input.asset.id),\n      this.ensureBookmarkOwnership(ctx, input.bookmarkId),\n    ]);\n    asset.ensureOwnership();\n\n    if (!isAllowedToAttachAsset(input.asset.assetType)) {\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message: \"You can't attach this type of asset\",\n      });\n    }\n\n    const [updatedAsset] = await ctx.db\n      .update(assets)\n      .set({\n        assetType: mapSchemaAssetTypeToDB(input.asset.assetType),\n        bookmarkId: input.bookmarkId,\n      })\n      .where(and(eq(assets.id, input.asset.id), eq(assets.userId, ctx.user.id)))\n      .returning();\n\n    return {\n      id: updatedAsset.id,\n      assetType: mapDBAssetTypeToUserType(updatedAsset.assetType),\n      fileName: updatedAsset.fileName,\n    };\n  }\n\n  static async replaceAsset(\n    ctx: AuthedContext,\n    input: {\n      bookmarkId: string;\n      oldAssetId: string;\n      newAssetId: string;\n    },\n  ) {\n    const [oldAsset, newAsset] = await Promise.all([\n      Asset.fromId(ctx, input.oldAssetId),\n      Asset.fromId(ctx, input.newAssetId),\n      this.ensureBookmarkOwnership(ctx, input.bookmarkId),\n    ]);\n    oldAsset.ensureOwnership();\n    newAsset.ensureOwnership();\n\n    if (\n      !isAllowedToAttachAsset(\n        mapDBAssetTypeToUserType(oldAsset.asset.assetType),\n      )\n    ) {\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message: \"You can't attach this type of asset\",\n      });\n    }\n\n    await ctx.db.transaction(async (tx) => {\n      await tx.delete(assets).where(eq(assets.id, input.oldAssetId));\n      await tx\n        .update(assets)\n        .set({\n          bookmarkId: input.bookmarkId,\n          assetType: oldAsset.asset.assetType,\n        })\n        .where(eq(assets.id, input.newAssetId));\n    });\n\n    await deleteAsset({\n      userId: ctx.user.id,\n      assetId: input.oldAssetId,\n    }).catch(() => ({}));\n  }\n\n  static async detachAsset(\n    ctx: AuthedContext,\n    input: {\n      bookmarkId: string;\n      assetId: string;\n    },\n  ) {\n    const [asset] = await Promise.all([\n      Asset.fromId(ctx, input.assetId),\n      this.ensureBookmarkOwnership(ctx, input.bookmarkId),\n    ]);\n\n    if (\n      !isAllowedToDetachAsset(mapDBAssetTypeToUserType(asset.asset.assetType))\n    ) {\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message: \"You can't detach this type of asset\",\n      });\n    }\n\n    const result = await ctx.db\n      .delete(assets)\n      .where(\n        and(\n          eq(assets.id, input.assetId),\n          eq(assets.bookmarkId, input.bookmarkId),\n        ),\n      );\n    if (result.changes == 0) {\n      throw new TRPCError({ code: \"NOT_FOUND\" });\n    }\n    await deleteAsset({ userId: ctx.user.id, assetId: input.assetId }).catch(\n      () => ({}),\n    );\n  }\n\n  private static async ensureBookmarkOwnership(\n    ctx: AuthedContext,\n    bookmarkId: string,\n  ) {\n    const bookmark = await BareBookmark.bareFromId(ctx, bookmarkId);\n    bookmark.ensureOwnership();\n  }\n\n  ensureOwnership() {\n    if (this.asset.userId != this.ctx.user.id) {\n      throw new TRPCError({\n        code: \"FORBIDDEN\",\n        message: \"User is not allowed to access resource\",\n      });\n    }\n  }\n\n  static async ensureOwnership(ctx: AuthedContext, assetId: string) {\n    return (await Asset.fromId(ctx, assetId)).ensureOwnership();\n  }\n\n  async canUserView(): Promise<boolean> {\n    // Asset owner can always view it\n    if (this.asset.userId === this.ctx.user.id) {\n      return true;\n    }\n\n    // Avatars are always public\n    if (this.asset.assetType === \"avatar\") {\n      return true;\n    }\n\n    // If asset is attached to a bookmark, check bookmark access permissions\n    if (this.asset.bookmarkId) {\n      try {\n        // This throws if the user doesn't have access to the bookmark\n        await BareBookmark.bareFromId(this.ctx, this.asset.bookmarkId);\n        return true;\n      } catch (e) {\n        if (e instanceof TRPCError && e.code === \"FORBIDDEN\") {\n          return false;\n        }\n        throw e;\n      }\n    }\n\n    return false;\n  }\n\n  async ensureCanView() {\n    if (!(await this.canUserView())) {\n      throw new TRPCError({\n        code: \"NOT_FOUND\",\n        message: \"Asset not found\",\n      });\n    }\n  }\n\n  getUrl() {\n    return getAssetUrl(this.asset.id);\n  }\n\n  static getPublicSignedAssetUrl(\n    assetId: string,\n    assetOwnerId: string,\n    expireAt: number,\n  ) {\n    const payload: z.infer<typeof zAssetSignedTokenSchema> = {\n      assetId,\n      userId: assetOwnerId,\n    };\n    const signedToken = createSignedToken(\n      payload,\n      serverConfig.signingSecret(),\n      expireAt,\n    );\n    return `${serverConfig.publicApiUrl}/public/assets/${assetId}?token=${signedToken}`;\n  }\n}\n"
  },
  {
    "path": "packages/trpc/models/backups.ts",
    "content": "import { TRPCError } from \"@trpc/server\";\nimport { and, desc, eq, lt } from \"drizzle-orm\";\nimport { z } from \"zod\";\n\nimport { assets, backupsTable } from \"@karakeep/db/schema\";\nimport { BackupQueue } from \"@karakeep/shared-server\";\nimport { deleteAsset } from \"@karakeep/shared/assetdb\";\nimport { zBackupSchema } from \"@karakeep/shared/types/backups\";\n\nimport { AuthedContext } from \"..\";\n\nexport class Backup {\n  private constructor(\n    private ctx: AuthedContext,\n    private backup: z.infer<typeof zBackupSchema>,\n  ) {}\n\n  static async fromId(ctx: AuthedContext, backupId: string): Promise<Backup> {\n    const backup = await ctx.db.query.backupsTable.findFirst({\n      where: and(\n        eq(backupsTable.id, backupId),\n        eq(backupsTable.userId, ctx.user.id),\n      ),\n    });\n\n    if (!backup) {\n      throw new TRPCError({\n        code: \"NOT_FOUND\",\n        message: \"Backup not found\",\n      });\n    }\n\n    return new Backup(ctx, backup);\n  }\n\n  private static fromData(\n    ctx: AuthedContext,\n    backup: z.infer<typeof zBackupSchema>,\n  ): Backup {\n    return new Backup(ctx, backup);\n  }\n\n  static async getAll(ctx: AuthedContext): Promise<Backup[]> {\n    const backups = await ctx.db.query.backupsTable.findMany({\n      where: eq(backupsTable.userId, ctx.user.id),\n      orderBy: [desc(backupsTable.createdAt)],\n    });\n\n    return backups.map((b) => new Backup(ctx, b));\n  }\n\n  static async create(ctx: AuthedContext): Promise<Backup> {\n    const [backup] = await ctx.db\n      .insert(backupsTable)\n      .values({\n        userId: ctx.user.id,\n        size: 0,\n        bookmarkCount: 0,\n        status: \"pending\",\n      })\n      .returning();\n    return new Backup(ctx, backup!);\n  }\n\n  async triggerBackgroundJob({\n    delayMs,\n    idempotencyKey,\n  }: { delayMs?: number; idempotencyKey?: string } = {}): Promise<void> {\n    await BackupQueue.enqueue(\n      {\n        userId: this.ctx.user.id,\n        backupId: this.backup.id,\n      },\n      {\n        delayMs,\n        idempotencyKey,\n      },\n    );\n  }\n\n  /**\n   * Generic update method for backup records\n   */\n  async update(\n    data: Partial<{\n      size: number;\n      bookmarkCount: number;\n      status: \"pending\" | \"success\" | \"failure\";\n      assetId: string | null;\n      errorMessage: string | null;\n    }>,\n  ): Promise<void> {\n    await this.ctx.db\n      .update(backupsTable)\n      .set(data)\n      .where(\n        and(\n          eq(backupsTable.id, this.backup.id),\n          eq(backupsTable.userId, this.ctx.user.id),\n        ),\n      );\n\n    // Update local state\n    this.backup = { ...this.backup, ...data };\n  }\n\n  async delete(): Promise<void> {\n    if (this.backup.assetId) {\n      // Delete asset\n      await deleteAsset({\n        userId: this.ctx.user.id,\n        assetId: this.backup.assetId,\n      });\n    }\n\n    await this.ctx.db.transaction(async (db) => {\n      // Delete asset first\n      if (this.backup.assetId) {\n        await db\n          .delete(assets)\n          .where(\n            and(\n              eq(assets.id, this.backup.assetId),\n              eq(assets.userId, this.ctx.user.id),\n            ),\n          );\n      }\n\n      // Delete backup record\n      await db\n        .delete(backupsTable)\n        .where(\n          and(\n            eq(backupsTable.id, this.backup.id),\n            eq(backupsTable.userId, this.ctx.user.id),\n          ),\n        );\n    });\n  }\n\n  /**\n   * Finds backups older than the retention period\n   */\n  static async findOldBackups(\n    ctx: AuthedContext,\n    retentionDays: number,\n  ): Promise<Backup[]> {\n    const cutoffDate = new Date();\n    cutoffDate.setDate(cutoffDate.getDate() - retentionDays);\n\n    const oldBackups = await ctx.db.query.backupsTable.findMany({\n      where: and(\n        eq(backupsTable.userId, ctx.user.id),\n        lt(backupsTable.createdAt, cutoffDate),\n      ),\n    });\n\n    return oldBackups.map((backup) => Backup.fromData(ctx, backup));\n  }\n\n  asPublic(): z.infer<typeof zBackupSchema> {\n    return this.backup;\n  }\n\n  get id() {\n    return this.backup.id;\n  }\n\n  get assetId() {\n    return this.backup.assetId;\n  }\n}\n"
  },
  {
    "path": "packages/trpc/models/bookmarks.ts",
    "content": "import { TRPCError } from \"@trpc/server\";\nimport {\n  and,\n  asc,\n  desc,\n  eq,\n  getTableColumns,\n  gt,\n  gte,\n  inArray,\n  lt,\n  lte,\n  or,\n  SQL,\n} from \"drizzle-orm\";\nimport invariant from \"tiny-invariant\";\nimport { z } from \"zod\";\n\nimport { db as DONT_USE_db } from \"@karakeep/db\";\nimport {\n  assets,\n  AssetTypes,\n  bookmarkAssets,\n  bookmarkLinks,\n  bookmarks,\n  bookmarksInLists,\n  bookmarkTags,\n  bookmarkTexts,\n  rssFeedImportsTable,\n  tagsOnBookmarks,\n} from \"@karakeep/db/schema\";\nimport { SearchIndexingQueue, triggerWebhook } from \"@karakeep/shared-server\";\nimport { deleteAsset, readAsset } from \"@karakeep/shared/assetdb\";\nimport { getAlignedExpiry } from \"@karakeep/shared/signedTokens\";\nimport {\n  BookmarkTypes,\n  DEFAULT_NUM_BOOKMARKS_PER_PAGE,\n  ZBareBookmark,\n  ZBookmark,\n  ZBookmarkContent,\n  zGetBookmarksRequestSchema,\n  ZPublicBookmark,\n} from \"@karakeep/shared/types/bookmarks\";\nimport { ZCursor } from \"@karakeep/shared/types/pagination\";\nimport {\n  getBookmarkLinkAssetIdOrUrl,\n  getBookmarkTitle,\n} from \"@karakeep/shared/utils/bookmarkUtils\";\nimport { htmlToPlainText } from \"@karakeep/shared/utils/htmlUtils\";\n\nimport { AuthedContext } from \"..\";\nimport { mapDBAssetTypeToUserType } from \"../lib/attachments\";\nimport { Asset } from \"./assets\";\nimport { List } from \"./lists\";\n\nasync function dummyDrizzleReturnType() {\n  const x = await DONT_USE_db.query.bookmarks.findFirst({\n    with: {\n      tagsOnBookmarks: {\n        with: {\n          tag: true,\n        },\n      },\n      link: true,\n      text: true,\n      asset: true,\n      assets: true,\n    },\n  });\n  if (!x) {\n    throw new Error();\n  }\n  return x;\n}\n\ntype BookmarkQueryReturnType = Awaited<\n  ReturnType<typeof dummyDrizzleReturnType>\n>;\n\nexport class BareBookmark {\n  protected constructor(\n    protected ctx: AuthedContext,\n    private bareBookmark: ZBareBookmark,\n  ) {}\n\n  get id() {\n    return this.bareBookmark.id;\n  }\n\n  get createdAt() {\n    return this.bareBookmark.createdAt;\n  }\n\n  static async bareFromId(ctx: AuthedContext, bookmarkId: string) {\n    const bookmark = await ctx.db.query.bookmarks.findFirst({\n      where: eq(bookmarks.id, bookmarkId),\n    });\n\n    if (!bookmark) {\n      throw new TRPCError({\n        code: \"NOT_FOUND\",\n        message: \"Bookmark not found\",\n      });\n    }\n\n    if (!(await BareBookmark.isAllowedToAccessBookmark(ctx, bookmark))) {\n      throw new TRPCError({\n        code: \"NOT_FOUND\",\n        message: \"Bookmark not found\",\n      });\n    }\n\n    return new BareBookmark(ctx, bookmark);\n  }\n\n  protected static async isAllowedToAccessBookmark(\n    ctx: AuthedContext,\n    { id: bookmarkId, userId: bookmarkOwnerId }: { id: string; userId: string },\n  ): Promise<boolean> {\n    if (bookmarkOwnerId == ctx.user.id) {\n      return true;\n    }\n    const bookmarkLists = await List.forBookmark(ctx, bookmarkId);\n    return bookmarkLists.some((l) => l.canUserView());\n  }\n\n  ensureOwnership() {\n    if (this.bareBookmark.userId != this.ctx.user.id) {\n      throw new TRPCError({\n        code: \"FORBIDDEN\",\n        message: \"User is not allowed to access resource\",\n      });\n    }\n  }\n}\n\nexport class Bookmark extends BareBookmark {\n  protected constructor(\n    ctx: AuthedContext,\n    private bookmark: ZBookmark,\n  ) {\n    super(ctx, bookmark);\n  }\n\n  private static async toZodSchema(\n    bookmark: BookmarkQueryReturnType,\n    includeContent: boolean,\n  ): Promise<ZBookmark> {\n    const { tagsOnBookmarks, link, text, asset, assets, ...rest } = bookmark;\n\n    let content: ZBookmarkContent = {\n      type: BookmarkTypes.UNKNOWN,\n    };\n    if (bookmark.link) {\n      content = {\n        type: BookmarkTypes.LINK,\n        screenshotAssetId: assets.find(\n          (a) => a.assetType == AssetTypes.LINK_SCREENSHOT,\n        )?.id,\n        pdfAssetId: assets.find((a) => a.assetType == AssetTypes.LINK_PDF)?.id,\n        fullPageArchiveAssetId: assets.find(\n          (a) => a.assetType == AssetTypes.LINK_FULL_PAGE_ARCHIVE,\n        )?.id,\n        precrawledArchiveAssetId: assets.find(\n          (a) => a.assetType == AssetTypes.LINK_PRECRAWLED_ARCHIVE,\n        )?.id,\n        imageAssetId: assets.find(\n          (a) => a.assetType == AssetTypes.LINK_BANNER_IMAGE,\n        )?.id,\n        videoAssetId: assets.find((a) => a.assetType == AssetTypes.LINK_VIDEO)\n          ?.id,\n        url: link.url,\n        title: link.title,\n        description: link.description,\n        imageUrl: link.imageUrl,\n        favicon: link.favicon,\n        htmlContent: includeContent\n          ? await Bookmark.getBookmarkHtmlContent(link, bookmark.userId)\n          : null,\n        crawledAt: link.crawledAt,\n        crawlStatus: link.crawlStatus,\n        author: link.author,\n        publisher: link.publisher,\n        datePublished: link.datePublished,\n        dateModified: link.dateModified,\n      };\n    }\n    if (bookmark.text) {\n      content = {\n        type: BookmarkTypes.TEXT,\n        // It's ok to include the text content as it's usually not big and is used to render the text bookmark card.\n        text: text.text ?? \"\",\n        sourceUrl: text.sourceUrl,\n      };\n    }\n    if (bookmark.asset) {\n      content = {\n        type: BookmarkTypes.ASSET,\n        assetType: asset.assetType,\n        assetId: asset.assetId,\n        fileName: asset.fileName,\n        sourceUrl: asset.sourceUrl,\n        size: assets.find((a) => a.id == asset.assetId)?.size,\n        content: includeContent ? asset.content : null,\n      };\n    }\n\n    return {\n      tags: tagsOnBookmarks\n        .map((t) => ({\n          attachedBy: t.attachedBy,\n          ...t.tag,\n        }))\n        .sort((a, b) =>\n          a.attachedBy === \"ai\" ? 1 : b.attachedBy === \"ai\" ? -1 : 0,\n        ),\n      content,\n      assets: assets.map((a) => ({\n        id: a.id,\n        assetType: mapDBAssetTypeToUserType(a.assetType),\n        fileName: a.fileName,\n      })),\n      ...rest,\n    };\n  }\n\n  static async fromId(\n    ctx: AuthedContext,\n    bookmarkId: string,\n    includeContent: boolean,\n  ) {\n    const bookmark = await ctx.db.query.bookmarks.findFirst({\n      where: eq(bookmarks.id, bookmarkId),\n      with: {\n        tagsOnBookmarks: {\n          with: {\n            tag: true,\n          },\n        },\n        link: true,\n        text: true,\n        asset: true,\n        assets: true,\n      },\n    });\n\n    if (!bookmark) {\n      throw new TRPCError({\n        code: \"NOT_FOUND\",\n        message: \"Bookmark not found\",\n      });\n    }\n\n    if (!(await BareBookmark.isAllowedToAccessBookmark(ctx, bookmark))) {\n      throw new TRPCError({\n        code: \"NOT_FOUND\",\n        message: \"Bookmark not found\",\n      });\n    }\n    return Bookmark.fromData(\n      ctx,\n      await Bookmark.toZodSchema(bookmark, includeContent),\n    );\n  }\n\n  static fromData(ctx: AuthedContext, data: ZBookmark) {\n    return new Bookmark(ctx, data);\n  }\n\n  static async buildDebugInfo(ctx: AuthedContext, bookmarkId: string) {\n    // Verify the user is an admin\n    if (ctx.user.role !== \"admin\") {\n      throw new TRPCError({\n        code: \"FORBIDDEN\",\n        message: \"Admin access required\",\n      });\n    }\n\n    const PRIVACY_REDACTED_ASSET_TYPES = new Set<AssetTypes>([\n      AssetTypes.USER_UPLOADED,\n      AssetTypes.BOOKMARK_ASSET,\n    ]);\n\n    const bookmark = await ctx.db.query.bookmarks.findFirst({\n      where: eq(bookmarks.id, bookmarkId),\n      with: {\n        link: true,\n        text: true,\n        asset: true,\n        tagsOnBookmarks: {\n          with: {\n            tag: true,\n          },\n        },\n        assets: true,\n      },\n    });\n\n    if (!bookmark) {\n      throw new TRPCError({\n        code: \"NOT_FOUND\",\n        message: \"Bookmark not found\",\n      });\n    }\n\n    // Build link info\n    let linkInfo = null;\n    if (bookmark.link) {\n      const htmlContentPreview = await (async () => {\n        try {\n          const content = await Bookmark.getBookmarkHtmlContent(\n            bookmark.link!,\n            bookmark.userId,\n          );\n          return content ? content.substring(0, 1000) : null;\n        } catch {\n          return null;\n        }\n      })();\n\n      linkInfo = {\n        url: bookmark.link.url,\n        crawlStatus: bookmark.link.crawlStatus ?? \"pending\",\n        crawlStatusCode: bookmark.link.crawlStatusCode,\n        crawledAt: bookmark.link.crawledAt,\n        hasHtmlContent: !!bookmark.link.htmlContent,\n        hasContentAsset: !!bookmark.link.contentAssetId,\n        htmlContentPreview,\n      };\n    }\n\n    // Build text info\n    let textInfo = null;\n    if (bookmark.text) {\n      textInfo = {\n        hasText: !!bookmark.text.text,\n        sourceUrl: bookmark.text.sourceUrl,\n      };\n    }\n\n    // Build asset info\n    let assetInfo = null;\n    if (bookmark.asset) {\n      assetInfo = {\n        assetType: bookmark.asset.assetType,\n        hasContent: !!bookmark.asset.content,\n        fileName: bookmark.asset.fileName,\n      };\n    }\n\n    // Build tags\n    const tags = bookmark.tagsOnBookmarks.map((t) => ({\n      id: t.tag.id,\n      name: t.tag.name,\n      attachedBy: t.attachedBy,\n    }));\n\n    // Build assets list with signed URLs (exclude userUploaded)\n    const assetsWithUrls = bookmark.assets.map((a) => {\n      // Generate signed token with 10 mins expiry\n      const expiresAt = Date.now() + 10 * 60 * 1000; // 10 mins\n      // Exclude userUploaded assets for privacy reasons\n      const url = !PRIVACY_REDACTED_ASSET_TYPES.has(a.assetType)\n        ? Asset.getPublicSignedAssetUrl(a.id, bookmark.userId, expiresAt)\n        : null;\n\n      return {\n        id: a.id,\n        assetType: a.assetType,\n        size: a.size,\n        url,\n      };\n    });\n\n    return {\n      id: bookmark.id,\n      type: bookmark.type,\n      source: bookmark.source,\n      createdAt: bookmark.createdAt,\n      modifiedAt: bookmark.modifiedAt,\n      title: bookmark.title,\n      summary: bookmark.summary,\n      taggingStatus: bookmark.taggingStatus,\n      summarizationStatus: bookmark.summarizationStatus,\n      userId: bookmark.userId,\n      linkInfo,\n      textInfo,\n      assetInfo,\n      tags,\n      assets: assetsWithUrls,\n    };\n  }\n\n  static async loadMulti(\n    ctx: AuthedContext,\n    input: z.infer<typeof zGetBookmarksRequestSchema>,\n  ): Promise<{\n    bookmarks: Bookmark[];\n    nextCursor: ZCursor | null;\n  }> {\n    if (input.ids && input.ids.length == 0) {\n      return { bookmarks: [], nextCursor: null };\n    }\n    if (!input.limit) {\n      input.limit = DEFAULT_NUM_BOOKMARKS_PER_PAGE;\n    }\n\n    // Validate that only one of listId, tagId, or rssFeedId is specified\n    // Combined filters are not supported as they would require different query strategies\n    const filterCount = [input.listId, input.tagId, input.rssFeedId].filter(\n      (f) => f !== undefined,\n    ).length;\n    if (filterCount > 1) {\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message:\n          \"Cannot filter by multiple of listId, tagId, and rssFeedId simultaneously\",\n      });\n    }\n\n    // Handle smart lists by converting to bookmark IDs\n    if (input.listId) {\n      const list = await List.fromId(ctx, input.listId);\n      if (list.type === \"smart\") {\n        input.ids = await list.getBookmarkIds();\n        delete input.listId;\n      }\n    }\n\n    // Build cursor condition for pagination\n    const buildCursorCondition = (\n      createdAtCol: typeof bookmarks.createdAt,\n      idCol: typeof bookmarks.id,\n    ): SQL | undefined => {\n      if (!input.cursor) return undefined;\n\n      if (input.sortOrder === \"asc\") {\n        return or(\n          gt(createdAtCol, input.cursor.createdAt),\n          and(\n            eq(createdAtCol, input.cursor.createdAt),\n            gte(idCol, input.cursor.id),\n          ),\n        );\n      }\n      return or(\n        lt(createdAtCol, input.cursor.createdAt),\n        and(\n          eq(createdAtCol, input.cursor.createdAt),\n          lte(idCol, input.cursor.id),\n        ),\n      );\n    };\n\n    // Build common filter conditions (archived, favourited, ids)\n    const buildCommonFilters = (): (SQL | undefined)[] => [\n      input.archived !== undefined\n        ? eq(bookmarks.archived, input.archived)\n        : undefined,\n      input.favourited !== undefined\n        ? eq(bookmarks.favourited, input.favourited)\n        : undefined,\n      input.ids ? inArray(bookmarks.id, input.ids) : undefined,\n    ];\n\n    // Build ORDER BY clause\n    const buildOrderBy = () =>\n      [\n        input.sortOrder === \"asc\"\n          ? asc(bookmarks.createdAt)\n          : desc(bookmarks.createdAt),\n        desc(bookmarks.id),\n      ] as const;\n\n    // Choose query strategy based on filters\n    // Strategy: Use the most selective filter as the driving table\n    let sq;\n\n    if (input.listId !== undefined) {\n      // PATH: List filter - start from bookmarksInLists (more selective)\n      // Access control is already verified by List.fromId() called above\n      sq = ctx.db.$with(\"bookmarksSq\").as(\n        ctx.db\n          .select(getTableColumns(bookmarks))\n          .from(bookmarksInLists)\n          .innerJoin(bookmarks, eq(bookmarks.id, bookmarksInLists.bookmarkId))\n          .where(\n            and(\n              eq(bookmarksInLists.listId, input.listId),\n              ...buildCommonFilters(),\n              buildCursorCondition(bookmarks.createdAt, bookmarks.id),\n            ),\n          )\n          .limit(input.limit + 1)\n          .orderBy(...buildOrderBy()),\n      );\n    } else if (input.tagId !== undefined) {\n      // PATH: Tag filter - start from tagsOnBookmarks (more selective)\n      sq = ctx.db.$with(\"bookmarksSq\").as(\n        ctx.db\n          .select(getTableColumns(bookmarks))\n          .from(tagsOnBookmarks)\n          .innerJoin(bookmarks, eq(bookmarks.id, tagsOnBookmarks.bookmarkId))\n          .where(\n            and(\n              eq(tagsOnBookmarks.tagId, input.tagId),\n              eq(bookmarks.userId, ctx.user.id), // Access control\n              ...buildCommonFilters(),\n              buildCursorCondition(bookmarks.createdAt, bookmarks.id),\n            ),\n          )\n          .limit(input.limit + 1)\n          .orderBy(...buildOrderBy()),\n      );\n    } else if (input.rssFeedId !== undefined) {\n      // PATH: RSS feed filter - start from rssFeedImportsTable (more selective)\n      sq = ctx.db.$with(\"bookmarksSq\").as(\n        ctx.db\n          .select(getTableColumns(bookmarks))\n          .from(rssFeedImportsTable)\n          .innerJoin(\n            bookmarks,\n            eq(bookmarks.id, rssFeedImportsTable.bookmarkId),\n          )\n          .where(\n            and(\n              eq(rssFeedImportsTable.rssFeedId, input.rssFeedId),\n              eq(bookmarks.userId, ctx.user.id), // Access control\n              ...buildCommonFilters(),\n              buildCursorCondition(bookmarks.createdAt, bookmarks.id),\n            ),\n          )\n          .limit(input.limit + 1)\n          .orderBy(...buildOrderBy()),\n      );\n    } else {\n      // PATH: No list/tag/rssFeed filter - query bookmarks directly\n      // Uses composite index: bookmarks_userId_createdAt_id_idx (or archived/favourited variants)\n      sq = ctx.db.$with(\"bookmarksSq\").as(\n        ctx.db\n          .select()\n          .from(bookmarks)\n          .where(\n            and(\n              eq(bookmarks.userId, ctx.user.id),\n              ...buildCommonFilters(),\n              buildCursorCondition(bookmarks.createdAt, bookmarks.id),\n            ),\n          )\n          .limit(input.limit + 1)\n          .orderBy(...buildOrderBy()),\n      );\n    }\n\n    // Execute the query with joins for related data\n    // TODO: Consider not inlining the tags in the response of getBookmarks as this query is getting kinda expensive\n    const results = await ctx.db\n      .with(sq)\n      .select()\n      .from(sq)\n      .leftJoin(tagsOnBookmarks, eq(sq.id, tagsOnBookmarks.bookmarkId))\n      .leftJoin(bookmarkTags, eq(tagsOnBookmarks.tagId, bookmarkTags.id))\n      .leftJoin(bookmarkLinks, eq(bookmarkLinks.id, sq.id))\n      .leftJoin(bookmarkTexts, eq(bookmarkTexts.id, sq.id))\n      .leftJoin(bookmarkAssets, eq(bookmarkAssets.id, sq.id))\n      .leftJoin(assets, eq(assets.bookmarkId, sq.id))\n      .orderBy(desc(sq.createdAt), desc(sq.id));\n\n    const bookmarksRes = results.reduce<Record<string, ZBookmark>>(\n      (acc, row) => {\n        const bookmarkId = row.bookmarksSq.id;\n        if (!acc[bookmarkId]) {\n          let content: ZBookmarkContent;\n          if (row.bookmarkLinks) {\n            content = {\n              type: BookmarkTypes.LINK,\n              url: row.bookmarkLinks.url,\n              title: row.bookmarkLinks.title,\n              description: row.bookmarkLinks.description,\n              imageUrl: row.bookmarkLinks.imageUrl,\n              favicon: row.bookmarkLinks.favicon,\n              htmlContent: input.includeContent\n                ? row.bookmarkLinks.contentAssetId\n                  ? null // Will be populated later from asset\n                  : row.bookmarkLinks.htmlContent\n                : null,\n              contentAssetId: row.bookmarkLinks.contentAssetId,\n              crawlStatus: row.bookmarkLinks.crawlStatus,\n              crawledAt: row.bookmarkLinks.crawledAt,\n              author: row.bookmarkLinks.author,\n              publisher: row.bookmarkLinks.publisher,\n              datePublished: row.bookmarkLinks.datePublished,\n              dateModified: row.bookmarkLinks.dateModified,\n            };\n          } else if (row.bookmarkTexts) {\n            content = {\n              type: BookmarkTypes.TEXT,\n              text: row.bookmarkTexts.text ?? \"\",\n              sourceUrl: row.bookmarkTexts.sourceUrl ?? null,\n            };\n          } else if (row.bookmarkAssets) {\n            content = {\n              type: BookmarkTypes.ASSET,\n              assetId: row.bookmarkAssets.assetId,\n              assetType: row.bookmarkAssets.assetType,\n              fileName: row.bookmarkAssets.fileName,\n              sourceUrl: row.bookmarkAssets.sourceUrl ?? null,\n              size: null, // This will get filled in the asset loop\n              content: input.includeContent\n                ? (row.bookmarkAssets.content ?? null)\n                : null,\n            };\n          } else {\n            content = {\n              type: BookmarkTypes.UNKNOWN,\n            };\n          }\n          acc[bookmarkId] = {\n            ...row.bookmarksSq,\n            content,\n            tags: [],\n            assets: [],\n          };\n        }\n\n        if (\n          row.bookmarkTags &&\n          // Duplicates may occur because of the join, so we need to make sure we're not adding the same tag twice\n          !acc[bookmarkId].tags.some((t) => t.id == row.bookmarkTags!.id)\n        ) {\n          invariant(\n            row.tagsOnBookmarks,\n            \"if bookmark tag is set, its many-to-many relation must also be set\",\n          );\n          acc[bookmarkId].tags.push({\n            ...row.bookmarkTags,\n            attachedBy: row.tagsOnBookmarks.attachedBy,\n          });\n        }\n\n        if (\n          row.assets &&\n          !acc[bookmarkId].assets.some((a) => a.id == row.assets!.id)\n        ) {\n          if (acc[bookmarkId].content.type == BookmarkTypes.LINK) {\n            const content = acc[bookmarkId].content;\n            invariant(content.type == BookmarkTypes.LINK);\n            if (row.assets.assetType == AssetTypes.LINK_SCREENSHOT) {\n              content.screenshotAssetId = row.assets.id;\n            }\n            if (row.assets.assetType == AssetTypes.LINK_PDF) {\n              content.pdfAssetId = row.assets.id;\n            }\n            if (row.assets.assetType == AssetTypes.LINK_FULL_PAGE_ARCHIVE) {\n              content.fullPageArchiveAssetId = row.assets.id;\n            }\n            if (row.assets.assetType == AssetTypes.LINK_BANNER_IMAGE) {\n              content.imageAssetId = row.assets.id;\n            }\n            if (row.assets.assetType == AssetTypes.LINK_VIDEO) {\n              content.videoAssetId = row.assets.id;\n            }\n            if (row.assets.assetType == AssetTypes.LINK_PRECRAWLED_ARCHIVE) {\n              content.precrawledArchiveAssetId = row.assets.id;\n            }\n            acc[bookmarkId].content = content;\n          }\n          if (acc[bookmarkId].content.type == BookmarkTypes.ASSET) {\n            const content = acc[bookmarkId].content;\n            if (row.assets.id == content.assetId) {\n              // If this is the bookmark's main aset, caputure its size.\n              content.size = row.assets.size;\n            }\n          }\n          acc[bookmarkId].assets.push({\n            id: row.assets.id,\n            assetType: mapDBAssetTypeToUserType(row.assets.assetType),\n            fileName: row.assets.fileName,\n          });\n        }\n\n        return acc;\n      },\n      {},\n    );\n\n    const bookmarksArr = Object.values(bookmarksRes);\n\n    // Fetch HTML content from assets for bookmarks that have contentAssetId (large content)\n    if (input.includeContent) {\n      await Promise.all(\n        bookmarksArr.map(async (bookmark) => {\n          if (\n            bookmark.content.type === BookmarkTypes.LINK &&\n            bookmark.content.contentAssetId &&\n            !bookmark.content.htmlContent // Only fetch if not already inline\n          ) {\n            try {\n              const asset = await readAsset({\n                userId: bookmark.userId,\n                assetId: bookmark.content.contentAssetId,\n              });\n              bookmark.content.htmlContent = asset.asset.toString(\"utf8\");\n            } catch (error) {\n              // If asset reading fails, keep htmlContent as null\n              console.warn(\n                `Failed to read HTML content asset ${bookmark.content.contentAssetId}:`,\n                error,\n              );\n            }\n          }\n        }),\n      );\n    }\n\n    bookmarksArr.sort((a, b) => {\n      if (a.createdAt != b.createdAt) {\n        return input.sortOrder === \"asc\"\n          ? a.createdAt.getTime() - b.createdAt.getTime()\n          : b.createdAt.getTime() - a.createdAt.getTime();\n      } else {\n        return b.id.localeCompare(a.id);\n      }\n    });\n\n    bookmarksArr.forEach((b) => {\n      b.tags.sort((a, b) =>\n        a.attachedBy === \"ai\" ? 1 : b.attachedBy === \"ai\" ? -1 : 0,\n      );\n    });\n\n    let nextCursor = null;\n    if (bookmarksArr.length > input.limit) {\n      const nextItem = bookmarksArr.pop()!;\n      nextCursor = {\n        id: nextItem.id,\n        createdAt: nextItem.createdAt,\n      };\n    }\n\n    return {\n      bookmarks: bookmarksArr.map((b) => Bookmark.fromData(ctx, b)),\n      nextCursor,\n    };\n  }\n\n  asZBookmark(): ZBookmark {\n    if (this.bookmark.userId === this.ctx.user.id) {\n      return this.bookmark;\n    }\n\n    // Collaborators shouldn't see owner-specific state such as favourites,\n    // archived flag, or personal notes.\n    return {\n      ...this.bookmark,\n      archived: false,\n      favourited: false,\n      note: null,\n    };\n  }\n\n  asPublicBookmark(): ZPublicBookmark {\n    const getPublicSignedAssetUrl = (assetId: string) => {\n      // Tokens will expire in 1 hour and will have a grace period of 15mins\n      return Asset.getPublicSignedAssetUrl(\n        assetId,\n        this.bookmark.userId,\n        getAlignedExpiry(3600, 900),\n      );\n    };\n    const getContent = (\n      content: ZBookmarkContent,\n    ): ZPublicBookmark[\"content\"] => {\n      switch (content.type) {\n        case BookmarkTypes.LINK: {\n          return {\n            type: BookmarkTypes.LINK,\n            url: content.url,\n          };\n        }\n        case BookmarkTypes.TEXT: {\n          return {\n            type: BookmarkTypes.TEXT,\n            text: content.text,\n          };\n        }\n        case BookmarkTypes.ASSET: {\n          return {\n            type: BookmarkTypes.ASSET,\n            assetType: content.assetType,\n            assetId: content.assetId,\n            assetUrl: getPublicSignedAssetUrl(content.assetId),\n            fileName: content.fileName,\n            sourceUrl: content.sourceUrl,\n          };\n        }\n        default: {\n          throw new Error(\"Unknown bookmark content type\");\n        }\n      }\n    };\n\n    const getBannerImageUrl = (content: ZBookmarkContent): string | null => {\n      switch (content.type) {\n        case BookmarkTypes.LINK: {\n          const assetIdOrUrl = getBookmarkLinkAssetIdOrUrl(content);\n          if (!assetIdOrUrl) {\n            return null;\n          }\n          if (assetIdOrUrl.localAsset) {\n            return getPublicSignedAssetUrl(assetIdOrUrl.assetId);\n          } else {\n            return assetIdOrUrl.url;\n          }\n        }\n        case BookmarkTypes.TEXT: {\n          return null;\n        }\n        case BookmarkTypes.ASSET: {\n          switch (content.assetType) {\n            case \"image\":\n              return `${getPublicSignedAssetUrl(content.assetId)}`;\n            case \"pdf\": {\n              const screenshotAssetId = this.bookmark.assets.find(\n                (r) => r.assetType === \"assetScreenshot\",\n              )?.id;\n              if (!screenshotAssetId) {\n                return null;\n              }\n              return getPublicSignedAssetUrl(screenshotAssetId);\n            }\n            default: {\n              const _exhaustiveCheck: never = content.assetType;\n              return null;\n            }\n          }\n        }\n        default: {\n          throw new Error(\"Unknown bookmark content type\");\n        }\n      }\n    };\n\n    // WARNING: Everything below is exposed in the public APIs, don't use spreads!\n    return {\n      id: this.bookmark.id,\n      createdAt: this.bookmark.createdAt,\n      modifiedAt: this.bookmark.modifiedAt,\n      title: getBookmarkTitle(this.bookmark),\n      tags: this.bookmark.tags.map((t) => t.name),\n      content: getContent(this.bookmark.content),\n      bannerImageUrl: getBannerImageUrl(this.bookmark.content),\n    };\n  }\n\n  static async getBookmarkHtmlContent(\n    {\n      contentAssetId,\n      htmlContent,\n    }: {\n      contentAssetId: string | null;\n      htmlContent: string | null;\n    },\n    userId: string,\n  ): Promise<string | null> {\n    if (contentAssetId) {\n      // Read large HTML content from asset\n      const asset = await readAsset({\n        userId,\n        assetId: contentAssetId,\n      });\n      return asset.asset.toString(\"utf8\");\n    } else if (htmlContent) {\n      return htmlContent;\n    }\n    return null;\n  }\n\n  static async getBookmarkPlainTextContent(\n    {\n      contentAssetId,\n      htmlContent,\n    }: {\n      contentAssetId: string | null;\n      htmlContent: string | null;\n    },\n    userId: string,\n  ): Promise<string | null> {\n    const content = await this.getBookmarkHtmlContent(\n      {\n        contentAssetId,\n        htmlContent,\n      },\n      userId,\n    );\n    if (!content) {\n      return null;\n    }\n    return htmlToPlainText(content);\n  }\n\n  private async cleanupAssets() {\n    const assetIds: Set<string> = new Set<string>(\n      this.bookmark.assets.map((a) => a.id),\n    );\n    // Todo: Remove when the bookmark asset is also in the assets table\n    if (this.bookmark.content.type == BookmarkTypes.ASSET) {\n      assetIds.add(this.bookmark.content.assetId);\n    }\n    await Promise.all(\n      Array.from(assetIds).map((assetId) =>\n        deleteAsset({ userId: this.bookmark.userId, assetId }),\n      ),\n    );\n  }\n\n  async delete() {\n    this.ensureOwnership();\n    const deleted = await this.ctx.db\n      .delete(bookmarks)\n      .where(\n        and(\n          eq(bookmarks.userId, this.ctx.user.id),\n          eq(bookmarks.id, this.bookmark.id),\n        ),\n      );\n\n    await SearchIndexingQueue.enqueue(\n      {\n        bookmarkId: this.bookmark.id,\n        type: \"delete\",\n      },\n      {\n        groupId: this.ctx.user.id,\n      },\n    );\n\n    await triggerWebhook(this.bookmark.id, \"deleted\", this.ctx.user.id, {\n      groupId: this.ctx.user.id,\n    });\n    if (deleted.changes > 0) {\n      await this.cleanupAssets();\n    }\n  }\n}\n"
  },
  {
    "path": "packages/trpc/models/feeds.ts",
    "content": "import { TRPCError } from \"@trpc/server\";\nimport { and, count, eq } from \"drizzle-orm\";\nimport { z } from \"zod\";\n\nimport { rssFeedsTable } from \"@karakeep/db/schema\";\nimport serverConfig from \"@karakeep/shared/config\";\nimport {\n  zFeedSchema,\n  zNewFeedSchema,\n  zUpdateFeedSchema,\n} from \"@karakeep/shared/types/feeds\";\n\nimport { AuthedContext } from \"..\";\n\nexport class Feed {\n  constructor(\n    protected ctx: AuthedContext,\n    private feed: typeof rssFeedsTable.$inferSelect,\n  ) {}\n\n  static async fromId(ctx: AuthedContext, id: string): Promise<Feed> {\n    const feed = await ctx.db.query.rssFeedsTable.findFirst({\n      where: eq(rssFeedsTable.id, id),\n    });\n\n    if (!feed) {\n      throw new TRPCError({\n        code: \"NOT_FOUND\",\n        message: \"Feed not found\",\n      });\n    }\n\n    // If it exists but belongs to another user, throw forbidden error\n    if (feed.userId !== ctx.user.id) {\n      throw new TRPCError({\n        code: \"FORBIDDEN\",\n        message: \"User is not allowed to access resource\",\n      });\n    }\n\n    return new Feed(ctx, feed);\n  }\n\n  static async create(\n    ctx: AuthedContext,\n    input: z.infer<typeof zNewFeedSchema>,\n  ): Promise<Feed> {\n    // Check if user has reached the maximum number of feeds\n    const [feedCount] = await ctx.db\n      .select({ count: count() })\n      .from(rssFeedsTable)\n      .where(eq(rssFeedsTable.userId, ctx.user.id));\n\n    const maxFeeds = serverConfig.feeds.maxRssFeedsPerUser;\n    if (feedCount.count >= maxFeeds) {\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message: `Maximum number of RSS feeds (${maxFeeds}) reached`,\n      });\n    }\n\n    const [result] = await ctx.db\n      .insert(rssFeedsTable)\n      .values({\n        name: input.name,\n        url: input.url,\n        userId: ctx.user.id,\n        enabled: input.enabled,\n        importTags: input.importTags ?? false,\n      })\n      .returning();\n\n    return new Feed(ctx, result);\n  }\n\n  static async getAll(ctx: AuthedContext): Promise<Feed[]> {\n    const feeds = await ctx.db.query.rssFeedsTable.findMany({\n      where: eq(rssFeedsTable.userId, ctx.user.id),\n    });\n\n    return feeds.map((f) => new Feed(ctx, f));\n  }\n\n  async delete(): Promise<void> {\n    const res = await this.ctx.db\n      .delete(rssFeedsTable)\n      .where(\n        and(\n          eq(rssFeedsTable.id, this.feed.id),\n          eq(rssFeedsTable.userId, this.ctx.user.id),\n        ),\n      );\n\n    if (res.changes === 0) {\n      throw new TRPCError({ code: \"NOT_FOUND\" });\n    }\n  }\n\n  async update(input: z.infer<typeof zUpdateFeedSchema>): Promise<void> {\n    const result = await this.ctx.db\n      .update(rssFeedsTable)\n      .set({\n        name: input.name,\n        url: input.url,\n        enabled: input.enabled,\n        importTags: input.importTags,\n      })\n      .where(\n        and(\n          eq(rssFeedsTable.id, this.feed.id),\n          eq(rssFeedsTable.userId, this.ctx.user.id),\n        ),\n      )\n      .returning();\n\n    if (result.length === 0) {\n      throw new TRPCError({ code: \"NOT_FOUND\" });\n    }\n\n    this.feed = result[0];\n  }\n\n  asPublicFeed(): z.infer<typeof zFeedSchema> {\n    return this.feed;\n  }\n}\n"
  },
  {
    "path": "packages/trpc/models/highlights.ts",
    "content": "import { TRPCError } from \"@trpc/server\";\nimport { and, desc, eq, like, lt, lte, or } from \"drizzle-orm\";\nimport { z } from \"zod\";\n\nimport { highlights } from \"@karakeep/db/schema\";\nimport {\n  zHighlightSchema,\n  zNewHighlightSchema,\n  zUpdateHighlightSchema,\n} from \"@karakeep/shared/types/highlights\";\nimport { zCursorV2 } from \"@karakeep/shared/types/pagination\";\n\nimport { AuthedContext } from \"..\";\nimport { BareBookmark } from \"./bookmarks\";\n\nexport class Highlight {\n  constructor(\n    protected ctx: AuthedContext,\n    private highlight: typeof highlights.$inferSelect,\n  ) {}\n\n  static async fromId(ctx: AuthedContext, id: string): Promise<Highlight> {\n    const highlight = await ctx.db.query.highlights.findFirst({\n      where: eq(highlights.id, id),\n    });\n\n    if (!highlight) {\n      throw new TRPCError({\n        code: \"NOT_FOUND\",\n        message: \"Highlight not found\",\n      });\n    }\n\n    // If it exists but belongs to another user, throw forbidden error\n    if (highlight.userId !== ctx.user.id) {\n      throw new TRPCError({\n        code: \"FORBIDDEN\",\n        message: \"User is not allowed to access resource\",\n      });\n    }\n\n    return new Highlight(ctx, highlight);\n  }\n\n  static async create(\n    ctx: AuthedContext,\n    input: z.infer<typeof zNewHighlightSchema>,\n  ): Promise<Highlight> {\n    const [result] = await ctx.db\n      .insert(highlights)\n      .values({\n        bookmarkId: input.bookmarkId,\n        startOffset: input.startOffset,\n        endOffset: input.endOffset,\n        color: input.color,\n        text: input.text,\n        note: input.note,\n        userId: ctx.user.id,\n      })\n      .returning();\n\n    return new Highlight(ctx, result);\n  }\n\n  static async getForBookmark(\n    ctx: AuthedContext,\n    bookmark: BareBookmark,\n  ): Promise<Highlight[]> {\n    const results = await ctx.db.query.highlights.findMany({\n      where: eq(highlights.bookmarkId, bookmark.id),\n      orderBy: [desc(highlights.createdAt), desc(highlights.id)],\n    });\n\n    return results.map((h) => new Highlight(ctx, h));\n  }\n\n  static async getAll(\n    ctx: AuthedContext,\n    cursor?: z.infer<typeof zCursorV2> | null,\n    limit = 50,\n  ): Promise<{\n    highlights: Highlight[];\n    nextCursor: z.infer<typeof zCursorV2> | null;\n  }> {\n    const results = await ctx.db.query.highlights.findMany({\n      where: and(\n        eq(highlights.userId, ctx.user.id),\n        cursor\n          ? or(\n              lt(highlights.createdAt, cursor.createdAt),\n              and(\n                eq(highlights.createdAt, cursor.createdAt),\n                lte(highlights.id, cursor.id),\n              ),\n            )\n          : undefined,\n      ),\n      limit: limit + 1,\n      orderBy: [desc(highlights.createdAt), desc(highlights.id)],\n    });\n\n    let nextCursor: z.infer<typeof zCursorV2> | null = null;\n    if (results.length > limit) {\n      const nextItem = results.pop()!;\n      nextCursor = {\n        id: nextItem.id,\n        createdAt: nextItem.createdAt,\n      };\n    }\n\n    return {\n      highlights: results.map((h) => new Highlight(ctx, h)),\n      nextCursor,\n    };\n  }\n\n  static async search(\n    ctx: AuthedContext,\n    searchText: string,\n    cursor?: z.infer<typeof zCursorV2> | null,\n    limit = 50,\n  ): Promise<{\n    highlights: Highlight[];\n    nextCursor: z.infer<typeof zCursorV2> | null;\n  }> {\n    const searchPattern = `%${searchText}%`;\n    const results = await ctx.db.query.highlights.findMany({\n      where: and(\n        eq(highlights.userId, ctx.user.id),\n        or(\n          like(highlights.text, searchPattern),\n          like(highlights.note, searchPattern),\n        ),\n        cursor\n          ? or(\n              lt(highlights.createdAt, cursor.createdAt),\n              and(\n                eq(highlights.createdAt, cursor.createdAt),\n                lte(highlights.id, cursor.id),\n              ),\n            )\n          : undefined,\n      ),\n      limit: limit + 1,\n      orderBy: [desc(highlights.createdAt), desc(highlights.id)],\n    });\n\n    let nextCursor: z.infer<typeof zCursorV2> | null = null;\n    if (results.length > limit) {\n      const nextItem = results.pop()!;\n      nextCursor = {\n        id: nextItem.id,\n        createdAt: nextItem.createdAt,\n      };\n    }\n\n    return {\n      highlights: results.map((h) => new Highlight(ctx, h)),\n      nextCursor,\n    };\n  }\n\n  async delete(): Promise<z.infer<typeof zHighlightSchema>> {\n    const result = await this.ctx.db\n      .delete(highlights)\n      .where(\n        and(\n          eq(highlights.id, this.highlight.id),\n          eq(highlights.userId, this.ctx.user.id),\n        ),\n      )\n      .returning();\n\n    if (result.length === 0) {\n      throw new TRPCError({ code: \"NOT_FOUND\" });\n    }\n\n    return result[0];\n  }\n\n  async update(input: z.infer<typeof zUpdateHighlightSchema>): Promise<void> {\n    const result = await this.ctx.db\n      .update(highlights)\n      .set({\n        color: input.color,\n        note: input.note,\n      })\n      .where(\n        and(\n          eq(highlights.id, this.highlight.id),\n          eq(highlights.userId, this.ctx.user.id),\n        ),\n      )\n      .returning();\n\n    if (result.length === 0) {\n      throw new TRPCError({ code: \"NOT_FOUND\" });\n    }\n\n    this.highlight = result[0];\n  }\n\n  asPublicHighlight(): z.infer<typeof zHighlightSchema> {\n    return this.highlight;\n  }\n}\n"
  },
  {
    "path": "packages/trpc/models/importSessions.ts",
    "content": "import { TRPCError } from \"@trpc/server\";\nimport { and, count, eq } from \"drizzle-orm\";\nimport { z } from \"zod\";\n\nimport { importSessions, importStagingBookmarks } from \"@karakeep/db/schema\";\nimport {\n  zCreateImportSessionRequestSchema,\n  ZImportSession,\n  ZImportSessionWithStats,\n} from \"@karakeep/shared/types/importSessions\";\n\nimport type { AuthedContext } from \"../index\";\n\nexport class ImportSession {\n  protected constructor(\n    protected ctx: AuthedContext,\n    public session: ZImportSession,\n  ) {}\n\n  static async fromId(\n    ctx: AuthedContext,\n    importSessionId: string,\n  ): Promise<ImportSession> {\n    const session = await ctx.db.query.importSessions.findFirst({\n      where: and(\n        eq(importSessions.id, importSessionId),\n        eq(importSessions.userId, ctx.user.id),\n      ),\n    });\n\n    if (!session) {\n      throw new TRPCError({\n        code: \"NOT_FOUND\",\n        message: \"Import session not found\",\n      });\n    }\n\n    return new ImportSession(ctx, session);\n  }\n\n  static async create(\n    ctx: AuthedContext,\n    input: z.infer<typeof zCreateImportSessionRequestSchema>,\n  ): Promise<ImportSession> {\n    const [session] = await ctx.db\n      .insert(importSessions)\n      .values({\n        name: input.name,\n        userId: ctx.user.id,\n        rootListId: input.rootListId,\n      })\n      .returning();\n\n    return new ImportSession(ctx, session);\n  }\n\n  static async getAll(ctx: AuthedContext): Promise<ImportSession[]> {\n    const sessions = await ctx.db.query.importSessions.findMany({\n      where: eq(importSessions.userId, ctx.user.id),\n      orderBy: (importSessions, { desc }) => [desc(importSessions.createdAt)],\n      limit: 50,\n    });\n\n    return sessions.map((session) => new ImportSession(ctx, session));\n  }\n\n  static async getAllWithStats(\n    ctx: AuthedContext,\n  ): Promise<ZImportSessionWithStats[]> {\n    const sessions = await this.getAll(ctx);\n\n    return await Promise.all(\n      sessions.map(async (session) => {\n        return await session.getWithStats();\n      }),\n    );\n  }\n\n  async getWithStats(): Promise<ZImportSessionWithStats> {\n    // Count by staging status - this now reflects the true state since\n    // items stay in \"processing\" until downstream crawl/tag is complete\n    const statusCounts = await this.ctx.db\n      .select({\n        status: importStagingBookmarks.status,\n        count: count(),\n      })\n      .from(importStagingBookmarks)\n      .where(eq(importStagingBookmarks.importSessionId, this.session.id))\n      .groupBy(importStagingBookmarks.status);\n\n    const stats = {\n      totalBookmarks: 0,\n      completedBookmarks: 0,\n      failedBookmarks: 0,\n      pendingBookmarks: 0,\n      processingBookmarks: 0,\n    };\n\n    statusCounts.forEach(({ status, count: itemCount }) => {\n      stats.totalBookmarks += itemCount;\n\n      switch (status) {\n        case \"pending\":\n          stats.pendingBookmarks += itemCount;\n          break;\n        case \"processing\":\n          stats.processingBookmarks += itemCount;\n          break;\n        case \"completed\":\n          stats.completedBookmarks += itemCount;\n          break;\n        case \"failed\":\n          stats.failedBookmarks += itemCount;\n          break;\n      }\n    });\n\n    return {\n      ...this.session,\n      ...stats,\n    };\n  }\n\n  async delete(): Promise<void> {\n    // Delete the session (cascade will handle the bookmarks)\n    const result = await this.ctx.db\n      .delete(importSessions)\n      .where(\n        and(\n          eq(importSessions.id, this.session.id),\n          eq(importSessions.userId, this.ctx.user.id),\n        ),\n      );\n\n    if (result.changes === 0) {\n      throw new TRPCError({\n        code: \"NOT_FOUND\",\n        message: \"Import session not found\",\n      });\n    }\n  }\n\n  async stageBookmarks(\n    bookmarks: {\n      type: \"link\" | \"text\" | \"asset\";\n      url?: string;\n      title?: string;\n      content?: string;\n      note?: string;\n      tags: string[];\n      listIds: string[];\n      sourceAddedAt?: Date;\n    }[],\n  ): Promise<void> {\n    if (this.session.status !== \"staging\") {\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message: \"Session not in staging status\",\n      });\n    }\n\n    // Filter out invalid bookmarks (link without url, text without content)\n    const validBookmarks = bookmarks.filter((bookmark) => {\n      if (bookmark.type === \"link\" && !bookmark.url) return false;\n      if (bookmark.type === \"text\" && !bookmark.content) return false;\n      return true;\n    });\n\n    if (validBookmarks.length === 0) {\n      return;\n    }\n\n    await this.ctx.db.insert(importStagingBookmarks).values(\n      validBookmarks.map((bookmark) => ({\n        importSessionId: this.session.id,\n        type: bookmark.type,\n        url: bookmark.url,\n        title: bookmark.title,\n        content: bookmark.content,\n        note: bookmark.note,\n        tags: bookmark.tags,\n        listIds: bookmark.listIds,\n        sourceAddedAt: bookmark.sourceAddedAt,\n        status: \"pending\" as const,\n      })),\n    );\n  }\n\n  async finalize(): Promise<void> {\n    if (this.session.status !== \"staging\") {\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message: \"Session not in staging status\",\n      });\n    }\n\n    await this.ctx.db\n      .update(importSessions)\n      .set({ status: \"pending\" })\n      .where(eq(importSessions.id, this.session.id));\n  }\n\n  async pause(): Promise<void> {\n    if (![\"pending\", \"running\"].includes(this.session.status)) {\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message: \"Session cannot be paused in current status\",\n      });\n    }\n\n    await this.ctx.db\n      .update(importSessions)\n      .set({ status: \"paused\" })\n      .where(eq(importSessions.id, this.session.id));\n  }\n\n  async resume(): Promise<void> {\n    if (this.session.status !== \"paused\") {\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message: \"Session not paused\",\n      });\n    }\n\n    await this.ctx.db\n      .update(importSessions)\n      .set({ status: \"pending\" })\n      .where(eq(importSessions.id, this.session.id));\n  }\n}\n"
  },
  {
    "path": "packages/trpc/models/listInvitations.ts",
    "content": "import { TRPCError } from \"@trpc/server\";\nimport { and, eq } from \"drizzle-orm\";\n\nimport { listCollaborators, listInvitations } from \"@karakeep/db/schema\";\n\nimport type { AuthedContext } from \"..\";\n\ntype Role = \"viewer\" | \"editor\";\ntype InvitationStatus = \"pending\" | \"declined\";\n\ninterface InvitationData {\n  id: string;\n  listId: string;\n  userId: string;\n  role: Role;\n  status: InvitationStatus;\n  invitedAt: Date;\n  invitedEmail: string | null;\n  invitedBy: string | null;\n  listOwnerUserId: string;\n}\n\nexport class ListInvitation {\n  protected constructor(\n    protected ctx: AuthedContext,\n    protected invitation: InvitationData,\n  ) {}\n\n  get id() {\n    return this.invitation.id;\n  }\n\n  /**\n   * Load an invitation by ID\n   * Can be accessed by:\n   * - The invited user (userId matches)\n   * - The list owner (via list ownership check)\n   */\n  static async fromId(\n    ctx: AuthedContext,\n    invitationId: string,\n  ): Promise<ListInvitation> {\n    const invitation = await ctx.db.query.listInvitations.findFirst({\n      where: eq(listInvitations.id, invitationId),\n      with: {\n        list: {\n          columns: {\n            userId: true,\n          },\n        },\n      },\n    });\n\n    if (!invitation) {\n      throw new TRPCError({\n        code: \"NOT_FOUND\",\n        message: \"Invitation not found\",\n      });\n    }\n\n    // Check if user has access to this invitation\n    const isInvitedUser = invitation.userId === ctx.user.id;\n    const isListOwner = invitation.list.userId === ctx.user.id;\n\n    if (!isInvitedUser && !isListOwner) {\n      throw new TRPCError({\n        code: \"NOT_FOUND\",\n        message: \"Invitation not found\",\n      });\n    }\n\n    return new ListInvitation(ctx, {\n      id: invitation.id,\n      listId: invitation.listId,\n      userId: invitation.userId,\n      role: invitation.role,\n      status: invitation.status,\n      invitedAt: invitation.invitedAt,\n      invitedEmail: invitation.invitedEmail,\n      invitedBy: invitation.invitedBy,\n      listOwnerUserId: invitation.list.userId,\n    });\n  }\n\n  /**\n   * Ensure the current user is the invited user\n   */\n  ensureIsInvitedUser() {\n    if (this.invitation.userId !== this.ctx.user.id) {\n      throw new TRPCError({\n        code: \"FORBIDDEN\",\n        message: \"Only the invited user can perform this action\",\n      });\n    }\n  }\n\n  /**\n   * Ensure the current user is the list owner\n   */\n  ensureIsListOwner() {\n    if (this.invitation.listOwnerUserId !== this.ctx.user.id) {\n      throw new TRPCError({\n        code: \"FORBIDDEN\",\n        message: \"Only the list owner can perform this action\",\n      });\n    }\n  }\n\n  /**\n   * Accept the invitation\n   */\n  async accept(): Promise<void> {\n    this.ensureIsInvitedUser();\n\n    if (this.invitation.status !== \"pending\") {\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message: \"Only pending invitations can be accepted\",\n      });\n    }\n\n    await this.ctx.db.transaction(async (tx) => {\n      await tx\n        .delete(listInvitations)\n        .where(eq(listInvitations.id, this.invitation.id));\n\n      await tx\n        .insert(listCollaborators)\n        .values({\n          listId: this.invitation.listId,\n          userId: this.invitation.userId,\n          role: this.invitation.role,\n          addedBy: this.invitation.invitedBy,\n        })\n        .onConflictDoNothing();\n    });\n  }\n\n  /**\n   * Decline the invitation\n   */\n  async decline(): Promise<void> {\n    this.ensureIsInvitedUser();\n\n    if (this.invitation.status !== \"pending\") {\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message: \"Only pending invitations can be declined\",\n      });\n    }\n\n    await this.ctx.db\n      .update(listInvitations)\n      .set({\n        status: \"declined\",\n      })\n      .where(eq(listInvitations.id, this.invitation.id));\n  }\n\n  /**\n   * Revoke the invitation (owner only)\n   */\n  async revoke(): Promise<void> {\n    this.ensureIsListOwner();\n\n    await this.ctx.db\n      .delete(listInvitations)\n      .where(eq(listInvitations.id, this.invitation.id));\n  }\n\n  /**\n   * @returns the invitation ID\n   */\n  static async inviteByEmail(\n    ctx: AuthedContext,\n    params: {\n      email: string;\n      role: Role;\n      listId: string;\n      listName: string;\n      listType: \"manual\" | \"smart\";\n      listOwnerId: string;\n      inviterUserId: string;\n      inviterName: string | null;\n    },\n  ): Promise<string> {\n    const {\n      email,\n      role,\n      listId,\n      listName,\n      listType,\n      listOwnerId,\n      inviterUserId,\n      inviterName,\n    } = params;\n\n    const user = await ctx.db.query.users.findFirst({\n      where: (users, { eq }) => eq(users.email, email),\n    });\n\n    if (!user) {\n      throw new TRPCError({\n        code: \"NOT_FOUND\",\n        message: \"No user found with that email address\",\n      });\n    }\n\n    if (user.id === listOwnerId) {\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message: \"Cannot add the list owner as a collaborator\",\n      });\n    }\n\n    if (listType !== \"manual\") {\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message: \"Only manual lists can have collaborators\",\n      });\n    }\n\n    const existingCollaborator = await ctx.db.query.listCollaborators.findFirst(\n      {\n        where: and(\n          eq(listCollaborators.listId, listId),\n          eq(listCollaborators.userId, user.id),\n        ),\n      },\n    );\n\n    if (existingCollaborator) {\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message: \"User is already a collaborator on this list\",\n      });\n    }\n\n    const existingInvitation = await ctx.db.query.listInvitations.findFirst({\n      where: and(\n        eq(listInvitations.listId, listId),\n        eq(listInvitations.userId, user.id),\n      ),\n    });\n\n    if (existingInvitation) {\n      if (existingInvitation.status === \"pending\") {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"User already has a pending invitation for this list\",\n        });\n      } else if (existingInvitation.status === \"declined\") {\n        await ctx.db\n          .update(listInvitations)\n          .set({\n            status: \"pending\",\n            role,\n            invitedAt: new Date(),\n            invitedEmail: email,\n            invitedBy: inviterUserId,\n          })\n          .where(eq(listInvitations.id, existingInvitation.id));\n\n        await this.sendInvitationEmail({\n          email,\n          inviterName,\n          listName,\n          listId,\n        });\n        return existingInvitation.id;\n      }\n    }\n\n    const res = await ctx.db\n      .insert(listInvitations)\n      .values({\n        listId,\n        userId: user.id,\n        role,\n        status: \"pending\",\n        invitedEmail: email,\n        invitedBy: inviterUserId,\n      })\n      .returning();\n\n    await this.sendInvitationEmail({\n      email,\n      inviterName,\n      listName,\n      listId,\n    });\n    return res[0].id;\n  }\n\n  static async pendingForUser(ctx: AuthedContext) {\n    const invitations = await ctx.db.query.listInvitations.findMany({\n      where: and(\n        eq(listInvitations.userId, ctx.user.id),\n        eq(listInvitations.status, \"pending\"),\n      ),\n      with: {\n        list: {\n          columns: {\n            id: true,\n            name: true,\n            icon: true,\n            description: true,\n            rssToken: false,\n          },\n          with: {\n            user: {\n              columns: {\n                id: true,\n                name: true,\n                email: true,\n              },\n            },\n          },\n        },\n      },\n    });\n\n    return invitations.map((inv) => ({\n      id: inv.id,\n      listId: inv.listId,\n      role: inv.role,\n      invitedAt: inv.invitedAt,\n      list: {\n        id: inv.list.id,\n        name: inv.list.name,\n        icon: inv.list.icon,\n        description: inv.list.description,\n        owner: inv.list.user\n          ? {\n              id: inv.list.user.id,\n              name: inv.list.user.name,\n              email: inv.list.user.email,\n            }\n          : null,\n      },\n    }));\n  }\n\n  static async invitationsForList(\n    ctx: AuthedContext,\n    params: { listId: string },\n  ) {\n    const invitations = await ctx.db.query.listInvitations.findMany({\n      where: eq(listInvitations.listId, params.listId),\n      with: {\n        user: {\n          columns: {\n            id: true,\n            name: true,\n            email: true,\n          },\n        },\n      },\n    });\n\n    return invitations.map((invitation) => ({\n      id: invitation.id,\n      listId: invitation.listId,\n      userId: invitation.userId,\n      role: invitation.role,\n      status: invitation.status,\n      invitedAt: invitation.invitedAt,\n      addedAt: invitation.invitedAt,\n      user: {\n        id: invitation.user.id,\n        // Don't show the actual user's name for any invitation (pending or declined)\n        // This protects user privacy until they accept\n        name: \"Pending User\",\n        email: invitation.user.email || \"\",\n        image: null,\n      },\n    }));\n  }\n\n  static async sendInvitationEmail(params: {\n    email: string;\n    inviterName: string | null;\n    listName: string;\n    listId: string;\n  }) {\n    try {\n      const { sendListInvitationEmail } = await import(\"../email\");\n      await sendListInvitationEmail(\n        params.email,\n        params.inviterName || \"A user\",\n        params.listName,\n        params.listId,\n      );\n    } catch (error) {\n      // Log the error but don't fail the invitation\n      console.error(\"Failed to send list invitation email:\", error);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/trpc/models/lists.ts",
    "content": "import crypto from \"node:crypto\";\nimport { TRPCError } from \"@trpc/server\";\nimport { and, count, eq, inArray, or } from \"drizzle-orm\";\nimport invariant from \"tiny-invariant\";\nimport { z } from \"zod\";\n\nimport { SqliteError } from \"@karakeep/db\";\nimport {\n  bookmarkLists,\n  bookmarksInLists,\n  listCollaborators,\n  users,\n} from \"@karakeep/db/schema\";\nimport { triggerRuleEngineOnEvent } from \"@karakeep/shared-server\";\nimport { parseSearchQuery } from \"@karakeep/shared/searchQueryParser\";\nimport { ZSortOrder } from \"@karakeep/shared/types/bookmarks\";\nimport {\n  ZBookmarkList,\n  zEditBookmarkListSchemaWithValidation,\n  zNewBookmarkListSchema,\n} from \"@karakeep/shared/types/lists\";\nimport { ZCursor } from \"@karakeep/shared/types/pagination\";\nimport { switchCase } from \"@karakeep/shared/utils/switch\";\n\nimport { AuthedContext, Context } from \"..\";\nimport { buildImpersonatingAuthedContext } from \"../lib/impersonate\";\nimport { getBookmarkIdsFromMatcher } from \"../lib/search\";\nimport { Bookmark } from \"./bookmarks\";\nimport { ListInvitation } from \"./listInvitations\";\n\ninterface ListCollaboratorEntry {\n  membershipId: string;\n}\n\nexport abstract class List {\n  protected constructor(\n    protected ctx: AuthedContext,\n    protected list: ZBookmarkList & { userId: string },\n  ) {}\n\n  get id() {\n    return this.list.id;\n  }\n\n  asZBookmarkList() {\n    if (this.list.userId === this.ctx.user.id) {\n      return this.list;\n    }\n\n    // There's some privacy implications here, so we need to think twice\n    // about the values that we return.\n    return {\n      id: this.list.id,\n      name: this.list.name,\n      description: this.list.description,\n      userId: this.list.userId,\n      icon: this.list.icon,\n      type: this.list.type,\n      query: this.list.query,\n      userRole: this.list.userRole,\n      hasCollaborators: this.list.hasCollaborators,\n\n      // Hide parentId as it is not relevant to the user\n      parentId: null,\n      // Hide whether the list is public or not.\n      public: false,\n    };\n  }\n\n  private static fromData(\n    ctx: AuthedContext,\n    data: ZBookmarkList & { userId: string },\n    collaboratorEntry: ListCollaboratorEntry | null,\n  ) {\n    if (data.type === \"smart\") {\n      return new SmartList(ctx, data);\n    } else {\n      return new ManualList(ctx, data, collaboratorEntry);\n    }\n  }\n\n  static async fromId(\n    ctx: AuthedContext,\n    id: string,\n  ): Promise<ManualList | SmartList> {\n    // First try to find the list owned by the user\n    let list = await (async (): Promise<\n      (ZBookmarkList & { userId: string }) | undefined\n    > => {\n      const l = await ctx.db.query.bookmarkLists.findFirst({\n        columns: {\n          rssToken: false,\n        },\n        where: and(\n          eq(bookmarkLists.id, id),\n          eq(bookmarkLists.userId, ctx.user.id),\n        ),\n        with: {\n          collaborators: {\n            columns: {\n              id: true,\n            },\n            limit: 1,\n          },\n        },\n      });\n      return l\n        ? {\n            ...l,\n            userRole: \"owner\",\n            hasCollaborators: l.collaborators.length > 0,\n          }\n        : l;\n    })();\n\n    // If not found, check if the user is a collaborator\n    let collaboratorEntry: ListCollaboratorEntry | null = null;\n    if (!list) {\n      const collaborator = await ctx.db.query.listCollaborators.findFirst({\n        where: and(\n          eq(listCollaborators.listId, id),\n          eq(listCollaborators.userId, ctx.user.id),\n        ),\n        with: {\n          list: {\n            columns: {\n              rssToken: false,\n            },\n          },\n        },\n      });\n\n      if (collaborator) {\n        list = {\n          ...collaborator.list,\n          userRole: collaborator.role,\n          hasCollaborators: true, // If you're a collaborator, the list has collaborators\n        };\n        collaboratorEntry = {\n          membershipId: collaborator.id,\n        };\n      }\n    }\n\n    if (!list) {\n      throw new TRPCError({\n        code: \"NOT_FOUND\",\n        message: \"List not found\",\n      });\n    }\n    if (list.type === \"smart\") {\n      return new SmartList(ctx, list);\n    } else {\n      return new ManualList(ctx, list, collaboratorEntry);\n    }\n  }\n\n  private static async getPublicList(\n    ctx: Context,\n    listId: string,\n    token: string | null,\n  ) {\n    const listdb = await ctx.db.query.bookmarkLists.findFirst({\n      where: and(\n        eq(bookmarkLists.id, listId),\n        or(\n          eq(bookmarkLists.public, true),\n          token !== null ? eq(bookmarkLists.rssToken, token) : undefined,\n        ),\n      ),\n      with: {\n        user: {\n          columns: {\n            name: true,\n          },\n        },\n      },\n    });\n    if (!listdb) {\n      throw new TRPCError({\n        code: \"NOT_FOUND\",\n        message: \"List not found\",\n      });\n    }\n    return listdb;\n  }\n\n  static async getPublicListMetadata(\n    ctx: Context,\n    listId: string,\n    token: string | null,\n  ) {\n    const listdb = await this.getPublicList(ctx, listId, token);\n    return {\n      userId: listdb.userId,\n      name: listdb.name,\n      description: listdb.description,\n      icon: listdb.icon,\n      ownerName: listdb.user.name,\n    };\n  }\n\n  static async getPublicListContents(\n    ctx: Context,\n    listId: string,\n    token: string | null,\n    pagination: {\n      limit: number;\n      order: Exclude<ZSortOrder, \"relevance\">;\n      cursor: ZCursor | null | undefined;\n    },\n  ) {\n    const listdb = await this.getPublicList(ctx, listId, token);\n\n    // The token here acts as an authed context, so we can create\n    // an impersonating context for the list owner as long as\n    // we don't leak the context.\n    const authedCtx = await buildImpersonatingAuthedContext(listdb.userId);\n    const listObj = List.fromData(\n      authedCtx,\n      {\n        ...listdb,\n        userRole: \"public\",\n        hasCollaborators: false, // Public lists don't expose collaborators\n      },\n      null,\n    );\n    const bookmarkIds = await listObj.getBookmarkIds();\n    const list = listObj.asZBookmarkList();\n\n    const bookmarks = await Bookmark.loadMulti(authedCtx, {\n      ids: bookmarkIds,\n      includeContent: false,\n      limit: pagination.limit,\n      sortOrder: pagination.order,\n      cursor: pagination.cursor,\n    });\n\n    return {\n      list: {\n        icon: list.icon,\n        name: list.name,\n        description: list.description,\n        ownerName: listdb.user.name,\n        numItems: bookmarkIds.length,\n      },\n      bookmarks: bookmarks.bookmarks.map((b) => b.asPublicBookmark()),\n      nextCursor: bookmarks.nextCursor,\n    };\n  }\n\n  static async create(\n    ctx: AuthedContext,\n    input: z.infer<typeof zNewBookmarkListSchema>,\n  ): Promise<ManualList | SmartList> {\n    const [result] = await ctx.db\n      .insert(bookmarkLists)\n      .values({\n        name: input.name,\n        description: input.description,\n        icon: input.icon,\n        userId: ctx.user.id,\n        parentId: input.parentId,\n        type: input.type,\n        query: input.query,\n      })\n      .returning();\n    return this.fromData(\n      ctx,\n      {\n        ...result,\n        userRole: \"owner\",\n        hasCollaborators: false, // Newly created lists have no collaborators\n      },\n      null,\n    );\n  }\n\n  static async getAll(ctx: AuthedContext) {\n    const [ownedLists, sharedLists] = await Promise.all([\n      this.getAllOwned(ctx),\n      this.getSharedWithUser(ctx),\n    ]);\n    return [...ownedLists, ...sharedLists];\n  }\n\n  static async getAllOwned(\n    ctx: AuthedContext,\n  ): Promise<(ManualList | SmartList)[]> {\n    const lists = await ctx.db.query.bookmarkLists.findMany({\n      columns: {\n        rssToken: false,\n      },\n      where: and(eq(bookmarkLists.userId, ctx.user.id)),\n      with: {\n        collaborators: {\n          columns: {\n            id: true,\n          },\n          limit: 1,\n        },\n      },\n    });\n    return lists.map((l) =>\n      this.fromData(\n        ctx,\n        {\n          ...l,\n          userRole: \"owner\",\n          hasCollaborators: l.collaborators.length > 0,\n        },\n        null /* this is an owned list */,\n      ),\n    );\n  }\n\n  static async forBookmark(ctx: AuthedContext, bookmarkId: string) {\n    const lists = await ctx.db.query.bookmarksInLists.findMany({\n      where: eq(bookmarksInLists.bookmarkId, bookmarkId),\n      with: {\n        list: {\n          columns: {\n            rssToken: false,\n          },\n          with: {\n            collaborators: {\n              where: eq(listCollaborators.userId, ctx.user.id),\n              columns: {\n                id: true,\n                role: true,\n              },\n            },\n          },\n        },\n      },\n    });\n\n    // For owner lists, we need to check if they actually have collaborators\n    // by querying the collaborators table separately (without user filter)\n    const ownerListIds = lists\n      .filter((l) => l.list.userId === ctx.user.id)\n      .map((l) => l.list.id);\n\n    const listsWithCollaborators = new Set<string>();\n    if (ownerListIds.length > 0) {\n      // Use a single query with inArray instead of N queries\n      const collaborators = await ctx.db.query.listCollaborators.findMany({\n        where: inArray(listCollaborators.listId, ownerListIds),\n        columns: {\n          listId: true,\n        },\n      });\n      collaborators.forEach((c) => {\n        listsWithCollaborators.add(c.listId);\n      });\n    }\n\n    return lists.flatMap((l) => {\n      let userRole: \"owner\" | \"editor\" | \"viewer\" | null;\n      let collaboratorEntry: ListCollaboratorEntry | null = null;\n      if (l.list.collaborators.length > 0) {\n        invariant(l.list.collaborators.length == 1);\n        userRole = l.list.collaborators[0].role;\n        collaboratorEntry = {\n          membershipId: l.list.collaborators[0].id,\n        };\n      } else if (l.list.userId === ctx.user.id) {\n        userRole = \"owner\";\n      } else {\n        userRole = null;\n      }\n      return userRole\n        ? [\n            this.fromData(\n              ctx,\n              {\n                ...l.list,\n                userRole,\n                hasCollaborators:\n                  userRole !== \"owner\"\n                    ? true\n                    : listsWithCollaborators.has(l.list.id),\n              },\n              collaboratorEntry,\n            ),\n          ]\n        : [];\n    });\n  }\n\n  /**\n   * Check if the user can view this list and its bookmarks.\n   */\n  canUserView(): boolean {\n    return switchCase(this.list.userRole, {\n      owner: true,\n      editor: true,\n      viewer: true,\n      public: true,\n    });\n  }\n\n  /**\n   * Check if the user can edit this list (add/remove bookmarks).\n   */\n  canUserEdit(): boolean {\n    return switchCase(this.list.userRole, {\n      owner: true,\n      editor: true,\n      viewer: false,\n      public: false,\n    });\n  }\n\n  /**\n   * Check if the user can manage this list (edit metadata, delete, manage collaborators).\n   * Only the owner can manage the list.\n   */\n  canUserManage(): boolean {\n    return switchCase(this.list.userRole, {\n      owner: true,\n      editor: false,\n      viewer: false,\n      public: false,\n    });\n  }\n\n  /**\n   * Ensure the user can view this list. Throws if they cannot.\n   */\n  ensureCanView(): void {\n    if (!this.canUserView()) {\n      throw new TRPCError({\n        code: \"FORBIDDEN\",\n        message: \"User is not allowed to view this list\",\n      });\n    }\n  }\n\n  /**\n   * Ensure the user can edit this list. Throws if they cannot.\n   */\n  ensureCanEdit(): void {\n    if (!this.canUserEdit()) {\n      throw new TRPCError({\n        code: \"FORBIDDEN\",\n        message: \"User is not allowed to edit this list\",\n      });\n    }\n  }\n\n  /**\n   * Ensure the user can manage this list. Throws if they cannot.\n   */\n  ensureCanManage(): void {\n    if (!this.canUserManage()) {\n      throw new TRPCError({\n        code: \"FORBIDDEN\",\n        message: \"User is not allowed to manage this list\",\n      });\n    }\n  }\n\n  async delete() {\n    this.ensureCanManage();\n    const res = await this.ctx.db\n      .delete(bookmarkLists)\n      .where(\n        and(\n          eq(bookmarkLists.id, this.list.id),\n          eq(bookmarkLists.userId, this.ctx.user.id),\n        ),\n      );\n    if (res.changes == 0) {\n      throw new TRPCError({ code: \"NOT_FOUND\" });\n    }\n  }\n\n  async getChildren(): Promise<(ManualList | SmartList)[]> {\n    const lists = await List.getAllOwned(this.ctx);\n    const listById = new Map(lists.map((l) => [l.id, l]));\n\n    const adjecencyList = new Map<string, string[]>();\n\n    // Initialize all lists with empty arrays first\n    lists.forEach((l) => {\n      adjecencyList.set(l.id, []);\n    });\n\n    // Then populate the parent-child relationships\n    lists.forEach((l) => {\n      const parentId = l.asZBookmarkList().parentId;\n      if (parentId) {\n        const currentChildren = adjecencyList.get(parentId) ?? [];\n        currentChildren.push(l.id);\n        adjecencyList.set(parentId, currentChildren);\n      }\n    });\n\n    const resultIds: string[] = [];\n    const queue: string[] = [this.list.id];\n\n    while (queue.length > 0) {\n      const id = queue.pop()!;\n      const children = adjecencyList.get(id) ?? [];\n      children.forEach((childId) => {\n        queue.push(childId);\n        resultIds.push(childId);\n      });\n    }\n\n    return resultIds.map((id) => listById.get(id)!);\n  }\n\n  async update(\n    input: z.infer<typeof zEditBookmarkListSchemaWithValidation>,\n  ): Promise<void> {\n    this.ensureCanManage();\n    const result = await this.ctx.db\n      .update(bookmarkLists)\n      .set({\n        name: input.name,\n        description: input.description,\n        icon: input.icon,\n        parentId: input.parentId,\n        query: input.query,\n        public: input.public,\n      })\n      .where(\n        and(\n          eq(bookmarkLists.id, this.list.id),\n          eq(bookmarkLists.userId, this.ctx.user.id),\n        ),\n      )\n      .returning();\n    if (result.length == 0) {\n      throw new TRPCError({ code: \"NOT_FOUND\" });\n    }\n    invariant(result[0].userId === this.ctx.user.id);\n    // Fetch current collaborators to update hasCollaborators\n    const collaboratorsCount =\n      await this.ctx.db.query.listCollaborators.findMany({\n        where: eq(listCollaborators.listId, this.list.id),\n        columns: {\n          id: true,\n        },\n        limit: 1,\n      });\n    this.list = {\n      ...result[0],\n      userRole: \"owner\",\n      hasCollaborators: collaboratorsCount.length > 0,\n    };\n  }\n\n  private async setRssToken(token: string | null) {\n    const result = await this.ctx.db\n      .update(bookmarkLists)\n      .set({ rssToken: token })\n      .where(\n        and(\n          eq(bookmarkLists.id, this.list.id),\n          eq(bookmarkLists.userId, this.ctx.user.id),\n        ),\n      )\n      .returning();\n    if (result.length == 0) {\n      throw new TRPCError({ code: \"NOT_FOUND\" });\n    }\n    return result[0].rssToken;\n  }\n\n  async getRssToken(): Promise<string | null> {\n    this.ensureCanManage();\n    const [result] = await this.ctx.db\n      .select({ rssToken: bookmarkLists.rssToken })\n      .from(bookmarkLists)\n      .where(\n        and(\n          eq(bookmarkLists.id, this.list.id),\n          eq(bookmarkLists.userId, this.ctx.user.id),\n        ),\n      )\n      .limit(1);\n    return result.rssToken ?? null;\n  }\n\n  async regenRssToken() {\n    this.ensureCanManage();\n    return await this.setRssToken(crypto.randomBytes(32).toString(\"hex\"));\n  }\n\n  async clearRssToken() {\n    this.ensureCanManage();\n    await this.setRssToken(null);\n  }\n\n  /**\n   * Add a collaborator to this list by email.\n   * Creates a pending invitation that must be accepted by the user.\n   * Returns the invitation ID.\n   */\n  async addCollaboratorByEmail(\n    email: string,\n    role: \"viewer\" | \"editor\",\n  ): Promise<string> {\n    this.ensureCanManage();\n\n    return await ListInvitation.inviteByEmail(this.ctx, {\n      email,\n      role,\n      listId: this.list.id,\n      listName: this.list.name,\n      listType: this.list.type,\n      listOwnerId: this.list.userId,\n      inviterUserId: this.ctx.user.id,\n      inviterName: this.ctx.user.name ?? null,\n    });\n  }\n\n  /**\n   * Remove a collaborator from this list.\n   * Only the list owner can remove collaborators.\n   * This also removes all bookmarks that the collaborator added to the list.\n   */\n  async removeCollaborator(userId: string): Promise<void> {\n    this.ensureCanManage();\n\n    const result = await this.ctx.db\n      .delete(listCollaborators)\n      .where(\n        and(\n          eq(listCollaborators.listId, this.list.id),\n          eq(listCollaborators.userId, userId),\n        ),\n      );\n\n    if (result.changes === 0) {\n      throw new TRPCError({\n        code: \"NOT_FOUND\",\n        message: \"Collaborator not found\",\n      });\n    }\n  }\n\n  /**\n   * Allow a user to leave a list (remove themselves as a collaborator).\n   * This bypasses the owner check since users should be able to leave lists they're collaborating on.\n   * This also removes all bookmarks that the user added to the list.\n   */\n  async leaveList(): Promise<void> {\n    if (this.list.userRole === \"owner\") {\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message:\n          \"List owners cannot leave their own list. Delete the list instead.\",\n      });\n    }\n\n    const result = await this.ctx.db\n      .delete(listCollaborators)\n      .where(\n        and(\n          eq(listCollaborators.listId, this.list.id),\n          eq(listCollaborators.userId, this.ctx.user.id),\n        ),\n      );\n\n    if (result.changes === 0) {\n      throw new TRPCError({\n        code: \"NOT_FOUND\",\n        message: \"Collaborator not found\",\n      });\n    }\n  }\n\n  /**\n   * Update a collaborator's role.\n   */\n  async updateCollaboratorRole(\n    userId: string,\n    role: \"viewer\" | \"editor\",\n  ): Promise<void> {\n    this.ensureCanManage();\n\n    const result = await this.ctx.db\n      .update(listCollaborators)\n      .set({ role })\n      .where(\n        and(\n          eq(listCollaborators.listId, this.list.id),\n          eq(listCollaborators.userId, userId),\n        ),\n      );\n\n    if (result.changes === 0) {\n      throw new TRPCError({\n        code: \"NOT_FOUND\",\n        message: \"Collaborator not found\",\n      });\n    }\n  }\n\n  /**\n   * Get all collaborators for this list, including pending invitations.\n   * For privacy, pending invitations show masked user info unless the invitation has been accepted.\n   */\n  async getCollaborators() {\n    this.ensureCanView();\n\n    const isOwner = this.list.userId === this.ctx.user.id;\n\n    const [collaborators, invitations] = await Promise.all([\n      this.ctx.db.query.listCollaborators.findMany({\n        where: eq(listCollaborators.listId, this.list.id),\n        with: {\n          user: {\n            columns: {\n              id: true,\n              name: true,\n              email: true,\n              image: true,\n            },\n          },\n        },\n      }),\n      // Only show invitations for the owner\n      isOwner\n        ? ListInvitation.invitationsForList(this.ctx, {\n            listId: this.list.id,\n          })\n        : [],\n    ]);\n\n    // Get the owner information\n    const owner = await this.ctx.db.query.users.findFirst({\n      where: eq(users.id, this.list.userId),\n      columns: {\n        id: true,\n        name: true,\n        email: true,\n        image: true,\n      },\n    });\n\n    const collaboratorEntries = collaborators.map((c) => {\n      return {\n        id: c.id,\n        userId: c.userId,\n        role: c.role,\n        status: \"accepted\" as const,\n        addedAt: c.addedAt,\n        invitedAt: c.addedAt,\n        user: {\n          id: c.user.id,\n          name: c.user.name,\n          // Only show email to the owner for privacy\n          email: isOwner ? c.user.email : null,\n          image: c.user.image,\n        },\n      };\n    });\n\n    return {\n      collaborators: [...collaboratorEntries, ...invitations],\n      owner: owner\n        ? {\n            id: owner.id,\n            name: owner.name,\n            // Only show owner email to the owner for privacy\n            email: isOwner ? owner.email : null,\n            image: owner.image,\n          }\n        : null,\n    };\n  }\n\n  /**\n   * Get all lists shared with the user (as a collaborator).\n   * Only includes lists where the invitation has been accepted.\n   */\n  static async getSharedWithUser(\n    ctx: AuthedContext,\n  ): Promise<(ManualList | SmartList)[]> {\n    const collaborations = await ctx.db.query.listCollaborators.findMany({\n      where: eq(listCollaborators.userId, ctx.user.id),\n      with: {\n        list: {\n          columns: {\n            rssToken: false,\n          },\n        },\n      },\n    });\n\n    return collaborations.map((c) =>\n      this.fromData(\n        ctx,\n        {\n          ...c.list,\n          userRole: c.role,\n          hasCollaborators: true, // If you're a collaborator, the list has collaborators\n        },\n        {\n          membershipId: c.id,\n        },\n      ),\n    );\n  }\n\n  abstract get type(): \"manual\" | \"smart\";\n  abstract getBookmarkIds(visitedListIds?: Set<string>): Promise<string[]>;\n  abstract getSize(): Promise<number>;\n  abstract addBookmark(bookmarkId: string): Promise<void>;\n  abstract removeBookmark(bookmarkId: string): Promise<void>;\n  abstract mergeInto(\n    targetList: List,\n    deleteSourceAfterMerge: boolean,\n  ): Promise<void>;\n}\n\nexport class SmartList extends List {\n  private static readonly MAX_VISITED_LISTS = 30;\n\n  parsedQuery: ReturnType<typeof parseSearchQuery> | null = null;\n\n  constructor(ctx: AuthedContext, list: ZBookmarkList & { userId: string }) {\n    super(ctx, list);\n  }\n\n  get type(): \"smart\" {\n    invariant(this.list.type === \"smart\");\n    return this.list.type;\n  }\n\n  get query() {\n    invariant(this.list.query);\n    return this.list.query;\n  }\n\n  getParsedQuery() {\n    if (!this.parsedQuery) {\n      const result = parseSearchQuery(this.query);\n      if (result.result !== \"full\") {\n        throw new Error(\"Invalid smart list query\");\n      }\n      this.parsedQuery = result;\n    }\n    return this.parsedQuery;\n  }\n\n  async getBookmarkIds(visitedListIds = new Set<string>()): Promise<string[]> {\n    if (visitedListIds.size >= SmartList.MAX_VISITED_LISTS) {\n      return [];\n    }\n\n    if (visitedListIds.has(this.list.id)) {\n      return [];\n    }\n\n    const newVisitedListIds = new Set(visitedListIds);\n    newVisitedListIds.add(this.list.id);\n\n    const parsedQuery = this.getParsedQuery();\n    if (!parsedQuery.matcher) {\n      return [];\n    }\n    return await getBookmarkIdsFromMatcher(\n      this.ctx,\n      parsedQuery.matcher,\n      newVisitedListIds,\n    );\n  }\n\n  async getSize(): Promise<number> {\n    return await this.getBookmarkIds().then((ids) => ids.length);\n  }\n\n  addBookmark(_bookmarkId: string): Promise<void> {\n    throw new TRPCError({\n      code: \"BAD_REQUEST\",\n      message: \"Smart lists cannot be added to\",\n    });\n  }\n\n  removeBookmark(_bookmarkId: string): Promise<void> {\n    throw new TRPCError({\n      code: \"BAD_REQUEST\",\n      message: \"Smart lists cannot be removed from\",\n    });\n  }\n\n  mergeInto(\n    _targetList: List,\n    _deleteSourceAfterMerge: boolean,\n  ): Promise<void> {\n    throw new TRPCError({\n      code: \"BAD_REQUEST\",\n      message: \"Smart lists cannot be merged\",\n    });\n  }\n}\n\nexport class ManualList extends List {\n  constructor(\n    ctx: AuthedContext,\n    list: ZBookmarkList & { userId: string },\n    private collaboratorEntry: ListCollaboratorEntry | null,\n  ) {\n    super(ctx, list);\n  }\n\n  get type(): \"manual\" {\n    invariant(this.list.type === \"manual\");\n    return this.list.type;\n  }\n\n  async getBookmarkIds(_visitedListIds?: Set<string>): Promise<string[]> {\n    const results = await this.ctx.db\n      .select({ id: bookmarksInLists.bookmarkId })\n      .from(bookmarksInLists)\n      .where(eq(bookmarksInLists.listId, this.list.id));\n    return results.map((r) => r.id);\n  }\n\n  async getSize(): Promise<number> {\n    const results = await this.ctx.db\n      .select({ count: count() })\n      .from(bookmarksInLists)\n      .where(eq(bookmarksInLists.listId, this.list.id));\n    return results[0].count;\n  }\n\n  async addBookmark(bookmarkId: string): Promise<void> {\n    this.ensureCanEdit();\n\n    try {\n      await this.ctx.db.insert(bookmarksInLists).values({\n        listId: this.list.id,\n        bookmarkId,\n        listMembershipId: this.collaboratorEntry?.membershipId,\n      });\n      await triggerRuleEngineOnEvent(bookmarkId, [\n        {\n          type: \"addedToList\",\n          listId: this.list.id,\n        },\n      ]);\n    } catch (e) {\n      if (e instanceof SqliteError) {\n        if (e.code == \"SQLITE_CONSTRAINT_PRIMARYKEY\") {\n          // this is fine, it just means the bookmark is already in the list\n          return;\n        }\n      }\n      throw new TRPCError({\n        code: \"INTERNAL_SERVER_ERROR\",\n        message: \"Something went wrong\",\n      });\n    }\n  }\n\n  async removeBookmark(bookmarkId: string): Promise<void> {\n    // Check that the user can edit this list\n    this.ensureCanEdit();\n\n    const deleted = await this.ctx.db\n      .delete(bookmarksInLists)\n      .where(\n        and(\n          eq(bookmarksInLists.listId, this.list.id),\n          eq(bookmarksInLists.bookmarkId, bookmarkId),\n        ),\n      );\n    if (deleted.changes == 0) {\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message: `Bookmark ${bookmarkId} is already not in list ${this.list.id}`,\n      });\n    }\n    await triggerRuleEngineOnEvent(bookmarkId, [\n      {\n        type: \"removedFromList\",\n        listId: this.list.id,\n      },\n    ]);\n  }\n\n  async update(input: z.infer<typeof zEditBookmarkListSchemaWithValidation>) {\n    if (input.query) {\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message: \"Manual lists cannot have a query\",\n      });\n    }\n    return super.update(input);\n  }\n\n  async mergeInto(\n    targetList: List,\n    deleteSourceAfterMerge: boolean,\n  ): Promise<void> {\n    this.ensureCanManage();\n    targetList.ensureCanManage();\n    if (targetList.type !== \"manual\") {\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message: \"You can only merge into a manual list\",\n      });\n    }\n\n    const bookmarkIds = await this.getBookmarkIds();\n\n    await this.ctx.db.transaction(async (tx) => {\n      await tx\n        .insert(bookmarksInLists)\n        .values(\n          bookmarkIds.map((id) => ({\n            bookmarkId: id,\n            listId: targetList.id,\n          })),\n        )\n        .onConflictDoNothing();\n\n      if (deleteSourceAfterMerge) {\n        await tx\n          .delete(bookmarkLists)\n          .where(eq(bookmarkLists.id, this.list.id));\n      }\n    });\n  }\n}\n"
  },
  {
    "path": "packages/trpc/models/rules.ts",
    "content": "import { TRPCError } from \"@trpc/server\";\nimport { and, eq } from \"drizzle-orm\";\nimport { z } from \"zod\";\n\nimport { db as DONT_USE_DB } from \"@karakeep/db\";\nimport {\n  ruleEngineActionsTable,\n  ruleEngineRulesTable,\n} from \"@karakeep/db/schema\";\nimport {\n  RuleEngineRule,\n  zNewRuleEngineRuleSchema,\n  zRuleEngineActionSchema,\n  zRuleEngineConditionSchema,\n  zRuleEngineEventSchema,\n  zUpdateRuleEngineRuleSchema,\n} from \"@karakeep/shared/types/rules\";\n\nimport { AuthedContext } from \"..\";\n\nfunction dummy_fetchRule(ctx: AuthedContext, id: string) {\n  return DONT_USE_DB.query.ruleEngineRulesTable.findFirst({\n    where: and(\n      eq(ruleEngineRulesTable.id, id),\n      eq(ruleEngineRulesTable.userId, ctx.user.id),\n    ),\n    with: {\n      actions: true, // Assuming actions are related; adjust if needed\n    },\n  });\n}\n\ntype FetchedRuleType = NonNullable<Awaited<ReturnType<typeof dummy_fetchRule>>>;\n\nexport class RuleEngineRuleModel {\n  protected constructor(\n    protected ctx: AuthedContext,\n    public rule: RuleEngineRule & { userId: string },\n  ) {}\n\n  private static fromData(\n    ctx: AuthedContext,\n    ruleData: FetchedRuleType,\n  ): RuleEngineRuleModel {\n    return new RuleEngineRuleModel(ctx, {\n      id: ruleData.id,\n      userId: ruleData.userId,\n      name: ruleData.name,\n      description: ruleData.description,\n      enabled: ruleData.enabled,\n      event: zRuleEngineEventSchema.parse(JSON.parse(ruleData.event)),\n      condition: zRuleEngineConditionSchema.parse(\n        JSON.parse(ruleData.condition),\n      ),\n      actions: ruleData.actions.map((a) =>\n        zRuleEngineActionSchema.parse(JSON.parse(a.action)),\n      ),\n    });\n  }\n\n  static async fromId(\n    ctx: AuthedContext,\n    id: string,\n  ): Promise<RuleEngineRuleModel> {\n    const ruleData = await ctx.db.query.ruleEngineRulesTable.findFirst({\n      where: and(\n        eq(ruleEngineRulesTable.id, id),\n        eq(ruleEngineRulesTable.userId, ctx.user.id),\n      ),\n      with: {\n        actions: true, // Assuming actions are related; adjust if needed\n      },\n    });\n\n    if (!ruleData) {\n      throw new TRPCError({\n        code: \"NOT_FOUND\",\n        message: \"Rule not found\",\n      });\n    }\n\n    return this.fromData(ctx, ruleData);\n  }\n\n  static async create(\n    ctx: AuthedContext,\n    input: z.infer<typeof zNewRuleEngineRuleSchema>,\n  ): Promise<RuleEngineRuleModel> {\n    // Similar to lists create, but for rules\n    const insertedRule = await ctx.db.transaction(async (tx) => {\n      const [newRule] = await tx\n        .insert(ruleEngineRulesTable)\n        .values({\n          name: input.name,\n          description: input.description,\n          enabled: input.enabled,\n          event: JSON.stringify(input.event),\n          condition: JSON.stringify(input.condition),\n          userId: ctx.user.id,\n          listId:\n            input.event.type === \"addedToList\" ||\n            input.event.type === \"removedFromList\"\n              ? input.event.listId\n              : null,\n          tagId:\n            input.event.type === \"tagAdded\" || input.event.type === \"tagRemoved\"\n              ? input.event.tagId\n              : null,\n        })\n        .returning();\n\n      if (input.actions.length > 0) {\n        await tx.insert(ruleEngineActionsTable).values(\n          input.actions.map((action) => ({\n            ruleId: newRule.id,\n            userId: ctx.user.id,\n            action: JSON.stringify(action),\n            listId:\n              action.type === \"addToList\" || action.type === \"removeFromList\"\n                ? action.listId\n                : null,\n            tagId:\n              action.type === \"addTag\" || action.type === \"removeTag\"\n                ? action.tagId\n                : null,\n          })),\n        );\n      }\n      return newRule;\n    });\n\n    // Fetch the full rule after insertion\n    return await RuleEngineRuleModel.fromId(ctx, insertedRule.id);\n  }\n\n  async update(\n    input: z.infer<typeof zUpdateRuleEngineRuleSchema>,\n  ): Promise<void> {\n    if (this.rule.id !== input.id) {\n      throw new TRPCError({ code: \"BAD_REQUEST\", message: \"ID mismatch\" });\n    }\n\n    await this.ctx.db.transaction(async (tx) => {\n      const result = await tx\n        .update(ruleEngineRulesTable)\n        .set({\n          name: input.name,\n          description: input.description,\n          enabled: input.enabled,\n          event: JSON.stringify(input.event),\n          condition: JSON.stringify(input.condition),\n          listId:\n            input.event.type === \"addedToList\" ||\n            input.event.type === \"removedFromList\"\n              ? input.event.listId\n              : null,\n          tagId:\n            input.event.type === \"tagAdded\" || input.event.type === \"tagRemoved\"\n              ? input.event.tagId\n              : null,\n        })\n        .where(\n          and(\n            eq(ruleEngineRulesTable.id, input.id),\n            eq(ruleEngineRulesTable.userId, this.ctx.user.id),\n          ),\n        );\n\n      if (result.changes === 0) {\n        throw new TRPCError({ code: \"NOT_FOUND\", message: \"Rule not found\" });\n      }\n\n      if (input.actions.length > 0) {\n        await tx\n          .delete(ruleEngineActionsTable)\n          .where(eq(ruleEngineActionsTable.ruleId, input.id));\n        await tx.insert(ruleEngineActionsTable).values(\n          input.actions.map((action) => ({\n            ruleId: input.id,\n            userId: this.ctx.user.id,\n            action: JSON.stringify(action),\n            listId:\n              action.type === \"addToList\" || action.type === \"removeFromList\"\n                ? action.listId\n                : null,\n            tagId:\n              action.type === \"addTag\" || action.type === \"removeTag\"\n                ? action.tagId\n                : null,\n          })),\n        );\n      }\n    });\n\n    this.rule = await RuleEngineRuleModel.fromId(this.ctx, this.rule.id).then(\n      (r) => r.rule,\n    );\n  }\n\n  async delete(): Promise<void> {\n    const result = await this.ctx.db\n      .delete(ruleEngineRulesTable)\n      .where(\n        and(\n          eq(ruleEngineRulesTable.id, this.rule.id),\n          eq(ruleEngineRulesTable.userId, this.ctx.user.id),\n        ),\n      );\n\n    if (result.changes === 0) {\n      throw new TRPCError({ code: \"NOT_FOUND\", message: \"Rule not found\" });\n    }\n  }\n\n  static async getAll(ctx: AuthedContext): Promise<RuleEngineRuleModel[]> {\n    const rulesData = await ctx.db.query.ruleEngineRulesTable.findMany({\n      where: eq(ruleEngineRulesTable.userId, ctx.user.id),\n      with: { actions: true },\n    });\n\n    return rulesData.map((r) => this.fromData(ctx, r));\n  }\n}\n"
  },
  {
    "path": "packages/trpc/models/tags.ts",
    "content": "import { TRPCError } from \"@trpc/server\";\nimport {\n  and,\n  asc,\n  count,\n  desc,\n  eq,\n  gt,\n  inArray,\n  like,\n  notExists,\n  sql,\n} from \"drizzle-orm\";\nimport { z } from \"zod\";\n\nimport type { ZAttachedByEnum } from \"@karakeep/shared/types/tags\";\nimport { SqliteError } from \"@karakeep/db\";\nimport { bookmarkTags, tagsOnBookmarks } from \"@karakeep/db/schema\";\nimport { triggerSearchReindex } from \"@karakeep/shared-server\";\nimport {\n  zCreateTagRequestSchema,\n  zGetTagResponseSchema,\n  zTagBasicSchema,\n  zUpdateTagRequestSchema,\n} from \"@karakeep/shared/types/tags\";\nimport { switchCase } from \"@karakeep/shared/utils/switch\";\n\nimport { AuthedContext } from \"..\";\n\nexport class Tag {\n  constructor(\n    protected ctx: AuthedContext,\n    public tag: typeof bookmarkTags.$inferSelect,\n  ) {}\n\n  static async fromId(ctx: AuthedContext, id: string): Promise<Tag> {\n    const tag = await ctx.db.query.bookmarkTags.findFirst({\n      where: eq(bookmarkTags.id, id),\n    });\n\n    if (!tag) {\n      throw new TRPCError({\n        code: \"NOT_FOUND\",\n        message: \"Tag not found\",\n      });\n    }\n\n    // If it exists but belongs to another user, throw forbidden error\n    if (tag.userId !== ctx.user.id) {\n      throw new TRPCError({\n        code: \"FORBIDDEN\",\n        message: \"User is not allowed to access resource\",\n      });\n    }\n\n    return new Tag(ctx, tag);\n  }\n\n  static async create(\n    ctx: AuthedContext,\n    input: z.infer<typeof zCreateTagRequestSchema>,\n  ): Promise<Tag> {\n    try {\n      const [result] = await ctx.db\n        .insert(bookmarkTags)\n        .values({\n          name: input.name,\n          userId: ctx.user.id,\n        })\n        .returning();\n\n      return new Tag(ctx, result);\n    } catch (e) {\n      if (e instanceof SqliteError && e.code === \"SQLITE_CONSTRAINT_UNIQUE\") {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"Tag name already exists for this user.\",\n        });\n      }\n      throw e;\n    }\n  }\n\n  static async getAll(\n    ctx: AuthedContext,\n    opts: {\n      nameContains?: string;\n      ids?: string[];\n      attachedBy?: \"ai\" | \"human\" | \"none\";\n      sortBy?: \"name\" | \"usage\" | \"relevance\";\n      pagination?: {\n        page: number;\n        limit: number;\n      };\n    } = {},\n  ) {\n    const sortBy = opts.sortBy ?? \"usage\";\n\n    const countAi = sql<number>`\n      SUM(CASE WHEN ${tagsOnBookmarks.attachedBy} = 'ai' THEN 1 ELSE 0 END)\n    `;\n    const countHuman = sql<number>`\n      SUM(CASE WHEN ${tagsOnBookmarks.attachedBy} = 'human' THEN 1 ELSE 0 END)\n    `;\n    // Count only matched right rows; will be 0 when there are none\n    const countAny = sql<number>`COUNT(${tagsOnBookmarks.tagId})`;\n    let qSql = ctx.db\n      .select({\n        id: bookmarkTags.id,\n        name: bookmarkTags.name,\n        countAttachedByAi: countAi.as(\"countAttachedByAi\"),\n        countAttachedByHuman: countHuman.as(\"countAttachedByHuman\"),\n        count: countAny.as(\"count\"),\n      })\n      .from(bookmarkTags)\n      .leftJoin(tagsOnBookmarks, eq(bookmarkTags.id, tagsOnBookmarks.tagId))\n      .where(\n        and(\n          eq(bookmarkTags.userId, ctx.user.id),\n          opts.nameContains\n            ? like(bookmarkTags.name, `%${opts.nameContains}%`)\n            : undefined,\n          opts.ids && opts.ids.length > 0\n            ? inArray(bookmarkTags.id, opts.ids)\n            : undefined,\n        ),\n      )\n      .groupBy(bookmarkTags.id, bookmarkTags.name)\n      .orderBy(\n        ...switchCase(sortBy, {\n          name: [asc(bookmarkTags.name)],\n          usage: [desc(sql`count`)],\n          relevance: [\n            desc(sql<number>`\n            CASE\n              WHEN lower(${opts.nameContains ?? \"\"}) = lower(${bookmarkTags.name}) THEN 2\n              WHEN ${bookmarkTags.name} LIKE ${opts.nameContains ? opts.nameContains + \"%\" : \"\"} THEN 1\n              ELSE 0\n            END`),\n            asc(sql<number>`length(${bookmarkTags.name})`),\n          ],\n        }),\n      )\n      .having(\n        opts.attachedBy\n          ? switchCase(opts.attachedBy, {\n              ai: and(eq(countHuman, 0), gt(countAi, 0)),\n              human: gt(countHuman, 0),\n              none: eq(countAny, 0),\n            })\n          : undefined,\n      );\n\n    if (opts.pagination) {\n      qSql.offset(opts.pagination.page * opts.pagination.limit);\n      qSql.limit(opts.pagination.limit + 1);\n    }\n    const tags = await qSql;\n\n    let nextCursor = null;\n    if (opts.pagination) {\n      if (tags.length > opts.pagination.limit) {\n        tags.pop();\n        nextCursor = {\n          page: opts.pagination.page + 1,\n        };\n      }\n    }\n\n    return {\n      tags: tags.map((t) => ({\n        id: t.id,\n        name: t.name,\n        numBookmarks: t.count,\n        numBookmarksByAttachedType: {\n          ai: t.countAttachedByAi,\n          human: t.countAttachedByHuman,\n        },\n      })),\n      nextCursor,\n    };\n  }\n\n  static async deleteUnused(ctx: AuthedContext): Promise<number> {\n    const res = await ctx.db\n      .delete(bookmarkTags)\n      .where(\n        and(\n          eq(bookmarkTags.userId, ctx.user.id),\n          notExists(\n            ctx.db\n              .select({ id: tagsOnBookmarks.tagId })\n              .from(tagsOnBookmarks)\n              .where(eq(tagsOnBookmarks.tagId, bookmarkTags.id)),\n          ),\n        ),\n      );\n    return res.changes;\n  }\n\n  static async merge(\n    ctx: AuthedContext,\n    input: {\n      intoTagId: string;\n      fromTagIds: string[];\n    },\n  ): Promise<{\n    mergedIntoTagId: string;\n    deletedTags: string[];\n  }> {\n    const requestedTags = new Set([input.intoTagId, ...input.fromTagIds]);\n    if (requestedTags.size === 0) {\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message: \"No tags provided\",\n      });\n    }\n    if (input.fromTagIds.includes(input.intoTagId)) {\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message: \"Cannot merge tag into itself\",\n      });\n    }\n\n    const affectedTags = await ctx.db.query.bookmarkTags.findMany({\n      where: and(\n        eq(bookmarkTags.userId, ctx.user.id),\n        inArray(bookmarkTags.id, [...requestedTags]),\n      ),\n      columns: {\n        id: true,\n        userId: true,\n      },\n    });\n\n    if (affectedTags.some((t) => t.userId !== ctx.user.id)) {\n      throw new TRPCError({\n        code: \"FORBIDDEN\",\n        message: \"User is not allowed to access resource\",\n      });\n    }\n    if (affectedTags.length !== requestedTags.size) {\n      throw new TRPCError({\n        code: \"NOT_FOUND\",\n        message: \"One or more tags not found\",\n      });\n    }\n\n    const { deletedTags, affectedBookmarks } = await ctx.db.transaction(\n      async (trx) => {\n        const unlinked = await trx\n          .delete(tagsOnBookmarks)\n          .where(and(inArray(tagsOnBookmarks.tagId, input.fromTagIds)))\n          .returning();\n\n        if (unlinked.length > 0) {\n          await trx\n            .insert(tagsOnBookmarks)\n            .values(\n              unlinked.map((u) => ({\n                ...u,\n                tagId: input.intoTagId,\n              })),\n            )\n            .onConflictDoNothing();\n        }\n\n        const deletedTags = await trx\n          .delete(bookmarkTags)\n          .where(\n            and(\n              inArray(bookmarkTags.id, input.fromTagIds),\n              eq(bookmarkTags.userId, ctx.user.id),\n            ),\n          )\n          .returning({ id: bookmarkTags.id });\n\n        return {\n          deletedTags,\n          affectedBookmarks: unlinked.map((u) => u.bookmarkId),\n        };\n      },\n    );\n\n    try {\n      await Promise.all(\n        affectedBookmarks.map((id) =>\n          triggerSearchReindex(id, {\n            groupId: ctx.user.id,\n          }),\n        ),\n      );\n    } catch (e) {\n      console.error(\"Failed to reindex affected bookmarks\", e);\n    }\n\n    return {\n      deletedTags: deletedTags.map((t) => t.id),\n      mergedIntoTagId: input.intoTagId,\n    };\n  }\n\n  async delete(): Promise<void> {\n    const affectedBookmarks = await this.ctx.db\n      .select({\n        bookmarkId: tagsOnBookmarks.bookmarkId,\n      })\n      .from(tagsOnBookmarks)\n      .where(eq(tagsOnBookmarks.tagId, this.tag.id));\n\n    const res = await this.ctx.db\n      .delete(bookmarkTags)\n      .where(\n        and(\n          eq(bookmarkTags.id, this.tag.id),\n          eq(bookmarkTags.userId, this.ctx.user.id),\n        ),\n      );\n\n    if (res.changes === 0) {\n      throw new TRPCError({ code: \"NOT_FOUND\" });\n    }\n\n    await Promise.all(\n      affectedBookmarks.map(({ bookmarkId }) =>\n        triggerSearchReindex(bookmarkId, {\n          groupId: this.ctx.user.id,\n        }),\n      ),\n    );\n  }\n\n  async update(input: z.infer<typeof zUpdateTagRequestSchema>): Promise<void> {\n    try {\n      const result = await this.ctx.db\n        .update(bookmarkTags)\n        .set({\n          name: input.name,\n        })\n        .where(\n          and(\n            eq(bookmarkTags.id, this.tag.id),\n            eq(bookmarkTags.userId, this.ctx.user.id),\n          ),\n        )\n        .returning();\n\n      if (result.length === 0) {\n        throw new TRPCError({ code: \"NOT_FOUND\" });\n      }\n\n      this.tag = result[0];\n\n      try {\n        const affectedBookmarks =\n          await this.ctx.db.query.tagsOnBookmarks.findMany({\n            where: eq(tagsOnBookmarks.tagId, this.tag.id),\n            columns: {\n              bookmarkId: true,\n            },\n          });\n        await Promise.all(\n          affectedBookmarks\n            .map((b) => b.bookmarkId)\n            .map((id) =>\n              triggerSearchReindex(id, { groupId: this.ctx.user.id }),\n            ),\n        );\n      } catch (e) {\n        console.error(\"Failed to reindex affected bookmarks\", e);\n      }\n    } catch (e) {\n      if (e instanceof SqliteError) {\n        if (e.code === \"SQLITE_CONSTRAINT_UNIQUE\") {\n          throw new TRPCError({\n            code: \"BAD_REQUEST\",\n            message:\n              \"Tag name already exists. You might want to consider a merge instead.\",\n          });\n        }\n      }\n      throw e;\n    }\n  }\n\n  static _aggregateStats(\n    res: { attachedBy: \"ai\" | \"human\" | null; count: number }[],\n  ) {\n    const numBookmarksByAttachedType = res.reduce<\n      Record<ZAttachedByEnum, number>\n    >(\n      (acc, curr) => {\n        if (curr.attachedBy) {\n          acc[curr.attachedBy] += curr.count;\n        }\n        return acc;\n      },\n      { ai: 0, human: 0 },\n    );\n    return {\n      numBookmarks:\n        numBookmarksByAttachedType.ai + numBookmarksByAttachedType.human,\n      numBookmarksByAttachedType,\n    };\n  }\n\n  async getStats(): Promise<z.infer<typeof zGetTagResponseSchema>> {\n    const res = await this.ctx.db\n      .select({\n        id: bookmarkTags.id,\n        name: bookmarkTags.name,\n        attachedBy: tagsOnBookmarks.attachedBy,\n        count: count(),\n      })\n      .from(bookmarkTags)\n      .leftJoin(tagsOnBookmarks, eq(bookmarkTags.id, tagsOnBookmarks.tagId))\n      .where(\n        and(\n          eq(bookmarkTags.id, this.tag.id),\n          eq(bookmarkTags.userId, this.ctx.user.id),\n        ),\n      )\n      .groupBy(tagsOnBookmarks.attachedBy);\n\n    if (res.length === 0) {\n      throw new TRPCError({ code: \"NOT_FOUND\" });\n    }\n\n    return {\n      id: res[0].id,\n      name: res[0].name,\n      ...Tag._aggregateStats(res),\n    };\n  }\n\n  asBasicTag(): z.infer<typeof zTagBasicSchema> {\n    return {\n      id: this.tag.id,\n      name: this.tag.name,\n    };\n  }\n}\n"
  },
  {
    "path": "packages/trpc/models/users.ts",
    "content": "import { randomBytes } from \"crypto\";\nimport { TRPCError } from \"@trpc/server\";\nimport { and, count, desc, eq, gte, lte, sql } from \"drizzle-orm\";\nimport invariant from \"tiny-invariant\";\nimport { z } from \"zod\";\n\nimport { SqliteError } from \"@karakeep/db\";\nimport {\n  assets,\n  AssetTypes,\n  bookmarkLinks,\n  bookmarkLists,\n  bookmarks,\n  bookmarkTags,\n  highlights,\n  passwordResetTokens,\n  tagsOnBookmarks,\n  users,\n  verificationTokens,\n} from \"@karakeep/db/schema\";\nimport { deleteAsset, deleteUserAssets } from \"@karakeep/shared/assetdb\";\nimport serverConfig from \"@karakeep/shared/config\";\nimport {\n  zResetPasswordSchema,\n  zSignUpSchema,\n  zUpdateUserSettingsSchema,\n  zUserSettingsSchema,\n  zUserStatsResponseSchema,\n  zWhoAmIResponseSchema,\n  zWrappedStatsResponseSchema,\n} from \"@karakeep/shared/types/users\";\n\nimport { AuthedContext, Context } from \"..\";\nimport { generatePasswordSalt, hashPassword, validatePassword } from \"../auth\";\nimport { sendPasswordResetEmail, sendVerificationEmail } from \"../email\";\n\nexport class User {\n  constructor(\n    protected ctx: AuthedContext,\n    public user: typeof users.$inferSelect,\n  ) {}\n\n  static async fromId_DANGEROUS(ctx: AuthedContext, id: string): Promise<User> {\n    const user = await ctx.db.query.users.findFirst({\n      where: eq(users.id, id),\n    });\n\n    if (!user) {\n      throw new TRPCError({\n        code: \"NOT_FOUND\",\n        message: \"User not found\",\n      });\n    }\n\n    return new User(ctx, user);\n  }\n\n  static async fromCtx(ctx: AuthedContext): Promise<User> {\n    return this.fromId_DANGEROUS(ctx, ctx.user.id);\n  }\n\n  static async create(\n    ctx: Context,\n    input: z.infer<typeof zSignUpSchema> & { redirectUrl?: string },\n    role?: \"user\" | \"admin\",\n  ) {\n    const salt = generatePasswordSalt();\n    const user = await User.createRaw(ctx.db, {\n      name: input.name,\n      email: input.email,\n      password: await hashPassword(input.password, salt),\n      salt,\n      role,\n    });\n\n    if (serverConfig.auth.emailVerificationRequired) {\n      const token = await User.genEmailVerificationToken(ctx.db, input.email);\n      try {\n        await sendVerificationEmail(\n          input.email,\n          input.name,\n          token,\n          input.redirectUrl,\n        );\n      } catch (error) {\n        console.error(\"Failed to send verification email:\", error);\n      }\n    }\n\n    return user;\n  }\n\n  static async createRaw(\n    db: Context[\"db\"],\n    input: {\n      name: string;\n      email: string;\n      password?: string;\n      salt?: string;\n      role?: \"user\" | \"admin\";\n      emailVerified?: Date | null;\n    },\n  ) {\n    return await db.transaction(async (trx) => {\n      let userRole = input.role;\n      if (!userRole) {\n        const [{ count: userCount }] = await trx\n          .select({ count: count() })\n          .from(users);\n        userRole = userCount === 0 ? \"admin\" : \"user\";\n      }\n\n      try {\n        const [result] = await trx\n          .insert(users)\n          .values({\n            name: input.name,\n            email: input.email,\n            password: input.password,\n            salt: input.salt,\n            role: userRole,\n            emailVerified: input.emailVerified,\n            bookmarkQuota: serverConfig.quotas.free.bookmarkLimit,\n            storageQuota: serverConfig.quotas.free.assetSizeBytes,\n          })\n          .returning();\n\n        return result;\n      } catch (e) {\n        if (e instanceof SqliteError) {\n          if (e.code === \"SQLITE_CONSTRAINT_UNIQUE\") {\n            throw new TRPCError({\n              code: \"BAD_REQUEST\",\n              message: \"Email is already taken\",\n            });\n          }\n        }\n        throw new TRPCError({\n          code: \"INTERNAL_SERVER_ERROR\",\n          message: \"Something went wrong\",\n        });\n      }\n    });\n  }\n\n  static async getAll(ctx: AuthedContext): Promise<User[]> {\n    const dbUsers = await ctx.db.select().from(users);\n\n    return dbUsers.map((u) => new User(ctx, u));\n  }\n\n  static async genEmailVerificationToken(\n    db: Context[\"db\"],\n    email: string,\n  ): Promise<string> {\n    const token = randomBytes(10).toString(\"hex\");\n    const expires = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours\n\n    await db.insert(verificationTokens).values({\n      identifier: email,\n      token,\n      expires,\n    });\n\n    return token;\n  }\n\n  static async verifyEmailToken(\n    db: Context[\"db\"],\n    email: string,\n    token: string,\n  ): Promise<boolean> {\n    const verificationToken = await db.query.verificationTokens.findFirst({\n      where: (vt, { and, eq }) =>\n        and(eq(vt.identifier, email), eq(vt.token, token)),\n    });\n\n    if (!verificationToken) {\n      return false;\n    }\n\n    if (verificationToken.expires < new Date()) {\n      await db\n        .delete(verificationTokens)\n        .where(\n          and(\n            eq(verificationTokens.identifier, email),\n            eq(verificationTokens.token, token),\n          ),\n        );\n      return false;\n    }\n\n    await db\n      .delete(verificationTokens)\n      .where(\n        and(\n          eq(verificationTokens.identifier, email),\n          eq(verificationTokens.token, token),\n        ),\n      );\n\n    return true;\n  }\n\n  static async verifyEmail(\n    ctx: Context,\n    email: string,\n    token: string,\n  ): Promise<void> {\n    const isValid = await User.verifyEmailToken(ctx.db, email, token);\n    if (!isValid) {\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message: \"Invalid or expired verification token\",\n      });\n    }\n\n    const result = await ctx.db\n      .update(users)\n      .set({ emailVerified: new Date() })\n      .where(eq(users.email, email));\n\n    if (result.changes === 0) {\n      throw new TRPCError({\n        code: \"NOT_FOUND\",\n        message: \"User not found\",\n      });\n    }\n  }\n\n  static async resendVerificationEmail(\n    ctx: Context,\n    email: string,\n    redirectUrl?: string,\n  ): Promise<void> {\n    if (\n      !serverConfig.auth.emailVerificationRequired ||\n      !serverConfig.email.smtp\n    ) {\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message: \"Email verification is not enabled\",\n      });\n    }\n\n    const user = await ctx.db.query.users.findFirst({\n      where: eq(users.email, email),\n    });\n\n    if (!user) {\n      return; // Don't reveal if user exists or not for security\n    }\n\n    if (user.emailVerified) {\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message: \"Email is already verified\",\n      });\n    }\n\n    const token = await User.genEmailVerificationToken(ctx.db, email);\n    try {\n      await sendVerificationEmail(email, user.name, token, redirectUrl);\n    } catch (error) {\n      console.error(\"Failed to send verification email:\", error);\n      throw new TRPCError({\n        code: \"INTERNAL_SERVER_ERROR\",\n        message: \"Failed to send verification email\",\n      });\n    }\n  }\n\n  static async forgotPassword(ctx: Context, email: string): Promise<void> {\n    if (!serverConfig.email.smtp) {\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message: \"Email service is not configured\",\n      });\n    }\n\n    const user = await ctx.db.query.users.findFirst({\n      where: eq(users.email, email),\n    });\n\n    if (!user || !user.password) {\n      return; // Don't reveal if user exists or not for security\n    }\n\n    try {\n      const token = randomBytes(32).toString(\"hex\");\n      const expires = new Date(Date.now() + 60 * 60 * 1000); // 1 hour\n\n      await ctx.db.insert(passwordResetTokens).values({\n        userId: user.id,\n        token,\n        expires,\n      });\n\n      await sendPasswordResetEmail(email, user.name, token);\n    } catch (error) {\n      console.error(\"Failed to send password reset email:\", error);\n      throw new TRPCError({\n        code: \"INTERNAL_SERVER_ERROR\",\n        message: \"Failed to send password reset email\",\n      });\n    }\n  }\n\n  static async resetPassword(\n    ctx: Context,\n    input: z.infer<typeof zResetPasswordSchema>,\n  ): Promise<void> {\n    const resetToken = await ctx.db.query.passwordResetTokens.findFirst({\n      where: eq(passwordResetTokens.token, input.token),\n      with: {\n        user: {\n          columns: {\n            id: true,\n          },\n        },\n      },\n    });\n\n    if (!resetToken) {\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message: \"Invalid or expired reset token\",\n      });\n    }\n\n    if (resetToken.expires < new Date()) {\n      await ctx.db\n        .delete(passwordResetTokens)\n        .where(eq(passwordResetTokens.token, input.token));\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message: \"Invalid or expired reset token\",\n      });\n    }\n\n    if (!resetToken.user) {\n      throw new TRPCError({\n        code: \"NOT_FOUND\",\n        message: \"User not found\",\n      });\n    }\n\n    const newSalt = generatePasswordSalt();\n    const hashedPassword = await hashPassword(input.newPassword, newSalt);\n\n    await ctx.db\n      .update(users)\n      .set({\n        password: hashedPassword,\n        salt: newSalt,\n      })\n      .where(eq(users.id, resetToken.user.id));\n\n    await ctx.db\n      .delete(passwordResetTokens)\n      .where(eq(passwordResetTokens.token, input.token));\n  }\n\n  private static async deleteInternal(db: Context[\"db\"], userId: string) {\n    const res = await db.delete(users).where(eq(users.id, userId));\n\n    if (res.changes === 0) {\n      throw new TRPCError({ code: \"NOT_FOUND\" });\n    }\n\n    await deleteUserAssets({ userId: userId });\n  }\n\n  static async deleteAsAdmin(\n    adminCtx: AuthedContext,\n    userId: string,\n  ): Promise<void> {\n    invariant(adminCtx.user.role === \"admin\", \"Only admins can delete users\");\n    await this.deleteInternal(adminCtx.db, userId);\n  }\n\n  async deleteAccount(password?: string): Promise<void> {\n    invariant(this.ctx.user.email, \"A user always has an email specified\");\n\n    if (this.user.password) {\n      if (!password) {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"Password is required for local accounts\",\n        });\n      }\n\n      try {\n        await validatePassword(this.ctx.user.email, password, this.ctx.db);\n      } catch {\n        throw new TRPCError({\n          code: \"UNAUTHORIZED\",\n          message: \"Invalid password\",\n        });\n      }\n    }\n\n    await User.deleteInternal(this.ctx.db, this.user.id);\n  }\n\n  async changePassword(\n    currentPassword: string,\n    newPassword: string,\n  ): Promise<void> {\n    invariant(this.ctx.user.email, \"A user always has an email specified\");\n\n    try {\n      const user = await validatePassword(\n        this.ctx.user.email,\n        currentPassword,\n        this.ctx.db,\n      );\n      invariant(user.id === this.ctx.user.id);\n    } catch {\n      throw new TRPCError({ code: \"UNAUTHORIZED\" });\n    }\n\n    const newSalt = generatePasswordSalt();\n    await this.ctx.db\n      .update(users)\n      .set({\n        password: await hashPassword(newPassword, newSalt),\n        salt: newSalt,\n      })\n      .where(eq(users.id, this.user.id));\n  }\n\n  async getSettings(): Promise<z.infer<typeof zUserSettingsSchema>> {\n    const settings = await this.ctx.db.query.users.findFirst({\n      where: eq(users.id, this.user.id),\n      columns: {\n        bookmarkClickAction: true,\n        archiveDisplayBehaviour: true,\n        timezone: true,\n        backupsEnabled: true,\n        backupsFrequency: true,\n        backupsRetentionDays: true,\n        readerFontSize: true,\n        readerLineHeight: true,\n        readerFontFamily: true,\n        autoTaggingEnabled: true,\n        autoSummarizationEnabled: true,\n        tagStyle: true,\n        curatedTagIds: true,\n        inferredTagLang: true,\n      },\n    });\n\n    if (!settings) {\n      throw new TRPCError({\n        code: \"NOT_FOUND\",\n        message: \"User settings not found\",\n      });\n    }\n\n    return {\n      bookmarkClickAction: settings.bookmarkClickAction,\n      archiveDisplayBehaviour: settings.archiveDisplayBehaviour,\n      timezone: settings.timezone || \"UTC\",\n      backupsEnabled: settings.backupsEnabled,\n      backupsFrequency: settings.backupsFrequency,\n      backupsRetentionDays: settings.backupsRetentionDays,\n      readerFontSize: settings.readerFontSize,\n      readerLineHeight: settings.readerLineHeight,\n      readerFontFamily: settings.readerFontFamily,\n      autoTaggingEnabled: settings.autoTaggingEnabled,\n      autoSummarizationEnabled: settings.autoSummarizationEnabled,\n      tagStyle: settings.tagStyle ?? \"as-generated\",\n      curatedTagIds: settings.curatedTagIds ?? null,\n      inferredTagLang: settings.inferredTagLang,\n    };\n  }\n\n  async updateSettings(\n    input: z.infer<typeof zUpdateUserSettingsSchema>,\n  ): Promise<void> {\n    if (Object.keys(input).length === 0) {\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message: \"No settings provided\",\n      });\n    }\n\n    await this.ctx.db\n      .update(users)\n      .set({\n        bookmarkClickAction: input.bookmarkClickAction,\n        archiveDisplayBehaviour: input.archiveDisplayBehaviour,\n        timezone: input.timezone,\n        backupsEnabled: input.backupsEnabled,\n        backupsFrequency: input.backupsFrequency,\n        backupsRetentionDays: input.backupsRetentionDays,\n        readerFontSize: input.readerFontSize,\n        readerLineHeight: input.readerLineHeight,\n        readerFontFamily: input.readerFontFamily,\n        autoTaggingEnabled: input.autoTaggingEnabled,\n        autoSummarizationEnabled: input.autoSummarizationEnabled,\n        tagStyle: input.tagStyle,\n        curatedTagIds: input.curatedTagIds,\n        inferredTagLang: input.inferredTagLang,\n      })\n      .where(eq(users.id, this.user.id));\n  }\n\n  async updateAvatar(assetId: string | null): Promise<void> {\n    const previousImage = this.user.image ?? null;\n    const [asset, previousAsset] = await Promise.all([\n      assetId\n        ? this.ctx.db.query.assets.findFirst({\n            where: and(eq(assets.id, assetId), eq(assets.userId, this.user.id)),\n            columns: {\n              id: true,\n              bookmarkId: true,\n              contentType: true,\n              assetType: true,\n            },\n          })\n        : Promise.resolve(null),\n      previousImage && previousImage !== assetId\n        ? this.ctx.db.query.assets.findFirst({\n            where: and(\n              eq(assets.id, previousImage),\n              eq(assets.userId, this.user.id),\n            ),\n            columns: {\n              id: true,\n              bookmarkId: true,\n            },\n          })\n        : Promise.resolve(null),\n    ]);\n\n    if (assetId) {\n      if (!asset) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Avatar asset not found\",\n        });\n      }\n\n      if (asset.bookmarkId) {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"Avatar asset must not be attached to a bookmark\",\n        });\n      }\n\n      if (asset.contentType && !asset.contentType.startsWith(\"image/\")) {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"Avatar asset must be an image\",\n        });\n      }\n\n      if (\n        asset.assetType !== AssetTypes.AVATAR &&\n        asset.assetType !== AssetTypes.UNKNOWN\n      ) {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"Avatar asset type is not supported\",\n        });\n      }\n\n      if (asset.assetType !== AssetTypes.AVATAR) {\n        await this.ctx.db\n          .update(assets)\n          .set({ assetType: AssetTypes.AVATAR })\n          .where(eq(assets.id, asset.id));\n      }\n    }\n    if (previousImage === assetId) {\n      return;\n    }\n\n    await this.ctx.db.transaction(async (tx) => {\n      await tx\n        .update(users)\n        .set({ image: assetId })\n        .where(eq(users.id, this.user.id));\n\n      if (!previousImage || previousImage === assetId) {\n        return;\n      }\n\n      if (previousAsset && !previousAsset.bookmarkId) {\n        await tx.delete(assets).where(eq(assets.id, previousAsset.id));\n      }\n    });\n\n    this.user.image = assetId;\n\n    if (!previousImage || previousImage === assetId) {\n      return;\n    }\n\n    await deleteAsset({\n      userId: this.user.id,\n      assetId: previousImage,\n    }).catch(() => ({}));\n  }\n\n  async getStats(): Promise<z.infer<typeof zUserStatsResponseSchema>> {\n    const userObj = await this.ctx.db.query.users.findFirst({\n      where: eq(users.id, this.user.id),\n      columns: {\n        timezone: true,\n      },\n    });\n    const userTimezone = userObj?.timezone || \"UTC\";\n    const now = new Date();\n    const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);\n    const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);\n    const yearAgo = new Date(now.getTime() - 365 * 24 * 60 * 60 * 1000);\n\n    const [\n      [{ numBookmarks }],\n      [{ numFavorites }],\n      [{ numArchived }],\n      [{ numTags }],\n      [{ numLists }],\n      [{ numHighlights }],\n      bookmarksByType,\n      topDomains,\n      [{ totalAssetSize }],\n      assetsByType,\n      [{ thisWeek }],\n      [{ thisMonth }],\n      [{ thisYear }],\n      bookmarkTimestamps,\n      tagUsage,\n      bookmarksBySource,\n    ] = await Promise.all([\n      // Basic counts\n      this.ctx.db\n        .select({ numBookmarks: count() })\n        .from(bookmarks)\n        .where(eq(bookmarks.userId, this.user.id)),\n      this.ctx.db\n        .select({ numFavorites: count() })\n        .from(bookmarks)\n        .where(\n          and(\n            eq(bookmarks.userId, this.user.id),\n            eq(bookmarks.favourited, true),\n          ),\n        ),\n      this.ctx.db\n        .select({ numArchived: count() })\n        .from(bookmarks)\n        .where(\n          and(eq(bookmarks.userId, this.user.id), eq(bookmarks.archived, true)),\n        ),\n      this.ctx.db\n        .select({ numTags: count() })\n        .from(bookmarkTags)\n        .where(eq(bookmarkTags.userId, this.user.id)),\n      this.ctx.db\n        .select({ numLists: count() })\n        .from(bookmarkLists)\n        .where(eq(bookmarkLists.userId, this.user.id)),\n      this.ctx.db\n        .select({ numHighlights: count() })\n        .from(highlights)\n        .where(eq(highlights.userId, this.user.id)),\n\n      // Bookmarks by type\n      this.ctx.db\n        .select({\n          type: bookmarks.type,\n          count: count(),\n        })\n        .from(bookmarks)\n        .where(eq(bookmarks.userId, this.user.id))\n        .groupBy(bookmarks.type),\n\n      // Top domains\n      this.ctx.db\n        .select({\n          domain: sql<string>`CASE\n            WHEN ${bookmarkLinks.url} LIKE 'https://%' THEN\n              CASE\n                WHEN INSTR(SUBSTR(${bookmarkLinks.url}, 9), '/') > 0 THEN\n                  SUBSTR(${bookmarkLinks.url}, 9, INSTR(SUBSTR(${bookmarkLinks.url}, 9), '/') - 1)\n                ELSE\n                  SUBSTR(${bookmarkLinks.url}, 9)\n              END\n            WHEN ${bookmarkLinks.url} LIKE 'http://%' THEN\n              CASE\n                WHEN INSTR(SUBSTR(${bookmarkLinks.url}, 8), '/') > 0 THEN\n                  SUBSTR(${bookmarkLinks.url}, 8, INSTR(SUBSTR(${bookmarkLinks.url}, 8), '/') - 1)\n                ELSE\n                  SUBSTR(${bookmarkLinks.url}, 8)\n              END\n            ELSE\n              CASE\n                WHEN INSTR(${bookmarkLinks.url}, '/') > 0 THEN\n                  SUBSTR(${bookmarkLinks.url}, 1, INSTR(${bookmarkLinks.url}, '/') - 1)\n                ELSE\n                  ${bookmarkLinks.url}\n              END\n          END`,\n          count: count(),\n        })\n        .from(bookmarkLinks)\n        .innerJoin(bookmarks, eq(bookmarks.id, bookmarkLinks.id))\n        .where(eq(bookmarks.userId, this.user.id))\n        .groupBy(\n          sql`CASE\n          WHEN ${bookmarkLinks.url} LIKE 'https://%' THEN\n            CASE\n              WHEN INSTR(SUBSTR(${bookmarkLinks.url}, 9), '/') > 0 THEN\n                SUBSTR(${bookmarkLinks.url}, 9, INSTR(SUBSTR(${bookmarkLinks.url}, 9), '/') - 1)\n              ELSE\n                SUBSTR(${bookmarkLinks.url}, 9)\n            END\n          WHEN ${bookmarkLinks.url} LIKE 'http://%' THEN\n            CASE\n              WHEN INSTR(SUBSTR(${bookmarkLinks.url}, 8), '/') > 0 THEN\n                SUBSTR(${bookmarkLinks.url}, 8, INSTR(SUBSTR(${bookmarkLinks.url}, 8), '/') - 1)\n              ELSE\n                SUBSTR(${bookmarkLinks.url}, 8)\n            END\n          ELSE\n            CASE\n              WHEN INSTR(${bookmarkLinks.url}, '/') > 0 THEN\n                SUBSTR(${bookmarkLinks.url}, 1, INSTR(${bookmarkLinks.url}, '/') - 1)\n              ELSE\n                ${bookmarkLinks.url}\n            END\n        END`,\n        )\n        .orderBy(desc(count()))\n        .limit(10),\n\n      // Total asset size\n      this.ctx.db\n        .select({\n          totalAssetSize: sql<number>`COALESCE(SUM(${assets.size}), 0)`,\n        })\n        .from(assets)\n        .where(eq(assets.userId, this.user.id)),\n\n      // Assets by type\n      this.ctx.db\n        .select({\n          type: assets.assetType,\n          count: count(),\n          totalSize: sql<number>`COALESCE(SUM(${assets.size}), 0)`,\n        })\n        .from(assets)\n        .where(eq(assets.userId, this.user.id))\n        .groupBy(assets.assetType),\n\n      // Activity stats\n      this.ctx.db\n        .select({ thisWeek: count() })\n        .from(bookmarks)\n        .where(\n          and(\n            eq(bookmarks.userId, this.user.id),\n            gte(bookmarks.createdAt, weekAgo),\n          ),\n        ),\n      this.ctx.db\n        .select({ thisMonth: count() })\n        .from(bookmarks)\n        .where(\n          and(\n            eq(bookmarks.userId, this.user.id),\n            gte(bookmarks.createdAt, monthAgo),\n          ),\n        ),\n      this.ctx.db\n        .select({ thisYear: count() })\n        .from(bookmarks)\n        .where(\n          and(\n            eq(bookmarks.userId, this.user.id),\n            gte(bookmarks.createdAt, yearAgo),\n          ),\n        ),\n\n      // Get all bookmark timestamps for timezone conversion\n      this.ctx.db\n        .select({\n          createdAt: bookmarks.createdAt,\n        })\n        .from(bookmarks)\n        .where(eq(bookmarks.userId, this.user.id)),\n\n      // Tag usage\n      this.ctx.db\n        .select({\n          name: bookmarkTags.name,\n          count: count(),\n        })\n        .from(bookmarkTags)\n        .innerJoin(tagsOnBookmarks, eq(tagsOnBookmarks.tagId, bookmarkTags.id))\n        .where(eq(bookmarkTags.userId, this.user.id))\n        .groupBy(bookmarkTags.name)\n        .orderBy(desc(count()))\n        .limit(10),\n\n      // Bookmarks by source\n      this.ctx.db\n        .select({\n          source: bookmarks.source,\n          count: count(),\n        })\n        .from(bookmarks)\n        .where(eq(bookmarks.userId, this.user.id))\n        .groupBy(bookmarks.source)\n        .orderBy(desc(count())),\n    ]);\n\n    // Process bookmarks by type\n    const bookmarkTypeMap = { link: 0, text: 0, asset: 0 };\n    bookmarksByType.forEach((item) => {\n      if (item.type in bookmarkTypeMap) {\n        bookmarkTypeMap[item.type as keyof typeof bookmarkTypeMap] = item.count;\n      }\n    });\n\n    // Process timestamps with user timezone\n    const hourCounts = Array.from({ length: 24 }, () => 0);\n    const dayCounts = Array.from({ length: 7 }, () => 0);\n\n    bookmarkTimestamps.forEach(({ createdAt }) => {\n      if (createdAt) {\n        const date = new Date(createdAt);\n        const userDate = new Date(\n          date.toLocaleString(\"en-US\", { timeZone: userTimezone }),\n        );\n\n        const hour = userDate.getHours();\n        const day = userDate.getDay();\n\n        hourCounts[hour]++;\n        dayCounts[day]++;\n      }\n    });\n\n    const hourlyActivity = Array.from({ length: 24 }, (_, i) => ({\n      hour: i,\n      count: hourCounts[i],\n    }));\n\n    const dailyActivity = Array.from({ length: 7 }, (_, i) => ({\n      day: i,\n      count: dayCounts[i],\n    }));\n\n    return {\n      numBookmarks,\n      numFavorites,\n      numArchived,\n      numTags,\n      numLists,\n      numHighlights,\n      bookmarksByType: bookmarkTypeMap,\n      topDomains: topDomains.filter((d) => d.domain && d.domain.length > 0),\n      totalAssetSize: totalAssetSize || 0,\n      assetsByType,\n      bookmarkingActivity: {\n        thisWeek: thisWeek || 0,\n        thisMonth: thisMonth || 0,\n        thisYear: thisYear || 0,\n        byHour: hourlyActivity,\n        byDayOfWeek: dailyActivity,\n      },\n      tagUsage,\n      bookmarksBySource,\n    };\n  }\n\n  async hasWrapped(): Promise<boolean> {\n    // Check for bookmarks created in 2025\n    const yearStart = new Date(\"2025-01-01T00:00:00Z\");\n    const yearEnd = new Date(\"2025-12-31T23:59:59Z\");\n\n    const [{ numBookmarks }] = await this.ctx.db\n      .select({\n        numBookmarks: count(bookmarks.id),\n      })\n      .from(bookmarks)\n      .where(\n        and(\n          eq(bookmarks.userId, this.user.id),\n          gte(bookmarks.createdAt, yearStart),\n          lte(bookmarks.createdAt, yearEnd),\n        ),\n      );\n\n    return numBookmarks >= 20;\n  }\n\n  async getWrappedStats(\n    year: number,\n  ): Promise<z.infer<typeof zWrappedStatsResponseSchema>> {\n    const userObj = await this.ctx.db.query.users.findFirst({\n      where: eq(users.id, this.user.id),\n      columns: {\n        timezone: true,\n      },\n    });\n    const userTimezone = userObj?.timezone || \"UTC\";\n\n    // Define year range for 2025\n    const yearStart = new Date(`${year}-01-01T00:00:00Z`);\n    const yearEnd = new Date(`${year}-12-31T23:59:59Z`);\n\n    const yearFilter = and(\n      eq(bookmarks.userId, this.user.id),\n      gte(bookmarks.createdAt, yearStart),\n      lte(bookmarks.createdAt, yearEnd),\n    );\n\n    const [\n      [{ totalBookmarks }],\n      [{ totalFavorites }],\n      [{ totalArchived }],\n      [{ numTags }],\n      [{ numLists }],\n      [{ numHighlights }],\n      firstBookmarkResult,\n      bookmarksByType,\n      topDomains,\n      topTags,\n      bookmarksBySource,\n      bookmarkTimestamps,\n    ] = await Promise.all([\n      // Total bookmarks in year\n      this.ctx.db\n        .select({ totalBookmarks: count() })\n        .from(bookmarks)\n        .where(yearFilter),\n\n      // Total favorites in year\n      this.ctx.db\n        .select({ totalFavorites: count() })\n        .from(bookmarks)\n        .where(and(yearFilter, eq(bookmarks.favourited, true))),\n\n      // Total archived in year\n      this.ctx.db\n        .select({ totalArchived: count() })\n        .from(bookmarks)\n        .where(and(yearFilter, eq(bookmarks.archived, true))),\n\n      // Total unique tags (created in year)\n      this.ctx.db\n        .select({ numTags: count() })\n        .from(bookmarkTags)\n        .where(\n          and(\n            eq(bookmarkTags.userId, this.user.id),\n            gte(bookmarkTags.createdAt, yearStart),\n            lte(bookmarkTags.createdAt, yearEnd),\n          ),\n        ),\n\n      // Total lists (created in year)\n      this.ctx.db\n        .select({ numLists: count() })\n        .from(bookmarkLists)\n        .where(\n          and(\n            eq(bookmarkLists.userId, this.user.id),\n            gte(bookmarkLists.createdAt, yearStart),\n            lte(bookmarkLists.createdAt, yearEnd),\n          ),\n        ),\n\n      // Total highlights (created in year)\n      this.ctx.db\n        .select({ numHighlights: count() })\n        .from(highlights)\n        .where(\n          and(\n            eq(highlights.userId, this.user.id),\n            gte(highlights.createdAt, yearStart),\n            lte(highlights.createdAt, yearEnd),\n          ),\n        ),\n\n      // First bookmark of the year\n      this.ctx.db\n        .select({\n          id: bookmarks.id,\n          title: bookmarks.title,\n          createdAt: bookmarks.createdAt,\n          type: bookmarks.type,\n        })\n        .from(bookmarks)\n        .where(yearFilter)\n        .orderBy(bookmarks.createdAt)\n        .limit(1),\n\n      // Bookmarks by type\n      this.ctx.db\n        .select({\n          type: bookmarks.type,\n          count: count(),\n        })\n        .from(bookmarks)\n        .where(yearFilter)\n        .groupBy(bookmarks.type),\n\n      // Top 5 domains\n      this.ctx.db\n        .select({\n          domain: sql<string>`CASE\n            WHEN ${bookmarkLinks.url} LIKE 'https://%' THEN\n              CASE\n                WHEN INSTR(SUBSTR(${bookmarkLinks.url}, 9), '/') > 0 THEN\n                  SUBSTR(${bookmarkLinks.url}, 9, INSTR(SUBSTR(${bookmarkLinks.url}, 9), '/') - 1)\n                ELSE\n                  SUBSTR(${bookmarkLinks.url}, 9)\n              END\n            WHEN ${bookmarkLinks.url} LIKE 'http://%' THEN\n              CASE\n                WHEN INSTR(SUBSTR(${bookmarkLinks.url}, 8), '/') > 0 THEN\n                  SUBSTR(${bookmarkLinks.url}, 8, INSTR(SUBSTR(${bookmarkLinks.url}, 8), '/') - 1)\n                ELSE\n                  SUBSTR(${bookmarkLinks.url}, 8)\n              END\n            ELSE\n              CASE\n                WHEN INSTR(${bookmarkLinks.url}, '/') > 0 THEN\n                  SUBSTR(${bookmarkLinks.url}, 1, INSTR(${bookmarkLinks.url}, '/') - 1)\n                ELSE\n                  ${bookmarkLinks.url}\n              END\n          END`,\n          count: count(),\n        })\n        .from(bookmarkLinks)\n        .innerJoin(bookmarks, eq(bookmarks.id, bookmarkLinks.id))\n        .where(yearFilter)\n        .groupBy(\n          sql`CASE\n          WHEN ${bookmarkLinks.url} LIKE 'https://%' THEN\n            CASE\n              WHEN INSTR(SUBSTR(${bookmarkLinks.url}, 9), '/') > 0 THEN\n                SUBSTR(${bookmarkLinks.url}, 9, INSTR(SUBSTR(${bookmarkLinks.url}, 9), '/') - 1)\n              ELSE\n                SUBSTR(${bookmarkLinks.url}, 9)\n            END\n          WHEN ${bookmarkLinks.url} LIKE 'http://%' THEN\n            CASE\n              WHEN INSTR(SUBSTR(${bookmarkLinks.url}, 8), '/') > 0 THEN\n                SUBSTR(${bookmarkLinks.url}, 8, INSTR(SUBSTR(${bookmarkLinks.url}, 8), '/') - 1)\n              ELSE\n                SUBSTR(${bookmarkLinks.url}, 8)\n            END\n          ELSE\n            CASE\n              WHEN INSTR(${bookmarkLinks.url}, '/') > 0 THEN\n                SUBSTR(${bookmarkLinks.url}, 1, INSTR(${bookmarkLinks.url}, '/') - 1)\n              ELSE\n                ${bookmarkLinks.url}\n            END\n        END`,\n        )\n        .orderBy(desc(count()))\n        .limit(5),\n\n      // Top 5 tags (used in bookmarks created this year)\n      this.ctx.db\n        .select({\n          name: bookmarkTags.name,\n          count: count(),\n        })\n        .from(bookmarkTags)\n        .innerJoin(tagsOnBookmarks, eq(tagsOnBookmarks.tagId, bookmarkTags.id))\n        .innerJoin(bookmarks, eq(bookmarks.id, tagsOnBookmarks.bookmarkId))\n        .where(yearFilter)\n        .groupBy(bookmarkTags.name)\n        .orderBy(desc(count()))\n        .limit(5),\n\n      // Bookmarks by source\n      this.ctx.db\n        .select({\n          source: bookmarks.source,\n          count: count(),\n        })\n        .from(bookmarks)\n        .where(yearFilter)\n        .groupBy(bookmarks.source)\n        .orderBy(desc(count())),\n\n      // All bookmark timestamps in the year for activity calculations\n      this.ctx.db\n        .select({\n          createdAt: bookmarks.createdAt,\n        })\n        .from(bookmarks)\n        .where(yearFilter),\n    ]);\n\n    // Process bookmarks by type\n    const bookmarkTypeMap = { link: 0, text: 0, asset: 0 };\n    bookmarksByType.forEach((item) => {\n      if (item.type in bookmarkTypeMap) {\n        bookmarkTypeMap[item.type as keyof typeof bookmarkTypeMap] = item.count;\n      }\n    });\n\n    // Process timestamps with user timezone for hourly/daily activity\n    const hourCounts = Array.from({ length: 24 }, () => 0);\n    const dayCounts = Array.from({ length: 7 }, () => 0);\n    const monthCounts = Array.from({ length: 12 }, () => 0);\n    const dayCounts_full: Record<string, number> = {};\n\n    bookmarkTimestamps.forEach(({ createdAt }) => {\n      if (createdAt) {\n        const date = new Date(createdAt);\n        const userDate = new Date(\n          date.toLocaleString(\"en-US\", { timeZone: userTimezone }),\n        );\n\n        const hour = userDate.getHours();\n        const day = userDate.getDay();\n        const month = userDate.getMonth();\n        const dateKey = userDate.toISOString().split(\"T\")[0];\n\n        hourCounts[hour]++;\n        dayCounts[day]++;\n        monthCounts[month]++;\n        dayCounts_full[dateKey] = (dayCounts_full[dateKey] || 0) + 1;\n      }\n    });\n\n    // Find peak hour and day\n    const peakHour = hourCounts.indexOf(Math.max(...hourCounts));\n    const peakDayOfWeek = dayCounts.indexOf(Math.max(...dayCounts));\n\n    // Find most active day\n    let mostActiveDay: { date: string; count: number } | null = null;\n    if (Object.keys(dayCounts_full).length > 0) {\n      const sortedDays = Object.entries(dayCounts_full).sort(\n        ([, a], [, b]) => b - a,\n      );\n      mostActiveDay = {\n        date: sortedDays[0][0],\n        count: sortedDays[0][1],\n      };\n    }\n\n    // Monthly activity\n    const monthlyActivity = Array.from({ length: 12 }, (_, i) => ({\n      month: i + 1,\n      count: monthCounts[i],\n    }));\n\n    // First bookmark\n    const firstBookmark =\n      firstBookmarkResult.length > 0\n        ? {\n            id: firstBookmarkResult[0].id,\n            title: firstBookmarkResult[0].title,\n            createdAt: firstBookmarkResult[0].createdAt,\n            type: firstBookmarkResult[0].type,\n          }\n        : null;\n\n    return {\n      year,\n      totalBookmarks: totalBookmarks || 0,\n      totalFavorites: totalFavorites || 0,\n      totalArchived: totalArchived || 0,\n      totalHighlights: numHighlights || 0,\n      totalTags: numTags || 0,\n      totalLists: numLists || 0,\n      firstBookmark,\n      mostActiveDay,\n      topDomains: topDomains.filter((d) => d.domain && d.domain.length > 0),\n      topTags,\n      bookmarksByType: bookmarkTypeMap,\n      bookmarksBySource,\n      monthlyActivity,\n      peakHour,\n      peakDayOfWeek,\n    };\n  }\n\n  asWhoAmI(): z.infer<typeof zWhoAmIResponseSchema> {\n    return {\n      id: this.user.id,\n      name: this.user.name,\n      email: this.user.email,\n      image: this.user.image,\n      localUser: this.user.password !== null,\n    };\n  }\n\n  asPublicUser() {\n    const { password, salt: _salt, ...rest } = this.user;\n    return {\n      ...rest,\n      localUser: password !== null,\n    };\n  }\n}\n"
  },
  {
    "path": "packages/trpc/models/webhooks.ts",
    "content": "import { TRPCError } from \"@trpc/server\";\nimport { and, count, eq } from \"drizzle-orm\";\nimport { z } from \"zod\";\n\nimport { webhooksTable } from \"@karakeep/db/schema\";\nimport serverConfig from \"@karakeep/shared/config\";\nimport {\n  zNewWebhookSchema,\n  zUpdateWebhookSchema,\n  zWebhookSchema,\n} from \"@karakeep/shared/types/webhooks\";\n\nimport { AuthedContext } from \"..\";\n\nexport class Webhook {\n  constructor(\n    protected ctx: AuthedContext,\n    public webhook: typeof webhooksTable.$inferSelect,\n  ) {}\n\n  static async fromId(ctx: AuthedContext, id: string): Promise<Webhook> {\n    const webhook = await ctx.db.query.webhooksTable.findFirst({\n      where: eq(webhooksTable.id, id),\n    });\n\n    if (!webhook) {\n      throw new TRPCError({\n        code: \"NOT_FOUND\",\n        message: \"Webhook not found\",\n      });\n    }\n\n    // If it exists but belongs to another user, throw forbidden error\n    if (webhook.userId !== ctx.user.id) {\n      throw new TRPCError({\n        code: \"FORBIDDEN\",\n        message: \"User is not allowed to access resource\",\n      });\n    }\n\n    return new Webhook(ctx, webhook);\n  }\n\n  static async create(\n    ctx: AuthedContext,\n    input: z.infer<typeof zNewWebhookSchema>,\n  ): Promise<Webhook> {\n    // Check if user has reached the maximum number of webhooks\n    const [webhookCount] = await ctx.db\n      .select({ count: count() })\n      .from(webhooksTable)\n      .where(eq(webhooksTable.userId, ctx.user.id));\n\n    const maxWebhooks = serverConfig.webhook.maxWebhooksPerUser;\n    if (webhookCount.count >= maxWebhooks) {\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message: `Maximum number of webhooks (${maxWebhooks}) reached`,\n      });\n    }\n\n    const [result] = await ctx.db\n      .insert(webhooksTable)\n      .values({\n        url: input.url,\n        events: input.events,\n        token: input.token ?? null,\n        userId: ctx.user.id,\n      })\n      .returning();\n\n    return new Webhook(ctx, result);\n  }\n\n  static async getAll(ctx: AuthedContext): Promise<Webhook[]> {\n    const webhooks = await ctx.db.query.webhooksTable.findMany({\n      where: eq(webhooksTable.userId, ctx.user.id),\n    });\n\n    return webhooks.map((w) => new Webhook(ctx, w));\n  }\n\n  async delete(): Promise<void> {\n    const res = await this.ctx.db\n      .delete(webhooksTable)\n      .where(\n        and(\n          eq(webhooksTable.id, this.webhook.id),\n          eq(webhooksTable.userId, this.ctx.user.id),\n        ),\n      );\n\n    if (res.changes === 0) {\n      throw new TRPCError({ code: \"NOT_FOUND\" });\n    }\n  }\n\n  async update(input: z.infer<typeof zUpdateWebhookSchema>): Promise<void> {\n    const result = await this.ctx.db\n      .update(webhooksTable)\n      .set({\n        url: input.url,\n        events: input.events,\n        token: input.token,\n      })\n      .where(\n        and(\n          eq(webhooksTable.id, this.webhook.id),\n          eq(webhooksTable.userId, this.ctx.user.id),\n        ),\n      )\n      .returning();\n\n    if (result.length === 0) {\n      throw new TRPCError({ code: \"NOT_FOUND\" });\n    }\n\n    this.webhook = result[0];\n  }\n\n  asPublicWebhook(): z.infer<typeof zWebhookSchema> {\n    const { token, ...rest } = this.webhook;\n    return {\n      ...rest,\n      hasToken: token !== null,\n    };\n  }\n}\n"
  },
  {
    "path": "packages/trpc/package.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/package.json\",\n  \"name\": \"@karakeep/trpc\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"typecheck\": \"tsc --noEmit\",\n    \"format\": \"oxfmt --check .\",\n    \"format:fix\": \"oxfmt .\",\n    \"lint\": \"oxlint .\",\n    \"lint:fix\": \"oxlint . --fix\",\n    \"test\": \"vitest\"\n  },\n  \"dependencies\": {\n    \"@karakeep/db\": \"workspace:*\",\n    \"@karakeep/plugins\": \"workspace:*\",\n    \"@karakeep/shared\": \"workspace:*\",\n    \"@karakeep/shared-server\": \"workspace:*\",\n    \"@trpc/server\": \"^11.9.0\",\n    \"bcryptjs\": \"^2.4.3\",\n    \"deep-equal\": \"^2.2.3\",\n    \"drizzle-orm\": \"^0.44.2\",\n    \"nodemailer\": \"^7.0.4\",\n    \"prom-client\": \"^15.1.3\",\n    \"stripe\": \"^20.3.1\",\n    \"superjson\": \"^2.2.1\",\n    \"tiny-invariant\": \"^1.3.3\",\n    \"zod\": \"^3.24.2\"\n  },\n  \"devDependencies\": {\n    \"@karakeep/tsconfig\": \"workspace:^0.1.0\",\n    \"@types/bcryptjs\": \"^2.4.6\",\n    \"@types/deep-equal\": \"^1.0.4\",\n    \"@types/nodemailer\": \"^6.4.17\",\n    \"vite-tsconfig-paths\": \"^4.3.1\",\n    \"vitest\": \"^3.2.4\"\n  }\n}\n"
  },
  {
    "path": "packages/trpc/routers/_app.ts",
    "content": "import { router } from \"../index\";\nimport { adminAppRouter } from \"./admin\";\nimport { apiKeysAppRouter } from \"./apiKeys\";\nimport { assetsAppRouter } from \"./assets\";\nimport { backupsAppRouter } from \"./backups\";\nimport { bookmarksAppRouter } from \"./bookmarks\";\nimport { configAppRouter } from \"./config\";\nimport { feedsAppRouter } from \"./feeds\";\nimport { highlightsAppRouter } from \"./highlights\";\nimport { importSessionsRouter } from \"./importSessions\";\nimport { invitesAppRouter } from \"./invites\";\nimport { listsAppRouter } from \"./lists\";\nimport { promptsAppRouter } from \"./prompts\";\nimport { publicBookmarks } from \"./publicBookmarks\";\nimport { rulesAppRouter } from \"./rules\";\nimport { subscriptionsRouter } from \"./subscriptions\";\nimport { tagsAppRouter } from \"./tags\";\nimport { usersAppRouter } from \"./users\";\nimport { webhooksAppRouter } from \"./webhooks\";\n\nexport const appRouter = router({\n  bookmarks: bookmarksAppRouter,\n  apiKeys: apiKeysAppRouter,\n  users: usersAppRouter,\n  lists: listsAppRouter,\n  tags: tagsAppRouter,\n  prompts: promptsAppRouter,\n  admin: adminAppRouter,\n  feeds: feedsAppRouter,\n  backups: backupsAppRouter,\n  highlights: highlightsAppRouter,\n  importSessions: importSessionsRouter,\n  webhooks: webhooksAppRouter,\n  assets: assetsAppRouter,\n  rules: rulesAppRouter,\n  invites: invitesAppRouter,\n  publicBookmarks: publicBookmarks,\n  subscriptions: subscriptionsRouter,\n  config: configAppRouter,\n});\n// export type definition of API\nexport type AppRouter = typeof appRouter;\n"
  },
  {
    "path": "packages/trpc/routers/admin.test.ts",
    "content": "import { eq } from \"drizzle-orm\";\nimport { assert, beforeEach, describe, expect, test } from \"vitest\";\n\nimport { bookmarkLinks, users } from \"@karakeep/db/schema\";\nimport { BookmarkTypes } from \"@karakeep/shared/types/bookmarks\";\n\nimport type { CustomTestContext } from \"../testUtils\";\nimport { buildTestContext, getApiCaller } from \"../testUtils\";\n\nbeforeEach<CustomTestContext>(async (context) => {\n  const testContext = await buildTestContext(true);\n  Object.assign(context, testContext);\n});\n\ndescribe(\"Admin Routes\", () => {\n  describe(\"getBookmarkDebugInfo\", () => {\n    test<CustomTestContext>(\"admin can access bookmark debug info for link bookmark\", async ({\n      apiCallers,\n      db,\n    }) => {\n      // Create an admin user\n      const adminUser = await db\n        .insert(users)\n        .values({\n          name: \"Admin User\",\n          email: \"admin@test.com\",\n          role: \"admin\",\n        })\n        .returning();\n      const adminApi = getApiCaller(\n        db,\n        adminUser[0].id,\n        adminUser[0].email,\n        \"admin\",\n      );\n\n      // Create a bookmark as a regular user\n      const bookmark = await apiCallers[0].bookmarks.createBookmark({\n        url: \"https://example.com\",\n        type: BookmarkTypes.LINK,\n      });\n\n      // Update the bookmark link with some metadata\n      await db\n        .update(bookmarkLinks)\n        .set({\n          crawlStatus: \"success\",\n          crawlStatusCode: 200,\n          crawledAt: new Date(),\n          htmlContent: \"<html><body>Test content</body></html>\",\n          title: \"Test Title\",\n          description: \"Test Description\",\n        })\n        .where(eq(bookmarkLinks.id, bookmark.id));\n\n      // Admin should be able to access debug info\n      const debugInfo = await adminApi.admin.getBookmarkDebugInfo({\n        bookmarkId: bookmark.id,\n      });\n\n      expect(debugInfo.id).toEqual(bookmark.id);\n      expect(debugInfo.type).toEqual(BookmarkTypes.LINK);\n      expect(debugInfo.linkInfo).toBeDefined();\n      assert(debugInfo.linkInfo);\n      expect(debugInfo.linkInfo.url).toEqual(\"https://example.com\");\n      expect(debugInfo.linkInfo.crawlStatus).toEqual(\"success\");\n      expect(debugInfo.linkInfo.crawlStatusCode).toEqual(200);\n      expect(debugInfo.linkInfo.hasHtmlContent).toEqual(true);\n      expect(debugInfo.linkInfo.htmlContentPreview).toBeDefined();\n      expect(debugInfo.linkInfo.htmlContentPreview).toContain(\"Test content\");\n    });\n\n    test<CustomTestContext>(\"admin can access bookmark debug info for text bookmark\", async ({\n      apiCallers,\n      db,\n    }) => {\n      // Create an admin user\n      const adminUser = await db\n        .insert(users)\n        .values({\n          name: \"Admin User\",\n          email: \"admin@test.com\",\n          role: \"admin\",\n        })\n        .returning();\n      const adminApi = getApiCaller(\n        db,\n        adminUser[0].id,\n        adminUser[0].email,\n        \"admin\",\n      );\n\n      // Create a text bookmark\n      const bookmark = await apiCallers[0].bookmarks.createBookmark({\n        text: \"This is a test text bookmark\",\n        type: BookmarkTypes.TEXT,\n      });\n\n      // Admin should be able to access debug info\n      const debugInfo = await adminApi.admin.getBookmarkDebugInfo({\n        bookmarkId: bookmark.id,\n      });\n\n      expect(debugInfo.id).toEqual(bookmark.id);\n      expect(debugInfo.type).toEqual(BookmarkTypes.TEXT);\n      expect(debugInfo.textInfo).toBeDefined();\n      assert(debugInfo.textInfo);\n      expect(debugInfo.textInfo.hasText).toEqual(true);\n    });\n\n    test<CustomTestContext>(\"admin can see bookmark tags in debug info\", async ({\n      apiCallers,\n      db,\n    }) => {\n      // Create an admin user\n      const adminUser = await db\n        .insert(users)\n        .values({\n          name: \"Admin User\",\n          email: \"admin@test.com\",\n          role: \"admin\",\n        })\n        .returning();\n      const adminApi = getApiCaller(\n        db,\n        adminUser[0].id,\n        adminUser[0].email,\n        \"admin\",\n      );\n\n      // Create a bookmark with tags\n      const bookmark = await apiCallers[0].bookmarks.createBookmark({\n        url: \"https://example.com\",\n        type: BookmarkTypes.LINK,\n      });\n\n      // Add tags to the bookmark\n      await apiCallers[0].bookmarks.updateTags({\n        bookmarkId: bookmark.id,\n        attach: [{ tagName: \"test-tag-1\" }, { tagName: \"test-tag-2\" }],\n        detach: [],\n      });\n\n      // Admin should be able to see tags in debug info\n      const debugInfo = await adminApi.admin.getBookmarkDebugInfo({\n        bookmarkId: bookmark.id,\n      });\n\n      expect(debugInfo.tags).toHaveLength(2);\n      expect(debugInfo.tags.map((t) => t.name).sort()).toEqual([\n        \"test-tag-1\",\n        \"test-tag-2\",\n      ]);\n      expect(debugInfo.tags[0].attachedBy).toEqual(\"human\");\n    });\n\n    test<CustomTestContext>(\"non-admin user cannot access bookmark debug info\", async ({\n      apiCallers,\n    }) => {\n      // Create a bookmark\n      const bookmark = await apiCallers[0].bookmarks.createBookmark({\n        url: \"https://example.com\",\n        type: BookmarkTypes.LINK,\n      });\n\n      // Non-admin user should not be able to access debug info\n      // The admin procedure itself will throw FORBIDDEN\n      await expect(() =>\n        apiCallers[0].admin.getBookmarkDebugInfo({ bookmarkId: bookmark.id }),\n      ).rejects.toThrow(/FORBIDDEN/);\n    });\n\n    test<CustomTestContext>(\"debug info includes asset URLs with signed tokens\", async ({\n      apiCallers,\n      db,\n    }) => {\n      // Create an admin user\n      const adminUser = await db\n        .insert(users)\n        .values({\n          name: \"Admin User\",\n          email: \"admin@test.com\",\n          role: \"admin\",\n        })\n        .returning();\n      const adminApi = getApiCaller(\n        db,\n        adminUser[0].id,\n        adminUser[0].email,\n        \"admin\",\n      );\n\n      // Create a bookmark\n      const bookmark = await apiCallers[0].bookmarks.createBookmark({\n        url: \"https://example.com\",\n        type: BookmarkTypes.LINK,\n      });\n\n      // Get debug info\n      const debugInfo = await adminApi.admin.getBookmarkDebugInfo({\n        bookmarkId: bookmark.id,\n      });\n\n      // Check that assets array is present\n      expect(debugInfo.assets).toBeDefined();\n      expect(Array.isArray(debugInfo.assets)).toBe(true);\n\n      // If there are assets, check that they have signed URLs\n      if (debugInfo.assets.length > 0) {\n        const asset = debugInfo.assets[0];\n        expect(asset.url).toBeDefined();\n        expect(asset.url).toContain(\"/api/public/assets/\");\n        expect(asset.url).toContain(\"token=\");\n      }\n    });\n\n    test<CustomTestContext>(\"debug info truncates HTML content preview\", async ({\n      apiCallers,\n      db,\n    }) => {\n      // Create an admin user\n      const adminUser = await db\n        .insert(users)\n        .values({\n          name: \"Admin User\",\n          email: \"admin@test.com\",\n          role: \"admin\",\n        })\n        .returning();\n      const adminApi = getApiCaller(\n        db,\n        adminUser[0].id,\n        adminUser[0].email,\n        \"admin\",\n      );\n\n      // Create a bookmark\n      const bookmark = await apiCallers[0].bookmarks.createBookmark({\n        url: \"https://example.com\",\n        type: BookmarkTypes.LINK,\n      });\n\n      // Create a large HTML content\n      const largeContent = \"<html><body>\" + \"x\".repeat(2000) + \"</body></html>\";\n      await db\n        .update(bookmarkLinks)\n        .set({\n          htmlContent: largeContent,\n        })\n        .where(eq(bookmarkLinks.id, bookmark.id));\n\n      // Get debug info\n      const debugInfo = await adminApi.admin.getBookmarkDebugInfo({\n        bookmarkId: bookmark.id,\n      });\n\n      // Check that HTML preview is truncated to 1000 characters\n      assert(debugInfo.linkInfo);\n      expect(debugInfo.linkInfo.htmlContentPreview).toBeDefined();\n      expect(debugInfo.linkInfo.htmlContentPreview!.length).toBeLessThanOrEqual(\n        1000,\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "packages/trpc/routers/admin.ts",
    "content": "import * as dns from \"dns\";\nimport { TRPCError } from \"@trpc/server\";\nimport { count, eq, or, sum } from \"drizzle-orm\";\nimport { z } from \"zod\";\n\nimport { assets, bookmarkLinks, bookmarks, users } from \"@karakeep/db/schema\";\nimport {\n  AdminMaintenanceQueue,\n  AssetPreprocessingQueue,\n  FeedQueue,\n  LinkCrawlerQueue,\n  LowPriorityCrawlerQueue,\n  OpenAIQueue,\n  QueuePriority,\n  SearchIndexingQueue,\n  triggerSearchReindex,\n  VideoWorkerQueue,\n  WebhookQueue,\n  zAdminMaintenanceTaskSchema,\n} from \"@karakeep/shared-server\";\nimport serverConfig from \"@karakeep/shared/config\";\nimport logger from \"@karakeep/shared/logger\";\nimport { PluginManager, PluginType } from \"@karakeep/shared/plugins\";\nimport { getSearchClient } from \"@karakeep/shared/search\";\nimport {\n  resetPasswordSchema,\n  updateUserSchema,\n  zAdminCreateUserSchema,\n} from \"@karakeep/shared/types/admin\";\nimport { BookmarkTypes } from \"@karakeep/shared/types/bookmarks\";\n\nimport { generatePasswordSalt, hashPassword } from \"../auth\";\nimport { adminProcedure, router } from \"../index\";\nimport { Bookmark } from \"../models/bookmarks\";\nimport { User } from \"../models/users\";\n\nexport const adminAppRouter = router({\n  stats: adminProcedure\n    .output(\n      z.object({\n        numUsers: z.number(),\n        numBookmarks: z.number(),\n      }),\n    )\n    .query(async ({ ctx }) => {\n      const [[{ value: numUsers }], [{ value: numBookmarks }]] =\n        await Promise.all([\n          ctx.db.select({ value: count() }).from(users),\n          ctx.db.select({ value: count() }).from(bookmarks),\n        ]);\n\n      return {\n        numUsers,\n        numBookmarks,\n      };\n    }),\n  backgroundJobsStats: adminProcedure\n    .output(\n      z.object({\n        crawlStats: z.object({\n          queued: z.number(),\n          pending: z.number(),\n          failed: z.number(),\n        }),\n        inferenceStats: z.object({\n          queued: z.number(),\n          pending: z.number(),\n          failed: z.number(),\n        }),\n        indexingStats: z.object({\n          queued: z.number(),\n        }),\n        adminMaintenanceStats: z.object({\n          queued: z.number(),\n        }),\n        videoStats: z.object({\n          queued: z.number(),\n        }),\n        webhookStats: z.object({\n          queued: z.number(),\n        }),\n        assetPreprocessingStats: z.object({\n          queued: z.number(),\n        }),\n        feedStats: z.object({\n          queued: z.number(),\n        }),\n      }),\n    )\n    .query(async ({ ctx }) => {\n      const [\n        // Crawls\n        queuedCrawls,\n        queuedLowPriorityCrawls,\n        [{ value: pendingCrawls }],\n        [{ value: failedCrawls }],\n\n        // Indexing\n        queuedIndexing,\n\n        // Inference\n        queuedInferences,\n        [{ value: pendingInference }],\n        [{ value: failedInference }],\n\n        // Admin maintenance\n        queuedAdminMaintenance,\n\n        // Video\n        queuedVideo,\n\n        // Webhook\n        queuedWebhook,\n\n        // Asset Preprocessing\n        queuedAssetPreprocessing,\n\n        // Feed\n        queuedFeed,\n      ] = await Promise.all([\n        // Crawls\n        LinkCrawlerQueue.stats(),\n        LowPriorityCrawlerQueue.stats(),\n        ctx.db\n          .select({ value: count() })\n          .from(bookmarkLinks)\n          .where(eq(bookmarkLinks.crawlStatus, \"pending\")),\n        ctx.db\n          .select({ value: count() })\n          .from(bookmarkLinks)\n          .where(eq(bookmarkLinks.crawlStatus, \"failure\")),\n\n        // Indexing\n        SearchIndexingQueue.stats(),\n\n        // Inference\n        OpenAIQueue.stats(),\n        ctx.db\n          .select({ value: count() })\n          .from(bookmarks)\n          .where(\n            or(\n              eq(bookmarks.taggingStatus, \"pending\"),\n              eq(bookmarks.summarizationStatus, \"pending\"),\n            ),\n          ),\n        ctx.db\n          .select({ value: count() })\n          .from(bookmarks)\n          .where(\n            or(\n              eq(bookmarks.taggingStatus, \"failure\"),\n              eq(bookmarks.summarizationStatus, \"failure\"),\n            ),\n          ),\n\n        // Admin maintenance\n        AdminMaintenanceQueue.stats(),\n\n        // Video\n        VideoWorkerQueue.stats(),\n\n        // Webhook\n        WebhookQueue.stats(),\n\n        // Asset Preprocessing\n        AssetPreprocessingQueue.stats(),\n\n        // Feed\n        FeedQueue.stats(),\n      ]);\n\n      return {\n        crawlStats: {\n          queued:\n            queuedCrawls.pending +\n            queuedCrawls.pending_retry +\n            queuedLowPriorityCrawls.pending +\n            queuedLowPriorityCrawls.pending_retry,\n          pending: pendingCrawls,\n          failed: failedCrawls,\n        },\n        inferenceStats: {\n          queued: queuedInferences.pending + queuedInferences.pending_retry,\n          pending: pendingInference,\n          failed: failedInference,\n        },\n        indexingStats: {\n          queued: queuedIndexing.pending + queuedIndexing.pending_retry,\n        },\n        adminMaintenanceStats: {\n          queued:\n            queuedAdminMaintenance.pending +\n            queuedAdminMaintenance.pending_retry,\n        },\n        videoStats: {\n          queued: queuedVideo.pending + queuedVideo.pending_retry,\n        },\n        webhookStats: {\n          queued: queuedWebhook.pending + queuedWebhook.pending_retry,\n        },\n        assetPreprocessingStats: {\n          queued:\n            queuedAssetPreprocessing.pending +\n            queuedAssetPreprocessing.pending_retry,\n        },\n        feedStats: {\n          queued: queuedFeed.pending + queuedFeed.pending_retry,\n        },\n      };\n    }),\n  recrawlLinks: adminProcedure\n    .input(\n      z.object({\n        crawlStatus: z.enum([\"success\", \"failure\", \"pending\", \"all\"]),\n        runInference: z.boolean(),\n      }),\n    )\n    .mutation(async ({ ctx, input }) => {\n      const bookmarkIds = await ctx.db.query.bookmarkLinks.findMany({\n        columns: {\n          id: true,\n        },\n        ...(input.crawlStatus === \"all\"\n          ? {}\n          : { where: eq(bookmarkLinks.crawlStatus, input.crawlStatus) }),\n      });\n\n      await Promise.all(\n        bookmarkIds.map((b) =>\n          LowPriorityCrawlerQueue.enqueue(\n            {\n              bookmarkId: b.id,\n              runInference: input.runInference,\n            },\n            {\n              priority: QueuePriority.Low,\n            },\n          ),\n        ),\n      );\n    }),\n  reindexAllBookmarks: adminProcedure.mutation(async ({ ctx }) => {\n    const searchIdx = await getSearchClient();\n    await searchIdx?.clearIndex();\n    const bookmarkIds = await ctx.db.query.bookmarks.findMany({\n      columns: {\n        id: true,\n      },\n    });\n\n    await Promise.all(\n      bookmarkIds.map((b) =>\n        triggerSearchReindex(b.id, {\n          priority: QueuePriority.Low,\n        }),\n      ),\n    );\n  }),\n  reprocessAssetsFixMode: adminProcedure.mutation(async ({ ctx }) => {\n    const bookmarkIds = await ctx.db.query.bookmarkAssets.findMany({\n      columns: {\n        id: true,\n      },\n    });\n\n    await Promise.all(\n      bookmarkIds.map((b) =>\n        AssetPreprocessingQueue.enqueue(\n          {\n            bookmarkId: b.id,\n            fixMode: true,\n          },\n          {\n            priority: QueuePriority.Low,\n          },\n        ),\n      ),\n    );\n  }),\n  reRunInferenceOnAllBookmarks: adminProcedure\n    .input(\n      z.object({\n        type: z.enum([\"tag\", \"summarize\"]),\n        status: z.enum([\"success\", \"failure\", \"pending\", \"all\"]),\n      }),\n    )\n    .mutation(async ({ input, ctx }) => {\n      const bookmarkIds = await ctx.db.query.bookmarks.findMany({\n        columns: {\n          id: true,\n        },\n        ...{\n          tag:\n            input.status === \"all\"\n              ? {}\n              : { where: eq(bookmarks.taggingStatus, input.status) },\n          summarize:\n            input.status === \"all\"\n              ? {}\n              : { where: eq(bookmarks.summarizationStatus, input.status) },\n        }[input.type],\n      });\n\n      await Promise.all(\n        bookmarkIds.map((b) =>\n          OpenAIQueue.enqueue(\n            { bookmarkId: b.id, type: input.type },\n            {\n              priority: QueuePriority.Low,\n            },\n          ),\n        ),\n      );\n    }),\n  runAdminMaintenanceTask: adminProcedure\n    .input(zAdminMaintenanceTaskSchema)\n    .mutation(async ({ input }) => {\n      await AdminMaintenanceQueue.enqueue(input);\n    }),\n  userStats: adminProcedure\n    .output(\n      z.record(\n        z.string(),\n        z.object({\n          numBookmarks: z.number(),\n          assetSizes: z.number(),\n        }),\n      ),\n    )\n    .query(async ({ ctx }) => {\n      const [userIds, bookmarkStats, assetStats] = await Promise.all([\n        ctx.db.select({ id: users.id }).from(users),\n        ctx.db\n          .select({ id: bookmarks.userId, value: count() })\n          .from(bookmarks)\n          .groupBy(bookmarks.userId),\n        ctx.db\n          .select({ id: assets.userId, value: sum(assets.size) })\n          .from(assets)\n          .groupBy(assets.userId),\n      ]);\n\n      const results: Record<\n        string,\n        { numBookmarks: number; assetSizes: number }\n      > = {};\n      for (const user of userIds) {\n        results[user.id] = {\n          numBookmarks: 0,\n          assetSizes: 0,\n        };\n      }\n      for (const stat of bookmarkStats) {\n        results[stat.id].numBookmarks = stat.value;\n      }\n      for (const stat of assetStats) {\n        results[stat.id].assetSizes = parseInt(stat.value ?? \"0\");\n      }\n\n      return results;\n    }),\n  createUser: adminProcedure\n    .input(zAdminCreateUserSchema)\n    .output(\n      z.object({\n        id: z.string(),\n        name: z.string(),\n        email: z.string(),\n        role: z.enum([\"user\", \"admin\"]).nullable(),\n      }),\n    )\n    .mutation(async ({ input, ctx }) => {\n      return await User.create(ctx, input, input.role);\n    }),\n  updateUser: adminProcedure\n    .input(updateUserSchema)\n    .mutation(async ({ input, ctx }) => {\n      if (ctx.user.id == input.userId) {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"Cannot update own user\",\n        });\n      }\n\n      const updateData: Partial<typeof users.$inferInsert> = {};\n\n      if (input.role !== undefined) {\n        updateData.role = input.role;\n      }\n\n      if (input.bookmarkQuota !== undefined) {\n        updateData.bookmarkQuota = input.bookmarkQuota;\n      }\n\n      if (input.storageQuota !== undefined) {\n        updateData.storageQuota = input.storageQuota;\n      }\n\n      if (input.browserCrawlingEnabled !== undefined) {\n        updateData.browserCrawlingEnabled = input.browserCrawlingEnabled;\n      }\n\n      if (Object.keys(updateData).length === 0) {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"No fields to update\",\n        });\n      }\n\n      const result = await ctx.db\n        .update(users)\n        .set(updateData)\n        .where(eq(users.id, input.userId));\n\n      if (!result.changes) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"User not found\",\n        });\n      }\n    }),\n  resetPassword: adminProcedure\n    .input(resetPasswordSchema)\n    .mutation(async ({ input, ctx }) => {\n      if (ctx.user.id == input.userId) {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"Cannot reset own password\",\n        });\n      }\n      const newSalt = generatePasswordSalt();\n      const hashedPassword = await hashPassword(input.newPassword, newSalt);\n      const result = await ctx.db\n        .update(users)\n        .set({ password: hashedPassword, salt: newSalt })\n        .where(eq(users.id, input.userId));\n\n      if (result.changes == 0) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"User not found\",\n        });\n      }\n    }),\n  getAdminNoticies: adminProcedure\n    .output(\n      z.object({\n        // Unused for now\n      }),\n    )\n    .query(() => {\n      return {\n        // Unused for now\n      };\n    }),\n  checkConnections: adminProcedure\n    .output(\n      z.object({\n        searchEngine: z.object({\n          configured: z.boolean(),\n          connected: z.boolean(),\n          pluginName: z.string().optional(),\n          error: z.string().optional(),\n        }),\n        browser: z.object({\n          configured: z.boolean(),\n          connected: z.boolean(),\n          pluginName: z.string().optional(),\n          error: z.string().optional(),\n        }),\n        queue: z.object({\n          configured: z.boolean(),\n          connected: z.boolean(),\n          pluginName: z.string().optional(),\n          error: z.string().optional(),\n        }),\n      }),\n    )\n    .query(async () => {\n      const searchEngineStatus: {\n        configured: boolean;\n        connected: boolean;\n        pluginName?: string;\n        error?: string;\n      } = { configured: false, connected: false };\n      const browserStatus: {\n        configured: boolean;\n        connected: boolean;\n        pluginName?: string;\n        error?: string;\n      } = { configured: false, connected: false };\n      const queueStatus: {\n        configured: boolean;\n        connected: boolean;\n        pluginName?: string;\n        error?: string;\n      } = { configured: true, connected: false };\n\n      const searchClient = await getSearchClient();\n      searchEngineStatus.configured = searchClient !== null;\n\n      if (searchClient) {\n        const pluginName = PluginManager.getPluginName(PluginType.Search);\n        if (pluginName) {\n          searchEngineStatus.pluginName = pluginName;\n        }\n        try {\n          await searchClient.search({ query: \"\", limit: 1 });\n          searchEngineStatus.connected = true;\n        } catch (error) {\n          searchEngineStatus.error =\n            error instanceof Error ? error.message : \"Unknown error\";\n        }\n      }\n\n      browserStatus.configured =\n        !!serverConfig.crawler.browserWebUrl ||\n        !!serverConfig.crawler.browserWebSocketUrl;\n\n      if (browserStatus.configured) {\n        if (serverConfig.crawler.browserWebUrl) {\n          browserStatus.pluginName = \"Browserless/Chrome\";\n        } else if (serverConfig.crawler.browserWebSocketUrl) {\n          browserStatus.pluginName = \"WebSocket Browser\";\n        }\n\n        try {\n          if (serverConfig.crawler.browserWebUrl) {\n            const webUrl = new URL(serverConfig.crawler.browserWebUrl);\n            const { address } = await dns.promises.lookup(webUrl.hostname);\n            webUrl.hostname = address;\n            webUrl.pathname = \"/json/version\";\n            const response = await fetch(`${webUrl.toString()}`, {\n              signal: AbortSignal.timeout(5000),\n            });\n            if (response.ok) {\n              browserStatus.connected = true;\n            } else {\n              browserStatus.error = `HTTP ${response.status}: ${response.statusText}`;\n            }\n          } else if (serverConfig.crawler.browserWebSocketUrl) {\n            browserStatus.connected = true;\n            browserStatus.error =\n              \"WebSocket URL configured (connection check not supported)\";\n          }\n        } catch (error) {\n          browserStatus.error =\n            error instanceof Error ? error.message : \"Unknown error\";\n        }\n      }\n\n      const queuePluginName = PluginManager.getPluginName(PluginType.Queue);\n      if (queuePluginName) {\n        queueStatus.pluginName = queuePluginName;\n      }\n\n      try {\n        await LinkCrawlerQueue.stats();\n        queueStatus.connected = true;\n      } catch (error) {\n        queueStatus.error =\n          error instanceof Error ? error.message : \"Unknown error\";\n      }\n\n      return {\n        searchEngine: searchEngineStatus,\n        browser: browserStatus,\n        queue: queueStatus,\n      };\n    }),\n  getBookmarkDebugInfo: adminProcedure\n    .input(z.object({ bookmarkId: z.string() }))\n    .output(\n      z.object({\n        id: z.string(),\n        type: z.enum([\n          BookmarkTypes.LINK,\n          BookmarkTypes.TEXT,\n          BookmarkTypes.ASSET,\n        ]),\n        source: z\n          .enum([\n            \"api\",\n            \"web\",\n            \"extension\",\n            \"cli\",\n            \"mobile\",\n            \"singlefile\",\n            \"rss\",\n            \"import\",\n          ])\n          .nullable(),\n        createdAt: z.date(),\n        modifiedAt: z.date().nullable(),\n        title: z.string().nullable(),\n        summary: z.string().nullable(),\n        taggingStatus: z.enum([\"pending\", \"failure\", \"success\"]).nullable(),\n        summarizationStatus: z\n          .enum([\"pending\", \"failure\", \"success\"])\n          .nullable(),\n        userId: z.string(),\n        linkInfo: z\n          .object({\n            url: z.string(),\n            crawlStatus: z.enum([\"pending\", \"failure\", \"success\"]),\n            crawlStatusCode: z.number().nullable(),\n            crawledAt: z.date().nullable(),\n            hasHtmlContent: z.boolean(),\n            hasContentAsset: z.boolean(),\n            htmlContentPreview: z.string().nullable(),\n          })\n          .nullable(),\n        textInfo: z\n          .object({\n            hasText: z.boolean(),\n            sourceUrl: z.string().nullable(),\n          })\n          .nullable(),\n        assetInfo: z\n          .object({\n            assetType: z.enum([\"image\", \"pdf\"]),\n            hasContent: z.boolean(),\n            fileName: z.string().nullable(),\n          })\n          .nullable(),\n        tags: z.array(\n          z.object({\n            id: z.string(),\n            name: z.string(),\n            attachedBy: z.enum([\"ai\", \"human\"]),\n          }),\n        ),\n        assets: z.array(\n          z.object({\n            id: z.string(),\n            assetType: z.string(),\n            size: z.number(),\n            url: z.string().nullable(),\n          }),\n        ),\n      }),\n    )\n    .query(async ({ input, ctx }) => {\n      logger.info(\n        `[admin] Admin ${ctx.user.id} accessed debug info for bookmark ${input.bookmarkId}`,\n      );\n\n      return await Bookmark.buildDebugInfo(ctx, input.bookmarkId);\n    }),\n  adminRecrawlBookmark: adminProcedure\n    .input(z.object({ bookmarkId: z.string() }))\n    .mutation(async ({ input, ctx }) => {\n      // Verify bookmark exists and is a link\n      const bookmark = await ctx.db.query.bookmarks.findFirst({\n        where: eq(bookmarks.id, input.bookmarkId),\n      });\n\n      if (!bookmark) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Bookmark not found\",\n        });\n      }\n\n      if (bookmark.type !== BookmarkTypes.LINK) {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"Only link bookmarks can be recrawled\",\n        });\n      }\n\n      await LowPriorityCrawlerQueue.enqueue(\n        {\n          bookmarkId: input.bookmarkId,\n        },\n        {\n          priority: QueuePriority.Low,\n          groupId: \"admin\",\n        },\n      );\n    }),\n  adminReindexBookmark: adminProcedure\n    .input(z.object({ bookmarkId: z.string() }))\n    .mutation(async ({ input, ctx }) => {\n      // Verify bookmark exists\n      const bookmark = await ctx.db.query.bookmarks.findFirst({\n        where: eq(bookmarks.id, input.bookmarkId),\n      });\n\n      if (!bookmark) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Bookmark not found\",\n        });\n      }\n\n      await triggerSearchReindex(input.bookmarkId, {\n        priority: QueuePriority.Low,\n        groupId: \"admin\",\n      });\n    }),\n  adminRetagBookmark: adminProcedure\n    .input(z.object({ bookmarkId: z.string() }))\n    .mutation(async ({ input, ctx }) => {\n      // Verify bookmark exists\n      const bookmark = await ctx.db.query.bookmarks.findFirst({\n        where: eq(bookmarks.id, input.bookmarkId),\n      });\n\n      if (!bookmark) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Bookmark not found\",\n        });\n      }\n\n      await OpenAIQueue.enqueue(\n        {\n          bookmarkId: input.bookmarkId,\n          type: \"tag\",\n        },\n        {\n          priority: QueuePriority.Low,\n          groupId: \"admin\",\n        },\n      );\n    }),\n  adminResummarizeBookmark: adminProcedure\n    .input(z.object({ bookmarkId: z.string() }))\n    .mutation(async ({ input, ctx }) => {\n      // Verify bookmark exists and is a link\n      const bookmark = await ctx.db.query.bookmarks.findFirst({\n        where: eq(bookmarks.id, input.bookmarkId),\n      });\n\n      if (!bookmark) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Bookmark not found\",\n        });\n      }\n\n      if (bookmark.type !== BookmarkTypes.LINK) {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"Only link bookmarks can be summarized\",\n        });\n      }\n\n      await OpenAIQueue.enqueue(\n        {\n          bookmarkId: input.bookmarkId,\n          type: \"summarize\",\n        },\n        {\n          priority: QueuePriority.Low,\n          groupId: \"admin\",\n        },\n      );\n    }),\n});\n"
  },
  {
    "path": "packages/trpc/routers/apiKeys.test.ts",
    "content": "import { eq } from \"drizzle-orm\";\nimport { beforeEach, describe, expect, test, vi } from \"vitest\";\n\nimport { apiKeys } from \"@karakeep/db/schema\";\n\nimport type { CustomTestContext } from \"../testUtils\";\nimport { defaultBeforeEach, getApiCaller } from \"../testUtils\";\n\nvi.mock(\"@karakeep/shared/config\", async (original) => {\n  const mod = (await original()) as typeof import(\"@karakeep/shared/config\");\n  return {\n    ...mod,\n    default: {\n      ...mod.default,\n      auth: {\n        ...mod.default.auth,\n        disablePasswordAuth: false,\n      },\n    },\n  };\n});\n\nbeforeEach<CustomTestContext>(defaultBeforeEach(false));\n\ndescribe(\"API Keys Routes\", () => {\n  describe(\"create\", () => {\n    test<CustomTestContext>(\"creates API key successfully\", async ({\n      unauthedAPICaller,\n      db,\n    }) => {\n      const user = await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"test@test.com\",\n        password: \"password123\",\n        confirmPassword: \"password123\",\n      });\n\n      const api = getApiCaller(db, user.id, user.email).apiKeys;\n      const result = await api.create({ name: \"Test Key\" });\n\n      expect(result.name).toBe(\"Test Key\");\n      expect(result.id).toBeDefined();\n      expect(result.key).toMatch(/^ak2_[a-f0-9]{20}_[a-f0-9]{32}$/);\n      expect(result.createdAt).toBeInstanceOf(Date);\n    });\n\n    test<CustomTestContext>(\"requires authentication\", async ({\n      unauthedAPICaller,\n    }) => {\n      await expect(() =>\n        unauthedAPICaller.apiKeys.create({ name: \"Test Key\" }),\n      ).rejects.toThrow(/UNAUTHORIZED/);\n    });\n  });\n\n  describe(\"list\", () => {\n    test<CustomTestContext>(\"lists user's API keys\", async ({\n      unauthedAPICaller,\n      db,\n    }) => {\n      const user = await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"test@test.com\",\n        password: \"password123\",\n        confirmPassword: \"password123\",\n      });\n\n      const api = getApiCaller(db, user.id, user.email).apiKeys;\n\n      await api.create({ name: \"Key 1\" });\n      await api.create({ name: \"Key 2\" });\n\n      const result = await api.list();\n\n      expect(result.keys).toHaveLength(2);\n      expect(result.keys[0]).toMatchObject({\n        id: expect.any(String),\n        name: expect.any(String),\n        createdAt: expect.any(Date),\n        keyId: expect.any(String),\n      });\n      expect(result.keys[0]).not.toHaveProperty(\"key\");\n    });\n\n    test<CustomTestContext>(\"returns empty list for new user\", async ({\n      unauthedAPICaller,\n      db,\n    }) => {\n      const user = await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"test@test.com\",\n        password: \"password123\",\n        confirmPassword: \"password123\",\n      });\n\n      const api = getApiCaller(db, user.id, user.email).apiKeys;\n      const result = await api.list();\n\n      expect(result.keys).toHaveLength(0);\n    });\n\n    test<CustomTestContext>(\"privacy isolation between users\", async ({\n      unauthedAPICaller,\n      db,\n    }) => {\n      const user1 = await unauthedAPICaller.users.create({\n        name: \"User 1\",\n        email: \"user1@test.com\",\n        password: \"password123\",\n        confirmPassword: \"password123\",\n      });\n\n      const user2 = await unauthedAPICaller.users.create({\n        name: \"User 2\",\n        email: \"user2@test.com\",\n        password: \"password123\",\n        confirmPassword: \"password123\",\n      });\n\n      const api1 = getApiCaller(db, user1.id, user1.email).apiKeys;\n      const api2 = getApiCaller(db, user2.id, user2.email).apiKeys;\n\n      await api1.create({ name: \"User 1 Key\" });\n      await api2.create({ name: \"User 2 Key\" });\n\n      const result1 = await api1.list();\n      const result2 = await api2.list();\n\n      expect(result1.keys).toHaveLength(1);\n      expect(result1.keys[0].name).toBe(\"User 1 Key\");\n\n      expect(result2.keys).toHaveLength(1);\n      expect(result2.keys[0].name).toBe(\"User 2 Key\");\n    });\n\n    test<CustomTestContext>(\"requires authentication\", async ({\n      unauthedAPICaller,\n    }) => {\n      await expect(() => unauthedAPICaller.apiKeys.list()).rejects.toThrow(\n        /UNAUTHORIZED/,\n      );\n    });\n  });\n  describe(\"regenerate\", () => {\n    test<CustomTestContext>(\"revokes API key successfully\", async ({\n      unauthedAPICaller,\n      db,\n    }) => {\n      const user = await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"test@test.com\",\n        password: \"password123\",\n        confirmPassword: \"password123\",\n      });\n\n      const api = getApiCaller(db, user.id, user.email).apiKeys;\n\n      const firstKey = await api.create({ name: \"Test Key\" });\n      const regeneratedKey = await api.regenerate({ id: firstKey.id });\n\n      // Validate the new key\n      const validationResult = await unauthedAPICaller.apiKeys.validate({\n        apiKey: regeneratedKey.key,\n      });\n      expect(validationResult.success).toBe(true);\n\n      // Validate the old key is revoked\n      await expect(() =>\n        unauthedAPICaller.apiKeys.validate({\n          apiKey: firstKey.key,\n        }),\n      ).rejects.toThrow();\n    });\n  });\n\n  describe(\"revoke\", () => {\n    test<CustomTestContext>(\"revokes API key successfully\", async ({\n      unauthedAPICaller,\n      db,\n    }) => {\n      const user = await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"test@test.com\",\n        password: \"password123\",\n        confirmPassword: \"password123\",\n      });\n\n      const api = getApiCaller(db, user.id, user.email).apiKeys;\n\n      const createdKey = await api.create({ name: \"Test Key\" });\n      await api.revoke({ id: createdKey.id });\n\n      const remainingKeys = await db\n        .select()\n        .from(apiKeys)\n        .where(eq(apiKeys.id, createdKey.id));\n\n      expect(remainingKeys).toHaveLength(0);\n    });\n\n    test<CustomTestContext>(\"cannot revoke another user's key\", async ({\n      unauthedAPICaller,\n      db,\n    }) => {\n      const user1 = await unauthedAPICaller.users.create({\n        name: \"User 1\",\n        email: \"user1@test.com\",\n        password: \"password123\",\n        confirmPassword: \"password123\",\n      });\n\n      const user2 = await unauthedAPICaller.users.create({\n        name: \"User 2\",\n        email: \"user2@test.com\",\n        password: \"password123\",\n        confirmPassword: \"password123\",\n      });\n\n      const api1 = getApiCaller(db, user1.id, user1.email).apiKeys;\n      const api2 = getApiCaller(db, user2.id, user2.email).apiKeys;\n\n      const user1Key = await api1.create({ name: \"User 1 Key\" });\n\n      await api2.revoke({ id: user1Key.id });\n\n      const remainingKeys = await db\n        .select()\n        .from(apiKeys)\n        .where(eq(apiKeys.id, user1Key.id));\n\n      expect(remainingKeys).toHaveLength(1);\n    });\n\n    test<CustomTestContext>(\"silently handles non-existent key\", async ({\n      unauthedAPICaller,\n      db,\n    }) => {\n      const user = await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"test@test.com\",\n        password: \"password123\",\n        confirmPassword: \"password123\",\n      });\n\n      const api = getApiCaller(db, user.id, user.email).apiKeys;\n\n      await expect(\n        api.revoke({ id: \"non-existent-id\" }),\n      ).resolves.toBeUndefined();\n    });\n\n    test<CustomTestContext>(\"requires authentication\", async ({\n      unauthedAPICaller,\n    }) => {\n      await expect(() =>\n        unauthedAPICaller.apiKeys.revoke({ id: \"some-id\" }),\n      ).rejects.toThrow(/UNAUTHORIZED/);\n    });\n  });\n\n  describe(\"validate\", () => {\n    test<CustomTestContext>(\"validates correct API key\", async ({\n      unauthedAPICaller,\n      db,\n    }) => {\n      const user = await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"test@test.com\",\n        password: \"password123\",\n        confirmPassword: \"password123\",\n      });\n\n      const api = getApiCaller(db, user.id, user.email).apiKeys;\n      const createdKey = await api.create({ name: \"Test Key\" });\n\n      const result = await unauthedAPICaller.apiKeys.validate({\n        apiKey: createdKey.key,\n      });\n\n      expect(result.success).toBe(true);\n    });\n\n    test<CustomTestContext>(\"rejects invalid API key\", async ({\n      unauthedAPICaller,\n    }) => {\n      await expect(() =>\n        unauthedAPICaller.apiKeys.validate({\n          apiKey: \"invalid-key\",\n        }),\n      ).rejects.toThrow();\n    });\n\n    test<CustomTestContext>(\"rejects malformed API key\", async ({\n      unauthedAPICaller,\n    }) => {\n      await expect(() =>\n        unauthedAPICaller.apiKeys.validate({\n          apiKey: \"ak2_invalid\",\n        }),\n      ).rejects.toThrow();\n    });\n\n    test<CustomTestContext>(\"rejects non-existent key ID\", async ({\n      unauthedAPICaller,\n    }) => {\n      await expect(() =>\n        unauthedAPICaller.apiKeys.validate({\n          apiKey: \"ak2_1234567890abcdef1234_1234567890abcdef1234\",\n        }),\n      ).rejects.toThrow();\n    });\n\n    test<CustomTestContext>(\"rejects key with wrong secret\", async ({\n      unauthedAPICaller,\n      db,\n    }) => {\n      const user = await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"test@test.com\",\n        password: \"password123\",\n        confirmPassword: \"password123\",\n      });\n\n      const api = getApiCaller(db, user.id, user.email).apiKeys;\n      const createdKey = await api.create({ name: \"Test Key\" });\n      const keyParts = createdKey.key.split(\"_\");\n      const wrongKey = `${keyParts[0]}_${keyParts[1]}_wrongsecret123456`;\n\n      await expect(() =>\n        unauthedAPICaller.apiKeys.validate({\n          apiKey: wrongKey,\n        }),\n      ).rejects.toThrow();\n    });\n\n    test<CustomTestContext>(\"validates revoked key fails\", async ({\n      unauthedAPICaller,\n      db,\n    }) => {\n      const user = await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"test@test.com\",\n        password: \"password123\",\n        confirmPassword: \"password123\",\n      });\n\n      const api = getApiCaller(db, user.id, user.email).apiKeys;\n      const createdKey = await api.create({ name: \"Test Key\" });\n      await api.revoke({ id: createdKey.id });\n\n      await expect(() =>\n        unauthedAPICaller.apiKeys.validate({\n          apiKey: createdKey.key,\n        }),\n      ).rejects.toThrow();\n    });\n  });\n\n  describe(\"exchange\", () => {\n    test<CustomTestContext>(\"exchanges credentials for API key\", async ({\n      db,\n      unauthedAPICaller,\n    }) => {\n      const user = await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"exchange@test.com\",\n        password: \"password123\",\n        confirmPassword: \"password123\",\n      });\n\n      const result = await unauthedAPICaller.apiKeys.exchange({\n        keyName: \"Extension Key\",\n        email: \"exchange@test.com\",\n        password: \"password123\",\n      });\n\n      expect(result.name).toBe(\"Extension Key\");\n      expect(result.key).toMatch(/^ak2_[a-f0-9]{20}_[a-f0-9]{32}$/);\n      expect(result.createdAt).toBeInstanceOf(Date);\n\n      const dbKeys = await db\n        .select()\n        .from(apiKeys)\n        .where(eq(apiKeys.userId, user.id));\n\n      expect(dbKeys).toHaveLength(1);\n      expect(dbKeys[0].name).toBe(\"Extension Key\");\n    });\n\n    test<CustomTestContext>(\"rejects wrong password\", async ({\n      unauthedAPICaller,\n    }) => {\n      await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"wrongpass@test.com\",\n        password: \"password123\",\n        confirmPassword: \"password123\",\n      });\n\n      await expect(() =>\n        unauthedAPICaller.apiKeys.exchange({\n          keyName: \"Extension Key\",\n          email: \"wrongpass@test.com\",\n          password: \"wrongpassword\",\n        }),\n      ).rejects.toThrow(/UNAUTHORIZED/);\n    });\n\n    test<CustomTestContext>(\"rejects non-existent user\", async ({\n      unauthedAPICaller,\n    }) => {\n      await expect(() =>\n        unauthedAPICaller.apiKeys.exchange({\n          keyName: \"Extension Key\",\n          email: \"nonexistent@test.com\",\n          password: \"password123\",\n        }),\n      ).rejects.toThrow(/UNAUTHORIZED/);\n    });\n\n    test<CustomTestContext>(\"rejects unverified user when email verification is enabled\", async ({\n      db,\n    }) => {\n      // Import User model to create an unverified user directly\n      const { User } = await import(\"../models/users\");\n\n      // Create user with password but without email verification\n      await User.createRaw(db, {\n        name: \"Unverified User\",\n        email: \"unverified@test.com\",\n        password: await (await import(\"../auth\")).hashPassword(\n          \"password123\",\n          \"test-salt\",\n        ),\n        salt: \"test-salt\",\n        emailVerified: null, // User is not verified\n      });\n\n      // Mock serverConfig to enable email verification requirement\n      const originalConfig = (await import(\"@karakeep/shared/config\")).default;\n      vi.spyOn(\n        originalConfig.auth,\n        \"emailVerificationRequired\",\n        \"get\",\n      ).mockReturnValue(true);\n\n      const { createCallerFactory } = await import(\"../index\");\n      const { appRouter } = await import(\"./_app\");\n      const createCaller = createCallerFactory(appRouter);\n      const caller = createCaller({\n        user: null,\n        db,\n        req: { ip: null },\n      });\n\n      // Attempting to exchange should fail with verification error\n      await expect(() =>\n        caller.apiKeys.exchange({\n          keyName: \"Extension Key\",\n          email: \"unverified@test.com\",\n          password: \"password123\",\n        }),\n      ).rejects.toThrow(/verify your email/i);\n\n      vi.restoreAllMocks();\n    });\n  });\n\n  describe(\"integration scenarios\", () => {\n    test<CustomTestContext>(\"full API key lifecycle\", async ({\n      unauthedAPICaller,\n      db,\n    }) => {\n      const user = await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"lifecycle@test.com\",\n        password: \"password123\",\n        confirmPassword: \"password123\",\n      });\n\n      const api = getApiCaller(db, user.id, user.email).apiKeys;\n\n      const createdKey = await api.create({ name: \"Lifecycle Test\" });\n\n      const validationResult = await unauthedAPICaller.apiKeys.validate({\n        apiKey: createdKey.key,\n      });\n      expect(validationResult.success).toBe(true);\n\n      const listResult = await api.list();\n      expect(listResult.keys).toHaveLength(1);\n      expect(listResult.keys[0].name).toBe(\"Lifecycle Test\");\n\n      await api.revoke({ id: createdKey.id });\n\n      await expect(() =>\n        unauthedAPICaller.apiKeys.validate({\n          apiKey: createdKey.key,\n        }),\n      ).rejects.toThrow();\n\n      const finalListResult = await api.list();\n      expect(finalListResult.keys).toHaveLength(0);\n    });\n\n    test<CustomTestContext>(\"multiple keys per user\", async ({\n      unauthedAPICaller,\n      db,\n    }) => {\n      const user = await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"multikey@test.com\",\n        password: \"password123\",\n        confirmPassword: \"password123\",\n      });\n\n      const api = getApiCaller(db, user.id, user.email).apiKeys;\n\n      await api.create({ name: \"Key 1\" });\n      const key2 = await api.create({ name: \"Key 2\" });\n      await api.create({ name: \"Key 3\" });\n\n      const listResult = await api.list();\n      expect(listResult.keys).toHaveLength(3);\n\n      const keyNames = listResult.keys.map((k) => k.name).sort();\n      expect(keyNames).toEqual([\"Key 1\", \"Key 2\", \"Key 3\"]);\n\n      await api.revoke({ id: key2.id });\n\n      const updatedListResult = await api.list();\n      expect(updatedListResult.keys).toHaveLength(2);\n\n      const remainingNames = updatedListResult.keys.map((k) => k.name).sort();\n      expect(remainingNames).toEqual([\"Key 1\", \"Key 3\"]);\n    });\n\n    test<CustomTestContext>(\"exchange creates usable key\", async ({\n      unauthedAPICaller,\n    }) => {\n      await unauthedAPICaller.users.create({\n        name: \"Exchange User\",\n        email: \"exchangetest@test.com\",\n        password: \"password123\",\n        confirmPassword: \"password123\",\n      });\n\n      const exchangedKey = await unauthedAPICaller.apiKeys.exchange({\n        keyName: \"Exchange Test Key\",\n        email: \"exchangetest@test.com\",\n        password: \"password123\",\n      });\n\n      const validationResult = await unauthedAPICaller.apiKeys.validate({\n        apiKey: exchangedKey.key,\n      });\n\n      expect(validationResult.success).toBe(true);\n    });\n  });\n\n  describe(\"backward compatibility\", () => {\n    test<CustomTestContext>(\"validates version 1 keys continues to work\", async ({\n      unauthedAPICaller,\n      db,\n    }) => {\n      const user = await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"test@test.com\",\n        password: \"password123\",\n        confirmPassword: \"password123\",\n      });\n\n      // Manually generated v1 key and its corresponding hash\n      const key = \"ak1_1316296acfe14b7961d5_aa88970c058be93e3c7a\";\n      await db\n        .insert(apiKeys)\n        .values({\n          name: \"Test Key\",\n          userId: user.id,\n          keyId: \"1316296acfe14b7961d5\",\n          keyHash:\n            \"$2a$10$pnJyG.0NPTHImX/nukeUteibD//ztBg4MTjWYRI9n3d54Z/TWvcNC\",\n        })\n        .returning();\n\n      const api = getApiCaller(db, user.id, user.email).apiKeys;\n      const result = await api.validate({ apiKey: key });\n\n      expect(result.success).toBe(true);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/trpc/routers/apiKeys.ts",
    "content": "import { TRPCError } from \"@trpc/server\";\nimport { and, desc, eq } from \"drizzle-orm\";\nimport { z } from \"zod\";\n\nimport { apiKeys } from \"@karakeep/db/schema\";\nimport serverConfig from \"@karakeep/shared/config\";\n\nimport {\n  authenticateApiKey,\n  generateApiKey,\n  regenerateApiKey,\n  validatePassword,\n} from \"../auth\";\nimport {\n  authedProcedure,\n  createRateLimitMiddleware,\n  publicProcedure,\n  router,\n} from \"../index\";\n\nconst zApiKeySchema = z.object({\n  id: z.string(),\n  name: z.string(),\n  key: z.string(),\n  createdAt: z.date(),\n});\n\nexport const apiKeysAppRouter = router({\n  create: authedProcedure\n    .input(\n      z.object({\n        name: z.string(),\n      }),\n    )\n    .output(zApiKeySchema)\n    .mutation(async ({ input, ctx }) => {\n      return await generateApiKey(input.name, ctx.user.id, ctx.db);\n    }),\n  regenerate: authedProcedure\n    .input(\n      z.object({\n        id: z.string(),\n      }),\n    )\n    .output(zApiKeySchema)\n    .mutation(async ({ input, ctx }) => {\n      // Find the existing API key to get its name\n      const existingKey = await ctx.db.query.apiKeys.findFirst({\n        where: and(eq(apiKeys.id, input.id), eq(apiKeys.userId, ctx.user.id)),\n      });\n\n      if (!existingKey) {\n        throw new TRPCError({\n          message: \"API key not found\",\n          code: \"NOT_FOUND\",\n        });\n      }\n\n      return {\n        id: existingKey.id,\n        name: existingKey.name,\n        createdAt: existingKey.createdAt,\n        key: await regenerateApiKey(existingKey.id, ctx.user.id, ctx.db),\n      };\n    }),\n  revoke: authedProcedure\n    .input(\n      z.object({\n        id: z.string(),\n      }),\n    )\n    .mutation(async ({ input, ctx }) => {\n      await ctx.db\n        .delete(apiKeys)\n        .where(and(eq(apiKeys.id, input.id), eq(apiKeys.userId, ctx.user.id)));\n    }),\n  list: authedProcedure\n    .output(\n      z.object({\n        keys: z.array(\n          z.object({\n            id: z.string(),\n            name: z.string(),\n            createdAt: z.date(),\n            keyId: z.string(),\n            lastUsedAt: z.date().nullish(),\n          }),\n        ),\n      }),\n    )\n    .query(async ({ ctx }) => {\n      const resp = await ctx.db.query.apiKeys.findMany({\n        where: eq(apiKeys.userId, ctx.user.id),\n        columns: {\n          id: true,\n          name: true,\n          createdAt: true,\n          lastUsedAt: true,\n          keyId: true,\n        },\n        orderBy: desc(apiKeys.createdAt),\n      });\n      return { keys: resp };\n    }),\n  // Exchange the username and password with an API key.\n  // Homemade oAuth. This is used by the extension.\n  exchange: publicProcedure\n    .use(\n      createRateLimitMiddleware({\n        name: \"apiKey.exchange\",\n        windowMs: 15 * 60 * 1000,\n        maxRequests: 10,\n      }),\n    ) // 10 requests per 15 minutes\n    .input(\n      z.object({\n        keyName: z.string(),\n        email: z.string(),\n        password: z.string(),\n      }),\n    )\n    .output(zApiKeySchema)\n    .mutation(async ({ input, ctx }) => {\n      let user;\n      // Special handling as otherwise the extension would show \"username or password is wrong\"\n      if (serverConfig.auth.disablePasswordAuth) {\n        throw new TRPCError({\n          message: \"Password authentication is currently disabled\",\n          code: \"FORBIDDEN\",\n        });\n      }\n      try {\n        user = await validatePassword(input.email, input.password, ctx.db);\n      } catch {\n        throw new TRPCError({ code: \"UNAUTHORIZED\" });\n      }\n\n      // Check if email verification is required and if the user has verified their email\n      if (serverConfig.auth.emailVerificationRequired && !user.emailVerified) {\n        throw new TRPCError({\n          message:\n            \"Please verify your email address before generating an API key\",\n          code: \"FORBIDDEN\",\n        });\n      }\n\n      return await generateApiKey(input.keyName, user.id, ctx.db);\n    }),\n  validate: publicProcedure\n    .use(\n      createRateLimitMiddleware({\n        name: \"apiKey.validate\",\n        windowMs: 60 * 1000,\n        maxRequests: 30,\n      }),\n    ) // 30 requests per minute\n    .input(z.object({ apiKey: z.string() }))\n    .output(z.object({ success: z.boolean() }))\n    .mutation(async ({ input, ctx }) => {\n      try {\n        await authenticateApiKey(input.apiKey, ctx.db); // Throws if the key is invalid\n      } catch {\n        throw new TRPCError({ code: \"UNAUTHORIZED\" });\n      }\n      return {\n        success: true,\n      };\n    }),\n});\n"
  },
  {
    "path": "packages/trpc/routers/assets.test.ts",
    "content": "import { beforeEach, describe, expect, test } from \"vitest\";\n\nimport { assets, AssetTypes } from \"@karakeep/db/schema\";\nimport { BookmarkTypes, ZAssetType } from \"@karakeep/shared/types/bookmarks\";\n\nimport type { CustomTestContext } from \"../testUtils\";\nimport { defaultBeforeEach } from \"../testUtils\";\n\nbeforeEach<CustomTestContext>(defaultBeforeEach(true));\n\ndescribe(\"Asset Routes\", () => {\n  test<CustomTestContext>(\"mutate assets\", async ({ apiCallers, db }) => {\n    const api = apiCallers[0].assets;\n    const userId = await apiCallers[0].users.whoami().then((u) => u.id);\n\n    const bookmark = await apiCallers[0].bookmarks.createBookmark({\n      url: \"https://google.com\",\n      type: BookmarkTypes.LINK,\n    });\n    await Promise.all([\n      db.insert(assets).values({\n        id: \"asset1\",\n        assetType: AssetTypes.LINK_SCREENSHOT,\n        bookmarkId: bookmark.id,\n        userId,\n      }),\n      db.insert(assets).values({\n        id: \"asset2\",\n        assetType: AssetTypes.LINK_BANNER_IMAGE,\n        bookmarkId: bookmark.id,\n        userId,\n      }),\n      db.insert(assets).values({\n        id: \"asset3\",\n        assetType: AssetTypes.LINK_FULL_PAGE_ARCHIVE,\n        bookmarkId: bookmark.id,\n        userId,\n      }),\n      db.insert(assets).values({\n        id: \"asset4\",\n        assetType: AssetTypes.UNKNOWN,\n        bookmarkId: null,\n        userId,\n      }),\n      db.insert(assets).values({\n        id: \"asset5\",\n        assetType: AssetTypes.UNKNOWN,\n        bookmarkId: null,\n        userId,\n      }),\n      db.insert(assets).values({\n        id: \"asset6\",\n        assetType: AssetTypes.UNKNOWN,\n        bookmarkId: null,\n        userId,\n      }),\n    ]);\n\n    const validateAssets = async (\n      expected: {\n        id: string;\n        assetType: ZAssetType;\n        fileName: string | null;\n      }[],\n    ) => {\n      const b = await apiCallers[0].bookmarks.getBookmark({\n        bookmarkId: bookmark.id,\n      });\n      b.assets.sort((a, b) => a.id.localeCompare(b.id));\n      expect(b.assets).toEqual(expected);\n    };\n\n    await api.attachAsset({\n      bookmarkId: bookmark.id,\n      asset: {\n        id: \"asset4\",\n        assetType: \"screenshot\",\n      },\n    });\n\n    await validateAssets([\n      { id: \"asset1\", assetType: \"screenshot\", fileName: null },\n      { id: \"asset2\", assetType: \"bannerImage\", fileName: null },\n      { id: \"asset3\", assetType: \"fullPageArchive\", fileName: null },\n      { id: \"asset4\", assetType: \"screenshot\", fileName: null },\n    ]);\n\n    await api.replaceAsset({\n      bookmarkId: bookmark.id,\n      oldAssetId: \"asset1\",\n      newAssetId: \"asset5\",\n    });\n\n    await validateAssets([\n      { id: \"asset2\", assetType: \"bannerImage\", fileName: null },\n      { id: \"asset3\", assetType: \"fullPageArchive\", fileName: null },\n      { id: \"asset4\", assetType: \"screenshot\", fileName: null },\n      { id: \"asset5\", assetType: \"screenshot\", fileName: null },\n    ]);\n\n    await api.detachAsset({\n      bookmarkId: bookmark.id,\n      assetId: \"asset4\",\n    });\n\n    await validateAssets([\n      { id: \"asset2\", assetType: \"bannerImage\", fileName: null },\n      { id: \"asset3\", assetType: \"fullPageArchive\", fileName: null },\n      { id: \"asset5\", assetType: \"screenshot\", fileName: null },\n    ]);\n\n    // You're not allowed to attach/replace a fullPageArchive\n    await expect(\n      async () =>\n        await api.replaceAsset({\n          bookmarkId: bookmark.id,\n          oldAssetId: \"asset3\",\n          newAssetId: \"asset6\",\n        }),\n    ).rejects.toThrow(/You can't attach this type of asset/);\n    await expect(\n      async () =>\n        await api.attachAsset({\n          bookmarkId: bookmark.id,\n          asset: {\n            id: \"asset6\",\n            assetType: \"fullPageArchive\",\n          },\n        }),\n    ).rejects.toThrow(/You can't attach this type of asset/);\n  });\n});\n"
  },
  {
    "path": "packages/trpc/routers/assets.ts",
    "content": "import { z } from \"zod\";\n\nimport {\n  zAssetSchema,\n  zAssetTypesSchema,\n} from \"@karakeep/shared/types/bookmarks\";\n\nimport { authedProcedure, router } from \"../index\";\nimport { Asset } from \"../models/assets\";\nimport { ensureBookmarkOwnership } from \"./bookmarks\";\n\nexport const assetsAppRouter = router({\n  list: authedProcedure\n    .input(\n      z.object({\n        limit: z.number().min(1).max(100).default(20),\n        cursor: z.number().nullish(),\n      }),\n    )\n    .output(\n      z.object({\n        assets: z.array(\n          z.object({\n            id: z.string(),\n            assetType: zAssetTypesSchema,\n            size: z.number(),\n            contentType: z.string().nullable(),\n            fileName: z.string().nullable(),\n            bookmarkId: z.string().nullable(),\n          }),\n        ),\n        nextCursor: z.number().nullish(),\n        totalCount: z.number(),\n      }),\n    )\n    .query(async ({ input, ctx }) => {\n      return await Asset.list(ctx, {\n        limit: input.limit,\n        cursor: input.cursor ?? null,\n      });\n    }),\n  attachAsset: authedProcedure\n    .input(\n      z.object({\n        bookmarkId: z.string(),\n        asset: z.object({\n          id: z.string(),\n          assetType: zAssetTypesSchema,\n        }),\n      }),\n    )\n    .output(zAssetSchema)\n    .use(ensureBookmarkOwnership)\n    .mutation(async ({ input, ctx }) => {\n      return await Asset.attachAsset(ctx, input);\n    }),\n  replaceAsset: authedProcedure\n    .input(\n      z.object({\n        bookmarkId: z.string(),\n        oldAssetId: z.string(),\n        newAssetId: z.string(),\n      }),\n    )\n    .output(z.void())\n    .use(ensureBookmarkOwnership)\n    .mutation(async ({ input, ctx }) => {\n      await Asset.replaceAsset(ctx, input);\n    }),\n  detachAsset: authedProcedure\n    .input(\n      z.object({\n        bookmarkId: z.string(),\n        assetId: z.string(),\n      }),\n    )\n    .output(z.void())\n    .use(ensureBookmarkOwnership)\n    .mutation(async ({ input, ctx }) => {\n      await Asset.detachAsset(ctx, input);\n    }),\n});\n"
  },
  {
    "path": "packages/trpc/routers/backups.ts",
    "content": "import { z } from \"zod\";\n\nimport { zBackupSchema } from \"@karakeep/shared/types/backups\";\n\nimport { authedProcedure, createRateLimitMiddleware, router } from \"../index\";\nimport { Backup } from \"../models/backups\";\n\nexport const backupsAppRouter = router({\n  list: authedProcedure\n    .output(z.object({ backups: z.array(zBackupSchema) }))\n    .query(async ({ ctx }) => {\n      const backups = await Backup.getAll(ctx);\n      return { backups: backups.map((b) => b.asPublic()) };\n    }),\n\n  get: authedProcedure\n    .input(\n      z.object({\n        backupId: z.string(),\n      }),\n    )\n    .output(zBackupSchema)\n    .query(async ({ ctx, input }) => {\n      const backup = await Backup.fromId(ctx, input.backupId);\n      return backup.asPublic();\n    }),\n\n  delete: authedProcedure\n    .input(\n      z.object({\n        backupId: z.string(),\n      }),\n    )\n    .mutation(async ({ input, ctx }) => {\n      const backup = await Backup.fromId(ctx, input.backupId);\n      await backup.delete();\n    }),\n\n  triggerBackup: authedProcedure\n    .use(\n      createRateLimitMiddleware({\n        name: \"backups.triggerBackup\",\n        windowMs: 60 * 60 * 1000, // 1 hour window\n        maxRequests: 5, // Max 5 backup triggers per hour\n      }),\n    )\n    .output(zBackupSchema)\n    .mutation(async ({ ctx }) => {\n      const backup = await Backup.create(ctx);\n      await backup.triggerBackgroundJob();\n\n      return backup.asPublic();\n    }),\n});\n"
  },
  {
    "path": "packages/trpc/routers/bookmarks.test.ts",
    "content": "import { eq } from \"drizzle-orm\";\nimport { assert, beforeEach, describe, expect, test, vi } from \"vitest\";\n\nimport {\n  bookmarkLinks,\n  bookmarks,\n  rssFeedImportsTable,\n  tagsOnBookmarks,\n  users,\n} from \"@karakeep/db/schema\";\nimport * as sharedServer from \"@karakeep/shared-server\";\nimport { BookmarkTypes } from \"@karakeep/shared/types/bookmarks\";\n\nimport type { APICallerType, CustomTestContext } from \"../testUtils\";\nimport { defaultBeforeEach } from \"../testUtils\";\n\nvi.mock(\"@karakeep/shared-server\", async (original) => {\n  const mod = (await original()) as typeof import(\"@karakeep/shared-server\");\n  return {\n    ...mod,\n    LinkCrawlerQueue: {\n      enqueue: vi.fn(),\n    },\n    OpenAIQueue: {\n      enqueue: vi.fn(),\n    },\n    SearchIndexingQueue: {\n      enqueue: vi.fn(),\n    },\n    triggerRuleEngineOnEvent: vi.fn(),\n    triggerSearchReindex: vi.fn(),\n    triggerWebhook: vi.fn(),\n  };\n});\n\nbeforeEach<CustomTestContext>(defaultBeforeEach(true));\n\ndescribe(\"Bookmark Routes\", () => {\n  async function createTestTag(api: APICallerType, tagName: string) {\n    const result = await api.tags.create({ name: tagName });\n    return result.id;\n  }\n\n  async function createTestFeed(\n    api: APICallerType,\n    feedName: string,\n    feedUrl: string,\n  ) {\n    // Create an RSS feed and return its ID\n    const feed = await api.feeds.create({\n      name: feedName,\n      url: feedUrl,\n      enabled: true,\n    });\n    return feed.id;\n  }\n\n  test<CustomTestContext>(\"create bookmark\", async ({ apiCallers }) => {\n    const api = apiCallers[0].bookmarks;\n    const bookmark = await api.createBookmark({\n      url: \"https://google.com\",\n      type: BookmarkTypes.LINK,\n    });\n\n    const res = await api.getBookmark({ bookmarkId: bookmark.id });\n    assert(res.content.type == BookmarkTypes.LINK);\n    expect(res.content.url).toEqual(\"https://google.com\");\n    expect(res.favourited).toEqual(false);\n    expect(res.archived).toEqual(false);\n    expect(res.content.type).toEqual(BookmarkTypes.LINK);\n  });\n\n  test<CustomTestContext>(\"delete bookmark\", async ({ apiCallers }) => {\n    const api = apiCallers[0].bookmarks;\n\n    // Create the bookmark\n    const bookmark = await api.createBookmark({\n      url: \"https://google.com\",\n      type: BookmarkTypes.LINK,\n    });\n\n    // It should exist\n    await api.getBookmark({ bookmarkId: bookmark.id });\n\n    // Delete it\n    await api.deleteBookmark({ bookmarkId: bookmark.id });\n\n    // It shouldn't be there anymore\n    await expect(() =>\n      api.getBookmark({ bookmarkId: bookmark.id }),\n    ).rejects.toThrow(/Bookmark not found/);\n  });\n\n  test<CustomTestContext>(\"update bookmark\", async ({ apiCallers }) => {\n    const api = apiCallers[0].bookmarks;\n\n    // Create the bookmark\n    const bookmark = await api.createBookmark({\n      url: \"https://google.com\",\n      type: BookmarkTypes.LINK,\n    });\n\n    await api.updateBookmark({\n      bookmarkId: bookmark.id,\n      archived: true,\n      favourited: true,\n    });\n\n    let res = await api.getBookmark({ bookmarkId: bookmark.id });\n    expect(res.archived).toBeTruthy();\n    expect(res.favourited).toBeTruthy();\n\n    // Update other common fields\n    const newDate = new Date(Date.now() - 1000 * 60 * 60 * 24); // Yesterday\n    newDate.setMilliseconds(0);\n    await api.updateBookmark({\n      bookmarkId: bookmark.id,\n      title: \"New Title\",\n      note: \"Test Note\",\n      summary: \"Test Summary\",\n      createdAt: newDate,\n    });\n\n    res = await api.getBookmark({ bookmarkId: bookmark.id });\n    expect(res.title).toEqual(\"New Title\");\n    expect(res.note).toEqual(\"Test Note\");\n    expect(res.summary).toEqual(\"Test Summary\");\n    expect(res.createdAt).toEqual(newDate);\n\n    // Update link-specific fields\n    const linkUpdateDate = new Date(Date.now() - 1000 * 60 * 60 * 48); // 2 days ago\n    linkUpdateDate.setMilliseconds(0);\n    await api.updateBookmark({\n      bookmarkId: bookmark.id,\n      url: \"https://new-google.com\",\n      description: \"New Description\",\n      author: \"New Author\",\n      publisher: \"New Publisher\",\n      datePublished: linkUpdateDate,\n      dateModified: linkUpdateDate,\n    });\n\n    res = await api.getBookmark({ bookmarkId: bookmark.id });\n    assert(res.content.type === BookmarkTypes.LINK);\n    expect(res.content.url).toEqual(\"https://new-google.com\");\n    expect(res.content.description).toEqual(\"New Description\");\n    expect(res.content.author).toEqual(\"New Author\");\n    expect(res.content.publisher).toEqual(\"New Publisher\");\n    expect(res.content.datePublished).toEqual(linkUpdateDate);\n    expect(res.content.dateModified).toEqual(linkUpdateDate);\n  });\n\n  test<CustomTestContext>(\"update bookmark - non-link type error\", async ({\n    apiCallers,\n  }) => {\n    const api = apiCallers[0].bookmarks;\n\n    // Create a TEXT bookmark\n    const bookmark = await api.createBookmark({\n      text: \"Initial text\",\n      type: BookmarkTypes.TEXT,\n    });\n\n    // Attempt to update link-specific fields\n    await expect(() =>\n      api.updateBookmark({\n        bookmarkId: bookmark.id,\n        url: \"https://should-fail.com\", // Link-specific field\n      }),\n    ).rejects.toThrow(\n      /Attempting to set link attributes for non-link type bookmark/,\n    );\n  });\n\n  test<CustomTestContext>(\"list bookmarks\", async ({ apiCallers, db }) => {\n    const api = apiCallers[0].bookmarks;\n    const emptyBookmarks = await api.getBookmarks({});\n    expect(emptyBookmarks.bookmarks.length).toEqual(0);\n\n    const bookmark1 = await api.createBookmark({\n      url: \"https://google.com\",\n      type: BookmarkTypes.LINK,\n    });\n\n    const bookmark2 = await api.createBookmark({\n      url: \"https://google2.com\",\n      type: BookmarkTypes.LINK,\n    });\n\n    {\n      const bookmarks = await api.getBookmarks({});\n      expect(bookmarks.bookmarks.length).toEqual(2);\n    }\n\n    // Archive and favourite bookmark1\n    await api.updateBookmark({\n      bookmarkId: bookmark1.id,\n      archived: true,\n      favourited: true,\n    });\n\n    {\n      const bookmarks = await api.getBookmarks({ archived: false });\n      expect(bookmarks.bookmarks.length).toEqual(1);\n      expect(bookmarks.bookmarks[0].id).toEqual(bookmark2.id);\n    }\n\n    {\n      const bookmarks = await api.getBookmarks({ favourited: true });\n      expect(bookmarks.bookmarks.length).toEqual(1);\n      expect(bookmarks.bookmarks[0].id).toEqual(bookmark1.id);\n    }\n\n    {\n      const bookmarks = await api.getBookmarks({ archived: true });\n      expect(bookmarks.bookmarks.length).toEqual(1);\n      expect(bookmarks.bookmarks[0].id).toEqual(bookmark1.id);\n    }\n\n    {\n      const bookmarks = await api.getBookmarks({ ids: [bookmark1.id] });\n      expect(bookmarks.bookmarks.length).toEqual(1);\n      expect(bookmarks.bookmarks[0].id).toEqual(bookmark1.id);\n    }\n\n    // Test tagId filter\n    {\n      const tagId = await createTestTag(apiCallers[0], \"testTag\");\n      await api.updateTags({\n        bookmarkId: bookmark1.id,\n        attach: [{ tagId }],\n        detach: [],\n      });\n      const tagResult = await api.getBookmarks({ tagId });\n      expect(tagResult.bookmarks.length).toBeGreaterThan(0);\n      expect(\n        tagResult.bookmarks.some((b) => b.id === bookmark1.id),\n      ).toBeTruthy();\n    }\n\n    // Test rssFeedId filter\n    {\n      const feedId = await createTestFeed(\n        apiCallers[0],\n        \"Test Feed\",\n        \"https://rss-feed.com\",\n      );\n      const rssBookmark = await api.createBookmark({\n        url: \"https://rss-feed.com\",\n        type: BookmarkTypes.LINK,\n      });\n      await db.insert(rssFeedImportsTable).values([\n        {\n          rssFeedId: feedId,\n          entryId: \"entry-id\",\n          bookmarkId: rssBookmark.id,\n        },\n      ]);\n      const rssResult = await api.getBookmarks({ rssFeedId: feedId });\n      expect(rssResult.bookmarks.length).toBeGreaterThan(0);\n      expect(\n        rssResult.bookmarks.some((b) => b.id === rssBookmark.id),\n      ).toBeTruthy();\n    }\n\n    // Test listId filter\n    {\n      const list = await apiCallers[0].lists.create({\n        name: \"Test List\",\n        type: \"manual\",\n        icon: \"😂\",\n      });\n      await apiCallers[0].lists.addToList({\n        listId: list.id,\n        bookmarkId: bookmark1.id,\n      });\n      const listResult = await api.getBookmarks({ listId: list.id });\n      expect(listResult.bookmarks.length).toBeGreaterThan(0);\n      expect(\n        listResult.bookmarks.some((b) => b.id === bookmark1.id),\n      ).toBeTruthy();\n    }\n  });\n\n  test<CustomTestContext>(\"update tags\", async ({ apiCallers }) => {\n    const api = apiCallers[0].bookmarks;\n    const createdBookmark = await api.createBookmark({\n      url: \"https://google.com\",\n      type: BookmarkTypes.LINK,\n    });\n\n    await api.updateTags({\n      bookmarkId: createdBookmark.id,\n      attach: [\n        { tagName: \"tag1\" },\n        { tagName: \"tag2\" },\n        { tagName: \"tag3\" },\n        { tagName: \"tag4\" },\n      ],\n      detach: [],\n    });\n\n    let bookmark = await api.getBookmark({ bookmarkId: createdBookmark.id });\n    expect(bookmark.tags.map((t) => t.name).sort()).toEqual([\n      \"tag1\",\n      \"tag2\",\n      \"tag3\",\n      \"tag4\",\n    ]);\n\n    const tag1Id = bookmark.tags.filter((t) => t.name == \"tag1\")[0].id;\n\n    await api.updateTags({\n      bookmarkId: bookmark.id,\n      attach: [{ tagName: \"tag5\" }],\n      detach: [{ tagId: tag1Id }, { tagName: \"tag4\" }],\n    });\n\n    bookmark = await api.getBookmark({ bookmarkId: bookmark.id });\n    expect(bookmark.tags.map((t) => t.name).sort()).toEqual([\n      \"tag2\",\n      \"tag3\",\n      \"tag5\",\n    ]);\n\n    await api.updateTags({\n      bookmarkId: bookmark.id,\n      attach: [{ tagId: tag1Id }, { tagName: \"tag4\" }],\n      detach: [],\n    });\n    bookmark = await api.getBookmark({ bookmarkId: bookmark.id });\n    expect(bookmark.tags.map((t) => t.name).sort()).toEqual([\n      \"tag1\",\n      \"tag2\",\n      \"tag3\",\n      \"tag4\",\n      \"tag5\",\n    ]);\n\n    await expect(() =>\n      api.updateTags({ bookmarkId: bookmark.id, attach: [{}], detach: [] }),\n    ).rejects.toThrow(/You must provide either a tagId or a tagName/);\n    await expect(() =>\n      api.updateTags({ bookmarkId: bookmark.id, attach: [], detach: [{}] }),\n    ).rejects.toThrow(/You must provide either a tagId or a tagName/);\n    await expect(() =>\n      api.updateTags({\n        bookmarkId: bookmark.id,\n        attach: [{ tagName: \"\" }],\n        detach: [{}],\n      }),\n    ).rejects.toThrow(/You must provide either a tagId or a tagName/);\n  });\n\n  test<CustomTestContext>(\"update tags - comprehensive edge cases\", async ({\n    apiCallers,\n  }) => {\n    const api = apiCallers[0].bookmarks;\n\n    // Create two bookmarks\n    const bookmark1 = await api.createBookmark({\n      url: \"https://bookmark1.com\",\n      type: BookmarkTypes.LINK,\n    });\n    const bookmark2 = await api.createBookmark({\n      url: \"https://bookmark2.com\",\n      type: BookmarkTypes.LINK,\n    });\n\n    // Test 1: Attach tags by name to bookmark1 (creates new tags)\n    await api.updateTags({\n      bookmarkId: bookmark1.id,\n      attach: [{ tagName: \"existing-tag\" }, { tagName: \"shared-tag\" }],\n      detach: [],\n    });\n\n    let b1 = await api.getBookmark({ bookmarkId: bookmark1.id });\n    expect(b1.tags.map((t) => t.name).sort()).toEqual([\n      \"existing-tag\",\n      \"shared-tag\",\n    ]);\n\n    const existingTagId = b1.tags.find((t) => t.name === \"existing-tag\")!.id;\n    const sharedTagId = b1.tags.find((t) => t.name === \"shared-tag\")!.id;\n\n    // Test 2: Attach existing tag by ID to bookmark2 (tag already exists in DB from bookmark1)\n    await api.updateTags({\n      bookmarkId: bookmark2.id,\n      attach: [{ tagId: existingTagId }],\n      detach: [],\n    });\n\n    let b2 = await api.getBookmark({ bookmarkId: bookmark2.id });\n    expect(b2.tags.map((t) => t.name)).toEqual([\"existing-tag\"]);\n\n    // Test 3: Attach existing tag by NAME to bookmark2 (tag already exists in DB)\n    await api.updateTags({\n      bookmarkId: bookmark2.id,\n      attach: [{ tagName: \"shared-tag\" }],\n      detach: [],\n    });\n\n    b2 = await api.getBookmark({ bookmarkId: bookmark2.id });\n    expect(b2.tags.map((t) => t.name).sort()).toEqual([\n      \"existing-tag\",\n      \"shared-tag\",\n    ]);\n\n    // Test 4: Re-attaching the same tag (idempotency) - should be no-op\n    await api.updateTags({\n      bookmarkId: bookmark2.id,\n      attach: [{ tagId: existingTagId }],\n      detach: [],\n    });\n\n    b2 = await api.getBookmark({ bookmarkId: bookmark2.id });\n    expect(b2.tags.map((t) => t.name).sort()).toEqual([\n      \"existing-tag\",\n      \"shared-tag\",\n    ]);\n\n    // Test 5: Detach non-existent tag by name (should be no-op)\n    await api.updateTags({\n      bookmarkId: bookmark2.id,\n      attach: [],\n      detach: [{ tagName: \"non-existent-tag\" }],\n    });\n\n    b2 = await api.getBookmark({ bookmarkId: bookmark2.id });\n    expect(b2.tags.map((t) => t.name).sort()).toEqual([\n      \"existing-tag\",\n      \"shared-tag\",\n    ]);\n\n    // Test 6: Mixed attach/detach with pre-existing tags\n    await api.updateTags({\n      bookmarkId: bookmark2.id,\n      attach: [{ tagName: \"new-tag\" }, { tagId: sharedTagId }], // sharedTagId already attached\n      detach: [{ tagName: \"existing-tag\" }],\n    });\n\n    b2 = await api.getBookmark({ bookmarkId: bookmark2.id });\n    expect(b2.tags.map((t) => t.name).sort()).toEqual([\n      \"new-tag\",\n      \"shared-tag\",\n    ]);\n\n    // Test 7: Detach by ID and re-attach by name in same operation\n    await api.updateTags({\n      bookmarkId: bookmark2.id,\n      attach: [{ tagName: \"new-tag\" }], // Already exists, should be idempotent\n      detach: [{ tagId: sharedTagId }],\n    });\n\n    b2 = await api.getBookmark({ bookmarkId: bookmark2.id });\n    expect(b2.tags.map((t) => t.name).sort()).toEqual([\"new-tag\"]);\n\n    // Verify bookmark1 still has its original tags (operations on bookmark2 didn't affect it)\n    b1 = await api.getBookmark({ bookmarkId: bookmark1.id });\n    expect(b1.tags.map((t) => t.name).sort()).toEqual([\n      \"existing-tag\",\n      \"shared-tag\",\n    ]);\n\n    // Test 8: Attach same tag multiple times in one operation (deduplication)\n    await api.updateTags({\n      bookmarkId: bookmark1.id,\n      attach: [{ tagName: \"duplicate-test\" }, { tagName: \"duplicate-test\" }],\n      detach: [],\n    });\n\n    b1 = await api.getBookmark({ bookmarkId: bookmark1.id });\n    const duplicateTagCount = b1.tags.filter(\n      (t) => t.name === \"duplicate-test\",\n    ).length;\n    expect(duplicateTagCount).toEqual(1); // Should only be attached once\n  });\n\n  test<CustomTestContext>(\"update tags no-op does not retrigger indexing or update modifiedAt\", async ({\n    apiCallers,\n    db,\n  }) => {\n    const api = apiCallers[0].bookmarks;\n    const triggerSearchReindexMock = vi.mocked(\n      sharedServer.triggerSearchReindex,\n    );\n    triggerSearchReindexMock.mockClear();\n\n    const bookmark = await api.createBookmark({\n      url: \"https://bookmark.com\",\n      type: BookmarkTypes.LINK,\n    });\n    const tag = await apiCallers[0].tags.create({ name: \"stable-tag\" });\n    await db.insert(tagsOnBookmarks).values({\n      bookmarkId: bookmark.id,\n      tagId: tag.id,\n      attachedBy: \"human\",\n    });\n\n    await api.updateTags({\n      bookmarkId: bookmark.id,\n      attach: [],\n      detach: [{ tagId: tag.id }],\n    });\n\n    const [beforeNoopUpdate] = await db\n      .select({ modifiedAt: bookmarks.modifiedAt })\n      .from(bookmarks)\n      .where(eq(bookmarks.id, bookmark.id));\n    assert(beforeNoopUpdate?.modifiedAt);\n\n    triggerSearchReindexMock.mockClear();\n    await new Promise((resolve) => setTimeout(resolve, 10));\n\n    await api.updateTags({\n      bookmarkId: bookmark.id,\n      attach: [],\n      detach: [{ tagId: tag.id }],\n    });\n\n    const [afterNoopUpdate] = await db\n      .select({ modifiedAt: bookmarks.modifiedAt })\n      .from(bookmarks)\n      .where(eq(bookmarks.id, bookmark.id));\n    assert(afterNoopUpdate?.modifiedAt);\n\n    expect(triggerSearchReindexMock).not.toHaveBeenCalled();\n    expect(afterNoopUpdate.modifiedAt.getTime()).toEqual(\n      beforeNoopUpdate.modifiedAt.getTime(),\n    );\n  });\n\n  test<CustomTestContext>(\"updateTags with attachedBy field\", async ({\n    apiCallers,\n  }) => {\n    const api = apiCallers[0].bookmarks;\n    const bookmark = await api.createBookmark({\n      url: \"https://bookmark.com\",\n      type: BookmarkTypes.LINK,\n    });\n\n    // Test 1: Attach tags with different attachedBy values\n    await api.updateTags({\n      bookmarkId: bookmark.id,\n      attach: [\n        { tagName: \"ai-tag\", attachedBy: \"ai\" },\n        { tagName: \"human-tag\", attachedBy: \"human\" },\n        { tagName: \"default-tag\" }, // Should default to \"human\"\n      ],\n      detach: [],\n    });\n\n    let b = await api.getBookmark({ bookmarkId: bookmark.id });\n    expect(b.tags.length).toEqual(3);\n\n    const aiTag = b.tags.find((t) => t.name === \"ai-tag\");\n    const humanTag = b.tags.find((t) => t.name === \"human-tag\");\n    const defaultTag = b.tags.find((t) => t.name === \"default-tag\");\n\n    expect(aiTag?.attachedBy).toEqual(\"ai\");\n    expect(humanTag?.attachedBy).toEqual(\"human\");\n    expect(defaultTag?.attachedBy).toEqual(\"human\");\n\n    // Test 2: Attach existing tag by ID with different attachedBy\n    // First detach the ai-tag\n    await api.updateTags({\n      bookmarkId: bookmark.id,\n      attach: [],\n      detach: [{ tagId: aiTag!.id }],\n    });\n\n    // Re-attach the same tag but as human\n    await api.updateTags({\n      bookmarkId: bookmark.id,\n      attach: [{ tagId: aiTag!.id, attachedBy: \"human\" }],\n      detach: [],\n    });\n\n    b = await api.getBookmark({ bookmarkId: bookmark.id });\n    const reAttachedTag = b.tags.find((t) => t.id === aiTag!.id);\n    expect(reAttachedTag?.attachedBy).toEqual(\"human\");\n\n    // Test 3: Attach existing tag by name with AI attachedBy\n    const bookmark2 = await api.createBookmark({\n      url: \"https://bookmark2.com\",\n      type: BookmarkTypes.LINK,\n    });\n\n    await api.updateTags({\n      bookmarkId: bookmark2.id,\n      attach: [{ tagName: \"ai-tag\", attachedBy: \"ai\" }],\n      detach: [],\n    });\n\n    const b2 = await api.getBookmark({ bookmarkId: bookmark2.id });\n    const aiTagOnB2 = b2.tags.find((t) => t.name === \"ai-tag\");\n    expect(aiTagOnB2?.attachedBy).toEqual(\"ai\");\n    expect(aiTagOnB2?.id).toEqual(aiTag!.id); // Should be the same tag\n  });\n\n  test<CustomTestContext>(\"update bookmark text\", async ({ apiCallers }) => {\n    const api = apiCallers[0].bookmarks;\n    const createdBookmark = await api.createBookmark({\n      text: \"HELLO WORLD\",\n      type: BookmarkTypes.TEXT,\n    });\n\n    await api.updateBookmarkText({\n      bookmarkId: createdBookmark.id,\n      text: \"WORLD HELLO\",\n    });\n\n    const bookmark = await api.getBookmark({ bookmarkId: createdBookmark.id });\n    assert(bookmark.content.type == BookmarkTypes.TEXT);\n    expect(bookmark.content.text).toEqual(\"WORLD HELLO\");\n  });\n\n  test<CustomTestContext>(\"privacy\", async ({ apiCallers }) => {\n    const user1Bookmark = await apiCallers[0].bookmarks.createBookmark({\n      type: BookmarkTypes.LINK,\n      url: \"https://google.com\",\n    });\n    const user2Bookmark = await apiCallers[1].bookmarks.createBookmark({\n      type: BookmarkTypes.LINK,\n      url: \"https://google.com\",\n    });\n\n    // All interactions with the wrong user should fail\n    await expect(() =>\n      apiCallers[0].bookmarks.deleteBookmark({ bookmarkId: user2Bookmark.id }),\n    ).rejects.toThrow(/Bookmark not found/);\n    await expect(() =>\n      apiCallers[0].bookmarks.getBookmark({ bookmarkId: user2Bookmark.id }),\n    ).rejects.toThrow(/Bookmark not found/);\n    await expect(() =>\n      apiCallers[0].bookmarks.updateBookmark({ bookmarkId: user2Bookmark.id }),\n    ).rejects.toThrow(/Bookmark not found/);\n    await expect(() =>\n      apiCallers[0].bookmarks.updateTags({\n        bookmarkId: user2Bookmark.id,\n        attach: [],\n        detach: [],\n      }),\n    ).rejects.toThrow(/Bookmark not found/);\n\n    // Get bookmarks should only show the correct one\n    expect(\n      (await apiCallers[0].bookmarks.getBookmarks({})).bookmarks.map(\n        (b) => b.id,\n      ),\n    ).toEqual([user1Bookmark.id]);\n    expect(\n      (await apiCallers[1].bookmarks.getBookmarks({})).bookmarks.map(\n        (b) => b.id,\n      ),\n    ).toEqual([user2Bookmark.id]);\n  });\n\n  test<CustomTestContext>(\"bookmark links dedup\", async ({ apiCallers }) => {\n    // Two users with google in their bookmarks\n    const bookmark1User1 = await apiCallers[0].bookmarks.createBookmark({\n      url: \"https://google.com\",\n      type: BookmarkTypes.LINK,\n    });\n    expect(bookmark1User1.alreadyExists).toEqual(false);\n\n    const bookmark1User2 = await apiCallers[1].bookmarks.createBookmark({\n      url: \"https://google.com\",\n      type: BookmarkTypes.LINK,\n    });\n    expect(bookmark1User2.alreadyExists).toEqual(false);\n\n    // User1 attempting to re-add google. Should return the existing bookmark\n    const bookmark2User1 = await apiCallers[0].bookmarks.createBookmark({\n      url: \"https://google.com\",\n      type: BookmarkTypes.LINK,\n    });\n    expect(bookmark2User1.alreadyExists).toEqual(true);\n    expect(bookmark2User1.id).toEqual(bookmark1User1.id);\n\n    // User2 attempting to re-add google. Should return the existing bookmark\n    const bookmark2User2 = await apiCallers[1].bookmarks.createBookmark({\n      url: \"https://google.com\",\n      type: BookmarkTypes.LINK,\n    });\n    expect(bookmark2User2.alreadyExists).toEqual(true);\n    expect(bookmark2User2.id).toEqual(bookmark1User2.id);\n\n    // User1 adding google2. Should not return an existing bookmark\n    const bookmark3User1 = await apiCallers[0].bookmarks.createBookmark({\n      url: \"https://google2.com\",\n      type: BookmarkTypes.LINK,\n    });\n    expect(bookmark3User1.alreadyExists).toEqual(false);\n  });\n\n  // Ensure that the pagination returns all the results\n  test<CustomTestContext>(\"pagination\", async ({ apiCallers, db }) => {\n    const user = await apiCallers[0].users.whoami();\n    let now = 100_000;\n\n    const bookmarkWithDate = (date_ms: number) => ({\n      userId: user.id,\n      createdAt: new Date(date_ms),\n      type: BookmarkTypes.TEXT as const,\n    });\n\n    // One normal bookmark\n    const values = [bookmarkWithDate(now)];\n    // 10 with a second in between\n    for (let i = 0; i < 10; i++) {\n      now -= 1000;\n      values.push(bookmarkWithDate(now));\n    }\n    // Another ten but at the same second\n    for (let i = 0; i < 10; i++) {\n      values.push(bookmarkWithDate(now));\n    }\n    // And then another one with a second afterwards\n    for (let i = 0; i < 10; i++) {\n      now -= 1000;\n      values.push(bookmarkWithDate(now));\n    }\n    // In total, we should have 31 bookmarks\n\n    const inserted = await db.insert(bookmarks).values(values).returning();\n\n    const validateWithLimit = async (limit: number) => {\n      const results: string[] = [];\n      let cursor = undefined;\n\n      // To avoid running the test forever\n      let i = 0;\n\n      do {\n        const res = await apiCallers[0].bookmarks.getBookmarks({\n          limit,\n          cursor,\n          useCursorV2: true,\n        });\n        results.push(...res.bookmarks.map((b) => b.id));\n        cursor = res.nextCursor;\n        i++;\n      } while (cursor && i < 100);\n\n      expect(results.sort()).toEqual(inserted.map((b) => b.id).sort());\n    };\n\n    await validateWithLimit(1);\n    await validateWithLimit(2);\n    await validateWithLimit(3);\n    await validateWithLimit(10);\n    await validateWithLimit(100);\n  });\n\n  test<CustomTestContext>(\"getBookmark\", async ({ apiCallers }) => {\n    const api = apiCallers[0].bookmarks;\n    const createdBookmark = await api.createBookmark({\n      url: \"https://example.com\",\n      type: BookmarkTypes.LINK,\n    });\n\n    // Test successful getBookmark with includeContent false\n    const bookmarkWithoutContent = await api.getBookmark({\n      bookmarkId: createdBookmark.id,\n      includeContent: false,\n    });\n    expect(bookmarkWithoutContent.id).toEqual(createdBookmark.id);\n    expect(bookmarkWithoutContent.content).toBeDefined(); // Content should still be present but might be partial\n    expect(bookmarkWithoutContent.content.type).toEqual(BookmarkTypes.LINK);\n    assert(bookmarkWithoutContent.content.type == BookmarkTypes.LINK);\n    expect(bookmarkWithoutContent.content.url).toEqual(\"https://example.com\");\n\n    // Test successful getBookmark with includeContent true\n    const bookmarkWithContent = await api.getBookmark({\n      bookmarkId: createdBookmark.id,\n      includeContent: true,\n    });\n    expect(bookmarkWithContent.id).toEqual(createdBookmark.id);\n    expect(bookmarkWithContent.content).toBeDefined();\n    expect(bookmarkWithContent.content.type).toEqual(BookmarkTypes.LINK);\n    assert(bookmarkWithContent.content.type == BookmarkTypes.LINK);\n    expect(bookmarkWithContent.content.url).toEqual(\"https://example.com\");\n    // Additional checks if content includes more details, e.g., htmlContent if available\n\n    // Test non-existent bookmark\n    await expect(() =>\n      api.getBookmark({ bookmarkId: \"non-existent-id\" }),\n    ).rejects.toThrow(/Bookmark not found/);\n  });\n\n  test<CustomTestContext>(\"getBrokenLinks\", async ({ apiCallers, db }) => {\n    const api = apiCallers[0].bookmarks;\n\n    // Create a broken link bookmark (simulate by setting crawlStatus to 'failure')\n    const brokenBookmark = await api.createBookmark({\n      url: \"https://broken-link.com\",\n      type: BookmarkTypes.LINK,\n    });\n    await db\n      .update(bookmarkLinks)\n      .set({ crawlStatus: \"failure\" })\n      .where(eq(bookmarkLinks.id, brokenBookmark.id));\n\n    const result = await api.getBrokenLinks();\n    expect(result.bookmarks.length).toBeGreaterThan(0);\n    expect(\n      result.bookmarks.some((b) => b.id === brokenBookmark.id),\n    ).toBeTruthy();\n    expect(result.bookmarks[0].url).toEqual(\"https://broken-link.com\");\n    expect(result.bookmarks[0].isCrawlingFailure).toBeTruthy();\n\n    // Test with no broken links\n    await db\n      .update(bookmarkLinks)\n      .set({ crawlStatus: \"success\" })\n      .where(eq(bookmarkLinks.id, brokenBookmark.id));\n    const emptyResult = await api.getBrokenLinks();\n    expect(emptyResult.bookmarks.length).toEqual(0);\n  });\n\n  describe(\"Bookmark Quotas\", () => {\n    test<CustomTestContext>(\"create bookmark with no quota (unlimited)\", async ({\n      apiCallers,\n    }) => {\n      const api = apiCallers[0].bookmarks;\n\n      // User should be able to create bookmarks without any quota restrictions\n      const bookmark1 = await api.createBookmark({\n        url: \"https://example1.com\",\n        type: BookmarkTypes.LINK,\n      });\n      expect(bookmark1.alreadyExists).toEqual(false);\n\n      const bookmark2 = await api.createBookmark({\n        url: \"https://example2.com\",\n        type: BookmarkTypes.LINK,\n      });\n      expect(bookmark2.alreadyExists).toEqual(false);\n\n      const bookmark3 = await api.createBookmark({\n        text: \"Test text bookmark\",\n        type: BookmarkTypes.TEXT,\n      });\n      expect(bookmark3.alreadyExists).toEqual(false);\n    });\n\n    test<CustomTestContext>(\"create bookmark with quota limit\", async ({\n      apiCallers,\n      db,\n    }) => {\n      const user = await apiCallers[0].users.whoami();\n      const api = apiCallers[0].bookmarks;\n\n      // Set quota to 2 bookmarks for this user\n      await db\n        .update(users)\n        .set({ bookmarkQuota: 2 })\n        .where(eq(users.id, user.id));\n\n      // First bookmark should succeed\n      const bookmark1 = await api.createBookmark({\n        url: \"https://example1.com\",\n        type: BookmarkTypes.LINK,\n      });\n      expect(bookmark1.alreadyExists).toEqual(false);\n\n      // Second bookmark should succeed\n      const bookmark2 = await api.createBookmark({\n        url: \"https://example2.com\",\n        type: BookmarkTypes.LINK,\n      });\n      expect(bookmark2.alreadyExists).toEqual(false);\n\n      // Third bookmark should fail due to quota\n      await expect(() =>\n        api.createBookmark({\n          url: \"https://example3.com\",\n          type: BookmarkTypes.LINK,\n        }),\n      ).rejects.toThrow(\n        /Bookmark quota exceeded. You can only have 2 bookmarks./,\n      );\n    });\n\n    test<CustomTestContext>(\"create bookmark with quota limit - different types\", async ({\n      apiCallers,\n      db,\n    }) => {\n      const user = await apiCallers[0].users.whoami();\n      const api = apiCallers[0].bookmarks;\n\n      // Set quota to 2 bookmarks for this user\n      await db\n        .update(users)\n        .set({ bookmarkQuota: 2 })\n        .where(eq(users.id, user.id));\n\n      // Create one link bookmark\n      await api.createBookmark({\n        url: \"https://example1.com\",\n        type: BookmarkTypes.LINK,\n      });\n\n      // Create one text bookmark\n      await api.createBookmark({\n        text: \"Test text content\",\n        type: BookmarkTypes.TEXT,\n      });\n\n      // Third bookmark (any type) should fail\n      await expect(() =>\n        api.createBookmark({\n          text: \"Another text bookmark\",\n          type: BookmarkTypes.TEXT,\n        }),\n      ).rejects.toThrow(\n        /Bookmark quota exceeded. You can only have 2 bookmarks./,\n      );\n    });\n\n    test<CustomTestContext>(\"quota enforcement after deletion\", async ({\n      apiCallers,\n      db,\n    }) => {\n      const user = await apiCallers[0].users.whoami();\n      const api = apiCallers[0].bookmarks;\n\n      // Set quota to 1 bookmark for this user\n      await db\n        .update(users)\n        .set({ bookmarkQuota: 1 })\n        .where(eq(users.id, user.id));\n\n      // Create first bookmark\n      const bookmark1 = await api.createBookmark({\n        url: \"https://example1.com\",\n        type: BookmarkTypes.LINK,\n      });\n\n      // Second bookmark should fail\n      await expect(() =>\n        api.createBookmark({\n          url: \"https://example2.com\",\n          type: BookmarkTypes.LINK,\n        }),\n      ).rejects.toThrow(\n        /Bookmark quota exceeded. You can only have 1 bookmarks./,\n      );\n\n      // Delete the first bookmark\n      await api.deleteBookmark({ bookmarkId: bookmark1.id });\n\n      // Now should be able to create a new bookmark\n      const bookmark2 = await api.createBookmark({\n        url: \"https://example2.com\",\n        type: BookmarkTypes.LINK,\n      });\n      expect(bookmark2.alreadyExists).toEqual(false);\n    });\n\n    test<CustomTestContext>(\"quota isolation between users\", async ({\n      apiCallers,\n      db,\n    }) => {\n      const user1 = await apiCallers[0].users.whoami();\n\n      // Set quota to 1 for user1, unlimited for user2\n      await db\n        .update(users)\n        .set({ bookmarkQuota: 1 })\n        .where(eq(users.id, user1.id));\n\n      // User1 creates one bookmark (reaches quota)\n      await apiCallers[0].bookmarks.createBookmark({\n        url: \"https://user1-example.com\",\n        type: BookmarkTypes.LINK,\n      });\n\n      // User1 cannot create another bookmark\n      await expect(() =>\n        apiCallers[0].bookmarks.createBookmark({\n          url: \"https://user1-example2.com\",\n          type: BookmarkTypes.LINK,\n        }),\n      ).rejects.toThrow(\n        /Bookmark quota exceeded. You can only have 1 bookmarks./,\n      );\n\n      // User2 should be able to create multiple bookmarks (no quota)\n      await apiCallers[1].bookmarks.createBookmark({\n        url: \"https://user2-example1.com\",\n        type: BookmarkTypes.LINK,\n      });\n\n      await apiCallers[1].bookmarks.createBookmark({\n        url: \"https://user2-example2.com\",\n        type: BookmarkTypes.LINK,\n      });\n\n      await apiCallers[1].bookmarks.createBookmark({\n        text: \"User2 text bookmark\",\n        type: BookmarkTypes.TEXT,\n      });\n    });\n\n    test<CustomTestContext>(\"quota with zero limit\", async ({\n      apiCallers,\n      db,\n    }) => {\n      const user = await apiCallers[0].users.whoami();\n      const api = apiCallers[0].bookmarks;\n\n      // Set quota to 0 bookmarks for this user\n      await db\n        .update(users)\n        .set({ bookmarkQuota: 0 })\n        .where(eq(users.id, user.id));\n\n      // Any bookmark creation should fail\n      await expect(() =>\n        api.createBookmark({\n          url: \"https://example.com\",\n          type: BookmarkTypes.LINK,\n        }),\n      ).rejects.toThrow(\n        /Bookmark quota exceeded. You can only have 0 bookmarks./,\n      );\n\n      await expect(() =>\n        api.createBookmark({\n          text: \"Test text\",\n          type: BookmarkTypes.TEXT,\n        }),\n      ).rejects.toThrow(\n        /Bookmark quota exceeded. You can only have 0 bookmarks./,\n      );\n    });\n\n    test<CustomTestContext>(\"quota does not affect duplicate link detection\", async ({\n      apiCallers,\n      db,\n    }) => {\n      const user = await apiCallers[0].users.whoami();\n      const api = apiCallers[0].bookmarks;\n\n      // Set quota to 1 bookmark for this user\n      await db\n        .update(users)\n        .set({ bookmarkQuota: 1 })\n        .where(eq(users.id, user.id));\n\n      // Create first bookmark\n      const bookmark1 = await api.createBookmark({\n        url: \"https://example.com\",\n        type: BookmarkTypes.LINK,\n      });\n      expect(bookmark1.alreadyExists).toEqual(false);\n\n      // Try to create the same URL again - should return existing bookmark, not fail with quota\n      const bookmark2 = await api.createBookmark({\n        url: \"https://example.com\",\n        type: BookmarkTypes.LINK,\n      });\n      expect(bookmark2.alreadyExists).toEqual(true);\n      expect(bookmark2.id).toEqual(bookmark1.id);\n\n      // But creating a different URL should fail due to quota\n      await expect(() =>\n        api.createBookmark({\n          url: \"https://different-example.com\",\n          type: BookmarkTypes.LINK,\n        }),\n      ).rejects.toThrow(\n        /Bookmark quota exceeded. You can only have 1 bookmarks./,\n      );\n    });\n  });\n\n  describe(\"Reading Progress\", () => {\n    test<CustomTestContext>(\"saves and retrieves reading progress\", async ({\n      apiCallers,\n    }) => {\n      const api = apiCallers[0].bookmarks;\n\n      // Create a link bookmark\n      const bookmark = await api.createBookmark({\n        url: \"https://example.com/article\",\n        type: BookmarkTypes.LINK,\n      });\n\n      // Save reading progress\n      await api.updateReadingProgress({\n        bookmarkId: bookmark.id,\n        readingProgressOffset: 1500,\n        readingProgressAnchor: \"This is the anchor text for verification\",\n      });\n\n      // Retrieve and verify progress via getReadingProgress\n      const progress = await api.getReadingProgress({\n        bookmarkId: bookmark.id,\n      });\n      expect(progress.readingProgressOffset).toBe(1500);\n      expect(progress.readingProgressAnchor).toBe(\n        \"This is the anchor text for verification\",\n      );\n    });\n\n    test<CustomTestContext>(\"updates existing progress (upsert behavior)\", async ({\n      apiCallers,\n    }) => {\n      const api = apiCallers[0].bookmarks;\n\n      const bookmark = await api.createBookmark({\n        url: \"https://example.com/article\",\n        type: BookmarkTypes.LINK,\n      });\n\n      // Save initial progress\n      await api.updateReadingProgress({\n        bookmarkId: bookmark.id,\n        readingProgressOffset: 500,\n        readingProgressAnchor: \"First anchor\",\n      });\n\n      // Update progress\n      await api.updateReadingProgress({\n        bookmarkId: bookmark.id,\n        readingProgressOffset: 2000,\n        readingProgressAnchor: \"Updated anchor\",\n      });\n\n      // Verify updated values\n      const progress = await api.getReadingProgress({\n        bookmarkId: bookmark.id,\n      });\n      expect(progress.readingProgressOffset).toBe(2000);\n      expect(progress.readingProgressAnchor).toBe(\"Updated anchor\");\n    });\n\n    test<CustomTestContext>(\"two users have independent progress on same bookmark\", async ({\n      apiCallers,\n    }) => {\n      const api1 = apiCallers[0].bookmarks;\n      const api2 = apiCallers[1].bookmarks;\n\n      // User 1 creates a bookmark\n      const bookmark = await api1.createBookmark({\n        url: \"https://example.com/shared-article\",\n        type: BookmarkTypes.LINK,\n      });\n\n      // User 1 saves progress at position 1000\n      await api1.updateReadingProgress({\n        bookmarkId: bookmark.id,\n        readingProgressOffset: 1000,\n        readingProgressAnchor: \"User 1 anchor\",\n      });\n\n      // User 2 creates the same bookmark (different bookmark ID, same URL)\n      const bookmark2 = await api2.createBookmark({\n        url: \"https://example.com/shared-article\",\n        type: BookmarkTypes.LINK,\n      });\n\n      // User 2 saves progress at position 3000\n      await api2.updateReadingProgress({\n        bookmarkId: bookmark2.id,\n        readingProgressOffset: 3000,\n        readingProgressAnchor: \"User 2 anchor\",\n      });\n\n      // Verify each user sees their own progress\n      const progress1 = await api1.getReadingProgress({\n        bookmarkId: bookmark.id,\n      });\n      const progress2 = await api2.getReadingProgress({\n        bookmarkId: bookmark2.id,\n      });\n\n      expect(progress1.readingProgressOffset).toBe(1000);\n      expect(progress1.readingProgressAnchor).toBe(\"User 1 anchor\");\n\n      expect(progress2.readingProgressOffset).toBe(3000);\n      expect(progress2.readingProgressAnchor).toBe(\"User 2 anchor\");\n    });\n\n    test<CustomTestContext>(\"rejects reading progress on TEXT bookmark\", async ({\n      apiCallers,\n    }) => {\n      const api = apiCallers[0].bookmarks;\n\n      const bookmark = await api.createBookmark({\n        text: \"Some text content\",\n        type: BookmarkTypes.TEXT,\n      });\n\n      await expect(() =>\n        api.updateReadingProgress({\n          bookmarkId: bookmark.id,\n          readingProgressOffset: 100,\n        }),\n      ).rejects.toThrow(\n        /Reading progress can only be saved for link bookmarks/,\n      );\n    });\n\n    test<CustomTestContext>(\"reading progress is deleted when bookmark is deleted\", async ({\n      apiCallers,\n    }) => {\n      const api = apiCallers[0].bookmarks;\n\n      const bookmark = await api.createBookmark({\n        url: \"https://example.com/to-delete\",\n        type: BookmarkTypes.LINK,\n      });\n\n      // Save reading progress\n      await api.updateReadingProgress({\n        bookmarkId: bookmark.id,\n        readingProgressOffset: 500,\n        readingProgressAnchor: \"Will be deleted\",\n      });\n\n      // Verify progress exists\n      const progress = await api.getReadingProgress({\n        bookmarkId: bookmark.id,\n      });\n      expect(progress.readingProgressOffset).toBe(500);\n\n      // Delete the bookmark\n      await api.deleteBookmark({ bookmarkId: bookmark.id });\n\n      // Verify bookmark is gone (and implicitly, the progress cascade deleted)\n      await expect(() =>\n        api.getBookmark({ bookmarkId: bookmark.id }),\n      ).rejects.toThrow(/Bookmark not found/);\n    });\n\n    test<CustomTestContext>(\"collaborator can save reading progress on shared bookmark\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      // Owner creates a link bookmark\n      const bookmark = await ownerApi.bookmarks.createBookmark({\n        url: \"https://example.com/shared-article\",\n        type: BookmarkTypes.LINK,\n      });\n\n      // Owner creates a list and adds the bookmark\n      const list = await ownerApi.lists.create({\n        name: \"Shared Reading List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      await ownerApi.lists.addToList({\n        listId: list.id,\n        bookmarkId: bookmark.id,\n      });\n\n      // Share the list with collaborator\n      const collaboratorUser = await collaboratorApi.users.whoami();\n      const { invitationId } = await ownerApi.lists.addCollaborator({\n        listId: list.id,\n        email: collaboratorUser.email!,\n        role: \"viewer\",\n      });\n      await collaboratorApi.lists.acceptInvitation({ invitationId });\n\n      // Collaborator saves their own reading progress on the shared bookmark\n      await collaboratorApi.bookmarks.updateReadingProgress({\n        bookmarkId: bookmark.id,\n        readingProgressOffset: 2500,\n        readingProgressAnchor: \"Collaborator's position\",\n      });\n\n      // Collaborator retrieves their progress\n      const collaboratorProgress =\n        await collaboratorApi.bookmarks.getReadingProgress({\n          bookmarkId: bookmark.id,\n        });\n      expect(collaboratorProgress.readingProgressOffset).toBe(2500);\n      expect(collaboratorProgress.readingProgressAnchor).toBe(\n        \"Collaborator's position\",\n      );\n\n      // Owner's progress should be independent (null since owner hasn't set any)\n      const ownerProgress = await ownerApi.bookmarks.getReadingProgress({\n        bookmarkId: bookmark.id,\n      });\n      expect(ownerProgress.readingProgressOffset).toBeNull();\n    });\n\n    test<CustomTestContext>(\"user without shared access cannot save reading progress\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const unauthorizedApi = apiCallers[1];\n\n      // Owner creates a bookmark (not shared with anyone)\n      const bookmark = await ownerApi.bookmarks.createBookmark({\n        url: \"https://example.com/private-article\",\n        type: BookmarkTypes.LINK,\n      });\n\n      // Unauthorized user tries to save reading progress\n      await expect(() =>\n        unauthorizedApi.bookmarks.updateReadingProgress({\n          bookmarkId: bookmark.id,\n          readingProgressOffset: 1000,\n        }),\n      ).rejects.toThrow(/Bookmark not found/);\n    });\n\n    test<CustomTestContext>(\"owner and collaborator have independent reading progress on same bookmark\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      // Owner creates a link bookmark\n      const bookmark = await ownerApi.bookmarks.createBookmark({\n        url: \"https://example.com/shared-reading\",\n        type: BookmarkTypes.LINK,\n      });\n\n      // Owner creates a list and adds the bookmark\n      const list = await ownerApi.lists.create({\n        name: \"Shared List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      await ownerApi.lists.addToList({\n        listId: list.id,\n        bookmarkId: bookmark.id,\n      });\n\n      // Share with collaborator\n      const collaboratorUser = await collaboratorApi.users.whoami();\n      const { invitationId } = await ownerApi.lists.addCollaborator({\n        listId: list.id,\n        email: collaboratorUser.email!,\n        role: \"viewer\",\n      });\n      await collaboratorApi.lists.acceptInvitation({ invitationId });\n\n      // Owner saves progress at position 1000\n      await ownerApi.bookmarks.updateReadingProgress({\n        bookmarkId: bookmark.id,\n        readingProgressOffset: 1000,\n        readingProgressAnchor: \"Owner position\",\n      });\n\n      // Collaborator saves progress at position 5000\n      await collaboratorApi.bookmarks.updateReadingProgress({\n        bookmarkId: bookmark.id,\n        readingProgressOffset: 5000,\n        readingProgressAnchor: \"Collaborator position\",\n      });\n\n      // Verify each user sees their own progress\n      const ownerProgress = await ownerApi.bookmarks.getReadingProgress({\n        bookmarkId: bookmark.id,\n      });\n      const collaboratorProgress =\n        await collaboratorApi.bookmarks.getReadingProgress({\n          bookmarkId: bookmark.id,\n        });\n\n      expect(ownerProgress.readingProgressOffset).toBe(1000);\n      expect(ownerProgress.readingProgressAnchor).toBe(\"Owner position\");\n\n      expect(collaboratorProgress.readingProgressOffset).toBe(5000);\n      expect(collaboratorProgress.readingProgressAnchor).toBe(\n        \"Collaborator position\",\n      );\n    });\n  });\n\n  describe(\"checkUrl\", () => {\n    test<CustomTestContext>(\"returns null for non-existent URL\", async ({\n      apiCallers,\n    }) => {\n      const api = apiCallers[0].bookmarks;\n      const result = await api.checkUrl({\n        url: \"https://nonexistent.example.com\",\n      });\n      expect(result.bookmarkId).toBeNull();\n    });\n\n    test<CustomTestContext>(\"returns bookmark id for exact URL match\", async ({\n      apiCallers,\n    }) => {\n      const api = apiCallers[0].bookmarks;\n      const bookmark = await api.createBookmark({\n        url: \"https://example.com/page\",\n        type: BookmarkTypes.LINK,\n      });\n\n      const result = await api.checkUrl({\n        url: \"https://example.com/page\",\n      });\n      expect(result.bookmarkId).toEqual(bookmark.id);\n    });\n\n    test<CustomTestContext>(\"matches URL ignoring trailing slash\", async ({\n      apiCallers,\n    }) => {\n      const api = apiCallers[0].bookmarks;\n      const bookmark = await api.createBookmark({\n        url: \"https://example.com/page/\",\n        type: BookmarkTypes.LINK,\n      });\n\n      const result = await api.checkUrl({\n        url: \"https://example.com/page\",\n      });\n      expect(result.bookmarkId).toEqual(bookmark.id);\n    });\n\n    test<CustomTestContext>(\"matches URL ignoring hash fragment\", async ({\n      apiCallers,\n    }) => {\n      const api = apiCallers[0].bookmarks;\n      const bookmark = await api.createBookmark({\n        url: \"https://example.com/page\",\n        type: BookmarkTypes.LINK,\n      });\n\n      const result = await api.checkUrl({\n        url: \"https://example.com/page#section\",\n      });\n      expect(result.bookmarkId).toEqual(bookmark.id);\n    });\n\n    test<CustomTestContext>(\"does not match different URLs on same domain\", async ({\n      apiCallers,\n    }) => {\n      const api = apiCallers[0].bookmarks;\n      await api.createBookmark({\n        url: \"https://example.com/page-one\",\n        type: BookmarkTypes.LINK,\n      });\n\n      const result = await api.checkUrl({\n        url: \"https://example.com/page-two\",\n      });\n      expect(result.bookmarkId).toBeNull();\n    });\n\n    test<CustomTestContext>(\"does not return bookmarks from other users\", async ({\n      apiCallers,\n    }) => {\n      const api1 = apiCallers[0].bookmarks;\n      const api2 = apiCallers[1].bookmarks;\n\n      await api1.createBookmark({\n        url: \"https://example.com/private\",\n        type: BookmarkTypes.LINK,\n      });\n\n      const result = await api2.checkUrl({\n        url: \"https://example.com/private\",\n      });\n      expect(result.bookmarkId).toBeNull();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/trpc/routers/bookmarks.ts",
    "content": "import { experimental_trpcMiddleware, TRPCError } from \"@trpc/server\";\nimport { and, eq, gt, inArray, like, lt, or } from \"drizzle-orm\";\nimport { z } from \"zod\";\n\nimport type { ZBookmarkContent } from \"@karakeep/shared/types/bookmarks\";\nimport type { ZBookmarkTags } from \"@karakeep/shared/types/tags\";\nimport {\n  assets,\n  AssetTypes,\n  bookmarkAssets,\n  bookmarkLinks,\n  bookmarks,\n  bookmarkTags,\n  bookmarkTexts,\n  customPrompts,\n  tagsOnBookmarks,\n  userReadingProgress,\n  users,\n} from \"@karakeep/db/schema\";\nimport {\n  AssetPreprocessingQueue,\n  LinkCrawlerQueue,\n  LowPriorityCrawlerQueue,\n  OpenAIQueue,\n  QueuePriority,\n  QuotaService,\n  triggerRuleEngineOnEvent,\n  triggerSearchReindex,\n  triggerWebhook,\n} from \"@karakeep/shared-server\";\nimport { SUPPORTED_BOOKMARK_ASSET_TYPES } from \"@karakeep/shared/assetdb\";\nimport serverConfig from \"@karakeep/shared/config\";\nimport { InferenceClientFactory } from \"@karakeep/shared/inference\";\nimport { buildSummaryPrompt } from \"@karakeep/shared/prompts.server\";\nimport { EnqueueOptions } from \"@karakeep/shared/queueing\";\nimport { getRateLimitClient } from \"@karakeep/shared/ratelimiting\";\nimport { FilterQuery, getSearchClient } from \"@karakeep/shared/search\";\nimport { parseSearchQuery } from \"@karakeep/shared/searchQueryParser\";\nimport {\n  BookmarkTypes,\n  DEFAULT_NUM_BOOKMARKS_PER_PAGE,\n  zBookmarkSchema,\n  zGetBookmarksRequestSchema,\n  zGetBookmarksResponseSchema,\n  zManipulatedTagSchema,\n  zNewBookmarkRequestSchema,\n  zSearchBookmarksCursor,\n  zSearchBookmarksRequestSchema,\n  zUpdateBookmarksRequestSchema,\n} from \"@karakeep/shared/types/bookmarks\";\nimport { ANCHOR_TEXT_MAX_LENGTH } from \"@karakeep/shared/utils/reading-progress-dom\";\nimport { normalizeTagName } from \"@karakeep/shared/utils/tag\";\n\nimport type { AuthedContext } from \"../index\";\nimport { authedProcedure, createRateLimitMiddleware, router } from \"../index\";\nimport { getBookmarkIdsFromMatcher } from \"../lib/search\";\nimport { Asset } from \"../models/assets\";\nimport { BareBookmark, Bookmark } from \"../models/bookmarks\";\n\nexport const ensureBookmarkOwnership = experimental_trpcMiddleware<{\n  ctx: AuthedContext;\n  input: { bookmarkId: string };\n}>().create(async (opts) => {\n  const bookmark = await BareBookmark.bareFromId(\n    opts.ctx,\n    opts.input.bookmarkId,\n  );\n  bookmark.ensureOwnership();\n\n  return opts.next({\n    ctx: {\n      ...opts.ctx,\n      bookmark,\n    },\n  });\n});\n\nexport const ensureBookmarkAccess = experimental_trpcMiddleware<{\n  ctx: AuthedContext;\n  input: { bookmarkId: string };\n}>().create(async (opts) => {\n  // Throws if bookmark doesn't exist or user doesn't have access\n  const bookmark = await BareBookmark.bareFromId(\n    opts.ctx,\n    opts.input.bookmarkId,\n  );\n\n  return opts.next({\n    ctx: {\n      ...opts.ctx,\n      bookmark,\n    },\n  });\n});\n\nasync function attemptToDedupLink(ctx: AuthedContext, url: string) {\n  const result = await ctx.db\n    .select({\n      id: bookmarkLinks.id,\n    })\n    .from(bookmarkLinks)\n    .leftJoin(bookmarks, eq(bookmarks.id, bookmarkLinks.id))\n    .where(and(eq(bookmarkLinks.url, url), eq(bookmarks.userId, ctx.user.id)));\n\n  if (result.length == 0) {\n    return null;\n  }\n  return (\n    await Bookmark.fromId(ctx, result[0].id, /* includeContent: */ false)\n  ).asZBookmark();\n}\n\nconst highBookmarkCreationRateLimitConfig = {\n  name: \"bookmarks.createBookmark.highVolume\",\n  windowMs: 5 * 60 * 1000,\n  maxRequests: 30,\n} as const;\n\nasync function shouldUseLowPriorityQueues(\n  ctx: AuthedContext,\n): Promise<boolean> {\n  if (!serverConfig.rateLimiting.enabled) {\n    return false;\n  }\n\n  const rateLimitClient = await getRateLimitClient();\n  if (!rateLimitClient) {\n    return false;\n  }\n\n  try {\n    const result = await rateLimitClient.checkRateLimit(\n      highBookmarkCreationRateLimitConfig,\n      ctx.user.id,\n    );\n    return !result.allowed;\n  } catch {\n    // Don't block bookmark creation if rate limiting is unavailable.\n    return false;\n  }\n}\n\nexport const bookmarksAppRouter = router({\n  createBookmark: authedProcedure\n    .input(zNewBookmarkRequestSchema)\n    .output(\n      zBookmarkSchema.merge(\n        z.object({\n          alreadyExists: z.boolean().optional().default(false),\n        }),\n      ),\n    )\n    .mutation(async ({ input, ctx }) => {\n      if (input.type == BookmarkTypes.LINK) {\n        // This doesn't 100% protect from duplicates because of races, but it's more than enough for this usecase.\n        const alreadyExists = await attemptToDedupLink(ctx, input.url);\n        if (alreadyExists) {\n          return { ...alreadyExists, alreadyExists: true };\n        }\n      }\n\n      const bookmark = await ctx.db.transaction(\n        async (tx) => {\n          // Check user quota\n          const quotaResult = await QuotaService.canCreateBookmark(\n            tx,\n            ctx.user.id,\n          );\n          if (!quotaResult.result) {\n            throw new TRPCError({\n              code: \"FORBIDDEN\",\n              message: quotaResult.error,\n            });\n          }\n          const bookmark = (\n            await tx\n              .insert(bookmarks)\n              .values({\n                userId: ctx.user.id,\n                title: input.title,\n                type: input.type,\n                archived: input.archived,\n                favourited: input.favourited,\n                note: input.note,\n                summary: input.summary,\n                createdAt: input.createdAt,\n                source: input.source,\n                // Only links currently support summarization. Let's set the status to null for other types for now.\n                summarizationStatus:\n                  input.type === BookmarkTypes.LINK ? \"pending\" : null,\n              })\n              .returning()\n          )[0];\n\n          let content: ZBookmarkContent;\n\n          switch (input.type) {\n            case BookmarkTypes.LINK: {\n              const link = (\n                await tx\n                  .insert(bookmarkLinks)\n                  .values({\n                    id: bookmark.id,\n                    url: input.url.trim(),\n                  })\n                  .returning()\n              )[0];\n              if (input.precrawledArchiveId) {\n                await Asset.ensureOwnership(ctx, input.precrawledArchiveId);\n                await tx\n                  .update(assets)\n                  .set({\n                    bookmarkId: bookmark.id,\n                    assetType: AssetTypes.LINK_PRECRAWLED_ARCHIVE,\n                  })\n                  .where(\n                    and(\n                      eq(assets.id, input.precrawledArchiveId),\n                      eq(assets.userId, ctx.user.id),\n                    ),\n                  );\n              }\n              content = {\n                type: BookmarkTypes.LINK,\n                ...link,\n              };\n              break;\n            }\n            case BookmarkTypes.TEXT: {\n              const text = (\n                await tx\n                  .insert(bookmarkTexts)\n                  .values({\n                    id: bookmark.id,\n                    text: input.text,\n                    sourceUrl: input.sourceUrl,\n                  })\n                  .returning()\n              )[0];\n              content = {\n                type: BookmarkTypes.TEXT,\n                text: text.text ?? \"\",\n                sourceUrl: text.sourceUrl,\n              };\n              break;\n            }\n            case BookmarkTypes.ASSET: {\n              const [asset] = await tx\n                .insert(bookmarkAssets)\n                .values({\n                  id: bookmark.id,\n                  assetType: input.assetType,\n                  assetId: input.assetId,\n                  content: null,\n                  metadata: null,\n                  fileName: input.fileName ?? null,\n                  sourceUrl: input.sourceUrl ?? null,\n                })\n                .returning();\n              const uploadedAsset = await Asset.fromId(ctx, input.assetId);\n              uploadedAsset.ensureOwnership();\n              if (\n                !uploadedAsset.asset.contentType ||\n                !SUPPORTED_BOOKMARK_ASSET_TYPES.has(\n                  uploadedAsset.asset.contentType,\n                )\n              ) {\n                throw new TRPCError({\n                  code: \"BAD_REQUEST\",\n                  message: \"Unsupported asset type\",\n                });\n              }\n              await tx\n                .update(assets)\n                .set({\n                  bookmarkId: bookmark.id,\n                  assetType: AssetTypes.BOOKMARK_ASSET,\n                })\n                .where(\n                  and(\n                    eq(assets.id, input.assetId),\n                    eq(assets.userId, ctx.user.id),\n                  ),\n                );\n              content = {\n                type: BookmarkTypes.ASSET,\n                assetType: asset.assetType,\n                assetId: asset.assetId,\n                fileName: asset.fileName,\n                sourceUrl: asset.sourceUrl,\n              };\n              break;\n            }\n          }\n\n          return {\n            alreadyExists: false,\n            tags: [] as ZBookmarkTags[],\n            assets: [],\n            content,\n            ...bookmark,\n          };\n        },\n        {\n          behavior: \"immediate\",\n        },\n      );\n\n      const forceLowPriority = await shouldUseLowPriorityQueues(ctx);\n      const shouldUseLowPriority =\n        input.crawlPriority === \"low\" || forceLowPriority;\n\n      const enqueueOpts: EnqueueOptions = {\n        // The lower the priority number, the sooner the job will be processed\n        priority: shouldUseLowPriority\n          ? QueuePriority.Low\n          : QueuePriority.Default,\n        groupId: ctx.user.id,\n      };\n\n      switch (bookmark.content.type) {\n        case BookmarkTypes.LINK: {\n          // The crawling job triggers openai when it's done\n          // Use a separate queue for low priority crawling to avoid impacting main queue parallelism\n          const crawlerQueue = shouldUseLowPriority\n            ? LowPriorityCrawlerQueue\n            : LinkCrawlerQueue;\n          await crawlerQueue.enqueue(\n            {\n              bookmarkId: bookmark.id,\n            },\n            enqueueOpts,\n          );\n          break;\n        }\n        case BookmarkTypes.TEXT: {\n          await OpenAIQueue.enqueue(\n            {\n              bookmarkId: bookmark.id,\n              type: \"tag\",\n            },\n            enqueueOpts,\n          );\n          break;\n        }\n        case BookmarkTypes.ASSET: {\n          await AssetPreprocessingQueue.enqueue(\n            {\n              bookmarkId: bookmark.id,\n              fixMode: false,\n            },\n            enqueueOpts,\n          );\n          break;\n        }\n      }\n\n      await Promise.all([\n        triggerRuleEngineOnEvent(\n          bookmark.id,\n          [\n            {\n              type: \"bookmarkAdded\",\n            },\n          ],\n          enqueueOpts,\n        ),\n        triggerSearchReindex(bookmark.id, enqueueOpts),\n        triggerWebhook(\n          bookmark.id,\n          \"created\",\n          /* userId */ undefined,\n          enqueueOpts,\n        ),\n      ]);\n      return bookmark;\n    }),\n\n  updateBookmark: authedProcedure\n    .input(zUpdateBookmarksRequestSchema)\n    .output(zBookmarkSchema)\n    .use(ensureBookmarkOwnership)\n    .mutation(async ({ input, ctx }) => {\n      await ctx.db.transaction(async (tx) => {\n        let somethingChanged = false;\n\n        // Update link-specific fields if any are provided\n        const linkUpdateData: Partial<{\n          url: string;\n          description: string | null;\n          author: string | null;\n          publisher: string | null;\n          datePublished: Date | null;\n          dateModified: Date | null;\n        }> = {};\n        if (input.url) {\n          linkUpdateData.url = input.url.trim();\n        }\n        if (input.description !== undefined) {\n          linkUpdateData.description = input.description;\n        }\n        if (input.author !== undefined) {\n          linkUpdateData.author = input.author;\n        }\n        if (input.publisher !== undefined) {\n          linkUpdateData.publisher = input.publisher;\n        }\n        if (input.datePublished !== undefined) {\n          linkUpdateData.datePublished = input.datePublished;\n        }\n        if (input.dateModified !== undefined) {\n          linkUpdateData.dateModified = input.dateModified;\n        }\n\n        if (Object.keys(linkUpdateData).length > 0) {\n          const result = await tx\n            .update(bookmarkLinks)\n            .set(linkUpdateData)\n            .where(eq(bookmarkLinks.id, input.bookmarkId));\n          if (result.changes == 0) {\n            throw new TRPCError({\n              code: \"BAD_REQUEST\",\n              message:\n                \"Attempting to set link attributes for non-link type bookmark\",\n            });\n          }\n          somethingChanged = true;\n        }\n\n        if (input.text) {\n          const result = await tx\n            .update(bookmarkTexts)\n            .set({\n              text: input.text,\n            })\n            .where(eq(bookmarkTexts.id, input.bookmarkId));\n\n          if (result.changes == 0) {\n            throw new TRPCError({\n              code: \"BAD_REQUEST\",\n              message:\n                \"Attempting to set link attributes for non-text type bookmark\",\n            });\n          }\n          somethingChanged = true;\n        }\n\n        if (input.assetContent !== undefined) {\n          const result = await tx\n            .update(bookmarkAssets)\n            .set({\n              content: input.assetContent,\n            })\n            .where(and(eq(bookmarkAssets.id, input.bookmarkId)));\n\n          if (result.changes == 0) {\n            throw new TRPCError({\n              code: \"BAD_REQUEST\",\n              message:\n                \"Attempting to set asset content for non-asset type bookmark\",\n            });\n          }\n          somethingChanged = true;\n        }\n\n        // Update common bookmark fields\n        const commonUpdateData: Partial<{\n          title: string | null;\n          archived: boolean;\n          favourited: boolean;\n          note: string | null;\n          summary: string | null;\n          createdAt: Date;\n          modifiedAt: Date; // Always update modifiedAt\n        }> = {\n          modifiedAt: new Date(),\n        };\n        if (input.title !== undefined) {\n          commonUpdateData.title = input.title;\n        }\n        if (input.archived !== undefined) {\n          commonUpdateData.archived = input.archived;\n        }\n        if (input.favourited !== undefined) {\n          commonUpdateData.favourited = input.favourited;\n        }\n        if (input.note !== undefined) {\n          commonUpdateData.note = input.note;\n        }\n        if (input.summary !== undefined) {\n          commonUpdateData.summary = input.summary;\n        }\n        if (input.createdAt !== undefined) {\n          commonUpdateData.createdAt = input.createdAt;\n        }\n\n        if (Object.keys(commonUpdateData).length > 1 || somethingChanged) {\n          await tx\n            .update(bookmarks)\n            .set(commonUpdateData)\n            .where(\n              and(\n                eq(bookmarks.userId, ctx.user.id),\n                eq(bookmarks.id, input.bookmarkId),\n              ),\n            );\n        }\n      });\n\n      // Refetch the updated bookmark data to return the full object\n      const updatedBookmark = (\n        await Bookmark.fromId(\n          ctx,\n          input.bookmarkId,\n          /* includeContent: */ false,\n        )\n      ).asZBookmark();\n\n      if (input.favourited === true || input.archived === true) {\n        await triggerRuleEngineOnEvent(\n          input.bookmarkId,\n          [\n            ...(input.favourited === true ? [\"favourited\" as const] : []),\n            ...(input.archived === true ? [\"archived\" as const] : []),\n          ].map((t) => ({\n            type: t,\n          })),\n        );\n      }\n      await Promise.all([\n        triggerSearchReindex(input.bookmarkId, {\n          groupId: ctx.user.id,\n        }),\n        triggerWebhook(input.bookmarkId, \"edited\", ctx.user.id, {\n          groupId: ctx.user.id,\n        }),\n      ]);\n\n      return updatedBookmark;\n    }),\n\n  // DEPRECATED: use updateBookmark instead\n  updateBookmarkText: authedProcedure\n    .input(\n      z.object({\n        bookmarkId: z.string(),\n        text: z.string(),\n      }),\n    )\n    .use(ensureBookmarkOwnership)\n    .mutation(async ({ input, ctx }) => {\n      await ctx.db.transaction(async (tx) => {\n        const res = await tx\n          .update(bookmarkTexts)\n          .set({\n            text: input.text,\n          })\n          .where(and(eq(bookmarkTexts.id, input.bookmarkId)))\n          .returning();\n        if (res.length == 0) {\n          throw new TRPCError({\n            code: \"NOT_FOUND\",\n            message: \"Bookmark not found\",\n          });\n        }\n        await tx\n          .update(bookmarks)\n          .set({ modifiedAt: new Date() })\n          .where(\n            and(\n              eq(bookmarks.id, input.bookmarkId),\n              eq(bookmarks.userId, ctx.user.id),\n            ),\n          );\n      });\n      await Promise.all([\n        triggerSearchReindex(input.bookmarkId, {\n          groupId: ctx.user.id,\n        }),\n        triggerWebhook(input.bookmarkId, \"edited\", ctx.user.id, {\n          groupId: ctx.user.id,\n        }),\n      ]);\n    }),\n\n  deleteBookmark: authedProcedure\n    .input(z.object({ bookmarkId: z.string() }))\n    .use(ensureBookmarkOwnership)\n    .mutation(async ({ input, ctx }) => {\n      const bookmark = await Bookmark.fromId(ctx, input.bookmarkId, false);\n      await bookmark.delete();\n    }),\n  recrawlBookmark: authedProcedure\n    .use(\n      createRateLimitMiddleware({\n        name: \"bookmarks.recrawlBookmark\",\n        windowMs: 30 * 60 * 1000,\n        maxRequests: 200,\n      }),\n    )\n    .input(\n      z.object({\n        bookmarkId: z.string(),\n        archiveFullPage: z.boolean().optional().default(false),\n        storePdf: z.boolean().optional().default(false),\n      }),\n    )\n    .use(ensureBookmarkOwnership)\n    .mutation(async ({ input, ctx }) => {\n      await LowPriorityCrawlerQueue.enqueue(\n        {\n          bookmarkId: input.bookmarkId,\n          archiveFullPage: input.archiveFullPage,\n          storePdf: input.storePdf,\n        },\n        {\n          groupId: ctx.user.id,\n          priority: QueuePriority.Low,\n        },\n      );\n    }),\n  updateReadingProgress: authedProcedure\n    .input(\n      z.object({\n        bookmarkId: z.string(),\n        readingProgressOffset: z.number().int().nonnegative(),\n        readingProgressAnchor: z.string().max(ANCHOR_TEXT_MAX_LENGTH).nullish(),\n        readingProgressPercent: z.number().int().min(0).max(100).nullish(),\n      }),\n    )\n    .use(ensureBookmarkAccess)\n    .mutation(async ({ input, ctx }) => {\n      // Validate this is a LINK bookmark - reading progress only applies to links\n      const linkBookmark = await ctx.db.query.bookmarkLinks.findFirst({\n        where: eq(bookmarkLinks.id, input.bookmarkId),\n      });\n      if (!linkBookmark) {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"Reading progress can only be saved for link bookmarks\",\n        });\n      }\n\n      await ctx.db\n        .insert(userReadingProgress)\n        .values({\n          bookmarkId: input.bookmarkId,\n          userId: ctx.user.id,\n          readingProgressOffset: input.readingProgressOffset,\n          readingProgressAnchor: input.readingProgressAnchor ?? null,\n          readingProgressPercent: input.readingProgressPercent ?? null,\n        })\n        .onConflictDoUpdate({\n          target: [userReadingProgress.bookmarkId, userReadingProgress.userId],\n          set: {\n            readingProgressOffset: input.readingProgressOffset,\n            readingProgressAnchor: input.readingProgressAnchor ?? null,\n            readingProgressPercent: input.readingProgressPercent ?? null,\n            modifiedAt: new Date(),\n          },\n        });\n    }),\n  getReadingProgress: authedProcedure\n    .input(\n      z.object({\n        bookmarkId: z.string(),\n      }),\n    )\n    .use(ensureBookmarkAccess)\n    .query(async ({ input, ctx }) => {\n      const progress = await ctx.db.query.userReadingProgress.findFirst({\n        where: and(\n          eq(userReadingProgress.bookmarkId, input.bookmarkId),\n          eq(userReadingProgress.userId, ctx.user.id),\n        ),\n      });\n      return {\n        readingProgressOffset: progress?.readingProgressOffset ?? null,\n        readingProgressAnchor: progress?.readingProgressAnchor ?? null,\n        readingProgressPercent: progress?.readingProgressPercent ?? null,\n      };\n    }),\n  getBookmark: authedProcedure\n    .input(\n      z.object({\n        bookmarkId: z.string(),\n        includeContent: z.boolean().optional().default(false),\n      }),\n    )\n    .output(zBookmarkSchema)\n    .use(ensureBookmarkAccess)\n    .query(async ({ input, ctx }) => {\n      return (\n        await Bookmark.fromId(ctx, input.bookmarkId, input.includeContent)\n      ).asZBookmark();\n    }),\n  searchBookmarks: authedProcedure\n    .input(zSearchBookmarksRequestSchema)\n    .output(\n      z.object({\n        bookmarks: z.array(zBookmarkSchema),\n        nextCursor: zSearchBookmarksCursor.nullable(),\n      }),\n    )\n    .query(async ({ input, ctx }) => {\n      if (!input.limit) {\n        input.limit = DEFAULT_NUM_BOOKMARKS_PER_PAGE;\n      }\n      const sortOrder = input.sortOrder || \"relevance\";\n      const client = await getSearchClient();\n      if (!client) {\n        throw new TRPCError({\n          code: \"INTERNAL_SERVER_ERROR\",\n          message: \"Search functionality is not configured\",\n        });\n      }\n      const parsedQuery = parseSearchQuery(input.text);\n\n      let filter: FilterQuery[];\n      if (parsedQuery.matcher) {\n        const bookmarkIds = await getBookmarkIdsFromMatcher(\n          ctx,\n          parsedQuery.matcher,\n        );\n        filter = [\n          { type: \"in\", field: \"id\", values: bookmarkIds },\n          { type: \"eq\", field: \"userId\", value: ctx.user.id },\n        ];\n      } else {\n        filter = [{ type: \"eq\", field: \"userId\", value: ctx.user.id }];\n      }\n\n      /**\n       * preserve legacy behaviour\n       */\n      const createdAtSortOrder = sortOrder === \"relevance\" ? \"desc\" : sortOrder;\n\n      const resp = await client.search({\n        query: parsedQuery.text,\n        filter,\n        sort: [{ field: \"createdAt\", order: createdAtSortOrder }],\n        limit: input.limit,\n        ...(input.cursor\n          ? {\n              offset: input.cursor.offset,\n            }\n          : {}),\n      });\n\n      if (resp.hits.length == 0) {\n        return { bookmarks: [], nextCursor: null };\n      }\n      const idToRank = resp.hits.reduce<Record<string, number>>((acc, r) => {\n        acc[r.id] = r.score || 0;\n        return acc;\n      }, {});\n\n      const { bookmarks: results } = await Bookmark.loadMulti(ctx, {\n        ids: resp.hits.map((h) => h.id),\n        includeContent: input.includeContent,\n        sortOrder: \"desc\", // Doesn't matter, we're sorting again afterwards and the list contain all data\n      });\n\n      switch (true) {\n        case sortOrder === \"relevance\":\n          results.sort((a, b) => idToRank[b.id] - idToRank[a.id]);\n          break;\n        case sortOrder === \"desc\":\n          results.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());\n          break;\n        case sortOrder === \"asc\":\n          results.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());\n          break;\n      }\n\n      return {\n        bookmarks: results.map((b) => b.asZBookmark()),\n        nextCursor:\n          resp.hits.length + (input.cursor?.offset || 0) >= resp.totalHits\n            ? null\n            : {\n                ver: 1 as const,\n                offset: resp.hits.length + (input.cursor?.offset || 0),\n              },\n      };\n    }),\n  checkUrl: authedProcedure\n    .input(\n      z.object({\n        url: z.string(),\n      }),\n    )\n    .output(\n      z.object({\n        bookmarkId: z.string().nullable(),\n      }),\n    )\n    .query(async ({ input, ctx }) => {\n      // Normalize and compare URLs (ignoring hash fragment and trailing slash)\n      function normalizeUrl(url: string): string {\n        const u = new URL(url);\n        u.hash = \"\";\n        let pathname = u.pathname;\n        if (pathname.endsWith(\"/\") && pathname !== \"/\") {\n          pathname = pathname.slice(0, -1);\n        }\n        u.pathname = pathname;\n        return u.toString();\n      }\n\n      // Strip hash before querying so the LIKE clause can match\n      const normalizedInput = normalizeUrl(input.url);\n\n      const results = await ctx.db\n        .select({ id: bookmarkLinks.id, url: bookmarkLinks.url })\n        .from(bookmarkLinks)\n        .leftJoin(bookmarks, eq(bookmarks.id, bookmarkLinks.id))\n        .where(\n          and(\n            eq(bookmarks.userId, ctx.user.id),\n            like(bookmarkLinks.url, `${normalizedInput}%`),\n          ),\n        );\n\n      if (results.length === 0) {\n        return { bookmarkId: null };\n      }\n\n      const exactMatch = results.find(\n        (r) => r.url && normalizeUrl(r.url) === normalizedInput,\n      );\n\n      return { bookmarkId: exactMatch?.id ?? null };\n    }),\n  getBookmarks: authedProcedure\n    .input(zGetBookmarksRequestSchema)\n    .output(zGetBookmarksResponseSchema)\n    .query(async ({ input, ctx }) => {\n      const res = await Bookmark.loadMulti(ctx, input);\n      return {\n        bookmarks: res.bookmarks.map((b) => b.asZBookmark()),\n        nextCursor: res.nextCursor,\n      };\n    }),\n\n  updateTags: authedProcedure\n    .input(\n      z.object({\n        bookmarkId: z.string(),\n        attach: z.array(zManipulatedTagSchema),\n        detach: z.array(zManipulatedTagSchema),\n      }),\n    )\n    .output(\n      z.object({\n        attached: z.array(z.string()),\n        detached: z.array(z.string()),\n      }),\n    )\n    .use(ensureBookmarkOwnership)\n    .mutation(async ({ input, ctx }) => {\n      // Helper function to fetch tag IDs and their names from a list of tag identifiers\n      const fetchTagIdsWithNames = async (\n        tagIdentifiers: { tagId?: string; tagName?: string }[],\n      ): Promise<{ id: string; name: string }[]> => {\n        const tagIds = tagIdentifiers.flatMap((t) =>\n          t.tagId ? [t.tagId] : [],\n        );\n        const tagNames = tagIdentifiers.flatMap((t) =>\n          t.tagName ? [t.tagName] : [],\n        );\n\n        // Fetch tag IDs in parallel\n        const [byIds, byNames] = await Promise.all([\n          tagIds.length > 0\n            ? ctx.db\n                .select({ id: bookmarkTags.id, name: bookmarkTags.name })\n                .from(bookmarkTags)\n                .where(\n                  and(\n                    eq(bookmarkTags.userId, ctx.user.id),\n                    inArray(bookmarkTags.id, tagIds),\n                  ),\n                )\n            : Promise.resolve([]),\n          tagNames.length > 0\n            ? ctx.db\n                .select({ id: bookmarkTags.id, name: bookmarkTags.name })\n                .from(bookmarkTags)\n                .where(\n                  and(\n                    eq(bookmarkTags.userId, ctx.user.id),\n                    inArray(bookmarkTags.name, tagNames),\n                  ),\n                )\n            : Promise.resolve([]),\n        ]);\n\n        // Union results and deduplicate by tag ID\n        const seen = new Set<string>();\n        const results: { id: string; name: string }[] = [];\n\n        for (const tag of [...byIds, ...byNames]) {\n          if (!seen.has(tag.id)) {\n            seen.add(tag.id);\n            results.push({ id: tag.id, name: tag.name });\n          }\n        }\n\n        return results;\n      };\n\n      // Normalize tag names and create new tags outside transaction to reduce transaction duration\n      const normalizedAttachTags = input.attach.map((tag) => ({\n        tagId: tag.tagId,\n        tagName: tag.tagName ? normalizeTagName(tag.tagName) : undefined,\n        attachedBy: tag.attachedBy,\n      }));\n\n      {\n        // Create new tags\n        const toAddTagNames = normalizedAttachTags\n          .flatMap((i) => (i.tagName ? [i.tagName] : []))\n          .filter((n) => n.length > 0); // drop empty results\n\n        if (toAddTagNames.length > 0) {\n          await ctx.db\n            .insert(bookmarkTags)\n            .values(\n              toAddTagNames.map((name) => ({ name, userId: ctx.user.id })),\n            )\n            .onConflictDoNothing();\n        }\n      }\n\n      // Fetch tag IDs for attachment/detachment now that we know that they all exist\n      const [attachTagsWithNames, detachTagsWithNames] = await Promise.all([\n        fetchTagIdsWithNames(normalizedAttachTags),\n        fetchTagIdsWithNames(input.detach),\n      ]);\n\n      // Build the attachedBy map from the fetched results\n      const tagIdToAttachedBy = new Map<string, \"ai\" | \"human\">();\n\n      for (const fetchedTag of attachTagsWithNames) {\n        // Find the corresponding input tag\n        const inputTag = normalizedAttachTags.find(\n          (t) =>\n            (t.tagId && t.tagId === fetchedTag.id) ||\n            (t.tagName && t.tagName === fetchedTag.name),\n        );\n\n        if (inputTag) {\n          tagIdToAttachedBy.set(fetchedTag.id, inputTag.attachedBy);\n        }\n      }\n\n      // Extract just the IDs for the transaction\n      const allIdsToAttach = attachTagsWithNames.map((t) => t.id);\n      const idsToRemove = detachTagsWithNames.map((t) => t.id);\n\n      const res = await ctx.db.transaction(async (tx) => {\n        let numChanges = 0;\n        // Detaches\n        if (idsToRemove.length > 0) {\n          const res = await tx\n            .delete(tagsOnBookmarks)\n            .where(\n              and(\n                eq(tagsOnBookmarks.bookmarkId, input.bookmarkId),\n                inArray(tagsOnBookmarks.tagId, idsToRemove),\n              ),\n            );\n          numChanges += res.changes;\n        }\n\n        // Attach tags\n        if (allIdsToAttach.length > 0) {\n          const res = await tx\n            .insert(tagsOnBookmarks)\n            .values(\n              allIdsToAttach.map((i) => ({\n                tagId: i,\n                bookmarkId: input.bookmarkId,\n                attachedBy: tagIdToAttachedBy.get(i) ?? \"human\",\n              })),\n            )\n            .onConflictDoNothing();\n          numChanges += res.changes;\n        }\n\n        // Update bookmark modified timestamp\n        if (numChanges > 0) {\n          await tx\n            .update(bookmarks)\n            .set({ modifiedAt: new Date() })\n            .where(\n              and(\n                eq(bookmarks.id, input.bookmarkId),\n                eq(bookmarks.userId, ctx.user.id),\n              ),\n            );\n        }\n\n        return {\n          bookmarkId: input.bookmarkId,\n          attached: allIdsToAttach,\n          detached: idsToRemove,\n          numChanges,\n        };\n      });\n\n      if (res.numChanges > 0) {\n        await Promise.allSettled([\n          triggerRuleEngineOnEvent(input.bookmarkId, [\n            ...res.detached.map((t) => ({\n              type: \"tagRemoved\" as const,\n              tagId: t,\n            })),\n            ...res.attached.map((t) => ({\n              type: \"tagAdded\" as const,\n              tagId: t,\n            })),\n          ]),\n          triggerSearchReindex(input.bookmarkId, {\n            groupId: ctx.user.id,\n          }),\n          triggerWebhook(input.bookmarkId, \"edited\", ctx.user.id, {\n            groupId: ctx.user.id,\n          }),\n        ]);\n      }\n      return res;\n    }),\n  getBrokenLinks: authedProcedure\n    .output(\n      z.object({\n        bookmarks: z.array(\n          z.object({\n            id: z.string(),\n            url: z.string(),\n            statusCode: z.number().nullable(),\n            isCrawlingFailure: z.boolean(),\n            crawledAt: z.date().nullable(),\n            createdAt: z.date().nullable(),\n          }),\n        ),\n      }),\n    )\n    .query(async ({ ctx }) => {\n      const brokenLinkBookmarks = await ctx.db\n        .select({\n          id: bookmarkLinks.id,\n          url: bookmarkLinks.url,\n          crawlStatusCode: bookmarkLinks.crawlStatusCode,\n          crawlingStatus: bookmarkLinks.crawlStatus,\n          crawledAt: bookmarkLinks.crawledAt,\n          createdAt: bookmarks.createdAt,\n        })\n        .from(bookmarkLinks)\n        .leftJoin(bookmarks, eq(bookmarks.id, bookmarkLinks.id))\n        .where(\n          and(\n            eq(bookmarks.userId, ctx.user.id),\n            or(\n              eq(bookmarkLinks.crawlStatus, \"failure\"),\n              lt(bookmarkLinks.crawlStatusCode, 200),\n              gt(bookmarkLinks.crawlStatusCode, 299),\n            ),\n          ),\n        );\n      return {\n        bookmarks: brokenLinkBookmarks.map((b) => ({\n          id: b.id,\n          url: b.url,\n          statusCode: b.crawlStatusCode,\n          isCrawlingFailure: b.crawlingStatus === \"failure\",\n          crawledAt: b.crawledAt,\n          createdAt: b.createdAt,\n        })),\n      };\n    }),\n  summarizeBookmark: authedProcedure\n    .use(\n      createRateLimitMiddleware({\n        name: \"bookmarks.summarizeBookmark\",\n        windowMs: 30 * 60 * 1000,\n        maxRequests: 100,\n      }),\n    )\n    .input(\n      z.object({\n        bookmarkId: z.string(),\n      }),\n    )\n    .output(\n      z.object({\n        summary: z.string(),\n      }),\n    )\n    .use(ensureBookmarkOwnership)\n    .mutation(async ({ input, ctx }) => {\n      const inferenceClient = InferenceClientFactory.build();\n      if (!inferenceClient) {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"No inference client configured\",\n        });\n      }\n      const bookmark = await ctx.db.query.bookmarkLinks.findFirst({\n        where: eq(bookmarkLinks.id, input.bookmarkId),\n      });\n\n      if (!bookmark) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Bookmark not found or not a link\",\n        });\n      }\n\n      const content = await Bookmark.getBookmarkPlainTextContent(\n        bookmark,\n        ctx.user.id,\n      );\n\n      const bookmarkDetails = `\nTitle: ${bookmark.title ?? \"\"}\nDescription: ${bookmark.description ?? \"\"}\nContent: ${content}\nPublisher: ${bookmark.publisher ?? \"\"}\nAuthor: ${bookmark.author ?? \"\"}\n`;\n\n      const prompts = await ctx.db.query.customPrompts.findMany({\n        where: and(\n          eq(customPrompts.userId, ctx.user.id),\n          eq(customPrompts.appliesTo, \"summary\"),\n        ),\n        columns: {\n          text: true,\n        },\n      });\n\n      const userSettings = await ctx.db.query.users.findFirst({\n        where: eq(users.id, ctx.user.id),\n        columns: {\n          inferredTagLang: true,\n        },\n      });\n\n      const summaryPrompt = await buildSummaryPrompt(\n        userSettings?.inferredTagLang ?? serverConfig.inference.inferredTagLang,\n        prompts.map((p) => p.text),\n        bookmarkDetails,\n        serverConfig.inference.contextLength,\n      );\n\n      const summary = await inferenceClient.inferFromText(summaryPrompt, {\n        schema: null,\n      });\n\n      if (!summary.response) {\n        throw new TRPCError({\n          code: \"INTERNAL_SERVER_ERROR\",\n          message: \"Failed to summarize bookmark\",\n        });\n      }\n      await ctx.db\n        .update(bookmarks)\n        .set({\n          summary: summary.response,\n        })\n        .where(eq(bookmarks.id, input.bookmarkId));\n      await Promise.all([\n        triggerSearchReindex(input.bookmarkId, {\n          groupId: ctx.user.id,\n        }),\n        triggerWebhook(input.bookmarkId, \"edited\", ctx.user.id, {\n          groupId: ctx.user.id,\n        }),\n      ]);\n\n      return {\n        bookmarkId: input.bookmarkId,\n        summary: summary.response,\n      };\n    }),\n});\n"
  },
  {
    "path": "packages/trpc/routers/config.ts",
    "content": "import { clientConfig } from \"@karakeep/shared/config\";\nimport { zClientConfigSchema } from \"@karakeep/shared/types/config\";\n\nimport { publicProcedure, router } from \"../index\";\n\nexport const configAppRouter = router({\n  clientConfig: publicProcedure\n    .output(zClientConfigSchema)\n    .query(() => clientConfig),\n});\n"
  },
  {
    "path": "packages/trpc/routers/feeds.test.ts",
    "content": "import { beforeEach, describe, expect, test } from \"vitest\";\n\nimport type { CustomTestContext } from \"../testUtils\";\nimport { defaultBeforeEach } from \"../testUtils\";\n\nbeforeEach<CustomTestContext>(defaultBeforeEach(true));\n\ndescribe(\"Feed Routes\", () => {\n  test<CustomTestContext>(\"create feed\", async ({ apiCallers }) => {\n    const api = apiCallers[0].feeds;\n    const newFeed = await api.create({\n      name: \"Test Feed\",\n      url: \"https://example.com/feed.xml\",\n      enabled: true,\n    });\n\n    expect(newFeed).toBeDefined();\n    expect(newFeed.name).toEqual(\"Test Feed\");\n    expect(newFeed.url).toEqual(\"https://example.com/feed.xml\");\n    expect(newFeed.enabled).toBe(true);\n  });\n\n  test<CustomTestContext>(\"update feed\", async ({ apiCallers }) => {\n    const api = apiCallers[0].feeds;\n\n    // First, create a feed to update\n    const createdFeed = await api.create({\n      name: \"Test Feed\",\n      url: \"https://example.com/feed.xml\",\n      enabled: true,\n    });\n\n    // Update it\n    const updatedFeed = await api.update({\n      feedId: createdFeed.id,\n      name: \"Updated Feed\",\n      url: \"https://updated-example.com/feed.xml\",\n      enabled: false,\n    });\n\n    expect(updatedFeed.name).toEqual(\"Updated Feed\");\n    expect(updatedFeed.url).toEqual(\"https://updated-example.com/feed.xml\");\n    expect(updatedFeed.enabled).toBe(false);\n\n    // Test updating a non-existent feed\n    await expect(() =>\n      api.update({\n        feedId: \"non-existent-id\",\n        name: \"Fail\",\n        url: \"https://fail.com\",\n        enabled: true,\n      }),\n    ).rejects.toThrow(/Feed not found/);\n  });\n\n  test<CustomTestContext>(\"list feeds\", async ({ apiCallers }) => {\n    const api = apiCallers[0].feeds;\n\n    // Create a couple of feeds\n    await api.create({\n      name: \"Feed 1\",\n      url: \"https://example1.com/feed.xml\",\n      enabled: true,\n    });\n    await api.create({\n      name: \"Feed 2\",\n      url: \"https://example2.com/feed.xml\",\n      enabled: true,\n    });\n\n    const result = await api.list();\n    expect(result.feeds).toBeDefined();\n    expect(result.feeds.length).toBeGreaterThanOrEqual(2);\n    expect(result.feeds.some((f) => f.name === \"Feed 1\")).toBe(true);\n    expect(result.feeds.some((f) => f.name === \"Feed 2\")).toBe(true);\n  });\n\n  test<CustomTestContext>(\"delete feed\", async ({ apiCallers }) => {\n    const api = apiCallers[0].feeds;\n\n    // Create a feed to delete\n    const createdFeed = await api.create({\n      name: \"Test Feed\",\n      url: \"https://example.com/feed.xml\",\n      enabled: true,\n    });\n\n    // Delete it\n    await api.delete({ feedId: createdFeed.id });\n\n    // Verify it's deleted\n    await expect(() =>\n      api.update({\n        feedId: createdFeed.id,\n        name: \"Updated\",\n        url: \"https://updated.com\",\n        enabled: true,\n      }),\n    ).rejects.toThrow(/Feed not found/);\n  });\n\n  test<CustomTestContext>(\"privacy for feeds\", async ({ apiCallers }) => {\n    const user1Feed = await apiCallers[0].feeds.create({\n      name: \"User 1 Feed\",\n      url: \"https://user1-feed.com/feed.xml\",\n      enabled: true,\n    });\n    const user2Feed = await apiCallers[1].feeds.create({\n      name: \"User 2 Feed\",\n      url: \"https://user2-feed.com/feed.xml\",\n      enabled: true,\n    });\n\n    // User 1 should not access User 2's feed\n    await expect(() =>\n      apiCallers[0].feeds.delete({ feedId: user2Feed.id }),\n    ).rejects.toThrow(/User is not allowed to access resource/);\n    await expect(() =>\n      apiCallers[0].feeds.update({\n        feedId: user2Feed.id,\n        name: \"Fail\",\n        url: \"https://fail.com\",\n        enabled: true,\n      }),\n    ).rejects.toThrow(/User is not allowed to access resource/);\n\n    // List should only show the correct user's feeds\n    const user1List = await apiCallers[0].feeds.list();\n    expect(user1List.feeds.some((f) => f.id === user1Feed.id)).toBe(true);\n    expect(user1List.feeds.some((f) => f.id === user2Feed.id)).toBe(false);\n  });\n\n  test<CustomTestContext>(\"feed limit enforcement\", async ({ apiCallers }) => {\n    const api = apiCallers[0].feeds;\n\n    // Create 1000 feeds (the maximum)\n    for (let i = 0; i < 1000; i++) {\n      await api.create({\n        name: `Feed ${i}`,\n        url: `https://example${i}.com/feed.xml`,\n        enabled: true,\n      });\n    }\n\n    // The 1001st feed should fail\n    await expect(() =>\n      api.create({\n        name: \"Feed 1001\",\n        url: \"https://example1001.com/feed.xml\",\n        enabled: true,\n      }),\n    ).rejects.toThrow(/Maximum number of RSS feeds \\(1000\\) reached/);\n  });\n});\n"
  },
  {
    "path": "packages/trpc/routers/feeds.ts",
    "content": "import { z } from \"zod\";\n\nimport { FeedQueue } from \"@karakeep/shared-server\";\nimport {\n  zFeedSchema,\n  zNewFeedSchema,\n  zUpdateFeedSchema,\n} from \"@karakeep/shared/types/feeds\";\n\nimport { authedProcedure, router } from \"../index\";\nimport { Feed } from \"../models/feeds\";\n\nexport const feedsAppRouter = router({\n  create: authedProcedure\n    .input(zNewFeedSchema)\n    .output(zFeedSchema)\n    .mutation(async ({ input, ctx }) => {\n      const feed = await Feed.create(ctx, input);\n      return feed.asPublicFeed();\n    }),\n  update: authedProcedure\n    .input(zUpdateFeedSchema)\n    .output(zFeedSchema)\n    .mutation(async ({ input, ctx }) => {\n      const feed = await Feed.fromId(ctx, input.feedId);\n      await feed.update(input);\n      return feed.asPublicFeed();\n    }),\n  get: authedProcedure\n    .input(\n      z.object({\n        feedId: z.string(),\n      }),\n    )\n    .output(zFeedSchema)\n    .query(async ({ ctx, input }) => {\n      const feed = await Feed.fromId(ctx, input.feedId);\n      return feed.asPublicFeed();\n    }),\n  list: authedProcedure\n    .output(z.object({ feeds: z.array(zFeedSchema) }))\n    .query(async ({ ctx }) => {\n      const feeds = await Feed.getAll(ctx);\n      return { feeds: feeds.map((f) => f.asPublicFeed()) };\n    }),\n  delete: authedProcedure\n    .input(\n      z.object({\n        feedId: z.string(),\n      }),\n    )\n    .mutation(async ({ input, ctx }) => {\n      const feed = await Feed.fromId(ctx, input.feedId);\n      await feed.delete();\n    }),\n  fetchNow: authedProcedure\n    .input(z.object({ feedId: z.string() }))\n    .mutation(async ({ input, ctx }) => {\n      await Feed.fromId(ctx, input.feedId);\n      await FeedQueue.enqueue(\n        {\n          feedId: input.feedId,\n        },\n        {\n          groupId: ctx.user.id,\n        },\n      );\n    }),\n});\n"
  },
  {
    "path": "packages/trpc/routers/highlights.test.ts",
    "content": "import { beforeEach, describe, expect, test } from \"vitest\";\n\nimport { BookmarkTypes } from \"@karakeep/shared/types/bookmarks\";\n\nimport type { CustomTestContext } from \"../testUtils\";\nimport { defaultBeforeEach } from \"../testUtils\";\n\nbeforeEach<CustomTestContext>(defaultBeforeEach(true));\n\ndescribe(\"Highlight Routes\", () => {\n  test<CustomTestContext>(\"create highlight\", async ({ apiCallers }) => {\n    const api = apiCallers[0].highlights;\n    const bookmarksApi = apiCallers[0].bookmarks;\n\n    // First, create a valid bookmark\n    const bookmark = await bookmarksApi.createBookmark({\n      url: \"https://example.com\",\n      type: BookmarkTypes.LINK,\n    });\n    const bookmarkId = bookmark.id;\n\n    const highlight = await api.create({\n      bookmarkId,\n      startOffset: 10,\n      endOffset: 20,\n      color: \"yellow\",\n      text: \"Test highlight text\",\n      note: \"Test note\",\n    });\n\n    const res = await api.get({ highlightId: highlight.id });\n    expect(res.bookmarkId).toEqual(bookmarkId);\n    expect(res.startOffset).toEqual(10);\n    expect(res.endOffset).toEqual(20);\n    expect(res.color).toEqual(\"yellow\");\n    expect(res.text).toEqual(\"Test highlight text\");\n    expect(res.note).toEqual(\"Test note\");\n  });\n\n  test<CustomTestContext>(\"delete highlight\", async ({ apiCallers }) => {\n    const api = apiCallers[0].highlights;\n    const bookmarksApi = apiCallers[0].bookmarks;\n\n    // First, create a valid bookmark\n    const bookmark = await bookmarksApi.createBookmark({\n      url: \"https://example.com\",\n      type: BookmarkTypes.LINK,\n    });\n    const bookmarkId = bookmark.id;\n\n    // Create the highlight first\n    const highlight = await api.create({\n      bookmarkId,\n      startOffset: 10,\n      endOffset: 20,\n      color: \"yellow\",\n      text: \"Test highlight text\",\n      note: \"Test note\",\n    });\n\n    // It should exist\n    await api.get({ highlightId: highlight.id });\n\n    // Delete it\n    await api.delete({ highlightId: highlight.id });\n\n    // It shouldn't be there anymore\n    await expect(() => api.get({ highlightId: highlight.id })).rejects.toThrow(\n      /Highlight not found/,\n    );\n  });\n\n  test<CustomTestContext>(\"update highlight\", async ({ apiCallers }) => {\n    const api = apiCallers[0].highlights;\n    const bookmarksApi = apiCallers[0].bookmarks;\n\n    // First, create a valid bookmark\n    const bookmark = await bookmarksApi.createBookmark({\n      url: \"https://example.com\",\n      type: BookmarkTypes.LINK,\n    });\n    const bookmarkId = bookmark.id;\n\n    // Create the highlight\n    const highlight = await api.create({\n      bookmarkId,\n      startOffset: 10,\n      endOffset: 20,\n      color: \"yellow\",\n      text: \"Original text\",\n      note: \"Original note\",\n    });\n\n    await api.update({\n      highlightId: highlight.id,\n      color: \"blue\",\n    });\n\n    const res = await api.get({ highlightId: highlight.id });\n    expect(res.color).toEqual(\"blue\");\n    expect(res.text).toEqual(\"Original text\"); // Only color is updated in the router\n  });\n\n  test<CustomTestContext>(\"get highlight\", async ({ apiCallers }) => {\n    const api = apiCallers[0].highlights;\n    const bookmarksApi = apiCallers[0].bookmarks;\n\n    // First, create a valid bookmark\n    const bookmark = await bookmarksApi.createBookmark({\n      url: \"https://example.com\",\n      type: BookmarkTypes.LINK,\n    });\n    const bookmarkId = bookmark.id;\n\n    // Create the highlight\n    const createdHighlight = await api.create({\n      bookmarkId,\n      startOffset: 10,\n      endOffset: 20,\n      color: \"yellow\",\n      text: \"Test text\",\n      note: \"Test note\",\n    });\n\n    const res = await api.get({ highlightId: createdHighlight.id });\n    expect(res.id).toEqual(createdHighlight.id);\n    expect(res.bookmarkId).toEqual(bookmarkId);\n  });\n\n  test<CustomTestContext>(\"get highlights for bookmark\", async ({\n    apiCallers,\n  }) => {\n    const api = apiCallers[0].highlights;\n    const bookmarksApi = apiCallers[0].bookmarks;\n    const bookmark = await bookmarksApi.createBookmark({\n      url: \"https://example.com\",\n      type: BookmarkTypes.LINK,\n    });\n    const bookmarkId = bookmark.id;\n\n    const highlight1 = await api.create({\n      bookmarkId,\n      startOffset: 10,\n      endOffset: 20,\n      color: \"yellow\",\n      text: \"Highlight 1\",\n      note: \"\",\n    });\n\n    const highlight2 = await api.create({\n      bookmarkId,\n      startOffset: 30,\n      endOffset: 40,\n      color: \"blue\",\n      text: \"Highlight 2\",\n      note: \"\",\n    });\n\n    const res = await api.getForBookmark({ bookmarkId });\n    expect(res.highlights.length).toBeGreaterThanOrEqual(2);\n    expect(res.highlights.some((h) => h.id === highlight1.id)).toBeTruthy();\n    expect(res.highlights.some((h) => h.id === highlight2.id)).toBeTruthy();\n  });\n\n  test<CustomTestContext>(\"get all highlights with pagination\", async ({\n    apiCallers,\n  }) => {\n    const api = apiCallers[0].highlights;\n    const bookmarksApi = apiCallers[0].bookmarks;\n    const bookmark = await bookmarksApi.createBookmark({\n      url: \"https://example.com\",\n      type: BookmarkTypes.LINK,\n    });\n    const bookmarkId = bookmark.id;\n\n    // Create multiple highlights\n    await api.create({\n      bookmarkId,\n      startOffset: 10,\n      endOffset: 20,\n      color: \"yellow\",\n      text: \"Highlight 1\",\n      note: \"\",\n    });\n    await api.create({\n      bookmarkId,\n      startOffset: 30,\n      endOffset: 40,\n      color: \"blue\",\n      text: \"Highlight 2\",\n      note: \"\",\n    });\n    await api.create({\n      bookmarkId,\n      startOffset: 50,\n      endOffset: 60,\n      color: \"green\",\n      text: \"Highlight 3\",\n      note: \"\",\n    });\n\n    const res = await api.getAll({ limit: 2 });\n    expect(res.highlights.length).toEqual(2);\n    expect(res.nextCursor).toBeDefined(); // Should have a next cursor\n  });\n\n  test<CustomTestContext>(\"privacy for highlights\", async ({ apiCallers }) => {\n    const apiUser1 = apiCallers[0].highlights;\n    const apiUser2 = apiCallers[1].highlights;\n    const bookmarksApiUser1 = apiCallers[0].bookmarks;\n    const bookmarksApiUser2 = apiCallers[1].bookmarks;\n\n    const bookmarkUser1 = await bookmarksApiUser1.createBookmark({\n      url: \"https://user1-example.com\",\n      type: BookmarkTypes.LINK,\n    });\n    const bookmarkIdUser1 = bookmarkUser1.id;\n\n    const bookmarkUser2 = await bookmarksApiUser2.createBookmark({\n      url: \"https://user2-example.com\",\n      type: BookmarkTypes.LINK,\n    });\n    const bookmarkIdUser2 = bookmarkUser2.id;\n\n    const highlightUser1 = await apiUser1.create({\n      bookmarkId: bookmarkIdUser1,\n      startOffset: 10,\n      endOffset: 20,\n      color: \"yellow\",\n      text: \"User1 highlight\",\n      note: \"\",\n    });\n\n    const highlightUser2 = await apiUser2.create({\n      bookmarkId: bookmarkIdUser2,\n      startOffset: 10,\n      endOffset: 20,\n      color: \"blue\",\n      text: \"User2 highlight\",\n      note: \"\",\n    });\n\n    // User1 should not access User2's highlight\n    await expect(() =>\n      apiUser1.get({ highlightId: highlightUser2.id }),\n    ).rejects.toThrow(/User is not allowed to access resource/);\n\n    // User2 should not access User1's highlight\n    await expect(() =>\n      apiUser2.get({ highlightId: highlightUser1.id }),\n    ).rejects.toThrow(/User is not allowed to access resource/);\n  });\n});\n"
  },
  {
    "path": "packages/trpc/routers/highlights.ts",
    "content": "import { z } from \"zod\";\n\nimport {\n  DEFAULT_NUM_HIGHLIGHTS_PER_PAGE,\n  zGetAllHighlightsResponseSchema,\n  zHighlightSchema,\n  zNewHighlightSchema,\n  zUpdateHighlightSchema,\n} from \"@karakeep/shared/types/highlights\";\nimport { zCursorV2 } from \"@karakeep/shared/types/pagination\";\n\nimport { authedProcedure, router } from \"../index\";\nimport { Highlight } from \"../models/highlights\";\nimport { ensureBookmarkAccess, ensureBookmarkOwnership } from \"./bookmarks\";\n\nexport const highlightsAppRouter = router({\n  create: authedProcedure\n    .input(zNewHighlightSchema)\n    .output(zHighlightSchema)\n    .use(ensureBookmarkOwnership)\n    .mutation(async ({ input, ctx }) => {\n      const highlight = await Highlight.create(ctx, input);\n      return highlight.asPublicHighlight();\n    }),\n  getForBookmark: authedProcedure\n    .input(z.object({ bookmarkId: z.string() }))\n    .output(z.object({ highlights: z.array(zHighlightSchema) }))\n    .use(ensureBookmarkAccess)\n    .query(async ({ ctx }) => {\n      const highlights = await Highlight.getForBookmark(ctx, ctx.bookmark);\n      return { highlights: highlights.map((h) => h.asPublicHighlight()) };\n    }),\n  get: authedProcedure\n    .input(z.object({ highlightId: z.string() }))\n    .output(zHighlightSchema)\n    .query(async ({ input, ctx }) => {\n      const highlight = await Highlight.fromId(ctx, input.highlightId);\n      return highlight.asPublicHighlight();\n    }),\n  getAll: authedProcedure\n    .input(\n      z.object({\n        cursor: z.any().nullish(),\n        limit: z.number().optional().default(DEFAULT_NUM_HIGHLIGHTS_PER_PAGE),\n      }),\n    )\n    .output(zGetAllHighlightsResponseSchema)\n    .query(async ({ input, ctx }) => {\n      const result = await Highlight.getAll(ctx, input.cursor, input.limit);\n      return {\n        highlights: result.highlights.map((h) => h.asPublicHighlight()),\n        nextCursor: result.nextCursor,\n      };\n    }),\n  search: authedProcedure\n    .input(\n      z.object({\n        text: z.string(),\n        cursor: zCursorV2.nullish(),\n        limit: z.number().optional().default(DEFAULT_NUM_HIGHLIGHTS_PER_PAGE),\n      }),\n    )\n    .output(zGetAllHighlightsResponseSchema)\n    .query(async ({ input, ctx }) => {\n      const result = await Highlight.search(\n        ctx,\n        input.text,\n        input.cursor,\n        input.limit,\n      );\n      return {\n        highlights: result.highlights.map((h) => h.asPublicHighlight()),\n        nextCursor: result.nextCursor,\n      };\n    }),\n  delete: authedProcedure\n    .input(z.object({ highlightId: z.string() }))\n    .output(zHighlightSchema)\n    .mutation(async ({ input, ctx }) => {\n      const highlight = await Highlight.fromId(ctx, input.highlightId);\n      return await highlight.delete();\n    }),\n  update: authedProcedure\n    .input(zUpdateHighlightSchema)\n    .output(zHighlightSchema)\n    .mutation(async ({ input, ctx }) => {\n      const highlight = await Highlight.fromId(ctx, input.highlightId);\n      await highlight.update(input);\n      return highlight.asPublicHighlight();\n    }),\n});\n"
  },
  {
    "path": "packages/trpc/routers/importSessions.test.ts",
    "content": "import { beforeEach, describe, expect, test } from \"vitest\";\nimport { z } from \"zod\";\n\nimport {\n  bookmarkLinks,\n  bookmarks,\n  bookmarkTexts,\n  importStagingBookmarks,\n} from \"@karakeep/db/schema\";\nimport { BookmarkTypes } from \"@karakeep/shared/types/bookmarks\";\nimport {\n  zCreateImportSessionRequestSchema,\n  zDeleteImportSessionRequestSchema,\n  zGetImportSessionStatsRequestSchema,\n} from \"@karakeep/shared/types/importSessions\";\nimport { zNewBookmarkListSchema } from \"@karakeep/shared/types/lists\";\n\nimport type { APICallerType, CustomTestContext } from \"../testUtils\";\nimport { defaultBeforeEach } from \"../testUtils\";\n\nbeforeEach<CustomTestContext>(defaultBeforeEach(true));\n\ndescribe(\"ImportSessions Routes\", () => {\n  async function createTestList(api: APICallerType) {\n    const newListInput: z.infer<typeof zNewBookmarkListSchema> = {\n      name: \"Test Import List\",\n      description: \"A test list for imports\",\n      icon: \"📋\",\n      type: \"manual\",\n    };\n    const createdList = await api.lists.create(newListInput);\n    return createdList.id;\n  }\n\n  test<CustomTestContext>(\"create import session\", async ({ apiCallers }) => {\n    const api = apiCallers[0].importSessions;\n    const listId = await createTestList(apiCallers[0]);\n\n    const newSessionInput: z.infer<typeof zCreateImportSessionRequestSchema> = {\n      name: \"Test Import Session\",\n      rootListId: listId,\n    };\n\n    const createdSession = await api.createImportSession(newSessionInput);\n\n    expect(createdSession).toMatchObject({\n      id: expect.any(String),\n    });\n\n    // Verify session appears in list\n    const sessions = await api.listImportSessions({});\n    const sessionFromList = sessions.sessions.find(\n      (s) => s.id === createdSession.id,\n    );\n    expect(sessionFromList).toBeDefined();\n    expect(sessionFromList?.name).toEqual(newSessionInput.name);\n    expect(sessionFromList?.rootListId).toEqual(listId);\n  });\n\n  test<CustomTestContext>(\"create import session without rootListId\", async ({\n    apiCallers,\n  }) => {\n    const api = apiCallers[0].importSessions;\n\n    const newSessionInput: z.infer<typeof zCreateImportSessionRequestSchema> = {\n      name: \"Test Import Session\",\n    };\n\n    const createdSession = await api.createImportSession(newSessionInput);\n\n    expect(createdSession).toMatchObject({\n      id: expect.any(String),\n    });\n\n    // Verify session appears in list\n    const sessions = await api.listImportSessions({});\n    const sessionFromList = sessions.sessions.find(\n      (s) => s.id === createdSession.id,\n    );\n    expect(sessionFromList?.rootListId).toBeNull();\n  });\n\n  test<CustomTestContext>(\"get import session stats\", async ({\n    apiCallers,\n  }) => {\n    const api = apiCallers[0];\n\n    const session = await api.importSessions.createImportSession({\n      name: \"Test Import Session\",\n    });\n\n    // Stage bookmarks using the staging flow\n    await api.importSessions.stageImportedBookmarks({\n      importSessionId: session.id,\n      bookmarks: [\n        { type: \"text\", content: \"Test bookmark 1\", tags: [], listIds: [] },\n        { type: \"text\", content: \"Test bookmark 2\", tags: [], listIds: [] },\n      ],\n    });\n\n    const statsInput: z.infer<typeof zGetImportSessionStatsRequestSchema> = {\n      importSessionId: session.id,\n    };\n\n    const stats = await api.importSessions.getImportSessionStats(statsInput);\n\n    expect(stats).toMatchObject({\n      id: session.id,\n      name: \"Test Import Session\",\n      status: \"staging\",\n      totalBookmarks: 2,\n      pendingBookmarks: 2,\n      completedBookmarks: 0,\n      failedBookmarks: 0,\n      processingBookmarks: 0,\n    });\n  });\n\n  test<CustomTestContext>(\"stats reflect crawl and tagging status for completed staging bookmarks\", async ({\n    apiCallers,\n    db,\n  }) => {\n    const api = apiCallers[0];\n\n    const session = await api.importSessions.createImportSession({\n      name: \"Test Import Session\",\n    });\n\n    // Create bookmarks with different crawl/tag statuses\n    const user = (await db.query.users.findFirst())!;\n\n    // 1. Link bookmark: crawl success, tag success -> completed\n    const [completedLinkBookmark] = await db\n      .insert(bookmarks)\n      .values({\n        userId: user.id,\n        type: BookmarkTypes.LINK,\n        taggingStatus: \"success\",\n      })\n      .returning();\n    await db.insert(bookmarkLinks).values({\n      id: completedLinkBookmark.id,\n      url: \"https://example.com/1\",\n      crawlStatus: \"success\",\n    });\n\n    // 2. Link bookmark: crawl pending, tag success -> processing\n    const [crawlPendingBookmark] = await db\n      .insert(bookmarks)\n      .values({\n        userId: user.id,\n        type: BookmarkTypes.LINK,\n        taggingStatus: \"success\",\n      })\n      .returning();\n    await db.insert(bookmarkLinks).values({\n      id: crawlPendingBookmark.id,\n      url: \"https://example.com/2\",\n      crawlStatus: \"pending\",\n    });\n\n    // 3. Text bookmark: tag pending -> processing\n    const [tagPendingBookmark] = await db\n      .insert(bookmarks)\n      .values({\n        userId: user.id,\n        type: BookmarkTypes.TEXT,\n        taggingStatus: \"pending\",\n      })\n      .returning();\n    await db.insert(bookmarkTexts).values({\n      id: tagPendingBookmark.id,\n      text: \"Test text\",\n    });\n\n    // 4. Link bookmark: crawl failure -> failed\n    const [crawlFailedBookmark] = await db\n      .insert(bookmarks)\n      .values({\n        userId: user.id,\n        type: BookmarkTypes.LINK,\n        taggingStatus: \"success\",\n      })\n      .returning();\n    await db.insert(bookmarkLinks).values({\n      id: crawlFailedBookmark.id,\n      url: \"https://example.com/3\",\n      crawlStatus: \"failure\",\n    });\n\n    // 5. Text bookmark: tag failure -> failed\n    const [tagFailedBookmark] = await db\n      .insert(bookmarks)\n      .values({\n        userId: user.id,\n        type: BookmarkTypes.TEXT,\n        taggingStatus: \"failure\",\n      })\n      .returning();\n    await db.insert(bookmarkTexts).values({\n      id: tagFailedBookmark.id,\n      text: \"Test text 2\",\n    });\n\n    // 6. Text bookmark: tag success (no crawl needed) -> completed\n    const [completedTextBookmark] = await db\n      .insert(bookmarks)\n      .values({\n        userId: user.id,\n        type: BookmarkTypes.TEXT,\n        taggingStatus: \"success\",\n      })\n      .returning();\n    await db.insert(bookmarkTexts).values({\n      id: completedTextBookmark.id,\n      text: \"Test text 3\",\n    });\n\n    // Create staging bookmarks in different states\n    // Note: With the new import worker design, items stay in \"processing\" until\n    // crawl/tag is done. Only then do they move to \"completed\".\n    await db.insert(importStagingBookmarks).values([\n      // Staging pending -> pendingBookmarks\n      {\n        importSessionId: session.id,\n        type: \"text\",\n        content: \"pending staging\",\n        status: \"pending\",\n      },\n      // Staging processing (no bookmark yet) -> processingBookmarks\n      {\n        importSessionId: session.id,\n        type: \"text\",\n        content: \"processing staging\",\n        status: \"processing\",\n      },\n      // Staging failed -> failedBookmarks\n      {\n        importSessionId: session.id,\n        type: \"text\",\n        content: \"failed staging\",\n        status: \"failed\",\n      },\n      // Staging completed + crawl/tag success -> completedBookmarks\n      {\n        importSessionId: session.id,\n        type: \"link\",\n        url: \"https://example.com/1\",\n        status: \"completed\",\n        resultBookmarkId: completedLinkBookmark.id,\n      },\n      // Staging processing + crawl pending -> processingBookmarks (waiting for crawl)\n      {\n        importSessionId: session.id,\n        type: \"link\",\n        url: \"https://example.com/2\",\n        status: \"processing\",\n        resultBookmarkId: crawlPendingBookmark.id,\n      },\n      // Staging processing + tag pending -> processingBookmarks (waiting for tag)\n      {\n        importSessionId: session.id,\n        type: \"text\",\n        content: \"tag pending\",\n        status: \"processing\",\n        resultBookmarkId: tagPendingBookmark.id,\n      },\n      // Staging completed + crawl failure -> completedBookmarks (failure is terminal)\n      {\n        importSessionId: session.id,\n        type: \"link\",\n        url: \"https://example.com/3\",\n        status: \"completed\",\n        resultBookmarkId: crawlFailedBookmark.id,\n      },\n      // Staging completed + tag failure -> completedBookmarks (failure is terminal)\n      {\n        importSessionId: session.id,\n        type: \"text\",\n        content: \"tag failed\",\n        status: \"completed\",\n        resultBookmarkId: tagFailedBookmark.id,\n      },\n      // Staging completed + tag success (text, no crawl) -> completedBookmarks\n      {\n        importSessionId: session.id,\n        type: \"text\",\n        content: \"completed text\",\n        status: \"completed\",\n        resultBookmarkId: completedTextBookmark.id,\n      },\n    ]);\n\n    const stats = await api.importSessions.getImportSessionStats({\n      importSessionId: session.id,\n    });\n\n    expect(stats).toMatchObject({\n      totalBookmarks: 9,\n      pendingBookmarks: 1, // staging pending\n      processingBookmarks: 3, // staging processing (no bookmark) + crawl pending + tag pending\n      completedBookmarks: 4, // link success + text success + crawl failure + tag failure\n      failedBookmarks: 1, // staging failed\n    });\n  });\n\n  test<CustomTestContext>(\"list import sessions returns all sessions\", async ({\n    apiCallers,\n  }) => {\n    const api = apiCallers[0].importSessions;\n\n    const sessionNames = [\"Session 1\", \"Session 2\", \"Session 3\"];\n    for (const name of sessionNames) {\n      await api.createImportSession({ name });\n    }\n\n    const result = await api.listImportSessions({});\n\n    expect(result.sessions).toHaveLength(3);\n    expect(result.sessions.map((session) => session.name)).toEqual(\n      sessionNames,\n    );\n    expect(\n      result.sessions.every((session) => session.totalBookmarks === 0),\n    ).toBe(true);\n  });\n\n  test<CustomTestContext>(\"delete import session\", async ({ apiCallers }) => {\n    const api = apiCallers[0].importSessions;\n\n    const session = await api.createImportSession({\n      name: \"Session to Delete\",\n    });\n\n    const deleteInput: z.infer<typeof zDeleteImportSessionRequestSchema> = {\n      importSessionId: session.id,\n    };\n\n    const result = await api.deleteImportSession(deleteInput);\n    expect(result.success).toBe(true);\n\n    // Verify session no longer exists\n    await expect(\n      api.getImportSessionStats({\n        importSessionId: session.id,\n      }),\n    ).rejects.toThrow(\"Import session not found\");\n  });\n\n  test<CustomTestContext>(\"cannot access other user's session\", async ({\n    apiCallers,\n  }) => {\n    const api1 = apiCallers[0].importSessions;\n    const api2 = apiCallers[1].importSessions;\n\n    // User 1 creates a session\n    const session = await api1.createImportSession({\n      name: \"User 1 Session\",\n    });\n\n    // User 2 tries to access it\n    await expect(\n      api2.getImportSessionStats({\n        importSessionId: session.id,\n      }),\n    ).rejects.toThrow(\"Import session not found\");\n\n    await expect(\n      api2.deleteImportSession({\n        importSessionId: session.id,\n      }),\n    ).rejects.toThrow(\"Import session not found\");\n  });\n\n  test<CustomTestContext>(\"cannot stage other user's session\", async ({\n    apiCallers,\n  }) => {\n    const api1 = apiCallers[0];\n    const api2 = apiCallers[1];\n\n    // User 1 creates session and bookmark\n    const session = await api1.importSessions.createImportSession({\n      name: \"User 1 Session\",\n    });\n\n    // User 1 tries to attach User 2's bookmark\n    await expect(\n      api2.importSessions.stageImportedBookmarks({\n        importSessionId: session.id,\n        bookmarks: [\n          {\n            type: \"text\",\n            content: \"Test bookmark\",\n            tags: [],\n            listIds: [],\n          },\n        ],\n      }),\n    ).rejects.toThrow(\"Import session not found\");\n  });\n});\n"
  },
  {
    "path": "packages/trpc/routers/importSessions.ts",
    "content": "import { experimental_trpcMiddleware } from \"@trpc/server\";\nimport { and, eq, gt } from \"drizzle-orm\";\nimport { z } from \"zod\";\n\nimport { importStagingBookmarks } from \"@karakeep/db/schema\";\nimport {\n  zCreateImportSessionRequestSchema,\n  zDeleteImportSessionRequestSchema,\n  zGetImportSessionStatsRequestSchema,\n  zImportSessionWithStatsSchema,\n  zListImportSessionsRequestSchema,\n  zListImportSessionsResponseSchema,\n} from \"@karakeep/shared/types/importSessions\";\n\nimport type { AuthedContext } from \"../index\";\nimport { authedProcedure, router } from \"../index\";\nimport { ImportSession } from \"../models/importSessions\";\n\nconst ensureImportSessionAccess = experimental_trpcMiddleware<{\n  ctx: AuthedContext;\n  input: { importSessionId: string };\n}>().create(async (opts) => {\n  const importSession = await ImportSession.fromId(\n    opts.ctx,\n    opts.input.importSessionId,\n  );\n  return opts.next({\n    ctx: {\n      ...opts.ctx,\n      importSession,\n    },\n  });\n});\n\nexport const importSessionsRouter = router({\n  createImportSession: authedProcedure\n    .input(zCreateImportSessionRequestSchema)\n    .output(z.object({ id: z.string() }))\n    .mutation(async ({ input, ctx }) => {\n      const session = await ImportSession.create(ctx, input);\n      return { id: session.session.id };\n    }),\n\n  getImportSessionStats: authedProcedure\n    .input(zGetImportSessionStatsRequestSchema)\n    .output(zImportSessionWithStatsSchema)\n    .query(async ({ input, ctx }) => {\n      const session = await ImportSession.fromId(ctx, input.importSessionId);\n      return await session.getWithStats();\n    }),\n\n  listImportSessions: authedProcedure\n    .input(zListImportSessionsRequestSchema)\n    .output(zListImportSessionsResponseSchema)\n    .query(async ({ ctx }) => {\n      const sessions = await ImportSession.getAllWithStats(ctx);\n      return { sessions };\n    }),\n\n  deleteImportSession: authedProcedure\n    .input(zDeleteImportSessionRequestSchema)\n    .output(z.object({ success: z.boolean() }))\n    .mutation(async ({ input, ctx }) => {\n      const session = await ImportSession.fromId(ctx, input.importSessionId);\n      await session.delete();\n      return { success: true };\n    }),\n\n  stageImportedBookmarks: authedProcedure\n    .input(\n      z.object({\n        importSessionId: z.string(),\n        bookmarks: z\n          .array(\n            z.object({\n              type: z.enum([\"link\", \"text\", \"asset\"]),\n              url: z.string().optional(),\n              title: z.string().optional(),\n              content: z.string().optional(),\n              note: z.string().optional(),\n              tags: z.array(z.string()).default([]),\n              listIds: z.array(z.string()).default([]),\n              sourceAddedAt: z.date().optional(),\n            }),\n          )\n          .max(50),\n      }),\n    )\n    .use(ensureImportSessionAccess)\n    .mutation(async ({ input, ctx }) => {\n      await ctx.importSession.stageBookmarks(input.bookmarks);\n    }),\n\n  finalizeImportStaging: authedProcedure\n    .input(z.object({ importSessionId: z.string() }))\n    .use(ensureImportSessionAccess)\n    .mutation(async ({ ctx }) => {\n      await ctx.importSession.finalize();\n    }),\n\n  pauseImportSession: authedProcedure\n    .input(z.object({ importSessionId: z.string() }))\n    .use(ensureImportSessionAccess)\n    .mutation(async ({ ctx }) => {\n      await ctx.importSession.pause();\n    }),\n\n  resumeImportSession: authedProcedure\n    .input(z.object({ importSessionId: z.string() }))\n    .use(ensureImportSessionAccess)\n    .mutation(async ({ ctx }) => {\n      await ctx.importSession.resume();\n    }),\n\n  getImportSessionResults: authedProcedure\n    .input(\n      z.object({\n        importSessionId: z.string(),\n        filter: z\n          .enum([\"all\", \"accepted\", \"rejected\", \"skipped_duplicate\", \"pending\"])\n          .optional(),\n        cursor: z.string().optional(),\n        limit: z.number().default(50),\n      }),\n    )\n    .use(ensureImportSessionAccess)\n    .query(async ({ ctx, input }) => {\n      const results = await ctx.db\n        .select()\n        .from(importStagingBookmarks)\n        .where(\n          and(\n            eq(\n              importStagingBookmarks.importSessionId,\n              ctx.importSession.session.id,\n            ),\n            input.filter && input.filter !== \"all\"\n              ? input.filter === \"pending\"\n                ? eq(importStagingBookmarks.status, \"pending\")\n                : eq(importStagingBookmarks.result, input.filter)\n              : undefined,\n            input.cursor\n              ? gt(importStagingBookmarks.id, input.cursor)\n              : undefined,\n          ),\n        )\n        .orderBy(importStagingBookmarks.id)\n        .limit(input.limit + 1);\n\n      // Return with pagination info\n      const hasMore = results.length > input.limit;\n      return {\n        items: results.slice(0, input.limit),\n        nextCursor: hasMore ? results[input.limit - 1].id : null,\n      };\n    }),\n});\n"
  },
  {
    "path": "packages/trpc/routers/invites.test.ts",
    "content": "import { eq } from \"drizzle-orm\";\nimport { beforeEach, describe, expect, test, vi } from \"vitest\";\n\nimport { invites, users } from \"@karakeep/db/schema\";\n\nimport type { CustomTestContext } from \"../testUtils\";\nimport { defaultBeforeEach, getApiCaller } from \"../testUtils\";\n\n// Mock server config with email settings\nvi.mock(\"@karakeep/shared/config\", async (original) => {\n  const mod = (await original()) as typeof import(\"@karakeep/shared/config\");\n  return {\n    ...mod,\n    default: {\n      ...mod.default,\n      email: {\n        smtp: {\n          host: \"test-smtp.example.com\",\n          port: 587,\n          secure: false,\n          user: \"test@example.com\",\n          password: \"test-password\",\n          from: \"test@example.com\",\n        },\n      },\n    },\n  };\n});\n\n// Mock email functions\nvi.mock(\"../email\", () => ({\n  sendInviteEmail: vi.fn().mockResolvedValue(undefined),\n}));\n\nbeforeEach<CustomTestContext>(defaultBeforeEach(false));\n\ndescribe(\"Invites Router\", () => {\n  test<CustomTestContext>(\"admin can create invite\", async ({\n    db,\n    unauthedAPICaller,\n  }) => {\n    const admin = await unauthedAPICaller.users.create({\n      name: \"Admin User\",\n      email: \"admin@test.com\",\n      password: \"pass1234\",\n      confirmPassword: \"pass1234\",\n    });\n\n    const adminCaller = getApiCaller(db, admin.id, admin.email, \"admin\");\n\n    const invite = await adminCaller.invites.create({\n      email: \"newuser@test.com\",\n    });\n\n    expect(invite.email).toBe(\"newuser@test.com\");\n    expect(invite.id).toBeDefined();\n\n    // Verify the invite was created in the database\n    const dbInvite = await db.query.invites.findFirst({\n      where: eq(invites.id, invite.id),\n    });\n    expect(dbInvite?.invitedBy).toBe(admin.id);\n    expect(dbInvite?.usedAt).toBeNull();\n    expect(dbInvite?.token).toBeDefined();\n  });\n\n  test<CustomTestContext>(\"non-admin cannot create invite\", async ({\n    db,\n    unauthedAPICaller,\n  }) => {\n    await unauthedAPICaller.users.create({\n      name: \"Admin User\",\n      email: \"admin@test.com\",\n      password: \"pass1234\",\n      confirmPassword: \"pass1234\",\n    });\n\n    const user = await unauthedAPICaller.users.create({\n      name: \"Regular User\",\n      email: \"user@test.com\",\n      password: \"pass1234\",\n      confirmPassword: \"pass1234\",\n    });\n\n    const userCaller = getApiCaller(db, user.id, user.email);\n\n    await expect(() =>\n      userCaller.invites.create({\n        email: \"newuser@test.com\",\n      }),\n    ).rejects.toThrow(/FORBIDDEN/);\n  });\n\n  test<CustomTestContext>(\"cannot invite existing user\", async ({\n    db,\n    unauthedAPICaller,\n  }) => {\n    const admin = await unauthedAPICaller.users.create({\n      name: \"Admin User\",\n      email: \"admin@test.com\",\n      password: \"pass1234\",\n      confirmPassword: \"pass1234\",\n    });\n\n    await unauthedAPICaller.users.create({\n      name: \"Existing User\",\n      email: \"existing@test.com\",\n      password: \"pass1234\",\n      confirmPassword: \"pass1234\",\n    });\n\n    const adminCaller = getApiCaller(db, admin.id, admin.email, \"admin\");\n\n    await expect(() =>\n      adminCaller.invites.create({\n        email: \"existing@test.com\",\n      }),\n    ).rejects.toThrow(/User with this email already exists/);\n  });\n\n  test<CustomTestContext>(\"cannot create duplicate pending invite\", async ({\n    db,\n    unauthedAPICaller,\n  }) => {\n    const admin = await unauthedAPICaller.users.create({\n      name: \"Admin User\",\n      email: \"admin@test.com\",\n      password: \"pass1234\",\n      confirmPassword: \"pass1234\",\n    });\n\n    const adminCaller = getApiCaller(db, admin.id, admin.email, \"admin\");\n\n    await adminCaller.invites.create({\n      email: \"newuser@test.com\",\n    });\n\n    await expect(() =>\n      adminCaller.invites.create({\n        email: \"newuser@test.com\",\n      }),\n    ).rejects.toThrow(/An active invite for this email already exists/);\n  });\n\n  test<CustomTestContext>(\"admin can list invites\", async ({\n    db,\n    unauthedAPICaller,\n  }) => {\n    const admin = await unauthedAPICaller.users.create({\n      name: \"Admin User\",\n      email: \"admin@test.com\",\n      password: \"pass1234\",\n      confirmPassword: \"pass1234\",\n    });\n\n    const adminCaller = getApiCaller(db, admin.id, admin.email, \"admin\");\n\n    await adminCaller.invites.create({\n      email: \"user1@test.com\",\n    });\n\n    await adminCaller.invites.create({\n      email: \"user2@test.com\",\n    });\n\n    const result = await adminCaller.invites.list();\n\n    expect(result.invites).toHaveLength(2);\n    expect(\n      result.invites.find((i) => i.email === \"user1@test.com\"),\n    ).toBeTruthy();\n    expect(\n      result.invites.find((i) => i.email === \"user2@test.com\"),\n    ).toBeTruthy();\n  });\n\n  test<CustomTestContext>(\"non-admin cannot list invites\", async ({\n    db,\n    unauthedAPICaller,\n  }) => {\n    await unauthedAPICaller.users.create({\n      name: \"Admin User\",\n      email: \"admin@test.com\",\n      password: \"pass1234\",\n      confirmPassword: \"pass1234\",\n    });\n\n    const user = await unauthedAPICaller.users.create({\n      name: \"Regular User\",\n      email: \"user@test.com\",\n      password: \"pass1234\",\n      confirmPassword: \"pass1234\",\n    });\n\n    const userCaller = getApiCaller(db, user.id, user.email);\n\n    await expect(() => userCaller.invites.list()).rejects.toThrow(/FORBIDDEN/);\n  });\n\n  test<CustomTestContext>(\"can get invite by token\", async ({\n    db,\n    unauthedAPICaller,\n  }) => {\n    const admin = await unauthedAPICaller.users.create({\n      name: \"Admin User\",\n      email: \"admin@test.com\",\n      password: \"pass1234\",\n      confirmPassword: \"pass1234\",\n    });\n\n    const adminCaller = getApiCaller(db, admin.id, admin.email, \"admin\");\n\n    const invite = await adminCaller.invites.create({\n      email: \"newuser@test.com\",\n    });\n\n    // Get the token from the database since it's not returned by create\n    const dbInvite = await db.query.invites.findFirst({\n      where: eq(invites.id, invite.id),\n    });\n\n    const retrievedInvite = await unauthedAPICaller.invites.get({\n      token: dbInvite!.token,\n    });\n\n    expect(retrievedInvite.email).toBe(\"newuser@test.com\");\n    expect(retrievedInvite.expired).toBe(false);\n  });\n\n  test<CustomTestContext>(\"cannot get invite with invalid token\", async ({\n    unauthedAPICaller,\n  }) => {\n    await expect(() =>\n      unauthedAPICaller.invites.get({\n        token: \"invalid-token\",\n      }),\n    ).rejects.toThrow(/Invite not found/);\n  });\n\n  test<CustomTestContext>(\"can get invite by token\", async ({\n    db,\n    unauthedAPICaller,\n  }) => {\n    const admin = await unauthedAPICaller.users.create({\n      name: \"Admin User\",\n      email: \"admin@test.com\",\n      password: \"pass1234\",\n      confirmPassword: \"pass1234\",\n    });\n\n    const adminCaller = getApiCaller(db, admin.id, admin.email, \"admin\");\n\n    const invite = await adminCaller.invites.create({\n      email: \"newuser@test.com\",\n    });\n\n    const dbInvite = await db.query.invites.findFirst({\n      where: eq(invites.id, invite.id),\n    });\n\n    const result = await unauthedAPICaller.invites.get({\n      token: dbInvite!.token,\n    });\n\n    expect(result.email).toBe(\"newuser@test.com\");\n    expect(result.expired).toBe(false);\n  });\n\n  test<CustomTestContext>(\"cannot get used invite (deleted)\", async ({\n    db,\n    unauthedAPICaller,\n  }) => {\n    const admin = await unauthedAPICaller.users.create({\n      name: \"Admin User\",\n      email: \"admin@test.com\",\n      password: \"pass1234\",\n      confirmPassword: \"pass1234\",\n    });\n\n    const adminCaller = getApiCaller(db, admin.id, admin.email, \"admin\");\n\n    const invite = await adminCaller.invites.create({\n      email: \"newuser@test.com\",\n    });\n\n    const dbInvite = await db.query.invites.findFirst({\n      where: eq(invites.id, invite.id),\n    });\n\n    // Accept the invite (which deletes it)\n    await unauthedAPICaller.invites.accept({\n      token: dbInvite!.token,\n      name: \"New User\",\n      password: \"newpass123\",\n    });\n\n    // Try to get the invite again - should fail\n    await expect(() =>\n      unauthedAPICaller.invites.get({\n        token: dbInvite!.token,\n      }),\n    ).rejects.toThrow(/Invite not found or has been used/);\n  });\n\n  test<CustomTestContext>(\"can accept valid invite\", async ({\n    db,\n    unauthedAPICaller,\n  }) => {\n    const admin = await unauthedAPICaller.users.create({\n      name: \"Admin User\",\n      email: \"admin@test.com\",\n      password: \"pass1234\",\n      confirmPassword: \"pass1234\",\n    });\n\n    const adminCaller = getApiCaller(db, admin.id, admin.email, \"admin\");\n\n    const invite = await adminCaller.invites.create({\n      email: \"newuser@test.com\",\n    });\n\n    const dbInvite = await db.query.invites.findFirst({\n      where: eq(invites.id, invite.id),\n    });\n\n    const newUser = await unauthedAPICaller.invites.accept({\n      token: dbInvite!.token,\n      name: \"New User\",\n      password: \"newpass123\",\n    });\n\n    expect(newUser.name).toBe(\"New User\");\n    expect(newUser.email).toBe(\"newuser@test.com\");\n\n    // Verify invite was deleted\n    const deletedInvite = await db.query.invites.findFirst({\n      where: eq(invites.id, invite.id),\n    });\n    expect(deletedInvite).toBeUndefined();\n  });\n\n  test<CustomTestContext>(\"can accept valid invite\", async ({\n    db,\n    unauthedAPICaller,\n  }) => {\n    const admin = await unauthedAPICaller.users.create({\n      name: \"Admin User\",\n      email: \"admin@test.com\",\n      password: \"pass1234\",\n      confirmPassword: \"pass1234\",\n    });\n\n    const adminCaller = getApiCaller(db, admin.id, admin.email, \"admin\");\n\n    const invite = await adminCaller.invites.create({\n      email: \"newuser@test.com\",\n    });\n\n    const dbInvite = await db.query.invites.findFirst({\n      where: eq(invites.id, invite.id),\n    });\n\n    const result = await unauthedAPICaller.invites.accept({\n      token: dbInvite!.token,\n      name: \"New User\",\n      password: \"newpass123\",\n    });\n\n    expect(result.email).toBe(\"newuser@test.com\");\n    expect(result.name).toBe(\"New User\");\n  });\n\n  test<CustomTestContext>(\"cannot accept used invite (deleted)\", async ({\n    db,\n    unauthedAPICaller,\n  }) => {\n    const admin = await unauthedAPICaller.users.create({\n      name: \"Admin User\",\n      email: \"admin@test.com\",\n      password: \"pass1234\",\n      confirmPassword: \"pass1234\",\n    });\n\n    const adminCaller = getApiCaller(db, admin.id, admin.email, \"admin\");\n\n    const invite = await adminCaller.invites.create({\n      email: \"newuser@test.com\",\n    });\n\n    const dbInvite = await db.query.invites.findFirst({\n      where: eq(invites.id, invite.id),\n    });\n\n    // Accept the invite first time\n    await unauthedAPICaller.invites.accept({\n      token: dbInvite!.token,\n      name: \"New User\",\n      password: \"newpass123\",\n    });\n\n    // Try to accept again - should fail because invite is deleted\n    await expect(() =>\n      unauthedAPICaller.invites.accept({\n        token: dbInvite!.token,\n        name: \"Another User\",\n        password: \"anotherpass123\",\n      }),\n    ).rejects.toThrow(/Invite not found or has been used/);\n  });\n\n  test<CustomTestContext>(\"admin can revoke invite\", async ({\n    db,\n    unauthedAPICaller,\n  }) => {\n    const admin = await unauthedAPICaller.users.create({\n      name: \"Admin User\",\n      email: \"admin@test.com\",\n      password: \"pass1234\",\n      confirmPassword: \"pass1234\",\n    });\n\n    const adminCaller = getApiCaller(db, admin.id, admin.email, \"admin\");\n\n    const invite = await adminCaller.invites.create({\n      email: \"newuser@test.com\",\n    });\n\n    const result = await adminCaller.invites.revoke({\n      inviteId: invite.id,\n    });\n\n    expect(result.success).toBe(true);\n\n    // Verify the invite is deleted\n    const revokedInvite = await db.query.invites.findFirst({\n      where: eq(invites.id, invite.id),\n    });\n    expect(revokedInvite).toBeUndefined();\n  });\n\n  test<CustomTestContext>(\"non-admin cannot revoke invite\", async ({\n    db,\n    unauthedAPICaller,\n  }) => {\n    const admin = await unauthedAPICaller.users.create({\n      name: \"Admin User\",\n      email: \"admin@test.com\",\n      password: \"pass1234\",\n      confirmPassword: \"pass1234\",\n    });\n\n    const user = await unauthedAPICaller.users.create({\n      name: \"Regular User\",\n      email: \"user@test.com\",\n      password: \"pass1234\",\n      confirmPassword: \"pass1234\",\n    });\n\n    const adminCaller = getApiCaller(db, admin.id, admin.email, \"admin\");\n    const userCaller = getApiCaller(db, user.id, user.email);\n\n    const invite = await adminCaller.invites.create({\n      email: \"newuser@test.com\",\n    });\n\n    await expect(() =>\n      userCaller.invites.revoke({\n        inviteId: invite.id,\n      }),\n    ).rejects.toThrow(/FORBIDDEN/);\n  });\n\n  test<CustomTestContext>(\"admin can resend invite\", async ({\n    db,\n    unauthedAPICaller,\n  }) => {\n    const admin = await unauthedAPICaller.users.create({\n      name: \"Admin User\",\n      email: \"admin@test.com\",\n      password: \"pass1234\",\n      confirmPassword: \"pass1234\",\n    });\n\n    const adminCaller = getApiCaller(db, admin.id, admin.email, \"admin\");\n\n    const invite = await adminCaller.invites.create({\n      email: \"newuser@test.com\",\n    });\n\n    const originalDbInvite = await db.query.invites.findFirst({\n      where: eq(invites.id, invite.id),\n    });\n\n    await new Promise((resolve) => setTimeout(resolve, 10));\n\n    const resentInvite = await adminCaller.invites.resend({\n      inviteId: invite.id,\n    });\n\n    expect(resentInvite.email).toBe(\"newuser@test.com\");\n    expect(resentInvite.id).toBe(originalDbInvite!.id);\n\n    // Verify token was updated in database\n    const updatedDbInvite = await db.query.invites.findFirst({\n      where: eq(invites.id, invite.id),\n    });\n    expect(updatedDbInvite?.token).not.toBe(originalDbInvite?.token);\n  });\n\n  test<CustomTestContext>(\"cannot resend used invite (deleted)\", async ({\n    db,\n    unauthedAPICaller,\n  }) => {\n    const admin = await unauthedAPICaller.users.create({\n      name: \"Admin User\",\n      email: \"admin@test.com\",\n      password: \"pass1234\",\n      confirmPassword: \"pass1234\",\n    });\n\n    const adminCaller = getApiCaller(db, admin.id, admin.email, \"admin\");\n\n    const invite = await adminCaller.invites.create({\n      email: \"newuser@test.com\",\n    });\n\n    const dbInvite = await db.query.invites.findFirst({\n      where: eq(invites.id, invite.id),\n    });\n\n    // Accept the invite (which deletes it)\n    await unauthedAPICaller.invites.accept({\n      token: dbInvite!.token,\n      name: \"New User\",\n      password: \"newpass123\",\n    });\n\n    await expect(() =>\n      adminCaller.invites.resend({\n        inviteId: invite.id,\n      }),\n    ).rejects.toThrow(/Invite not found/);\n  });\n\n  test<CustomTestContext>(\"invite creation works without expiration\", async ({\n    db,\n    unauthedAPICaller,\n  }) => {\n    const admin = await unauthedAPICaller.users.create({\n      name: \"Admin User\",\n      email: \"admin@test.com\",\n      password: \"pass1234\",\n      confirmPassword: \"pass1234\",\n    });\n\n    const adminCaller = getApiCaller(db, admin.id, admin.email, \"admin\");\n\n    const invite = await adminCaller.invites.create({\n      email: \"newuser@test.com\",\n    });\n\n    expect(invite.email).toBe(\"newuser@test.com\");\n    expect(invite.id).toBeDefined();\n  });\n\n  test<CustomTestContext>(\"invite includes inviter information\", async ({\n    db,\n    unauthedAPICaller,\n  }) => {\n    const admin = await unauthedAPICaller.users.create({\n      name: \"Admin User\",\n      email: \"admin@test.com\",\n      password: \"pass1234\",\n      confirmPassword: \"pass1234\",\n    });\n\n    const adminCaller = getApiCaller(db, admin.id, admin.email, \"admin\");\n\n    const invite = await adminCaller.invites.create({\n      email: \"newuser@test.com\",\n    });\n\n    const result = await adminCaller.invites.list();\n    const createdInvite = result.invites.find((i) => i.id === invite.id);\n\n    expect(createdInvite?.invitedBy.id).toBe(admin.id);\n    expect(createdInvite?.invitedBy.name).toBe(\"Admin User\");\n    expect(createdInvite?.invitedBy.email).toBe(\"admin@test.com\");\n  });\n\n  test<CustomTestContext>(\"all invites create user role\", async ({\n    db,\n    unauthedAPICaller,\n  }) => {\n    const admin = await unauthedAPICaller.users.create({\n      name: \"Admin User\",\n      email: \"admin@test.com\",\n      password: \"pass1234\",\n      confirmPassword: \"pass1234\",\n    });\n\n    const adminCaller = getApiCaller(db, admin.id, admin.email, \"admin\");\n\n    const invite = await adminCaller.invites.create({\n      email: \"newuser@test.com\",\n    });\n\n    const dbInvite = await db.query.invites.findFirst({\n      where: eq(invites.id, invite.id),\n    });\n\n    const newUser = await unauthedAPICaller.invites.accept({\n      token: dbInvite!.token,\n      name: \"New User\",\n      password: \"userpass123\",\n    });\n\n    const user = await db.query.users.findFirst({\n      where: eq(users.email, newUser.email),\n    });\n    expect(user?.role).toBe(\"user\");\n  });\n\n  test<CustomTestContext>(\"email sending is called during invite creation\", async ({\n    db,\n    unauthedAPICaller,\n  }) => {\n    // Mock the email module\n    const mockSendInviteEmail = vi.fn().mockResolvedValue(undefined);\n    vi.doMock(\"../email\", () => ({\n      sendInviteEmail: mockSendInviteEmail,\n    }));\n\n    const admin = await unauthedAPICaller.users.create({\n      name: \"Admin User\",\n      email: \"admin@test.com\",\n      password: \"pass1234\",\n      confirmPassword: \"pass1234\",\n    });\n\n    const adminCaller = getApiCaller(db, admin.id, admin.email, \"admin\");\n\n    await adminCaller.invites.create({\n      email: \"newuser@test.com\",\n    });\n\n    // Note: In a real test environment, we'd need to properly mock the email module\n    // This test demonstrates the structure but may not actually verify the mock call\n    // due to how the module is imported in the router\n  });\n});\n"
  },
  {
    "path": "packages/trpc/routers/invites.ts",
    "content": "import { randomBytes } from \"crypto\";\nimport { TRPCError } from \"@trpc/server\";\nimport { eq } from \"drizzle-orm\";\nimport { z } from \"zod\";\n\nimport { invites, users } from \"@karakeep/db/schema\";\n\nimport { generatePasswordSalt, hashPassword } from \"../auth\";\nimport { sendInviteEmail } from \"../email\";\nimport {\n  adminProcedure,\n  createRateLimitMiddleware,\n  publicProcedure,\n  router,\n} from \"../index\";\nimport { User } from \"../models/users\";\n\nexport const invitesAppRouter = router({\n  create: adminProcedure\n    .input(\n      z.object({\n        email: z.string().email(),\n      }),\n    )\n    .mutation(async ({ input, ctx }) => {\n      const existingUser = await ctx.db.query.users.findFirst({\n        where: eq(users.email, input.email),\n      });\n\n      if (existingUser) {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"User with this email already exists\",\n        });\n      }\n\n      const existingInvite = await ctx.db.query.invites.findFirst({\n        where: eq(invites.email, input.email),\n      });\n\n      if (existingInvite) {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"An active invite for this email already exists\",\n        });\n      }\n\n      const token = randomBytes(32).toString(\"hex\");\n\n      const [invite] = await ctx.db\n        .insert(invites)\n        .values({\n          email: input.email,\n          token,\n          invitedBy: ctx.user.id,\n        })\n        .returning();\n\n      // Send invite email\n      try {\n        await sendInviteEmail(\n          input.email,\n          token,\n          ctx.user.name || \"A Karakeep admin\",\n        );\n      } catch (error) {\n        console.error(\"Failed to send invite email:\", error);\n        // Don't fail the invite creation if email sending fails\n      }\n\n      return {\n        id: invite.id,\n        email: invite.email,\n      };\n    }),\n\n  list: adminProcedure\n    .output(\n      z.object({\n        invites: z.array(\n          z.object({\n            id: z.string(),\n            email: z.string(),\n            createdAt: z.date(),\n            invitedBy: z.object({\n              id: z.string(),\n              name: z.string(),\n              email: z.string(),\n            }),\n          }),\n        ),\n      }),\n    )\n    .query(async ({ ctx }) => {\n      const dbInvites = await ctx.db.query.invites.findMany({\n        with: {\n          invitedBy: {\n            columns: {\n              id: true,\n              name: true,\n              email: true,\n            },\n          },\n        },\n        orderBy: (invites, { desc }) => [desc(invites.createdAt)],\n      });\n\n      return {\n        invites: dbInvites,\n      };\n    }),\n\n  get: publicProcedure\n    .use(\n      createRateLimitMiddleware({\n        name: \"invites.get\",\n        windowMs: 60 * 1000,\n        maxRequests: 10,\n      }),\n    )\n    .input(\n      z.object({\n        token: z.string(),\n      }),\n    )\n    .output(\n      z.object({\n        email: z.string(),\n        expired: z.boolean(),\n      }),\n    )\n    .query(async ({ input, ctx }) => {\n      const invite = await ctx.db.query.invites.findFirst({\n        where: eq(invites.token, input.token),\n      });\n\n      if (!invite) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Invite not found or has been used\",\n        });\n      }\n\n      return {\n        email: invite.email,\n        expired: false,\n      };\n    }),\n\n  accept: publicProcedure\n    .use(\n      createRateLimitMiddleware({\n        name: \"invites.accept\",\n        windowMs: 60 * 1000,\n        maxRequests: 10,\n      }),\n    )\n    .input(\n      z.object({\n        token: z.string(),\n        name: z.string().min(1),\n        password: z.string().min(8),\n      }),\n    )\n    .mutation(async ({ input, ctx }) => {\n      const invite = await ctx.db.query.invites.findFirst({\n        where: eq(invites.token, input.token),\n      });\n\n      if (!invite) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Invite not found or has been used\",\n        });\n      }\n\n      const existingUser = await ctx.db.query.users.findFirst({\n        where: eq(users.email, invite.email),\n      });\n\n      if (existingUser) {\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"User with this email already exists\",\n        });\n      }\n\n      const salt = generatePasswordSalt();\n      const user = await User.createRaw(ctx.db, {\n        name: input.name,\n        email: invite.email,\n        password: await hashPassword(input.password, salt),\n        salt,\n        role: \"user\",\n        emailVerified: new Date(), // Auto-verify invited users\n      });\n\n      // Delete the invite after successful user creation\n      await ctx.db.delete(invites).where(eq(invites.id, invite.id));\n\n      return {\n        id: user.id,\n        name: user.name,\n        email: user.email,\n      };\n    }),\n\n  revoke: adminProcedure\n    .input(\n      z.object({\n        inviteId: z.string(),\n      }),\n    )\n    .mutation(async ({ input, ctx }) => {\n      const invite = await ctx.db.query.invites.findFirst({\n        where: eq(invites.id, input.inviteId),\n      });\n\n      if (!invite) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Invite not found\",\n        });\n      }\n\n      // Delete the invite to revoke it\n      await ctx.db.delete(invites).where(eq(invites.id, input.inviteId));\n\n      return { success: true };\n    }),\n\n  resend: adminProcedure\n    .input(\n      z.object({\n        inviteId: z.string(),\n      }),\n    )\n    .mutation(async ({ input, ctx }) => {\n      const invite = await ctx.db.query.invites.findFirst({\n        where: eq(invites.id, input.inviteId),\n      });\n\n      if (!invite) {\n        throw new TRPCError({\n          code: \"NOT_FOUND\",\n          message: \"Invite not found\",\n        });\n      }\n\n      const newToken = randomBytes(32).toString(\"hex\");\n\n      await ctx.db\n        .update(invites)\n        .set({\n          token: newToken,\n        })\n        .where(eq(invites.id, input.inviteId));\n\n      // Send invite email with new token\n      try {\n        await sendInviteEmail(\n          invite.email,\n          newToken,\n          ctx.user.name || \"A Karakeep admin\",\n        );\n      } catch (error) {\n        console.error(\"Failed to send invite email:\", error);\n        // Don't fail the resend if email sending fails\n      }\n\n      return {\n        id: invite.id,\n        email: invite.email,\n      };\n    }),\n});\n"
  },
  {
    "path": "packages/trpc/routers/lists.test.ts",
    "content": "import { beforeEach, describe, expect, test } from \"vitest\";\nimport { z } from \"zod\";\n\nimport {\n  BookmarkTypes,\n  zNewBookmarkRequestSchema,\n} from \"@karakeep/shared/types/bookmarks\";\nimport { zNewBookmarkListSchema } from \"@karakeep/shared/types/lists\";\n\nimport type { APICallerType, CustomTestContext } from \"../testUtils\";\nimport { defaultBeforeEach } from \"../testUtils\";\n\nasync function createTestBookmark(api: APICallerType) {\n  const newBookmarkInput: z.infer<typeof zNewBookmarkRequestSchema> = {\n    type: BookmarkTypes.TEXT,\n    text: \"Test bookmark text\",\n  };\n  const createdBookmark = await api.bookmarks.createBookmark(newBookmarkInput);\n  return createdBookmark.id;\n}\n\nbeforeEach<CustomTestContext>(defaultBeforeEach(true));\n\ndescribe(\"Lists Routes\", () => {\n  test<CustomTestContext>(\"create list\", async ({ apiCallers }) => {\n    const api = apiCallers[0].lists;\n    const newListInput: z.infer<typeof zNewBookmarkListSchema> = {\n      name: \"Test List\",\n      description: \"A test list\",\n      icon: \"📋\",\n      type: \"manual\",\n    };\n\n    const createdList = await api.create(newListInput);\n\n    expect(createdList).toMatchObject({\n      name: newListInput.name,\n      description: newListInput.description,\n      icon: newListInput.icon,\n      type: newListInput.type,\n    });\n\n    const lists = await api.list();\n    const listFromList = lists.lists.find((l) => l.id === createdList.id);\n    expect(listFromList).toBeDefined();\n    expect(listFromList?.name).toEqual(newListInput.name);\n  });\n\n  test<CustomTestContext>(\"edit list\", async ({ apiCallers }) => {\n    const api = apiCallers[0].lists;\n\n    // First, create a list\n    const createdListInput: z.infer<typeof zNewBookmarkListSchema> = {\n      name: \"Original List\",\n      description: \"Original description\",\n      icon: \"📋\",\n      type: \"manual\",\n    };\n    const createdList = await api.create(createdListInput);\n\n    // Update it\n    const updatedListInput = {\n      listId: createdList.id,\n      name: \"Updated List\",\n      description: \"Updated description\",\n      icon: \"⭐️\",\n    };\n    const updatedList = await api.edit(updatedListInput);\n\n    expect(updatedList.name).toEqual(updatedListInput.name);\n    expect(updatedList.description).toEqual(updatedListInput.description);\n    expect(updatedList.icon).toEqual(updatedListInput.icon);\n\n    // Verify the update\n    const lists = await api.list();\n    const listFromList = lists.lists.find((l) => l.id === createdList.id);\n    expect(listFromList).toBeDefined();\n    expect(listFromList?.name).toEqual(updatedListInput.name);\n\n    // Test editing a non-existent list\n    await expect(() =>\n      api.edit({ listId: \"non-existent-id\", name: \"Fail\" }),\n    ).rejects.toThrow(/List not found/);\n  });\n\n  test<CustomTestContext>(\"merge lists\", async ({ apiCallers }) => {\n    const api = apiCallers[0].lists;\n\n    // First, create a real bookmark\n    const bookmarkId = await createTestBookmark(apiCallers[0]);\n\n    // Create two lists\n    const sourceListInput: z.infer<typeof zNewBookmarkListSchema> = {\n      name: \"Source List\",\n      type: \"manual\",\n      icon: \"📚\",\n    };\n    const targetListInput: z.infer<typeof zNewBookmarkListSchema> = {\n      name: \"Target List\",\n      type: \"manual\",\n      icon: \"📖\",\n    };\n    const sourceList = await api.create(sourceListInput);\n    const targetList = await api.create(targetListInput);\n\n    // Add the real bookmark to source list\n    await api.addToList({ listId: sourceList.id, bookmarkId });\n\n    // Merge\n    await api.merge({\n      sourceId: sourceList.id,\n      targetId: targetList.id,\n      deleteSourceAfterMerge: true,\n    });\n\n    // Verify source list is deleted and bookmark is in target\n    const lists = await api.list();\n    expect(lists.lists.find((l) => l.id === sourceList.id)).toBeUndefined();\n    const targetListsOfBookmark = await api.getListsOfBookmark({\n      bookmarkId,\n    });\n    expect(\n      targetListsOfBookmark.lists.find((l) => l.id === targetList.id),\n    ).toBeDefined();\n\n    // Test merging invalid lists\n    await expect(() =>\n      api.merge({\n        sourceId: sourceList.id,\n        targetId: \"non-existent-id\",\n        deleteSourceAfterMerge: true,\n      }),\n    ).rejects.toThrow(/List not found/);\n  });\n\n  test<CustomTestContext>(\"delete list\", async ({ apiCallers }) => {\n    const api = apiCallers[0].lists;\n\n    // Create a list\n    const createdListInput: z.infer<typeof zNewBookmarkListSchema> = {\n      name: \"List to Delete\",\n      type: \"manual\",\n      icon: \"📚\",\n    };\n    const createdList = await api.create(createdListInput);\n\n    // Delete it\n    await api.delete({ listId: createdList.id });\n\n    // Verify it's deleted\n    const lists = await api.list();\n    expect(lists.lists.find((l) => l.id === createdList.id)).toBeUndefined();\n\n    // Test deleting a non-existent list\n    await expect(() =>\n      api.delete({ listId: \"non-existent-id\" }),\n    ).rejects.toThrow(/List not found/);\n  });\n\n  test<CustomTestContext>(\"add and remove from list\", async ({\n    apiCallers,\n  }) => {\n    const api = apiCallers[0].lists;\n\n    // First, create a real bookmark\n    const bookmarkId = await createTestBookmark(apiCallers[0]);\n\n    // Create a manual list\n    const listInput: z.infer<typeof zNewBookmarkListSchema> = {\n      name: \"Manual List\",\n      type: \"manual\",\n      icon: \"📚\",\n    };\n    const createdList = await api.create(listInput);\n\n    // Add to list\n    await api.addToList({ listId: createdList.id, bookmarkId });\n\n    // Verify addition\n    const listsOfBookmark = await api.getListsOfBookmark({\n      bookmarkId,\n    });\n    expect(\n      listsOfBookmark.lists.find((l) => l.id === createdList.id),\n    ).toBeDefined();\n\n    // Remove from list\n    await api.removeFromList({ listId: createdList.id, bookmarkId });\n\n    // Verify removal\n    const updatedListsOfBookmark = await api.getListsOfBookmark({\n      bookmarkId,\n    });\n    expect(\n      updatedListsOfBookmark.lists.find((l) => l.id === createdList.id),\n    ).toBeUndefined();\n\n    // Test on smart list (should fail)\n    const smartListInput: z.infer<typeof zNewBookmarkListSchema> = {\n      name: \"Smart List\",\n      type: \"smart\",\n      query: \"#example\",\n      icon: \"📚\",\n    };\n    const smartList = await api.create(smartListInput);\n    await expect(() =>\n      api.addToList({ listId: smartList.id, bookmarkId }),\n    ).rejects.toThrow(/Smart lists cannot be added to/);\n  });\n\n  test<CustomTestContext>(\"get and list lists\", async ({ apiCallers }) => {\n    const api = apiCallers[0].lists;\n\n    const newListInput: z.infer<typeof zNewBookmarkListSchema> = {\n      name: \"Get Test List\",\n      type: \"manual\",\n      icon: \"📚\",\n    };\n    const createdList = await api.create(newListInput);\n\n    const getList = await api.get({ listId: createdList.id });\n    expect(getList.name).toEqual(newListInput.name);\n\n    const lists = await api.list();\n    expect(lists.lists.length).toBeGreaterThan(0);\n    expect(lists.lists.find((l) => l.id === createdList.id)).toBeDefined();\n  });\n\n  test<CustomTestContext>(\"get lists of bookmark and stats\", async ({\n    apiCallers,\n  }) => {\n    const api = apiCallers[0].lists;\n\n    // First, create a real bookmark\n    const bookmarkId = await createTestBookmark(apiCallers[0]);\n\n    // Create a list and add the bookmark\n    const listInput: z.infer<typeof zNewBookmarkListSchema> = {\n      name: \"Stats Test List\",\n      type: \"manual\",\n      icon: \"📚\",\n    };\n    const createdList = await api.create(listInput);\n    await api.addToList({ listId: createdList.id, bookmarkId });\n\n    const listsOfBookmark = await api.getListsOfBookmark({\n      bookmarkId,\n    });\n    expect(listsOfBookmark.lists.length).toBeGreaterThan(0);\n\n    const stats = await api.stats();\n    expect(stats.stats.get(createdList.id)).toBeGreaterThan(0);\n  });\n});\n\ndescribe(\"recursive delete\", () => {\n  test<CustomTestContext>(\"non-recursive delete (deleteChildren=false)\", async ({\n    apiCallers,\n  }) => {\n    const api = apiCallers[0].lists;\n\n    // Create parent list\n    const parentInput: z.infer<typeof zNewBookmarkListSchema> = {\n      name: \"Parent List\",\n      type: \"manual\",\n      icon: \"📂\",\n    };\n    const parentList = await api.create(parentInput);\n\n    // Create child list\n    const childInput: z.infer<typeof zNewBookmarkListSchema> = {\n      name: \"Child List\",\n      parentId: parentList.id,\n      type: \"manual\",\n      icon: \"📄\",\n    };\n    const childList = await api.create(childInput);\n\n    // Test both default behavior and explicit false\n    // Default (should be false)\n    await api.delete({ listId: parentList.id });\n\n    let lists = await api.list();\n    expect(lists.lists.find((l) => l.id === parentList.id)).toBeUndefined();\n    let remainingChild = lists.lists.find((l) => l.id === childList.id);\n    expect(remainingChild).toBeDefined();\n    expect(remainingChild?.parentId).toBeNull();\n\n    // Create another parent-child pair to test explicit false\n    const parent2 = await api.create({\n      name: \"Parent List 2\",\n      type: \"manual\",\n      icon: \"📂\",\n    });\n    const child2 = await api.create({\n      name: \"Child List 2\",\n      parentId: parent2.id,\n      type: \"manual\",\n      icon: \"📄\",\n    });\n\n    // Explicit deleteChildren=false\n    await api.delete({ listId: parent2.id, deleteChildren: false });\n\n    lists = await api.list();\n    expect(lists.lists.find((l) => l.id === parent2.id)).toBeUndefined();\n    remainingChild = lists.lists.find((l) => l.id === child2.id);\n    expect(remainingChild).toBeDefined();\n    expect(remainingChild?.parentId).toBeNull();\n  });\n\n  test<CustomTestContext>(\"recursive delete with multiple children\", async ({\n    apiCallers,\n  }) => {\n    const api = apiCallers[0].lists;\n\n    // Create parent list\n    const parentInput: z.infer<typeof zNewBookmarkListSchema> = {\n      name: \"Parent List\",\n      type: \"manual\",\n      icon: \"📂\",\n    };\n    const parentList = await api.create(parentInput);\n\n    // Create multiple child lists\n    const child1Input: z.infer<typeof zNewBookmarkListSchema> = {\n      name: \"Child List 1\",\n      parentId: parentList.id,\n      type: \"manual\",\n      icon: \"📄\",\n    };\n    const child1 = await api.create(child1Input);\n\n    const child2Input: z.infer<typeof zNewBookmarkListSchema> = {\n      name: \"Child List 2\",\n      parentId: parentList.id,\n      type: \"manual\",\n      icon: \"📄\",\n    };\n    const child2 = await api.create(child2Input);\n\n    const child3Input: z.infer<typeof zNewBookmarkListSchema> = {\n      name: \"Child List 3\",\n      parentId: parentList.id,\n      type: \"smart\",\n      query: \"is:fav\",\n      icon: \"⭐\",\n    };\n    const child3 = await api.create(child3Input);\n\n    // Delete parent with deleteChildren=true\n    await api.delete({ listId: parentList.id, deleteChildren: true });\n\n    // Verify all lists are deleted\n    const lists = await api.list();\n    expect(lists.lists.find((l) => l.id === parentList.id)).toBeUndefined();\n    expect(lists.lists.find((l) => l.id === child1.id)).toBeUndefined();\n    expect(lists.lists.find((l) => l.id === child2.id)).toBeUndefined();\n    expect(lists.lists.find((l) => l.id === child3.id)).toBeUndefined();\n  });\n\n  test<CustomTestContext>(\"recursive delete preserves bookmarks in deleted lists\", async ({\n    apiCallers,\n  }) => {\n    const api = apiCallers[0].lists;\n\n    // Create a bookmark first\n    const bookmarkId = await createTestBookmark(apiCallers[0]);\n\n    // Create parent list\n    const parentInput: z.infer<typeof zNewBookmarkListSchema> = {\n      name: \"Parent List\",\n      type: \"manual\",\n      icon: \"📂\",\n    };\n    const parentList = await api.create(parentInput);\n\n    // Create child list with bookmark\n    const childInput: z.infer<typeof zNewBookmarkListSchema> = {\n      name: \"Child List\",\n      parentId: parentList.id,\n      type: \"manual\",\n      icon: \"📄\",\n    };\n    const childList = await api.create(childInput);\n\n    // Add bookmark to child list\n    await api.addToList({ listId: childList.id, bookmarkId });\n\n    // Verify bookmark is in the list\n    const listsBeforeDelete = await api.getListsOfBookmark({ bookmarkId });\n    expect(\n      listsBeforeDelete.lists.find((l) => l.id === childList.id),\n    ).toBeDefined();\n\n    // Delete parent with deleteChildren=true\n    await api.delete({ listId: parentList.id, deleteChildren: true });\n\n    // Verify lists are deleted\n    const allLists = await api.list();\n    expect(allLists.lists.find((l) => l.id === parentList.id)).toBeUndefined();\n    expect(allLists.lists.find((l) => l.id === childList.id)).toBeUndefined();\n\n    // Verify bookmark still exists but is not in any list\n    const listsAfterDelete = await api.getListsOfBookmark({ bookmarkId });\n    expect(listsAfterDelete.lists).toHaveLength(0);\n\n    // Verify the bookmark itself still exists by trying to access it\n    const bookmark = await apiCallers[0].bookmarks.getBookmark({\n      bookmarkId,\n    });\n    expect(bookmark).toBeDefined();\n    expect(bookmark.id).toBe(bookmarkId);\n  });\n\n  test<CustomTestContext>(\"recursive delete with complex hierarchy\", async ({\n    apiCallers,\n  }) => {\n    const api = apiCallers[0].lists;\n\n    // Create a complex tree structure:\n    //     root\n    //    /  |  \\\n    //   A   B   C\n    //  /|   |   |\\\n    // D E   F   G H\n    //       |\n    //       I\n\n    const root = await api.create({\n      name: \"Root\",\n      type: \"manual\",\n      icon: \"🌳\",\n    });\n\n    const listA = await api.create({\n      name: \"List A\",\n      parentId: root.id,\n      type: \"manual\",\n      icon: \"📂\",\n    });\n\n    const listB = await api.create({\n      name: \"List B\",\n      parentId: root.id,\n      type: \"smart\",\n      query: \"is:fav\",\n      icon: \"📂\",\n    });\n\n    const listC = await api.create({\n      name: \"List C\",\n      parentId: root.id,\n      type: \"manual\",\n      icon: \"📂\",\n    });\n\n    const listD = await api.create({\n      name: \"List D\",\n      parentId: listA.id,\n      type: \"manual\",\n      icon: \"📄\",\n    });\n\n    const listE = await api.create({\n      name: \"List E\",\n      parentId: listA.id,\n      type: \"smart\",\n      query: \"is:archived\",\n      icon: \"📄\",\n    });\n\n    const listF = await api.create({\n      name: \"List F\",\n      parentId: listB.id,\n      type: \"manual\",\n      icon: \"📄\",\n    });\n\n    const listG = await api.create({\n      name: \"List G\",\n      parentId: listC.id,\n      type: \"manual\",\n      icon: \"📄\",\n    });\n\n    const listH = await api.create({\n      name: \"List H\",\n      parentId: listC.id,\n      type: \"smart\",\n      query: \"is:fav\",\n      icon: \"📄\",\n    });\n\n    const listI = await api.create({\n      name: \"List I\",\n      parentId: listF.id,\n      type: \"manual\",\n      icon: \"📄\",\n    });\n\n    const allCreatedIds = [\n      root.id,\n      listA.id,\n      listB.id,\n      listC.id,\n      listD.id,\n      listE.id,\n      listF.id,\n      listG.id,\n      listH.id,\n      listI.id,\n    ];\n\n    // Delete root with deleteChildren=true\n    await api.delete({ listId: root.id, deleteChildren: true });\n\n    // Verify entire tree is deleted\n    const remainingLists = await api.list();\n    allCreatedIds.forEach((id) => {\n      expect(remainingLists.lists.find((l) => l.id === id)).toBeUndefined();\n    });\n  });\n\n  test<CustomTestContext>(\"recursive delete edge cases\", async ({\n    apiCallers,\n  }) => {\n    const api = apiCallers[0].lists;\n\n    // Test 1: Delete list with no children (should work fine)\n    const standaloneList = await api.create({\n      name: \"Standalone List\",\n      type: \"manual\",\n      icon: \"📄\",\n    });\n\n    await api.delete({ listId: standaloneList.id, deleteChildren: true });\n    let lists = await api.list();\n    expect(lists.lists.find((l) => l.id === standaloneList.id)).toBeUndefined();\n\n    // Test 2: Delete child directly (no recursion needed)\n    const parent = await api.create({\n      name: \"Parent\",\n      type: \"manual\",\n      icon: \"📂\",\n    });\n\n    const child = await api.create({\n      name: \"Child\",\n      parentId: parent.id,\n      type: \"manual\",\n      icon: \"📄\",\n    });\n\n    await api.delete({ listId: child.id, deleteChildren: true });\n    lists = await api.list();\n    expect(lists.lists.find((l) => l.id === parent.id)).toBeDefined();\n    expect(lists.lists.find((l) => l.id === child.id)).toBeUndefined();\n  });\n\n  test<CustomTestContext>(\"partial recursive delete on middle node\", async ({\n    apiCallers,\n  }) => {\n    const api = apiCallers[0].lists;\n\n    // Create hierarchy: grandparent -> parent -> child\n    const grandparent = await api.create({\n      name: \"Grandparent\",\n      type: \"manual\",\n      icon: \"📂\",\n    });\n\n    const parent = await api.create({\n      name: \"Parent\",\n      parentId: grandparent.id,\n      type: \"manual\",\n      icon: \"📂\",\n    });\n\n    const child = await api.create({\n      name: \"Child\",\n      parentId: parent.id,\n      type: \"manual\",\n      icon: \"📄\",\n    });\n\n    // Delete middle node (parent) with deleteChildren=true\n    await api.delete({ listId: parent.id, deleteChildren: true });\n\n    // Verify parent and child are deleted, but grandparent remains\n    const lists = await api.list();\n    expect(lists.lists.find((l) => l.id === grandparent.id)).toBeDefined();\n    expect(lists.lists.find((l) => l.id === parent.id)).toBeUndefined();\n    expect(lists.lists.find((l) => l.id === child.id)).toBeUndefined();\n  });\n});\n\ndescribe(\"Nested smart lists\", () => {\n  test<CustomTestContext>(\"smart list can reference another smart list\", async ({\n    apiCallers,\n  }) => {\n    const api = apiCallers[0];\n\n    // Create a bookmark that is favourited\n    const bookmark1 = await api.bookmarks.createBookmark({\n      type: BookmarkTypes.TEXT,\n      text: \"Favourited bookmark\",\n    });\n    await api.bookmarks.updateBookmark({\n      bookmarkId: bookmark1.id,\n      favourited: true,\n    });\n\n    // Create a bookmark that is not favourited\n    const bookmark2 = await api.bookmarks.createBookmark({\n      type: BookmarkTypes.TEXT,\n      text: \"Non-favourited bookmark\",\n    });\n\n    // Create a smart list that matches favourited bookmarks\n    await api.lists.create({\n      name: \"Favourites\",\n      type: \"smart\",\n      query: \"is:fav\",\n      icon: \"⭐\",\n    });\n\n    // Create a smart list that references the first smart list\n    const smartListB = await api.lists.create({\n      name: \"From Favourites\",\n      type: \"smart\",\n      query: \"list:Favourites\",\n      icon: \"📋\",\n    });\n\n    // Get bookmarks from the nested smart list\n    const bookmarksInSmartListB = await api.bookmarks.getBookmarks({\n      listId: smartListB.id,\n    });\n\n    // Should contain the favourited bookmark\n    expect(bookmarksInSmartListB.bookmarks.length).toBe(1);\n    expect(bookmarksInSmartListB.bookmarks[0].id).toBe(bookmark1.id);\n\n    // Verify bookmark2 is not in the nested smart list\n    expect(\n      bookmarksInSmartListB.bookmarks.find((b) => b.id === bookmark2.id),\n    ).toBeUndefined();\n  });\n\n  test<CustomTestContext>(\"nested smart lists with multiple levels\", async ({\n    apiCallers,\n  }) => {\n    const api = apiCallers[0];\n\n    // Create a bookmark that is archived\n    const bookmark = await api.bookmarks.createBookmark({\n      type: BookmarkTypes.TEXT,\n      text: \"Archived bookmark\",\n    });\n    await api.bookmarks.updateBookmark({\n      bookmarkId: bookmark.id,\n      archived: true,\n    });\n\n    // Create smart list A: matches archived bookmarks\n    await api.lists.create({\n      name: \"Archived\",\n      type: \"smart\",\n      query: \"is:archived\",\n      icon: \"📦\",\n    });\n\n    // Create smart list B: references list A\n    await api.lists.create({\n      name: \"Level1\",\n      type: \"smart\",\n      query: \"list:Archived\",\n      icon: \"1️⃣\",\n    });\n\n    // Create smart list C: references list B (3 levels deep)\n    const smartListC = await api.lists.create({\n      name: \"Level2\",\n      type: \"smart\",\n      query: \"list:Level1\",\n      icon: \"2️⃣\",\n    });\n\n    // Get bookmarks from the deepest nested smart list\n    const bookmarksInSmartListC = await api.bookmarks.getBookmarks({\n      listId: smartListC.id,\n    });\n\n    // Should contain the archived bookmark\n    expect(bookmarksInSmartListC.bookmarks.length).toBe(1);\n    expect(bookmarksInSmartListC.bookmarks[0].id).toBe(bookmark.id);\n  });\n\n  test<CustomTestContext>(\"smart list with inverse reference to another smart list\", async ({\n    apiCallers,\n  }) => {\n    const api = apiCallers[0];\n\n    // Create two bookmarks\n    const favouritedBookmark = await api.bookmarks.createBookmark({\n      type: BookmarkTypes.TEXT,\n      text: \"Favourited bookmark\",\n    });\n    await api.bookmarks.updateBookmark({\n      bookmarkId: favouritedBookmark.id,\n      favourited: true,\n    });\n\n    const normalBookmark = await api.bookmarks.createBookmark({\n      type: BookmarkTypes.TEXT,\n      text: \"Normal bookmark\",\n    });\n\n    // Create a smart list that matches favourited bookmarks\n    await api.lists.create({\n      name: \"Favourites\",\n      type: \"smart\",\n      query: \"is:fav\",\n      icon: \"⭐\",\n    });\n\n    // Create a smart list with negative reference to Favourites\n    const notInFavourites = await api.lists.create({\n      name: \"Not In Favourites\",\n      type: \"smart\",\n      query: \"-list:Favourites\",\n      icon: \"❌\",\n    });\n\n    // Get bookmarks from the smart list\n    const bookmarksNotInFav = await api.bookmarks.getBookmarks({\n      listId: notInFavourites.id,\n    });\n\n    // Should contain only the non-favourited bookmark\n    expect(bookmarksNotInFav.bookmarks.length).toBe(1);\n    expect(bookmarksNotInFav.bookmarks[0].id).toBe(normalBookmark.id);\n  });\n\n  test<CustomTestContext>(\"circular reference between smart lists returns empty\", async ({\n    apiCallers,\n  }) => {\n    const api = apiCallers[0];\n\n    // Create a bookmark\n    const bookmark = await api.bookmarks.createBookmark({\n      type: BookmarkTypes.TEXT,\n      text: \"Test bookmark\",\n    });\n    await api.bookmarks.updateBookmark({\n      bookmarkId: bookmark.id,\n      favourited: true,\n    });\n\n    // Create smart list A that references smart list B\n    const smartListA = await api.lists.create({\n      name: \"ListA\",\n      type: \"smart\",\n      query: \"list:ListB\",\n      icon: \"🅰️\",\n    });\n\n    // Create smart list B that references smart list A (circular!)\n    await api.lists.create({\n      name: \"ListB\",\n      type: \"smart\",\n      query: \"list:ListA\",\n      icon: \"🅱️\",\n    });\n\n    // Querying ListA should return empty because of the circular reference\n    const bookmarksInListA = await api.bookmarks.getBookmarks({\n      listId: smartListA.id,\n    });\n\n    // Should be empty due to circular reference detection\n    expect(bookmarksInListA.bookmarks.length).toBe(0);\n  });\n\n  test<CustomTestContext>(\"self-referencing smart list returns empty\", async ({\n    apiCallers,\n  }) => {\n    const api = apiCallers[0];\n\n    // Create a bookmark\n    await api.bookmarks.createBookmark({\n      type: BookmarkTypes.TEXT,\n      text: \"Test bookmark\",\n    });\n\n    // Create a smart list that references itself\n    const selfRefList = await api.lists.create({\n      name: \"SelfRef\",\n      type: \"smart\",\n      query: \"list:SelfRef\",\n      icon: \"🔄\",\n    });\n\n    // Querying should return empty because of self-reference\n    const bookmarks = await api.bookmarks.getBookmarks({\n      listId: selfRefList.id,\n    });\n\n    expect(bookmarks.bookmarks.length).toBe(0);\n  });\n\n  test<CustomTestContext>(\"three-way circular reference returns empty\", async ({\n    apiCallers,\n  }) => {\n    const api = apiCallers[0];\n\n    // Create a bookmark\n    await api.bookmarks.createBookmark({\n      type: BookmarkTypes.TEXT,\n      text: \"Test bookmark\",\n    });\n\n    // Create three smart lists with circular references: A -> B -> C -> A\n    const listA = await api.lists.create({\n      name: \"CircularA\",\n      type: \"smart\",\n      query: \"list:CircularB\",\n      icon: \"🅰️\",\n    });\n\n    await api.lists.create({\n      name: \"CircularB\",\n      type: \"smart\",\n      query: \"list:CircularC\",\n      icon: \"🅱️\",\n    });\n\n    await api.lists.create({\n      name: \"CircularC\",\n      type: \"smart\",\n      query: \"list:CircularA\",\n      icon: \"©️\",\n    });\n\n    // Querying any of them should return empty due to circular reference\n    const bookmarksInListA = await api.bookmarks.getBookmarks({\n      listId: listA.id,\n    });\n\n    expect(bookmarksInListA.bookmarks.length).toBe(0);\n  });\n\n  test<CustomTestContext>(\"smart list traversal above max visited lists returns empty\", async ({\n    apiCallers,\n  }) => {\n    const api = apiCallers[0];\n\n    const bookmark = await api.bookmarks.createBookmark({\n      type: BookmarkTypes.TEXT,\n      text: \"Depth test bookmark\",\n    });\n\n    const manualList = await api.lists.create({\n      name: \"DepthBaseManual\",\n      type: \"manual\",\n      icon: \"📋\",\n    });\n    await api.lists.addToList({\n      listId: manualList.id,\n      bookmarkId: bookmark.id,\n    });\n\n    const maxVisitedLists = 30;\n    const overLimitChainLength = maxVisitedLists + 1;\n\n    for (let i = overLimitChainLength; i >= 2; i--) {\n      await api.lists.create({\n        name: `DepthL${i}`,\n        type: \"smart\",\n        query:\n          i === overLimitChainLength\n            ? \"list:DepthBaseManual\"\n            : `list:DepthL${i + 1}`,\n        icon: \"D\",\n      });\n    }\n\n    const depthRoot = await api.lists.create({\n      name: \"DepthL1\",\n      type: \"smart\",\n      query: \"list:DepthL2\",\n      icon: \"D\",\n    });\n\n    const bookmarksInRoot = await api.bookmarks.getBookmarks({\n      listId: depthRoot.id,\n    });\n\n    expect(bookmarksInRoot.bookmarks.length).toBe(0);\n  });\n\n  test<CustomTestContext>(\"smart list references non-existent list returns empty\", async ({\n    apiCallers,\n  }) => {\n    const api = apiCallers[0];\n\n    // Create a bookmark\n    await api.bookmarks.createBookmark({\n      type: BookmarkTypes.TEXT,\n      text: \"Test bookmark\",\n    });\n\n    // Create a smart list that references a non-existent list\n    const smartList = await api.lists.create({\n      name: \"RefNonExistent\",\n      type: \"smart\",\n      query: \"list:NonExistentList\",\n      icon: \"❓\",\n    });\n\n    // Should return empty since the referenced list doesn't exist\n    const bookmarks = await api.bookmarks.getBookmarks({\n      listId: smartList.id,\n    });\n\n    expect(bookmarks.bookmarks.length).toBe(0);\n  });\n\n  test<CustomTestContext>(\"smart list can reference manual list\", async ({\n    apiCallers,\n  }) => {\n    const api = apiCallers[0];\n\n    // Create bookmarks\n    const bookmark1 = await api.bookmarks.createBookmark({\n      type: BookmarkTypes.TEXT,\n      text: \"Bookmark in manual list\",\n    });\n    const bookmark2 = await api.bookmarks.createBookmark({\n      type: BookmarkTypes.TEXT,\n      text: \"Bookmark not in list\",\n    });\n\n    // Create a manual list and add bookmark1\n    const manualList = await api.lists.create({\n      name: \"ManualList\",\n      type: \"manual\",\n      icon: \"📋\",\n    });\n    await api.lists.addToList({\n      listId: manualList.id,\n      bookmarkId: bookmark1.id,\n    });\n\n    // Create a smart list that references the manual list\n    const smartList = await api.lists.create({\n      name: \"SmartRefManual\",\n      type: \"smart\",\n      query: \"list:ManualList\",\n      icon: \"🔗\",\n    });\n\n    // Get bookmarks from the smart list\n    const bookmarksInSmartList = await api.bookmarks.getBookmarks({\n      listId: smartList.id,\n    });\n\n    // Should contain only bookmark1\n    expect(bookmarksInSmartList.bookmarks.length).toBe(1);\n    expect(bookmarksInSmartList.bookmarks[0].id).toBe(bookmark1.id);\n\n    // Verify bookmark2 is not in the smart list\n    expect(\n      bookmarksInSmartList.bookmarks.find((b) => b.id === bookmark2.id),\n    ).toBeUndefined();\n  });\n});\n"
  },
  {
    "path": "packages/trpc/routers/lists.ts",
    "content": "import { experimental_trpcMiddleware } from \"@trpc/server\";\nimport { z } from \"zod\";\n\nimport {\n  zBookmarkListSchema,\n  zEditBookmarkListSchemaWithValidation,\n  zMergeListSchema,\n  zNewBookmarkListSchema,\n} from \"@karakeep/shared/types/lists\";\n\nimport type { AuthedContext } from \"../index\";\nimport { authedProcedure, createRateLimitMiddleware, router } from \"../index\";\nimport { ListInvitation } from \"../models/listInvitations\";\nimport { List } from \"../models/lists\";\nimport { ensureBookmarkOwnership } from \"./bookmarks\";\n\nexport const ensureListAtLeastViewer = experimental_trpcMiddleware<{\n  ctx: AuthedContext;\n  input: { listId: string };\n}>().create(async (opts) => {\n  // This would throw if the user can't view the list\n  const list = await List.fromId(opts.ctx, opts.input.listId);\n  return opts.next({\n    ctx: {\n      ...opts.ctx,\n      list,\n    },\n  });\n});\n\nexport const ensureListAtLeastEditor = experimental_trpcMiddleware<{\n  ctx: AuthedContext & { list: List };\n  input: { listId: string };\n}>().create(async (opts) => {\n  opts.ctx.list.ensureCanEdit();\n  return opts.next({\n    ctx: opts.ctx,\n  });\n});\n\nexport const ensureListAtLeastOwner = experimental_trpcMiddleware<{\n  ctx: AuthedContext & { list: List };\n  input: { listId: string };\n}>().create(async (opts) => {\n  opts.ctx.list.ensureCanManage();\n  return opts.next({\n    ctx: opts.ctx,\n  });\n});\n\nexport const ensureInvitationAccess = experimental_trpcMiddleware<{\n  ctx: AuthedContext;\n  input: { invitationId: string };\n}>().create(async (opts) => {\n  const invitation = await ListInvitation.fromId(\n    opts.ctx,\n    opts.input.invitationId,\n  );\n  return opts.next({\n    ctx: {\n      ...opts.ctx,\n      invitation,\n    },\n  });\n});\n\nexport const listsAppRouter = router({\n  create: authedProcedure\n    .input(zNewBookmarkListSchema)\n    .output(zBookmarkListSchema)\n    .mutation(async ({ input, ctx }) => {\n      return await List.create(ctx, input).then((l) => l.asZBookmarkList());\n    }),\n  edit: authedProcedure\n    .input(zEditBookmarkListSchemaWithValidation)\n    .output(zBookmarkListSchema)\n    .use(ensureListAtLeastViewer)\n    .use(ensureListAtLeastOwner)\n    .mutation(async ({ input, ctx }) => {\n      await ctx.list.update(input);\n      return ctx.list.asZBookmarkList();\n    }),\n  merge: authedProcedure\n    .input(zMergeListSchema)\n    .mutation(async ({ input, ctx }) => {\n      const [sourceList, targetList] = await Promise.all([\n        List.fromId(ctx, input.sourceId),\n        List.fromId(ctx, input.targetId),\n      ]);\n      sourceList.ensureCanManage();\n      targetList.ensureCanManage();\n      return await sourceList.mergeInto(\n        targetList,\n        input.deleteSourceAfterMerge,\n      );\n    }),\n  delete: authedProcedure\n    .input(\n      z.object({\n        listId: z.string(),\n        deleteChildren: z.boolean().optional().default(false),\n      }),\n    )\n    .use(ensureListAtLeastViewer)\n    .use(ensureListAtLeastOwner)\n    .mutation(async ({ ctx, input }) => {\n      if (input.deleteChildren) {\n        const children = await ctx.list.getChildren();\n        await Promise.all(children.map((l) => l.delete()));\n      }\n      await ctx.list.delete();\n    }),\n  addToList: authedProcedure\n    .input(\n      z.object({\n        listId: z.string(),\n        bookmarkId: z.string(),\n      }),\n    )\n    .use(ensureListAtLeastViewer)\n    .use(ensureListAtLeastEditor)\n    .use(ensureBookmarkOwnership)\n    .mutation(async ({ input, ctx }) => {\n      await ctx.list.addBookmark(input.bookmarkId);\n    }),\n  removeFromList: authedProcedure\n    .input(\n      z.object({\n        listId: z.string(),\n        bookmarkId: z.string(),\n      }),\n    )\n    .use(ensureListAtLeastViewer)\n    .use(ensureListAtLeastEditor)\n    .mutation(async ({ input, ctx }) => {\n      await ctx.list.removeBookmark(input.bookmarkId);\n    }),\n  get: authedProcedure\n    .input(\n      z.object({\n        listId: z.string(),\n      }),\n    )\n    .output(zBookmarkListSchema)\n    .use(ensureListAtLeastViewer)\n    .query(async ({ ctx }) => {\n      return ctx.list.asZBookmarkList();\n    }),\n  list: authedProcedure\n    .output(\n      z.object({\n        lists: z.array(zBookmarkListSchema),\n      }),\n    )\n    .query(async ({ ctx }) => {\n      const results = await List.getAll(ctx);\n      return { lists: results.map((l) => l.asZBookmarkList()) };\n    }),\n  getListsOfBookmark: authedProcedure\n    .input(z.object({ bookmarkId: z.string() }))\n    .output(\n      z.object({\n        lists: z.array(zBookmarkListSchema),\n      }),\n    )\n    .use(ensureBookmarkOwnership)\n    .query(async ({ input, ctx }) => {\n      const lists = await List.forBookmark(ctx, input.bookmarkId);\n      return { lists: lists.map((l) => l.asZBookmarkList()) };\n    }),\n  stats: authedProcedure\n    .output(\n      z.object({\n        stats: z.map(z.string(), z.number()),\n      }),\n    )\n    .query(async ({ ctx }) => {\n      const lists = await List.getAll(ctx);\n      const sizes = await Promise.all(lists.map((l) => l.getSize()));\n      return { stats: new Map(lists.map((l, i) => [l.id, sizes[i]])) };\n    }),\n\n  // Rss endpoints\n  regenRssToken: authedProcedure\n    .input(\n      z.object({\n        listId: z.string(),\n      }),\n    )\n    .output(\n      z.object({\n        token: z.string(),\n      }),\n    )\n    .use(ensureListAtLeastViewer)\n    .use(ensureListAtLeastOwner)\n    .mutation(async ({ ctx }) => {\n      const token = await ctx.list.regenRssToken();\n      return { token: token! };\n    }),\n  clearRssToken: authedProcedure\n    .input(\n      z.object({\n        listId: z.string(),\n      }),\n    )\n    .use(ensureListAtLeastViewer)\n    .use(ensureListAtLeastOwner)\n    .mutation(async ({ ctx }) => {\n      await ctx.list.clearRssToken();\n    }),\n  getRssToken: authedProcedure\n    .input(\n      z.object({\n        listId: z.string(),\n      }),\n    )\n    .output(\n      z.object({\n        token: z.string().nullable(),\n      }),\n    )\n    .use(ensureListAtLeastViewer)\n    .use(ensureListAtLeastOwner)\n    .query(async ({ ctx }) => {\n      return { token: await ctx.list.getRssToken() };\n    }),\n\n  // Collaboration endpoints\n  addCollaborator: authedProcedure\n    .input(\n      z.object({\n        listId: z.string(),\n        email: z.string().email(),\n        role: z.enum([\"viewer\", \"editor\"]),\n      }),\n    )\n    .output(\n      z.object({\n        invitationId: z.string(),\n      }),\n    )\n    .use(\n      createRateLimitMiddleware({\n        name: \"lists.addCollaborator\",\n        windowMs: 15 * 60 * 1000,\n        maxRequests: 20,\n      }),\n    )\n    .use(ensureListAtLeastViewer)\n    .use(ensureListAtLeastOwner)\n    .mutation(async ({ input, ctx }) => {\n      return {\n        invitationId: await ctx.list.addCollaboratorByEmail(\n          input.email,\n          input.role,\n        ),\n      };\n    }),\n  removeCollaborator: authedProcedure\n    .input(\n      z.object({\n        listId: z.string(),\n        userId: z.string(),\n      }),\n    )\n    .use(ensureListAtLeastViewer)\n    .use(ensureListAtLeastOwner)\n    .mutation(async ({ input, ctx }) => {\n      await ctx.list.removeCollaborator(input.userId);\n    }),\n  updateCollaboratorRole: authedProcedure\n    .input(\n      z.object({\n        listId: z.string(),\n        userId: z.string(),\n        role: z.enum([\"viewer\", \"editor\"]),\n      }),\n    )\n    .use(ensureListAtLeastViewer)\n    .use(ensureListAtLeastOwner)\n    .mutation(async ({ input, ctx }) => {\n      await ctx.list.updateCollaboratorRole(input.userId, input.role);\n    }),\n  getCollaborators: authedProcedure\n    .input(\n      z.object({\n        listId: z.string(),\n      }),\n    )\n    .output(\n      z.object({\n        collaborators: z.array(\n          z.object({\n            id: z.string(),\n            userId: z.string(),\n            role: z.enum([\"viewer\", \"editor\"]),\n            status: z.enum([\"pending\", \"accepted\", \"declined\"]),\n            addedAt: z.date(),\n            invitedAt: z.date(),\n            user: z.object({\n              id: z.string(),\n              name: z.string(),\n              email: z.string().nullable(),\n              image: z.string().nullable(),\n            }),\n          }),\n        ),\n        owner: z\n          .object({\n            id: z.string(),\n            name: z.string(),\n            email: z.string().nullable(),\n            image: z.string().nullable(),\n          })\n          .nullable(),\n      }),\n    )\n    .use(ensureListAtLeastViewer)\n    .query(async ({ ctx }) => {\n      return await ctx.list.getCollaborators();\n    }),\n\n  acceptInvitation: authedProcedure\n    .input(\n      z.object({\n        invitationId: z.string(),\n      }),\n    )\n    .use(ensureInvitationAccess)\n    .mutation(async ({ ctx }) => {\n      await ctx.invitation.accept();\n    }),\n\n  declineInvitation: authedProcedure\n    .input(\n      z.object({\n        invitationId: z.string(),\n      }),\n    )\n    .use(ensureInvitationAccess)\n    .mutation(async ({ ctx }) => {\n      await ctx.invitation.decline();\n    }),\n\n  revokeInvitation: authedProcedure\n    .input(\n      z.object({\n        invitationId: z.string(),\n      }),\n    )\n    .use(ensureInvitationAccess)\n    .mutation(async ({ ctx }) => {\n      await ctx.invitation.revoke();\n    }),\n\n  getPendingInvitations: authedProcedure\n    .output(\n      z.array(\n        z.object({\n          id: z.string(),\n          listId: z.string(),\n          role: z.enum([\"viewer\", \"editor\"]),\n          invitedAt: z.date(),\n          list: z.object({\n            id: z.string(),\n            name: z.string(),\n            icon: z.string(),\n            description: z.string().nullable(),\n            owner: z\n              .object({\n                id: z.string(),\n                name: z.string(),\n                email: z.string(),\n              })\n              .nullable(),\n          }),\n        }),\n      ),\n    )\n    .query(async ({ ctx }) => {\n      return ListInvitation.pendingForUser(ctx);\n    }),\n\n  leaveList: authedProcedure\n    .input(\n      z.object({\n        listId: z.string(),\n      }),\n    )\n    .use(ensureListAtLeastViewer)\n    .mutation(async ({ ctx }) => {\n      await ctx.list.leaveList();\n    }),\n});\n"
  },
  {
    "path": "packages/trpc/routers/prompts.test.ts",
    "content": "import { beforeEach, describe, expect, test } from \"vitest\";\nimport { z } from \"zod\";\n\nimport { zNewPromptSchema } from \"@karakeep/shared/types/prompts\";\n\nimport type { CustomTestContext } from \"../testUtils\";\nimport { defaultBeforeEach } from \"../testUtils\";\n\nbeforeEach<CustomTestContext>(defaultBeforeEach(true));\n\ndescribe(\"Prompts Routes\", () => {\n  test<CustomTestContext>(\"create prompt\", async ({ apiCallers }) => {\n    const api = apiCallers[0].prompts;\n    const newPromptInput: z.infer<typeof zNewPromptSchema> = {\n      text: \"Test prompt text\",\n      appliesTo: \"summary\",\n    };\n\n    const createdPrompt = await api.create({ ...newPromptInput });\n\n    expect(createdPrompt).toMatchObject({\n      text: newPromptInput.text,\n      appliesTo: newPromptInput.appliesTo,\n      enabled: true,\n    });\n\n    const prompts = await api.list();\n    const promptFromList = prompts.find((p) => p.id === createdPrompt.id);\n    expect(promptFromList).toBeDefined();\n    expect(promptFromList?.text).toEqual(newPromptInput.text);\n  });\n\n  test<CustomTestContext>(\"update prompt\", async ({ apiCallers }) => {\n    const api = apiCallers[0].prompts;\n\n    // First, create a prompt\n    const createdPrompt = await api.create({\n      text: \"Original text\",\n      appliesTo: \"summary\",\n    });\n\n    // Update it\n    const updatedPrompt = await api.update({\n      promptId: createdPrompt.id,\n      text: \"Updated text\",\n      appliesTo: \"summary\",\n      enabled: false,\n    });\n\n    expect(updatedPrompt.text).toEqual(\"Updated text\");\n    expect(updatedPrompt.appliesTo).toEqual(\"summary\");\n    expect(updatedPrompt.enabled).toEqual(false);\n\n    // Instead of api.getPrompt, use api.list() to verify\n    const prompts = await api.list();\n    const promptFromList = prompts.find((p) => p.id === createdPrompt.id);\n    expect(promptFromList).toBeDefined();\n    expect(promptFromList?.text).toEqual(\"Updated text\");\n    expect(promptFromList?.enabled).toEqual(false);\n\n    // Test updating a non-existent prompt\n    await expect(() =>\n      api.update({\n        promptId: \"non-existent-id\",\n        text: \"Should fail\",\n        appliesTo: \"summary\",\n        enabled: true, // Assuming this matches the schema\n      }),\n    ).rejects.toThrow(/Prompt not found/);\n  });\n\n  test<CustomTestContext>(\"list prompts\", async ({ apiCallers }) => {\n    const api = apiCallers[0].prompts;\n\n    const emptyPrompts = await api.list();\n    expect(emptyPrompts).toEqual([]);\n\n    const prompt1Input: z.infer<typeof zNewPromptSchema> = {\n      text: \"Prompt 1\",\n      appliesTo: \"summary\",\n    };\n    await api.create(prompt1Input);\n\n    const prompt2Input: z.infer<typeof zNewPromptSchema> = {\n      text: \"Prompt 2\",\n      appliesTo: \"summary\",\n    };\n    await api.create(prompt2Input);\n\n    const prompts = await api.list();\n    expect(prompts.length).toEqual(2);\n    expect(prompts.some((p) => p.text === \"Prompt 1\")).toBeTruthy();\n    expect(prompts.some((p) => p.text === \"Prompt 2\")).toBeTruthy();\n  });\n\n  test<CustomTestContext>(\"delete prompt\", async ({ apiCallers }) => {\n    const api = apiCallers[0].prompts;\n\n    // Create a prompt\n    const createdPromptInput: z.infer<typeof zNewPromptSchema> = {\n      text: \"To be deleted\",\n      appliesTo: \"summary\",\n    };\n    const createdPrompt = await api.create(createdPromptInput);\n\n    // Delete it\n    await api.delete({ promptId: createdPrompt.id });\n\n    // Instead of api.getPrompt, use api.list() to verify\n    const prompts = await api.list();\n    expect(prompts.some((p) => p.id === createdPrompt.id)).toBeFalsy();\n  });\n\n  test<CustomTestContext>(\"privacy for prompts\", async ({ apiCallers }) => {\n    const user1PromptInput: z.infer<typeof zNewPromptSchema> = {\n      text: \"User 1 prompt\",\n      appliesTo: \"summary\",\n    };\n    const user1Prompt = await apiCallers[0].prompts.create(user1PromptInput);\n\n    const user2PromptInput: z.infer<typeof zNewPromptSchema> = {\n      text: \"User 2 prompt\",\n      appliesTo: \"summary\",\n    };\n    const user2Prompt = await apiCallers[1].prompts.create(user2PromptInput);\n\n    // User 1 should not access User 2's prompt\n    await expect(() =>\n      apiCallers[0].prompts.delete({ promptId: user2Prompt.id }),\n    ).rejects.toThrow(/User is not allowed to access resource/);\n\n    // List should only show the correct user's prompts\n    const user1Prompts = await apiCallers[0].prompts.list();\n    expect(user1Prompts.length).toEqual(1);\n    expect(user1Prompts[0].id).toEqual(user1Prompt.id);\n\n    const user2Prompts = await apiCallers[1].prompts.list();\n    expect(user2Prompts.length).toEqual(1);\n    expect(user2Prompts[0].id).toEqual(user2Prompt.id);\n  });\n});\n"
  },
  {
    "path": "packages/trpc/routers/prompts.ts",
    "content": "import { experimental_trpcMiddleware, TRPCError } from \"@trpc/server\";\nimport { and, eq } from \"drizzle-orm\";\nimport { z } from \"zod\";\n\nimport { customPrompts } from \"@karakeep/db/schema\";\nimport {\n  zNewPromptSchema,\n  zPromptSchema,\n  zUpdatePromptSchema,\n} from \"@karakeep/shared/types/prompts\";\n\nimport { authedProcedure, Context, router } from \"../index\";\n\nexport const ensurePromptOwnership = experimental_trpcMiddleware<{\n  ctx: Context;\n  input: { promptId: string };\n}>().create(async (opts) => {\n  const prompt = await opts.ctx.db.query.customPrompts.findFirst({\n    where: eq(customPrompts.id, opts.input.promptId),\n    columns: {\n      userId: true,\n    },\n  });\n  if (!opts.ctx.user) {\n    throw new TRPCError({\n      code: \"UNAUTHORIZED\",\n      message: \"User is not authorized\",\n    });\n  }\n  if (!prompt) {\n    throw new TRPCError({\n      code: \"NOT_FOUND\",\n      message: \"Prompt not found\",\n    });\n  }\n  if (prompt.userId != opts.ctx.user.id) {\n    throw new TRPCError({\n      code: \"FORBIDDEN\",\n      message: \"User is not allowed to access resource\",\n    });\n  }\n\n  return opts.next();\n});\n\nexport const promptsAppRouter = router({\n  create: authedProcedure\n    .input(zNewPromptSchema)\n    .output(zPromptSchema)\n    .mutation(async ({ input, ctx }) => {\n      const [prompt] = await ctx.db\n        .insert(customPrompts)\n        .values({\n          text: input.text,\n          appliesTo: input.appliesTo,\n          userId: ctx.user.id,\n          enabled: true,\n        })\n        .returning();\n      return prompt;\n    }),\n  update: authedProcedure\n    .input(zUpdatePromptSchema)\n    .output(zPromptSchema)\n    .use(ensurePromptOwnership)\n    .mutation(async ({ input, ctx }) => {\n      const res = await ctx.db\n        .update(customPrompts)\n        .set({\n          text: input.text,\n          appliesTo: input.appliesTo,\n          enabled: input.enabled,\n        })\n        .where(\n          and(\n            eq(customPrompts.userId, ctx.user.id),\n            eq(customPrompts.id, input.promptId),\n          ),\n        )\n        .returning();\n      if (res.length == 0) {\n        throw new TRPCError({ code: \"NOT_FOUND\" });\n      }\n      return res[0];\n    }),\n  list: authedProcedure\n    .output(z.array(zPromptSchema))\n    .query(async ({ ctx }) => {\n      const prompts = await ctx.db.query.customPrompts.findMany({\n        where: eq(customPrompts.userId, ctx.user.id),\n      });\n      return prompts;\n    }),\n  delete: authedProcedure\n    .input(\n      z.object({\n        promptId: z.string(),\n      }),\n    )\n    .use(ensurePromptOwnership)\n    .mutation(async ({ input, ctx }) => {\n      const res = await ctx.db\n        .delete(customPrompts)\n        .where(\n          and(\n            eq(customPrompts.userId, ctx.user.id),\n            eq(customPrompts.id, input.promptId),\n          ),\n        );\n      if (res.changes == 0) {\n        throw new TRPCError({ code: \"NOT_FOUND\" });\n      }\n    }),\n});\n"
  },
  {
    "path": "packages/trpc/routers/publicBookmarks.ts",
    "content": "import { z } from \"zod\";\n\nimport {\n  MAX_NUM_BOOKMARKS_PER_PAGE,\n  zPublicBookmarkSchema,\n  zSortOrder,\n} from \"@karakeep/shared/types/bookmarks\";\nimport { zBookmarkListSchema } from \"@karakeep/shared/types/lists\";\nimport { zCursorV2 } from \"@karakeep/shared/types/pagination\";\n\nimport { publicProcedure, router } from \"../index\";\nimport { List } from \"../models/lists\";\n\nexport const publicBookmarks = router({\n  getPublicListMetadata: publicProcedure\n    .input(\n      z.object({\n        listId: z.string(),\n      }),\n    )\n    .output(\n      zBookmarkListSchema\n        .pick({\n          name: true,\n          description: true,\n          icon: true,\n        })\n        .merge(z.object({ ownerName: z.string() })),\n    )\n    .query(async ({ input, ctx }) => {\n      return await List.getPublicListMetadata(\n        ctx,\n        input.listId,\n        /* token */ null,\n      );\n    }),\n  getPublicBookmarksInList: publicProcedure\n    .input(\n      z.object({\n        listId: z.string(),\n        cursor: zCursorV2.nullish(),\n        limit: z.number().max(MAX_NUM_BOOKMARKS_PER_PAGE).default(20),\n        sortOrder: zSortOrder.exclude([\"relevance\"]).optional().default(\"desc\"),\n      }),\n    )\n    .output(\n      z.object({\n        list: zBookmarkListSchema\n          .pick({\n            name: true,\n            description: true,\n            icon: true,\n          })\n          .merge(z.object({ numItems: z.number(), ownerName: z.string() })),\n        bookmarks: z.array(zPublicBookmarkSchema),\n        nextCursor: zCursorV2.nullable(),\n      }),\n    )\n    .query(async ({ input, ctx }) => {\n      return await List.getPublicListContents(\n        ctx,\n        input.listId,\n        /* token */ null,\n        {\n          limit: input.limit,\n          order: input.sortOrder,\n          cursor: input.cursor,\n        },\n      );\n    }),\n});\n"
  },
  {
    "path": "packages/trpc/routers/rules.test.ts",
    "content": "import { beforeEach, describe, expect, test } from \"vitest\";\n\nimport { RuleEngineRule } from \"@karakeep/shared/types/rules\";\n\nimport type { CustomTestContext } from \"../testUtils\";\nimport { defaultBeforeEach } from \"../testUtils\";\n\ndescribe(\"Rules Routes\", () => {\n  let tagId1: string;\n  let tagId2: string;\n  let otherUserTagId: string;\n\n  let listId: string;\n  let otherUserListId: string;\n\n  beforeEach<CustomTestContext>(async (ctx) => {\n    await defaultBeforeEach(true)(ctx);\n\n    tagId1 = (\n      await ctx.apiCallers[0].tags.create({\n        name: \"Tag 1\",\n      })\n    ).id;\n\n    tagId2 = (\n      await ctx.apiCallers[0].tags.create({\n        name: \"Tag 2\",\n      })\n    ).id;\n\n    otherUserTagId = (\n      await ctx.apiCallers[1].tags.create({\n        name: \"Tag 1\",\n      })\n    ).id;\n\n    listId = (\n      await ctx.apiCallers[0].lists.create({\n        name: \"List 1\",\n        icon: \"😘\",\n      })\n    ).id;\n\n    otherUserListId = (\n      await ctx.apiCallers[1].lists.create({\n        name: \"List 1\",\n        icon: \"😘\",\n      })\n    ).id;\n  });\n\n  test<CustomTestContext>(\"create rule with valid data\", async ({\n    apiCallers,\n  }) => {\n    const api = apiCallers[0].rules;\n\n    const validRuleInput: Omit<RuleEngineRule, \"id\"> = {\n      name: \"Valid Rule\",\n      description: \"A test rule\",\n      enabled: true,\n      event: { type: \"bookmarkAdded\" },\n      condition: { type: \"alwaysTrue\" },\n      actions: [\n        { type: \"addTag\", tagId: tagId1 },\n        { type: \"addToList\", listId: listId },\n      ],\n    };\n\n    const createdRule = await api.create(validRuleInput);\n    expect(createdRule).toMatchObject({\n      name: \"Valid Rule\",\n      description: \"A test rule\",\n      enabled: true,\n      event: validRuleInput.event,\n      condition: validRuleInput.condition,\n      actions: validRuleInput.actions,\n    });\n  });\n\n  test<CustomTestContext>(\"create rule fails with invalid data (no actions)\", async ({\n    apiCallers,\n  }) => {\n    const api = apiCallers[0].rules;\n\n    const invalidRuleInput: Omit<RuleEngineRule, \"id\"> = {\n      name: \"Invalid Rule\",\n      description: \"Missing actions\",\n      enabled: true,\n      event: { type: \"bookmarkAdded\" },\n      condition: { type: \"alwaysTrue\" },\n      actions: [], // Empty actions array - should fail validation\n    };\n\n    await expect(() => api.create(invalidRuleInput)).rejects.toThrow(\n      /You must specify at least one action/,\n    );\n  });\n\n  test<CustomTestContext>(\"create rule fails with invalid event (empty tagId)\", async ({\n    apiCallers,\n  }) => {\n    const api = apiCallers[0].rules;\n\n    const invalidRuleInput: Omit<RuleEngineRule, \"id\"> = {\n      name: \"Invalid Rule\",\n      description: \"Invalid event\",\n      enabled: true,\n      event: { type: \"tagAdded\", tagId: \"\" }, // Empty tagId - should fail\n      condition: { type: \"alwaysTrue\" },\n      actions: [{ type: \"addTag\", tagId: tagId1 }],\n    };\n\n    await expect(() => api.create(invalidRuleInput)).rejects.toThrow(\n      /You must specify a tag for this event type/,\n    );\n  });\n\n  test<CustomTestContext>(\"create rule fails with invalid condition (empty tagId in hasTag)\", async ({\n    apiCallers,\n  }) => {\n    const api = apiCallers[0].rules;\n\n    const invalidRuleInput: Omit<RuleEngineRule, \"id\"> = {\n      name: \"Invalid Rule\",\n      description: \"Invalid condition\",\n      enabled: true,\n      event: { type: \"bookmarkAdded\" },\n      condition: { type: \"hasTag\", tagId: \"\" }, // Empty tagId - should fail\n      actions: [{ type: \"addTag\", tagId: tagId1 }],\n    };\n\n    await expect(() => api.create(invalidRuleInput)).rejects.toThrow(\n      /You must specify a tag for this condition type/,\n    );\n  });\n\n  test<CustomTestContext>(\"update rule with valid data\", async ({\n    apiCallers,\n  }) => {\n    const api = apiCallers[0].rules;\n\n    // First, create a rule\n    const createdRule = await api.create({\n      name: \"Original Rule\",\n      description: \"Original desc\",\n      enabled: true,\n      event: { type: \"bookmarkAdded\" },\n      condition: { type: \"alwaysTrue\" },\n      actions: [{ type: \"addTag\", tagId: tagId1 }],\n    });\n\n    const validUpdateInput: RuleEngineRule = {\n      id: createdRule.id,\n      name: \"Updated Rule\",\n      description: \"Updated desc\",\n      enabled: false,\n      event: { type: \"bookmarkAdded\" },\n      condition: { type: \"alwaysTrue\" },\n      actions: [{ type: \"removeTag\", tagId: tagId2 }],\n    };\n\n    const updatedRule = await api.update(validUpdateInput);\n    expect(updatedRule).toMatchObject({\n      id: createdRule.id,\n      name: \"Updated Rule\",\n      description: \"Updated desc\",\n      enabled: false,\n      event: validUpdateInput.event,\n      condition: validUpdateInput.condition,\n      actions: validUpdateInput.actions,\n    });\n  });\n\n  test<CustomTestContext>(\"update rule fails with invalid data (empty action tagId)\", async ({\n    apiCallers,\n  }) => {\n    const api = apiCallers[0].rules;\n\n    // First, create a rule\n    const createdRule = await api.create({\n      name: \"Original Rule\",\n      description: \"Original desc\",\n      enabled: true,\n      event: { type: \"bookmarkAdded\" },\n      condition: { type: \"alwaysTrue\" },\n      actions: [{ type: \"addTag\", tagId: tagId1 }],\n    });\n\n    const invalidUpdateInput: RuleEngineRule = {\n      id: createdRule.id,\n      name: \"Updated Rule\",\n      description: \"Updated desc\",\n      enabled: true,\n      event: { type: \"bookmarkAdded\" },\n      condition: { type: \"alwaysTrue\" },\n      actions: [{ type: \"removeTag\", tagId: \"\" }], // Empty tagId - should fail\n    };\n\n    await expect(() => api.update(invalidUpdateInput)).rejects.toThrow(\n      /You must specify a tag for this action type/,\n    );\n  });\n\n  test<CustomTestContext>(\"delete rule\", async ({ apiCallers }) => {\n    const api = apiCallers[0].rules;\n\n    const createdRule = await api.create({\n      name: \"Rule to Delete\",\n      description: \"\",\n      enabled: true,\n      event: { type: \"bookmarkAdded\" },\n      condition: { type: \"alwaysTrue\" },\n      actions: [{ type: \"addTag\", tagId: tagId1 }],\n    });\n\n    await api.delete({ id: createdRule.id });\n\n    // Attempt to fetch the rule should fail\n    await expect(() =>\n      api.update({ ...createdRule, name: \"Updated\" }),\n    ).rejects.toThrow(/Rule not found/);\n  });\n\n  test<CustomTestContext>(\"list rules\", async ({ apiCallers }) => {\n    const api = apiCallers[0].rules;\n\n    await api.create({\n      name: \"Rule 1\",\n      description: \"\",\n      enabled: true,\n      event: { type: \"bookmarkAdded\" },\n      condition: { type: \"alwaysTrue\" },\n      actions: [{ type: \"addTag\", tagId: tagId1 }],\n    });\n\n    await api.create({\n      name: \"Rule 2\",\n      description: \"\",\n      enabled: true,\n      event: { type: \"bookmarkAdded\" },\n      condition: { type: \"alwaysTrue\" },\n      actions: [{ type: \"addTag\", tagId: tagId2 }],\n    });\n\n    const rulesList = await api.list();\n    expect(rulesList.rules.length).toBeGreaterThanOrEqual(2);\n    expect(rulesList.rules.some((rule) => rule.name === \"Rule 1\")).toBeTruthy();\n    expect(rulesList.rules.some((rule) => rule.name === \"Rule 2\")).toBeTruthy();\n  });\n\n  describe(\"privacy checks\", () => {\n    test<CustomTestContext>(\"cannot access or manipulate another user's rule\", async ({\n      apiCallers,\n    }) => {\n      const apiUserA = apiCallers[0].rules; // First user\n      const apiUserB = apiCallers[1].rules; // Second user\n\n      // User A creates a rule\n      const createdRule = await apiUserA.create({\n        name: \"User A's Rule\",\n        description: \"A rule for User A\",\n        enabled: true,\n        event: { type: \"bookmarkAdded\" },\n        condition: { type: \"alwaysTrue\" },\n        actions: [{ type: \"addTag\", tagId: tagId1 }],\n      });\n\n      // User B tries to update User A's rule\n      const updateInput: RuleEngineRule = {\n        id: createdRule.id,\n        name: \"Trying to Update\",\n        description: \"Unauthorized update\",\n        enabled: true,\n        event: createdRule.event,\n        condition: createdRule.condition,\n        actions: createdRule.actions,\n      };\n\n      await expect(() => apiUserB.update(updateInput)).rejects.toThrow(\n        /Rule not found/,\n      );\n    });\n\n    test<CustomTestContext>(\"cannot create rule with event on another user's tag\", async ({\n      apiCallers,\n    }) => {\n      const api = apiCallers[0].rules; // First user trying to use second user's tag\n\n      const invalidRuleInput: Omit<RuleEngineRule, \"id\"> = {\n        name: \"Invalid Rule\",\n        description: \"Event with other user's tag\",\n        enabled: true,\n        event: { type: \"tagAdded\", tagId: otherUserTagId }, // Other user's tag\n        condition: { type: \"alwaysTrue\" },\n        actions: [{ type: \"addTag\", tagId: tagId1 }],\n      };\n\n      await expect(() => api.create(invalidRuleInput)).rejects.toThrow(\n        /Tag not found/, // Expect an error indicating lack of ownership\n      );\n    });\n\n    test<CustomTestContext>(\"cannot create rule with condition on another user's tag\", async ({\n      apiCallers,\n    }) => {\n      const api = apiCallers[0].rules; // First user trying to use second user's tag\n\n      const invalidRuleInput: Omit<RuleEngineRule, \"id\"> = {\n        name: \"Invalid Rule\",\n        description: \"Condition with other user's tag\",\n        enabled: true,\n        event: { type: \"bookmarkAdded\" },\n        condition: { type: \"hasTag\", tagId: otherUserTagId }, // Other user's tag\n        actions: [{ type: \"addTag\", tagId: tagId1 }],\n      };\n\n      await expect(() => api.create(invalidRuleInput)).rejects.toThrow(\n        /Tag not found/,\n      );\n    });\n\n    test<CustomTestContext>(\"cannot create rule with action on another user's tag\", async ({\n      apiCallers,\n    }) => {\n      const api = apiCallers[0].rules; // First user trying to use second user's tag\n\n      const invalidRuleInput: Omit<RuleEngineRule, \"id\"> = {\n        name: \"Invalid Rule\",\n        description: \"Action with other user's tag\",\n        enabled: true,\n        event: { type: \"bookmarkAdded\" },\n        condition: { type: \"alwaysTrue\" },\n        actions: [{ type: \"addTag\", tagId: otherUserTagId }], // Other user's tag\n      };\n\n      await expect(() => api.create(invalidRuleInput)).rejects.toThrow(\n        /Tag not found/,\n      );\n    });\n\n    test<CustomTestContext>(\"cannot create rule with event on another user's list\", async ({\n      apiCallers,\n    }) => {\n      const api = apiCallers[0].rules; // First user trying to use second user's list\n\n      const invalidRuleInput: Omit<RuleEngineRule, \"id\"> = {\n        name: \"Invalid Rule\",\n        description: \"Event with other user's list\",\n        enabled: true,\n        event: { type: \"addedToList\", listId: otherUserListId }, // Other user's list\n        condition: { type: \"alwaysTrue\" },\n        actions: [{ type: \"addTag\", tagId: tagId1 }],\n      };\n\n      await expect(() => api.create(invalidRuleInput)).rejects.toThrow(\n        /List not found/,\n      );\n    });\n\n    test<CustomTestContext>(\"cannot create rule with action on another user's list\", async ({\n      apiCallers,\n    }) => {\n      const api = apiCallers[0].rules; // First user trying to use second user's list\n\n      const invalidRuleInput: Omit<RuleEngineRule, \"id\"> = {\n        name: \"Invalid Rule\",\n        description: \"Action with other user's list\",\n        enabled: true,\n        event: { type: \"bookmarkAdded\" },\n        condition: { type: \"alwaysTrue\" },\n        actions: [{ type: \"addToList\", listId: otherUserListId }], // Other user's list\n      };\n\n      await expect(() => api.create(invalidRuleInput)).rejects.toThrow(\n        /List not found/,\n      );\n    });\n  });\n});\n"
  },
  {
    "path": "packages/trpc/routers/rules.ts",
    "content": "import { experimental_trpcMiddleware, TRPCError } from \"@trpc/server\";\nimport { and, eq, inArray } from \"drizzle-orm\";\nimport { z } from \"zod\";\n\nimport { bookmarkTags } from \"@karakeep/db/schema\";\nimport {\n  RuleEngineRule,\n  zNewRuleEngineRuleSchema,\n  zRuleEngineRuleSchema,\n  zUpdateRuleEngineRuleSchema,\n} from \"@karakeep/shared/types/rules\";\n\nimport { AuthedContext, authedProcedure, router } from \"../index\";\nimport { List } from \"../models/lists\";\nimport { RuleEngineRuleModel } from \"../models/rules\";\n\nconst ensureRuleOwnership = experimental_trpcMiddleware<{\n  ctx: AuthedContext;\n  input: { id: string };\n}>().create(async (opts) => {\n  const rule = await RuleEngineRuleModel.fromId(opts.ctx, opts.input.id);\n  return opts.next({\n    ctx: {\n      ...opts.ctx,\n      rule,\n    },\n  });\n});\n\nconst ensureTagListOwnership = experimental_trpcMiddleware<{\n  ctx: AuthedContext;\n  input: Omit<RuleEngineRule, \"id\">;\n}>().create(async (opts) => {\n  const tagIds = [\n    ...(opts.input.event.type === \"tagAdded\" ||\n    opts.input.event.type === \"tagRemoved\"\n      ? [opts.input.event.tagId]\n      : []),\n    ...(opts.input.condition.type === \"hasTag\"\n      ? [opts.input.condition.tagId]\n      : []),\n    ...opts.input.actions.flatMap((a) =>\n      a.type == \"addTag\" || a.type == \"removeTag\" ? [a.tagId] : [],\n    ),\n  ];\n\n  const validateTags = async () => {\n    if (tagIds.length == 0) {\n      return;\n    }\n    const userTags = await opts.ctx.db.query.bookmarkTags.findMany({\n      where: and(\n        eq(bookmarkTags.userId, opts.ctx.user.id),\n        inArray(bookmarkTags.id, tagIds),\n      ),\n      columns: {\n        id: true,\n      },\n    });\n    if (tagIds.some((t) => userTags.find((u) => u.id == t) == null)) {\n      throw new TRPCError({\n        code: \"NOT_FOUND\",\n        message: \"Tag not found\",\n      });\n    }\n  };\n\n  const listIds = [\n    ...(opts.input.event.type === \"addedToList\" ||\n    opts.input.event.type === \"removedFromList\"\n      ? [opts.input.event.listId]\n      : []),\n    ...opts.input.actions.flatMap((a) =>\n      a.type == \"addToList\" || a.type == \"removeFromList\" ? [a.listId] : [],\n    ),\n  ];\n\n  const [_tags, _lists] = await Promise.all([\n    validateTags(),\n    Promise.all(listIds.map((l) => List.fromId(opts.ctx, l))),\n  ]);\n  return opts.next();\n});\n\nexport const rulesAppRouter = router({\n  create: authedProcedure\n    .input(zNewRuleEngineRuleSchema)\n    .output(zRuleEngineRuleSchema)\n    .use(ensureTagListOwnership)\n    .mutation(async ({ input, ctx }) => {\n      const newRule = await RuleEngineRuleModel.create(ctx, input);\n      return newRule.rule;\n    }),\n  update: authedProcedure\n    .input(zUpdateRuleEngineRuleSchema)\n    .output(zRuleEngineRuleSchema)\n    .use(ensureRuleOwnership)\n    .use(ensureTagListOwnership)\n    .mutation(async ({ ctx, input }) => {\n      await ctx.rule.update(input);\n      return ctx.rule.rule;\n    }),\n  delete: authedProcedure\n    .input(z.object({ id: z.string() }))\n    .use(ensureRuleOwnership)\n    .mutation(async ({ ctx }) => {\n      await ctx.rule.delete();\n    }),\n  list: authedProcedure\n    .output(\n      z.object({\n        rules: z.array(zRuleEngineRuleSchema),\n      }),\n    )\n    .query(async ({ ctx }) => {\n      return {\n        rules: (await RuleEngineRuleModel.getAll(ctx)).map((r) => r.rule),\n      };\n    }),\n});\n"
  },
  {
    "path": "packages/trpc/routers/sharedLists.test.ts",
    "content": "import { beforeEach, describe, expect, test } from \"vitest\";\n\nimport { BookmarkTypes } from \"@karakeep/shared/types/bookmarks\";\n\nimport type { APICallerType, CustomTestContext } from \"../testUtils\";\nimport { defaultBeforeEach } from \"../testUtils\";\n\nbeforeEach<CustomTestContext>(defaultBeforeEach(true));\n\n/**\n * Helper function to add a collaborator and have them accept the invitation\n */\nasync function addAndAcceptCollaborator(\n  ownerApi: APICallerType,\n  collaboratorApi: APICallerType,\n  listId: string,\n  role: \"viewer\" | \"editor\",\n) {\n  const collaboratorUser = await collaboratorApi.users.whoami();\n\n  // Owner invites the collaborator\n  const { invitationId } = await ownerApi.lists.addCollaborator({\n    listId,\n    email: collaboratorUser.email!,\n    role,\n  });\n\n  // Collaborator accepts the invitation\n  await collaboratorApi.lists.acceptInvitation({\n    invitationId,\n  });\n}\n\ndescribe(\"Shared Lists\", () => {\n  describe(\"List Collaboration Management\", () => {\n    test<CustomTestContext>(\"should allow owner to add a collaborator by email\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      // Create a list as owner\n      const list = await ownerApi.lists.create({\n        name: \"Test Shared List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      // Get collaborator email\n      const collaboratorUser = await collaboratorApi.users.whoami();\n      const collaboratorEmail = collaboratorUser.email!;\n\n      // Add collaborator (creates pending invitation)\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"viewer\",\n      );\n\n      // Verify collaborator was added\n      const { collaborators, owner } = await ownerApi.lists.getCollaborators({\n        listId: list.id,\n      });\n\n      expect(collaborators).toHaveLength(1);\n      expect(collaborators[0].user.email).toBe(collaboratorEmail);\n      expect(collaborators[0].role).toBe(\"viewer\");\n\n      // Verify owner is included\n      const ownerUser = await ownerApi.users.whoami();\n      expect(owner).toBeDefined();\n      expect(owner?.email).toBe(ownerUser.email);\n    });\n\n    test<CustomTestContext>(\"should not allow adding owner as collaborator\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n\n      const list = await ownerApi.lists.create({\n        name: \"Test List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      const ownerUser = await ownerApi.users.whoami();\n\n      await expect(\n        ownerApi.lists.addCollaborator({\n          listId: list.id,\n          email: ownerUser.email!,\n          role: \"viewer\",\n        }),\n      ).rejects.toThrow(\"Cannot add the list owner as a collaborator\");\n    });\n\n    test<CustomTestContext>(\"should not allow adding duplicate collaborator\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Test List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      const collaboratorEmail = (await collaboratorApi.users.whoami()).email!;\n\n      const { invitationId } = await ownerApi.lists.addCollaborator({\n        listId: list.id,\n        email: collaboratorEmail,\n        role: \"viewer\",\n      });\n\n      // Try to add same collaborator again (should fail - pending invitation exists)\n      await expect(\n        ownerApi.lists.addCollaborator({\n          listId: list.id,\n          email: collaboratorEmail,\n          role: \"editor\",\n        }),\n      ).rejects.toThrow(\"User already has a pending invitation for this list\");\n\n      // Accept the invitation\n      await collaboratorApi.lists.acceptInvitation({\n        invitationId,\n      });\n\n      // Try to add them again after they're a collaborator\n      await expect(\n        ownerApi.lists.addCollaborator({\n          listId: list.id,\n          email: collaboratorEmail,\n          role: \"editor\",\n        }),\n      ).rejects.toThrow(\"User is already a collaborator on this list\");\n    });\n\n    test<CustomTestContext>(\"should allow owner to update collaborator role\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Test List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      const collaboratorUser = await collaboratorApi.users.whoami();\n\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"viewer\",\n      );\n\n      // Update role to editor\n      await ownerApi.lists.updateCollaboratorRole({\n        listId: list.id,\n        userId: collaboratorUser.id,\n        role: \"editor\",\n      });\n\n      // Verify role was updated\n      const { collaborators, owner } = await ownerApi.lists.getCollaborators({\n        listId: list.id,\n      });\n\n      expect(collaborators[0].role).toBe(\"editor\");\n      expect(owner).toBeDefined();\n    });\n\n    test<CustomTestContext>(\"should allow owner to remove collaborator\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Test List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      const collaboratorUser = await collaboratorApi.users.whoami();\n\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"viewer\",\n      );\n\n      // Remove collaborator\n      await ownerApi.lists.removeCollaborator({\n        listId: list.id,\n        userId: collaboratorUser.id,\n      });\n\n      // Verify collaborator was removed\n      const { collaborators, owner } = await ownerApi.lists.getCollaborators({\n        listId: list.id,\n      });\n\n      expect(collaborators).toHaveLength(0);\n      expect(owner).toBeDefined();\n    });\n\n    test<CustomTestContext>(\"should include owner information in getCollaborators response\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n\n      const list = await ownerApi.lists.create({\n        name: \"Test List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      const ownerUser = await ownerApi.users.whoami();\n\n      const { collaborators, owner } = await ownerApi.lists.getCollaborators({\n        listId: list.id,\n      });\n\n      // Verify owner information is present\n      expect(owner).toBeDefined();\n      expect(owner?.id).toBe(ownerUser.id);\n      expect(owner?.name).toBe(ownerUser.name);\n      expect(owner?.email).toBe(ownerUser.email);\n\n      // List with no collaborators should still have owner\n      expect(collaborators).toHaveLength(0);\n    });\n\n    test<CustomTestContext>(\"should remove collaborator's bookmarks when they are removed\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Test List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      // Owner adds a bookmark\n      const ownerBookmark = await ownerApi.bookmarks.createBookmark({\n        type: BookmarkTypes.TEXT,\n        text: \"Owner's bookmark\",\n      });\n\n      await ownerApi.lists.addToList({\n        listId: list.id,\n        bookmarkId: ownerBookmark.id,\n      });\n\n      const collaboratorUser = await collaboratorApi.users.whoami();\n\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"editor\",\n      );\n\n      // Collaborator adds their own bookmark\n      const collabBookmark = await collaboratorApi.bookmarks.createBookmark({\n        type: BookmarkTypes.TEXT,\n        text: \"Collaborator's bookmark\",\n      });\n\n      await collaboratorApi.lists.addToList({\n        listId: list.id,\n        bookmarkId: collabBookmark.id,\n      });\n\n      // Verify both bookmarks are in the list\n      const bookmarksBefore = await ownerApi.bookmarks.getBookmarks({\n        listId: list.id,\n      });\n      expect(bookmarksBefore.bookmarks).toHaveLength(2);\n\n      // Remove collaborator\n      await ownerApi.lists.removeCollaborator({\n        listId: list.id,\n        userId: collaboratorUser.id,\n      });\n\n      // Verify only owner's bookmark remains in the list\n      const bookmarksAfter = await ownerApi.bookmarks.getBookmarks({\n        listId: list.id,\n      });\n      expect(bookmarksAfter.bookmarks).toHaveLength(1);\n      expect(bookmarksAfter.bookmarks[0].id).toBe(ownerBookmark.id);\n\n      // Verify collaborator's bookmark still exists (just not in the list)\n      const collabBookmarkStillExists =\n        await collaboratorApi.bookmarks.getBookmark({\n          bookmarkId: collabBookmark.id,\n        });\n      expect(collabBookmarkStillExists.id).toBe(collabBookmark.id);\n    });\n\n    test<CustomTestContext>(\"should allow collaborator to leave list\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Test List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"viewer\",\n      );\n\n      // Collaborator leaves the list\n      await collaboratorApi.lists.leaveList({\n        listId: list.id,\n      });\n\n      // Verify collaborator was removed\n      const { collaborators, owner } = await ownerApi.lists.getCollaborators({\n        listId: list.id,\n      });\n\n      expect(collaborators).toHaveLength(0);\n      expect(owner).toBeDefined();\n\n      // Verify list no longer appears in shared lists\n      const { lists: allLists } = await collaboratorApi.lists.list();\n      const sharedLists = allLists.filter(\n        (l) => l.userRole === \"viewer\" || l.userRole === \"editor\",\n      );\n      expect(sharedLists.find((l) => l.id === list.id)).toBeUndefined();\n    });\n\n    test<CustomTestContext>(\"should remove collaborator's bookmarks when they leave list\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Test List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      // Owner adds a bookmark\n      const ownerBookmark = await ownerApi.bookmarks.createBookmark({\n        type: BookmarkTypes.TEXT,\n        text: \"Owner's bookmark\",\n      });\n\n      await ownerApi.lists.addToList({\n        listId: list.id,\n        bookmarkId: ownerBookmark.id,\n      });\n\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"editor\",\n      );\n\n      // Collaborator adds their own bookmark\n      const collabBookmark = await collaboratorApi.bookmarks.createBookmark({\n        type: BookmarkTypes.TEXT,\n        text: \"Collaborator's bookmark\",\n      });\n\n      await collaboratorApi.lists.addToList({\n        listId: list.id,\n        bookmarkId: collabBookmark.id,\n      });\n\n      // Verify both bookmarks are in the list\n      const bookmarksBefore = await ownerApi.bookmarks.getBookmarks({\n        listId: list.id,\n      });\n      expect(bookmarksBefore.bookmarks).toHaveLength(2);\n\n      // Collaborator leaves the list\n      await collaboratorApi.lists.leaveList({\n        listId: list.id,\n      });\n\n      // Verify only owner's bookmark remains in the list\n      const bookmarksAfter = await ownerApi.bookmarks.getBookmarks({\n        listId: list.id,\n      });\n      expect(bookmarksAfter.bookmarks).toHaveLength(1);\n      expect(bookmarksAfter.bookmarks[0].id).toBe(ownerBookmark.id);\n\n      // Verify collaborator's bookmark still exists (just not in the list)\n      const collabBookmarkStillExists =\n        await collaboratorApi.bookmarks.getBookmark({\n          bookmarkId: collabBookmark.id,\n        });\n      expect(collabBookmarkStillExists.id).toBe(collabBookmark.id);\n    });\n\n    test<CustomTestContext>(\"should not allow owner to leave their own list\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n\n      const list = await ownerApi.lists.create({\n        name: \"Test List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      await expect(\n        ownerApi.lists.leaveList({\n          listId: list.id,\n        }),\n      ).rejects.toThrow(\n        \"List owners cannot leave their own list. Delete the list instead.\",\n      );\n    });\n\n    test<CustomTestContext>(\"should not allow non-collaborator to manage collaborators\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const thirdUserApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Test List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      const thirdUser = await thirdUserApi.users.whoami();\n\n      // Third user tries to add themselves as collaborator\n      await expect(\n        thirdUserApi.lists.addCollaborator({\n          listId: list.id,\n          email: thirdUser.email!,\n          role: \"viewer\",\n        }),\n      ).rejects.toThrow(\"List not found\");\n    });\n  });\n\n  describe(\"List Access and Visibility\", () => {\n    test<CustomTestContext>(\"should show shared list in list endpoint\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Shared List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"viewer\",\n      );\n\n      const { lists: allLists } = await collaboratorApi.lists.list();\n      const sharedLists = allLists.filter(\n        (l) => l.userRole === \"viewer\" || l.userRole === \"editor\",\n      );\n\n      expect(sharedLists).toHaveLength(1);\n      expect(sharedLists[0].id).toBe(list.id);\n      expect(sharedLists[0].name).toBe(\"Shared List\");\n    });\n\n    test<CustomTestContext>(\"should allow collaborator to get list details\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Shared List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"viewer\",\n      );\n\n      const retrievedList = await collaboratorApi.lists.get({\n        listId: list.id,\n      });\n\n      expect(retrievedList.id).toBe(list.id);\n      expect(retrievedList.name).toBe(\"Shared List\");\n      expect(retrievedList.userRole).toBe(\"viewer\");\n    });\n\n    test<CustomTestContext>(\"should not allow non-collaborator to access list\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n      const thirdUserApi = apiCallers[2];\n\n      const list = await ownerApi.lists.create({\n        name: \"Private List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"viewer\",\n      );\n\n      await expect(\n        thirdUserApi.lists.get({\n          listId: list.id,\n        }),\n      ).rejects.toThrow(\"List not found\");\n    });\n\n    test<CustomTestContext>(\"should show correct userRole for owner\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n\n      const list = await ownerApi.lists.create({\n        name: \"My List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      const retrievedList = await ownerApi.lists.get({\n        listId: list.id,\n      });\n\n      expect(retrievedList.userRole).toBe(\"owner\");\n    });\n\n    test<CustomTestContext>(\"should show correct userRole for editor\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Shared List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"editor\",\n      );\n\n      const retrievedList = await collaboratorApi.lists.get({\n        listId: list.id,\n      });\n\n      expect(retrievedList.userRole).toBe(\"editor\");\n    });\n  });\n\n  describe(\"Bookmark Access in Shared Lists\", () => {\n    test<CustomTestContext>(\"should allow collaborator to view bookmarks in shared list\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      // Owner creates list and bookmark\n      const list = await ownerApi.lists.create({\n        name: \"Shared List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      const bookmark = await ownerApi.bookmarks.createBookmark({\n        type: BookmarkTypes.TEXT,\n        text: \"Shared bookmark\",\n      });\n\n      await ownerApi.lists.addToList({\n        listId: list.id,\n        bookmarkId: bookmark.id,\n      });\n\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"viewer\",\n      );\n\n      // Collaborator fetches bookmarks from shared list\n      const bookmarks = await collaboratorApi.bookmarks.getBookmarks({\n        listId: list.id,\n      });\n\n      expect(bookmarks.bookmarks).toHaveLength(1);\n      expect(bookmarks.bookmarks[0].id).toBe(bookmark.id);\n    });\n\n    test<CustomTestContext>(\"should hide owner-specific bookmark state from collaborators\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Shared List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      const bookmark = await ownerApi.bookmarks.createBookmark({\n        type: BookmarkTypes.TEXT,\n        text: \"Shared bookmark\",\n      });\n\n      await ownerApi.bookmarks.updateBookmark({\n        bookmarkId: bookmark.id,\n        archived: true,\n        favourited: true,\n        note: \"Private note\",\n      });\n\n      await ownerApi.lists.addToList({\n        listId: list.id,\n        bookmarkId: bookmark.id,\n      });\n\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"viewer\",\n      );\n\n      const ownerView = await ownerApi.bookmarks.getBookmarks({\n        listId: list.id,\n      });\n\n      const collaboratorView = await collaboratorApi.bookmarks.getBookmarks({\n        listId: list.id,\n      });\n\n      const ownerBookmark = ownerView.bookmarks.find(\n        (b) => b.id === bookmark.id,\n      );\n      expect(ownerBookmark?.favourited).toBe(true);\n      expect(ownerBookmark?.archived).toBe(true);\n      expect(ownerBookmark?.note).toBe(\"Private note\");\n\n      const collaboratorBookmark = collaboratorView.bookmarks.find(\n        (b) => b.id === bookmark.id,\n      );\n      expect(collaboratorBookmark?.favourited).toBe(false);\n      expect(collaboratorBookmark?.archived).toBe(false);\n      expect(collaboratorBookmark?.note).toBeNull();\n    });\n\n    // Note: Asset handling for shared bookmarks is tested via the REST API in e2e tests\n    // This is because tRPC tests don't have easy access to file upload functionality\n\n    test<CustomTestContext>(\"should allow collaborator to view individual shared bookmark\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Shared List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      const bookmark = await ownerApi.bookmarks.createBookmark({\n        type: BookmarkTypes.TEXT,\n        text: \"Shared bookmark\",\n      });\n\n      await ownerApi.lists.addToList({\n        listId: list.id,\n        bookmarkId: bookmark.id,\n      });\n\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"viewer\",\n      );\n\n      // Collaborator gets individual bookmark\n      const response = await collaboratorApi.bookmarks.getBookmark({\n        bookmarkId: bookmark.id,\n      });\n\n      expect(response.id).toBe(bookmark.id);\n    });\n\n    test<CustomTestContext>(\"should not show shared bookmarks on collaborator's homepage\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Shared List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      const sharedBookmark = await ownerApi.bookmarks.createBookmark({\n        type: BookmarkTypes.TEXT,\n        text: \"Shared bookmark\",\n      });\n\n      await ownerApi.lists.addToList({\n        listId: list.id,\n        bookmarkId: sharedBookmark.id,\n      });\n\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"viewer\",\n      );\n\n      // Collaborator creates their own bookmark\n      const ownBookmark = await collaboratorApi.bookmarks.createBookmark({\n        type: BookmarkTypes.TEXT,\n        text: \"My own bookmark\",\n      });\n\n      // Fetch all bookmarks (no listId filter)\n      const allBookmarks = await collaboratorApi.bookmarks.getBookmarks({});\n\n      // Should only see own bookmark, not shared one\n      expect(allBookmarks.bookmarks).toHaveLength(1);\n      expect(allBookmarks.bookmarks[0].id).toBe(ownBookmark.id);\n    });\n\n    test<CustomTestContext>(\"should not allow non-collaborator to access shared bookmark\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n      const thirdUserApi = apiCallers[2]; // User 3 will be the non-collaborator\n\n      const list = await ownerApi.lists.create({\n        name: \"Shared List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      const bookmark = await ownerApi.bookmarks.createBookmark({\n        type: BookmarkTypes.TEXT,\n        text: \"Shared bookmark\",\n      });\n\n      await ownerApi.lists.addToList({\n        listId: list.id,\n        bookmarkId: bookmark.id,\n      });\n\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"viewer\",\n      );\n\n      // Don't add thirdUserApi as a collaborator\n      // Third user tries to access the bookmark\n      await expect(\n        thirdUserApi.bookmarks.getBookmark({\n          bookmarkId: bookmark.id,\n        }),\n      ).rejects.toThrow(\"Bookmark not found\");\n    });\n\n    test<CustomTestContext>(\"should show all bookmarks in shared list regardless of owner\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Shared List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      // Owner adds a bookmark\n      const ownerBookmark = await ownerApi.bookmarks.createBookmark({\n        type: BookmarkTypes.TEXT,\n        text: \"Owner's bookmark\",\n      });\n\n      await ownerApi.lists.addToList({\n        listId: list.id,\n        bookmarkId: ownerBookmark.id,\n      });\n\n      // Share list with collaborator as editor\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"editor\",\n      );\n\n      // Collaborator adds their own bookmark\n      const collabBookmark = await collaboratorApi.bookmarks.createBookmark({\n        type: BookmarkTypes.TEXT,\n        text: \"Collaborator's bookmark\",\n      });\n\n      await collaboratorApi.lists.addToList({\n        listId: list.id,\n        bookmarkId: collabBookmark.id,\n      });\n\n      // Both users should see both bookmarks in the list\n      const ownerView = await ownerApi.bookmarks.getBookmarks({\n        listId: list.id,\n      });\n\n      const collabView = await collaboratorApi.bookmarks.getBookmarks({\n        listId: list.id,\n      });\n\n      expect(ownerView.bookmarks).toHaveLength(2);\n      expect(collabView.bookmarks).toHaveLength(2);\n    });\n  });\n\n  describe(\"Bookmark Editing Permissions\", () => {\n    test<CustomTestContext>(\"should not allow viewer to add bookmarks to list\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Shared List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"viewer\",\n      );\n\n      // Viewer creates their own bookmark\n      const bookmark = await collaboratorApi.bookmarks.createBookmark({\n        type: BookmarkTypes.TEXT,\n        text: \"My bookmark\",\n      });\n\n      // Viewer tries to add it to shared list\n      await expect(\n        collaboratorApi.lists.addToList({\n          listId: list.id,\n          bookmarkId: bookmark.id,\n        }),\n      ).rejects.toThrow(\"User is not allowed to edit this list\");\n    });\n\n    test<CustomTestContext>(\"should allow editor to add bookmarks to list\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Shared List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"editor\",\n      );\n\n      // Editor creates their own bookmark\n      const bookmark = await collaboratorApi.bookmarks.createBookmark({\n        type: BookmarkTypes.TEXT,\n        text: \"My bookmark\",\n      });\n\n      // Editor adds it to shared list\n      await collaboratorApi.lists.addToList({\n        listId: list.id,\n        bookmarkId: bookmark.id,\n      });\n\n      // Verify bookmark was added\n      const bookmarks = await ownerApi.bookmarks.getBookmarks({\n        listId: list.id,\n      });\n\n      expect(bookmarks.bookmarks).toHaveLength(1);\n      expect(bookmarks.bookmarks[0].id).toBe(bookmark.id);\n    });\n\n    test<CustomTestContext>(\"should not allow viewer to remove bookmarks from list\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Shared List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      const bookmark = await ownerApi.bookmarks.createBookmark({\n        type: BookmarkTypes.TEXT,\n        text: \"Test bookmark\",\n      });\n\n      await ownerApi.lists.addToList({\n        listId: list.id,\n        bookmarkId: bookmark.id,\n      });\n\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"viewer\",\n      );\n\n      // Viewer tries to remove bookmark\n      await expect(\n        collaboratorApi.lists.removeFromList({\n          listId: list.id,\n          bookmarkId: bookmark.id,\n        }),\n      ).rejects.toThrow(\"User is not allowed to edit this list\");\n    });\n\n    test<CustomTestContext>(\"should allow editor to remove bookmarks from list\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Shared List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      const bookmark = await ownerApi.bookmarks.createBookmark({\n        type: BookmarkTypes.TEXT,\n        text: \"Test bookmark\",\n      });\n\n      await ownerApi.lists.addToList({\n        listId: list.id,\n        bookmarkId: bookmark.id,\n      });\n\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"editor\",\n      );\n\n      // Editor removes bookmark\n      await collaboratorApi.lists.removeFromList({\n        listId: list.id,\n        bookmarkId: bookmark.id,\n      });\n\n      // Verify bookmark was removed\n      const bookmarks = await ownerApi.bookmarks.getBookmarks({\n        listId: list.id,\n      });\n\n      expect(bookmarks.bookmarks).toHaveLength(0);\n    });\n\n    test<CustomTestContext>(\"should not allow collaborator to edit bookmark they don't own\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Shared List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      const bookmark = await ownerApi.bookmarks.createBookmark({\n        type: BookmarkTypes.TEXT,\n        text: \"Owner's bookmark\",\n      });\n\n      await ownerApi.lists.addToList({\n        listId: list.id,\n        bookmarkId: bookmark.id,\n      });\n\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"editor\",\n      );\n\n      // Collaborator tries to edit owner's bookmark\n      await expect(\n        collaboratorApi.bookmarks.updateBookmark({\n          bookmarkId: bookmark.id,\n          title: \"Modified title\",\n        }),\n      ).rejects.toThrow(\"User is not allowed to access resource\");\n    });\n\n    test<CustomTestContext>(\"should not allow collaborator to delete bookmark they don't own\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Shared List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      const bookmark = await ownerApi.bookmarks.createBookmark({\n        type: BookmarkTypes.TEXT,\n        text: \"Owner's bookmark\",\n      });\n\n      await ownerApi.lists.addToList({\n        listId: list.id,\n        bookmarkId: bookmark.id,\n      });\n\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"editor\",\n      );\n\n      // Collaborator tries to delete owner's bookmark\n      await expect(\n        collaboratorApi.bookmarks.deleteBookmark({\n          bookmarkId: bookmark.id,\n        }),\n      ).rejects.toThrow(\"User is not allowed to access resource\");\n    });\n  });\n\n  describe(\"List Management Permissions\", () => {\n    test<CustomTestContext>(\"should not allow collaborator to edit list metadata\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Shared List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"editor\",\n      );\n\n      // Collaborator tries to edit list\n      await expect(\n        collaboratorApi.lists.edit({\n          listId: list.id,\n          name: \"Modified Name\",\n        }),\n      ).rejects.toThrow(\"User is not allowed to manage this list\");\n    });\n\n    test<CustomTestContext>(\"should not allow collaborator to delete list\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Shared List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"editor\",\n      );\n\n      // Collaborator tries to delete list\n      await expect(\n        collaboratorApi.lists.delete({\n          listId: list.id,\n        }),\n      ).rejects.toThrow(\"User is not allowed to manage this list\");\n    });\n\n    test<CustomTestContext>(\"should not allow collaborator to manage other collaborators\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n      const thirdUserApi = apiCallers[2];\n\n      const list = await ownerApi.lists.create({\n        name: \"Shared List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"editor\",\n      );\n\n      const thirdUserEmail = (await thirdUserApi.users.whoami()).email!;\n\n      // Collaborator tries to add another user\n      await expect(\n        collaboratorApi.lists.addCollaborator({\n          listId: list.id,\n          email: thirdUserEmail,\n          role: \"viewer\",\n        }),\n      ).rejects.toThrow(\"User is not allowed to manage this list\");\n    });\n\n    test<CustomTestContext>(\"should only allow collaborators to view collaborator list\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n      const thirdUserApi = apiCallers[2];\n\n      const list = await ownerApi.lists.create({\n        name: \"Shared List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"viewer\",\n      );\n\n      // Collaborator can view collaborators\n      const { collaborators, owner } =\n        await collaboratorApi.lists.getCollaborators({\n          listId: list.id,\n        });\n\n      expect(collaborators).toHaveLength(1);\n      expect(owner).toBeDefined();\n\n      // Non-collaborator cannot view\n      await expect(\n        thirdUserApi.lists.getCollaborators({\n          listId: list.id,\n        }),\n      ).rejects.toThrow(\"List not found\");\n    });\n  });\n\n  describe(\"Access After Removal\", () => {\n    test<CustomTestContext>(\"should revoke access after removing collaborator\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Shared List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      const bookmark = await ownerApi.bookmarks.createBookmark({\n        type: BookmarkTypes.TEXT,\n        text: \"Shared bookmark\",\n      });\n\n      await ownerApi.lists.addToList({\n        listId: list.id,\n        bookmarkId: bookmark.id,\n      });\n\n      const collaboratorUser = await collaboratorApi.users.whoami();\n\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"viewer\",\n      );\n\n      // Verify collaborator has access to list\n      const bookmarksBefore = await collaboratorApi.bookmarks.getBookmarks({\n        listId: list.id,\n      });\n      expect(bookmarksBefore.bookmarks).toHaveLength(1);\n\n      // Verify collaborator has access to individual bookmark\n      const bookmarkBefore = await collaboratorApi.bookmarks.getBookmark({\n        bookmarkId: bookmark.id,\n      });\n      expect(bookmarkBefore.id).toBe(bookmark.id);\n\n      // Remove collaborator\n      await ownerApi.lists.removeCollaborator({\n        listId: list.id,\n        userId: collaboratorUser.id,\n      });\n\n      // Verify list access is revoked\n      await expect(\n        collaboratorApi.bookmarks.getBookmarks({\n          listId: list.id,\n        }),\n      ).rejects.toThrow(\"List not found\");\n\n      // Verify bookmark access is revoked\n      await expect(\n        collaboratorApi.bookmarks.getBookmark({\n          bookmarkId: bookmark.id,\n        }),\n      ).rejects.toThrow(\"Bookmark not found\");\n    });\n\n    test<CustomTestContext>(\"should revoke access after leaving list\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Shared List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"viewer\",\n      );\n\n      // Collaborator leaves\n      await collaboratorApi.lists.leaveList({\n        listId: list.id,\n      });\n\n      // Verify access is revoked\n      await expect(\n        collaboratorApi.lists.get({\n          listId: list.id,\n        }),\n      ).rejects.toThrow(\"List not found\");\n    });\n  });\n\n  describe(\"Smart Lists\", () => {\n    test<CustomTestContext>(\"should not allow adding collaborators to smart lists\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Smart List\",\n        icon: \"🔍\",\n        type: \"smart\",\n        query: \"is:fav\",\n      });\n\n      const collaboratorEmail = (await collaboratorApi.users.whoami()).email!;\n\n      await expect(\n        ownerApi.lists.addCollaborator({\n          listId: list.id,\n          email: collaboratorEmail,\n          role: \"viewer\",\n        }),\n      ).rejects.toThrow(\"Only manual lists can have collaborators\");\n    });\n  });\n\n  describe(\"List Operations Privacy\", () => {\n    test<CustomTestContext>(\"should not allow collaborator to merge lists\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list1 = await ownerApi.lists.create({\n        name: \"List 1\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      const list2 = await ownerApi.lists.create({\n        name: \"List 2\",\n        icon: \"📖\",\n        type: \"manual\",\n      });\n\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list1.id,\n        \"editor\",\n      );\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list2.id,\n        \"editor\",\n      );\n\n      // Collaborator tries to merge the shared list into another list\n      await expect(\n        collaboratorApi.lists.merge({\n          sourceId: list1.id,\n          targetId: list2.id,\n          deleteSourceAfterMerge: false,\n        }),\n      ).rejects.toThrow(\"User is not allowed to manage this list\");\n    });\n\n    test<CustomTestContext>(\"should not allow collaborator to access RSS token operations\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Shared List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"editor\",\n      );\n\n      // Collaborator tries to generate RSS token\n      await expect(\n        collaboratorApi.lists.regenRssToken({\n          listId: list.id,\n        }),\n      ).rejects.toThrow(\"User is not allowed to manage this list\");\n\n      // Collaborator tries to get RSS token\n      await expect(\n        collaboratorApi.lists.getRssToken({\n          listId: list.id,\n        }),\n      ).rejects.toThrow(\"User is not allowed to manage this list\");\n\n      // Owner generates token first\n      await ownerApi.lists.regenRssToken({\n        listId: list.id,\n      });\n\n      // Collaborator tries to clear RSS token\n      await expect(\n        collaboratorApi.lists.clearRssToken({\n          listId: list.id,\n        }),\n      ).rejects.toThrow(\"User is not allowed to manage this list\");\n    });\n\n    test<CustomTestContext>(\"should not allow collaborator to access getListsOfBookmark for bookmark they don't own\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Shared List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      const bookmark = await ownerApi.bookmarks.createBookmark({\n        type: BookmarkTypes.TEXT,\n        text: \"Owner's bookmark\",\n      });\n\n      await ownerApi.lists.addToList({\n        listId: list.id,\n        bookmarkId: bookmark.id,\n      });\n\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"viewer\",\n      );\n\n      // Collaborator cannot use getListsOfBookmark for owner's bookmark\n      // This is expected - only bookmark owners can query which lists contain their bookmarks\n      await expect(\n        collaboratorApi.lists.getListsOfBookmark({\n          bookmarkId: bookmark.id,\n        }),\n      ).rejects.toThrow(\"User is not allowed to access resource\");\n    });\n\n    test<CustomTestContext>(\"should allow collaborator to use getListsOfBookmark for their own bookmarks in shared lists\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Shared List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"editor\",\n      );\n\n      // Collaborator creates their own bookmark and adds it to the shared list\n      const bookmark = await collaboratorApi.bookmarks.createBookmark({\n        type: BookmarkTypes.TEXT,\n        text: \"Collaborator's bookmark\",\n      });\n\n      await collaboratorApi.lists.addToList({\n        listId: list.id,\n        bookmarkId: bookmark.id,\n      });\n\n      // Collaborator can see the shared list in getListsOfBookmark for their own bookmark\n      const { lists } = await collaboratorApi.lists.getListsOfBookmark({\n        bookmarkId: bookmark.id,\n      });\n\n      expect(lists).toHaveLength(1);\n      expect(lists[0].id).toBe(list.id);\n      expect(lists[0].userRole).toBe(\"editor\");\n      expect(lists[0].hasCollaborators).toBe(true);\n    });\n\n    test<CustomTestContext>(\"should show hasCollaborators=true for owner when their bookmark is in a list with collaborators\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      // Owner creates a list\n      const list = await ownerApi.lists.create({\n        name: \"Shared List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      // Owner creates and adds a bookmark\n      const bookmark = await ownerApi.bookmarks.createBookmark({\n        type: BookmarkTypes.TEXT,\n        text: \"Owner's bookmark\",\n      });\n\n      await ownerApi.lists.addToList({\n        listId: list.id,\n        bookmarkId: bookmark.id,\n      });\n\n      // Add a collaborator\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"viewer\",\n      );\n\n      // Owner queries which lists contain their bookmark\n      const { lists } = await ownerApi.lists.getListsOfBookmark({\n        bookmarkId: bookmark.id,\n      });\n\n      expect(lists).toHaveLength(1);\n      expect(lists[0].id).toBe(list.id);\n      expect(lists[0].userRole).toBe(\"owner\");\n      expect(lists[0].hasCollaborators).toBe(true);\n    });\n\n    test<CustomTestContext>(\"should show hasCollaborators=false for owner when their bookmark is in a list without collaborators\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n\n      // Owner creates a list\n      const list = await ownerApi.lists.create({\n        name: \"Private List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      // Owner creates and adds a bookmark\n      const bookmark = await ownerApi.bookmarks.createBookmark({\n        type: BookmarkTypes.TEXT,\n        text: \"Owner's bookmark\",\n      });\n\n      await ownerApi.lists.addToList({\n        listId: list.id,\n        bookmarkId: bookmark.id,\n      });\n\n      // Owner queries which lists contain their bookmark\n      const { lists } = await ownerApi.lists.getListsOfBookmark({\n        bookmarkId: bookmark.id,\n      });\n\n      expect(lists).toHaveLength(1);\n      expect(lists[0].id).toBe(list.id);\n      expect(lists[0].userRole).toBe(\"owner\");\n      expect(lists[0].hasCollaborators).toBe(false);\n    });\n\n    test<CustomTestContext>(\"should include shared lists in stats\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Shared List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      // Add bookmarks to the list\n      const bookmark1 = await ownerApi.bookmarks.createBookmark({\n        type: BookmarkTypes.TEXT,\n        text: \"Bookmark 1\",\n      });\n      const bookmark2 = await ownerApi.bookmarks.createBookmark({\n        type: BookmarkTypes.TEXT,\n        text: \"Bookmark 2\",\n      });\n\n      await ownerApi.lists.addToList({\n        listId: list.id,\n        bookmarkId: bookmark1.id,\n      });\n      await ownerApi.lists.addToList({\n        listId: list.id,\n        bookmarkId: bookmark2.id,\n      });\n\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"viewer\",\n      );\n\n      // Collaborator gets stats\n      const { stats } = await collaboratorApi.lists.stats();\n\n      // Shared list should appear in stats with correct count\n      expect(stats.get(list.id)).toBe(2);\n    });\n\n    test<CustomTestContext>(\"should allow editor to add their own bookmark to shared list via addToList\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Shared List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"editor\",\n      );\n\n      // Editor creates their own bookmark\n      const bookmark = await collaboratorApi.bookmarks.createBookmark({\n        type: BookmarkTypes.TEXT,\n        text: \"Editor's bookmark\",\n      });\n\n      // Editor should be able to add their bookmark to the shared list\n      await collaboratorApi.lists.addToList({\n        listId: list.id,\n        bookmarkId: bookmark.id,\n      });\n\n      // Verify bookmark was added\n      const bookmarks = await ownerApi.bookmarks.getBookmarks({\n        listId: list.id,\n      });\n\n      expect(bookmarks.bookmarks).toHaveLength(1);\n      expect(bookmarks.bookmarks[0].id).toBe(bookmark.id);\n    });\n\n    test<CustomTestContext>(\"should not allow viewer to add their own bookmark to shared list via addToList\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const viewerApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Shared List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      await addAndAcceptCollaborator(ownerApi, viewerApi, list.id, \"viewer\");\n\n      // Viewer creates their own bookmark\n      const bookmark = await viewerApi.bookmarks.createBookmark({\n        type: BookmarkTypes.TEXT,\n        text: \"Viewer's bookmark\",\n      });\n\n      // Viewer should not be able to add their bookmark to the shared list\n      await expect(\n        viewerApi.lists.addToList({\n          listId: list.id,\n          bookmarkId: bookmark.id,\n        }),\n      ).rejects.toThrow();\n    });\n\n    test<CustomTestContext>(\"should not allow editor to add someone else's bookmark to shared list\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const editorApi = apiCallers[1];\n      const thirdUserApi = apiCallers[2];\n\n      const list = await ownerApi.lists.create({\n        name: \"Shared List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      await addAndAcceptCollaborator(ownerApi, editorApi, list.id, \"editor\");\n\n      // Third user creates a bookmark (or owner if only 2 users)\n      const bookmark = await thirdUserApi.bookmarks.createBookmark({\n        type: BookmarkTypes.TEXT,\n        text: \"Someone else's bookmark\",\n      });\n\n      // Editor should not be able to add someone else's bookmark\n      await expect(\n        editorApi.lists.addToList({\n          listId: list.id,\n          bookmarkId: bookmark.id,\n        }),\n      ).rejects.toThrow(/Bookmark not found/);\n    });\n\n    test<CustomTestContext>(\"should not allow collaborator to update list metadata fields\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const editorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Shared List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      await addAndAcceptCollaborator(ownerApi, editorApi, list.id, \"editor\");\n\n      // Editor tries to change list name\n      await expect(\n        editorApi.lists.edit({\n          listId: list.id,\n          name: \"Modified Name\",\n        }),\n      ).rejects.toThrow();\n\n      // Editor tries to make list public\n      await expect(\n        editorApi.lists.edit({\n          listId: list.id,\n          public: true,\n        }),\n      ).rejects.toThrow();\n    });\n  });\n\n  describe(\"hasCollaborators Field\", () => {\n    test<CustomTestContext>(\"should be false for newly created list\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n\n      const list = await ownerApi.lists.create({\n        name: \"New List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      expect(list.hasCollaborators).toBe(false);\n    });\n\n    test<CustomTestContext>(\"should be true for owner after adding a collaborator\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Shared List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"viewer\",\n      );\n\n      // Fetch the list again to get updated hasCollaborators\n      const updatedList = await ownerApi.lists.get({\n        listId: list.id,\n      });\n\n      expect(updatedList.hasCollaborators).toBe(true);\n    });\n\n    test<CustomTestContext>(\"should be true for collaborator viewing shared list\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Shared List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"viewer\",\n      );\n\n      // Collaborator fetches the list\n      const sharedList = await collaboratorApi.lists.get({\n        listId: list.id,\n      });\n\n      expect(sharedList.hasCollaborators).toBe(true);\n    });\n\n    test<CustomTestContext>(\"should be false for owner after removing all collaborators\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Shared List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      const collaboratorUser = await collaboratorApi.users.whoami();\n\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"viewer\",\n      );\n\n      // Remove the collaborator\n      await ownerApi.lists.removeCollaborator({\n        listId: list.id,\n        userId: collaboratorUser.id,\n      });\n\n      // Fetch the list again\n      const updatedList = await ownerApi.lists.get({\n        listId: list.id,\n      });\n\n      expect(updatedList.hasCollaborators).toBe(false);\n    });\n\n    test<CustomTestContext>(\"should show correct value in lists.list() endpoint\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      // Create list without collaborators\n      const list1 = await ownerApi.lists.create({\n        name: \"Private List\",\n        icon: \"🔒\",\n        type: \"manual\",\n      });\n\n      // Create list with collaborators\n      const list2 = await ownerApi.lists.create({\n        name: \"Shared List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list2.id,\n        \"viewer\",\n      );\n\n      // Get all lists\n      const { lists } = await ownerApi.lists.list();\n\n      const privateList = lists.find((l) => l.id === list1.id);\n      const sharedList = lists.find((l) => l.id === list2.id);\n\n      expect(privateList?.hasCollaborators).toBe(false);\n      expect(sharedList?.hasCollaborators).toBe(true);\n    });\n\n    test<CustomTestContext>(\"should show true for collaborator in lists.list() endpoint\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Shared List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"editor\",\n      );\n\n      // Collaborator gets all lists\n      const { lists } = await collaboratorApi.lists.list();\n\n      const sharedList = lists.find((l) => l.id === list.id);\n\n      expect(sharedList?.hasCollaborators).toBe(true);\n      expect(sharedList?.userRole).toBe(\"editor\");\n    });\n  });\n\n  describe(\"List Invitations\", () => {\n    test<CustomTestContext>(\"should create pending invitation when adding collaborator\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Test List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      const collaboratorEmail = (await collaboratorApi.users.whoami()).email!;\n\n      // Add collaborator (creates pending invitation)\n      await ownerApi.lists.addCollaborator({\n        listId: list.id,\n        email: collaboratorEmail,\n        role: \"viewer\",\n      });\n\n      // Check that collaborator has a pending invitation\n      const pendingInvitations =\n        await collaboratorApi.lists.getPendingInvitations();\n\n      expect(pendingInvitations).toHaveLength(1);\n      expect(pendingInvitations[0].listId).toBe(list.id);\n      expect(pendingInvitations[0].role).toBe(\"viewer\");\n    });\n\n    test<CustomTestContext>(\"should allow collaborator to accept invitation\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Test List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      const collaboratorEmail = (await collaboratorApi.users.whoami()).email!;\n\n      const { invitationId } = await ownerApi.lists.addCollaborator({\n        listId: list.id,\n        email: collaboratorEmail,\n        role: \"viewer\",\n      });\n\n      // Accept the invitation\n      await collaboratorApi.lists.acceptInvitation({\n        invitationId,\n      });\n\n      // Verify collaborator was added\n      const { collaborators } = await ownerApi.lists.getCollaborators({\n        listId: list.id,\n      });\n\n      expect(collaborators).toHaveLength(1);\n      expect(collaborators[0].user.email).toBe(collaboratorEmail);\n      expect(collaborators[0].status).toBe(\"accepted\");\n\n      // Verify no more pending invitations\n      const pendingInvitations =\n        await collaboratorApi.lists.getPendingInvitations();\n\n      expect(pendingInvitations).toHaveLength(0);\n    });\n\n    test<CustomTestContext>(\"should allow collaborator to decline invitation\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Test List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      const collaboratorEmail = (await collaboratorApi.users.whoami()).email!;\n\n      const { invitationId } = await ownerApi.lists.addCollaborator({\n        listId: list.id,\n        email: collaboratorEmail,\n        role: \"viewer\",\n      });\n\n      // Decline the invitation\n      await collaboratorApi.lists.declineInvitation({\n        invitationId,\n      });\n\n      // Verify collaborator was not added\n      const { collaborators } = await ownerApi.lists.getCollaborators({\n        listId: list.id,\n      });\n\n      expect(collaborators).toHaveLength(1);\n      expect(collaborators[0].status).toBe(\"declined\");\n\n      // Verify no pending invitations\n      const pendingInvitations =\n        await collaboratorApi.lists.getPendingInvitations();\n\n      expect(pendingInvitations).toHaveLength(0);\n    });\n\n    test<CustomTestContext>(\"should allow owner to revoke pending invitation\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Test List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      const collaboratorUser = await collaboratorApi.users.whoami();\n\n      const { invitationId } = await ownerApi.lists.addCollaborator({\n        listId: list.id,\n        email: collaboratorUser.email!,\n        role: \"viewer\",\n      });\n\n      // Owner revokes the invitation\n      await ownerApi.lists.revokeInvitation({\n        invitationId,\n      });\n\n      // Verify invitation was revoked\n      const pendingInvitations =\n        await collaboratorApi.lists.getPendingInvitations();\n\n      expect(pendingInvitations).toHaveLength(0);\n    });\n\n    test<CustomTestContext>(\"should not allow access to list with pending invitation\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Test List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      const bookmark = await ownerApi.bookmarks.createBookmark({\n        type: BookmarkTypes.TEXT,\n        text: \"Test bookmark\",\n      });\n\n      await ownerApi.lists.addToList({\n        listId: list.id,\n        bookmarkId: bookmark.id,\n      });\n\n      const collaboratorEmail = (await collaboratorApi.users.whoami()).email!;\n\n      // Add collaborator but don't accept invitation\n      await ownerApi.lists.addCollaborator({\n        listId: list.id,\n        email: collaboratorEmail,\n        role: \"viewer\",\n      });\n\n      // Collaborator should not be able to access the list yet\n      await expect(\n        collaboratorApi.lists.get({\n          listId: list.id,\n        }),\n      ).rejects.toThrow(\"List not found\");\n\n      // Collaborator should not be able to access bookmarks in the list\n      await expect(\n        collaboratorApi.bookmarks.getBookmarks({\n          listId: list.id,\n        }),\n      ).rejects.toThrow(\"List not found\");\n\n      // Collaborator should not be able to access individual bookmarks\n      await expect(\n        collaboratorApi.bookmarks.getBookmark({\n          bookmarkId: bookmark.id,\n        }),\n      ).rejects.toThrow(\"Bookmark not found\");\n    });\n\n    test<CustomTestContext>(\"should show pending invitations with list details\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Test Shared List\",\n        icon: \"📚\",\n        description: \"A test list for sharing\",\n        type: \"manual\",\n      });\n\n      const collaboratorEmail = (await collaboratorApi.users.whoami()).email!;\n\n      await ownerApi.lists.addCollaborator({\n        listId: list.id,\n        email: collaboratorEmail,\n        role: \"editor\",\n      });\n\n      const pendingInvitations =\n        await collaboratorApi.lists.getPendingInvitations();\n\n      expect(pendingInvitations).toHaveLength(1);\n      const invitation = pendingInvitations[0];\n\n      expect(invitation.listId).toBe(list.id);\n      expect(invitation.role).toBe(\"editor\");\n      expect(invitation.list.name).toBe(\"Test Shared List\");\n      expect(invitation.list.icon).toBe(\"📚\");\n      expect(invitation.list.description).toBe(\"A test list for sharing\");\n      expect(invitation.list.owner).toBeDefined();\n    });\n\n    test<CustomTestContext>(\"should show pending invitations in getCollaborators for owner\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Test List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      const collaboratorEmail = (await collaboratorApi.users.whoami()).email!;\n\n      await ownerApi.lists.addCollaborator({\n        listId: list.id,\n        email: collaboratorEmail,\n        role: \"viewer\",\n      });\n\n      // Owner should see pending invitation in collaborators list\n      const { collaborators } = await ownerApi.lists.getCollaborators({\n        listId: list.id,\n      });\n\n      expect(collaborators).toHaveLength(1);\n      expect(collaborators[0].status).toBe(\"pending\");\n      expect(collaborators[0].role).toBe(\"viewer\");\n      expect(collaborators[0].user.email).toBe(collaboratorEmail);\n    });\n\n    test<CustomTestContext>(\"should update hasCollaborators after invitation is accepted\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Test List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      const collaboratorEmail = (await collaboratorApi.users.whoami()).email!;\n\n      // hasCollaborators should be false initially\n      expect(list.hasCollaborators).toBe(false);\n\n      const { invitationId } = await ownerApi.lists.addCollaborator({\n        listId: list.id,\n        email: collaboratorEmail,\n        role: \"viewer\",\n      });\n\n      // hasCollaborators should be false after adding invitation (pending does not counts)\n      const listAfterInvite = await ownerApi.lists.get({\n        listId: list.id,\n      });\n      expect(listAfterInvite.hasCollaborators).toBe(false);\n\n      // Accept the invitation\n      await collaboratorApi.lists.acceptInvitation({\n        invitationId,\n      });\n\n      // hasCollaborators should still be true\n      const listAfterAccept = await ownerApi.lists.get({\n        listId: list.id,\n      });\n      expect(listAfterAccept.hasCollaborators).toBe(true);\n    });\n\n    test<CustomTestContext>(\"should update hasCollaborators after invitation is declined\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Test List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      const collaboratorEmail = (await collaboratorApi.users.whoami()).email!;\n\n      const { invitationId } = await ownerApi.lists.addCollaborator({\n        listId: list.id,\n        email: collaboratorEmail,\n        role: \"viewer\",\n      });\n\n      // hasCollaborators should be false with pending invitation\n      const listAfterInvite = await ownerApi.lists.get({\n        listId: list.id,\n      });\n      expect(listAfterInvite.hasCollaborators).toBe(false);\n\n      // Decline the invitation\n      await collaboratorApi.lists.declineInvitation({\n        invitationId,\n      });\n\n      // hasCollaborators should be false after declining\n      const listAfterDecline = await ownerApi.lists.get({\n        listId: list.id,\n      });\n      expect(listAfterDecline.hasCollaborators).toBe(false);\n    });\n\n    test<CustomTestContext>(\"should not show declined invitations in pending list\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Test List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      const collaboratorEmail = (await collaboratorApi.users.whoami()).email!;\n\n      const { invitationId } = await ownerApi.lists.addCollaborator({\n        listId: list.id,\n        email: collaboratorEmail,\n        role: \"viewer\",\n      });\n\n      // Decline the invitation\n      await collaboratorApi.lists.declineInvitation({\n        invitationId,\n      });\n\n      // Should not appear in pending invitations\n      const pendingInvitations =\n        await collaboratorApi.lists.getPendingInvitations();\n\n      expect(pendingInvitations).toHaveLength(0);\n    });\n\n    test<CustomTestContext>(\"should allow re-inviting after decline\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Test List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      const collaboratorEmail = (await collaboratorApi.users.whoami()).email!;\n\n      // First invitation\n      const { invitationId } = await ownerApi.lists.addCollaborator({\n        listId: list.id,\n        email: collaboratorEmail,\n        role: \"viewer\",\n      });\n\n      // Decline it\n      await collaboratorApi.lists.declineInvitation({\n        invitationId,\n      });\n\n      // Re-invite with different role\n      await ownerApi.lists.addCollaborator({\n        listId: list.id,\n        email: collaboratorEmail,\n        role: \"editor\",\n      });\n\n      // Should have a new pending invitation\n      const pendingInvitations =\n        await collaboratorApi.lists.getPendingInvitations();\n\n      expect(pendingInvitations).toHaveLength(1);\n      expect(pendingInvitations[0].role).toBe(\"editor\");\n    });\n\n    test<CustomTestContext>(\"should not allow accepting non-existent invitation\", async ({\n      apiCallers,\n    }) => {\n      const collaboratorApi = apiCallers[1];\n\n      const fakeInvitationId = \"non-existent-invitation-id\";\n\n      await expect(\n        collaboratorApi.lists.acceptInvitation({\n          invitationId: fakeInvitationId,\n        }),\n      ).rejects.toThrow(\"Invitation not found\");\n    });\n\n    test<CustomTestContext>(\"should not allow accepting already accepted invitation\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Test List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      const collaboratorEmail = (await collaboratorApi.users.whoami()).email!;\n\n      const { invitationId } = await ownerApi.lists.addCollaborator({\n        listId: list.id,\n        email: collaboratorEmail,\n        role: \"viewer\",\n      });\n\n      // Accept once\n      await collaboratorApi.lists.acceptInvitation({\n        invitationId,\n      });\n\n      // Try to accept again (should fail since invitation is already accepted and deleted)\n      await expect(\n        collaboratorApi.lists.acceptInvitation({\n          invitationId,\n        }),\n      ).rejects.toThrow(\"Invitation not found\");\n    });\n\n    test<CustomTestContext>(\"should show list in shared lists only after accepting invitation\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Test List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      const collaboratorEmail = (await collaboratorApi.users.whoami()).email!;\n\n      const { invitationId } = await ownerApi.lists.addCollaborator({\n        listId: list.id,\n        email: collaboratorEmail,\n        role: \"viewer\",\n      });\n\n      // List should not appear in collaborator's lists yet\n      const listsBefore = await collaboratorApi.lists.list();\n      expect(listsBefore.lists.find((l) => l.id === list.id)).toBeUndefined();\n\n      // Accept invitation\n      await collaboratorApi.lists.acceptInvitation({\n        invitationId,\n      });\n\n      // Now list should appear\n      const listsAfter = await collaboratorApi.lists.list();\n      const sharedList = listsAfter.lists.find((l) => l.id === list.id);\n      expect(sharedList).toBeDefined();\n      expect(sharedList?.userRole).toBe(\"viewer\");\n    });\n\n    test<CustomTestContext>(\"should handle multiple pending invitations for different lists\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list1 = await ownerApi.lists.create({\n        name: \"List 1\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      const list2 = await ownerApi.lists.create({\n        name: \"List 2\",\n        icon: \"📖\",\n        type: \"manual\",\n      });\n\n      const collaboratorEmail = (await collaboratorApi.users.whoami()).email!;\n\n      // Invite to both lists\n      const { invitationId: invitationId1 } =\n        await ownerApi.lists.addCollaborator({\n          listId: list1.id,\n          email: collaboratorEmail,\n          role: \"viewer\",\n        });\n\n      const { invitationId: invitationId2 } =\n        await ownerApi.lists.addCollaborator({\n          listId: list2.id,\n          email: collaboratorEmail,\n          role: \"editor\",\n        });\n\n      // Should have 2 pending invitations\n      const pendingInvitations =\n        await collaboratorApi.lists.getPendingInvitations();\n\n      expect(pendingInvitations).toHaveLength(2);\n\n      // Accept one\n      await collaboratorApi.lists.acceptInvitation({\n        invitationId: invitationId1,\n      });\n\n      // Should have 1 pending invitation left\n      const remainingInvitations =\n        await collaboratorApi.lists.getPendingInvitations();\n\n      expect(remainingInvitations).toHaveLength(1);\n      expect(remainingInvitations[0].id).toBe(invitationId2);\n      expect(remainingInvitations[0].listId).toBe(list2.id);\n    });\n\n    test<CustomTestContext>(\"should not allow collaborator to revoke invitation\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n      const thirdUserApi = apiCallers[2];\n\n      const list = await ownerApi.lists.create({\n        name: \"Test List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      // Owner adds collaborator 1 and they accept\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"editor\",\n      );\n\n      // Owner invites third user\n      const thirdUserEmail = (await thirdUserApi.users.whoami()).email!;\n      const { invitationId } = await ownerApi.lists.addCollaborator({\n        listId: list.id,\n        email: thirdUserEmail,\n        role: \"viewer\",\n      });\n\n      // Collaborator tries to revoke the third user's invitation\n      // Collaborator cannot access the invitation at all (not the invitee, not the owner)\n      await expect(\n        collaboratorApi.lists.revokeInvitation({\n          invitationId,\n        }),\n      ).rejects.toThrow(\"Invitation not found\");\n    });\n\n    test<CustomTestContext>(\"should not allow invited user to revoke their own invitation\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Test List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      const collaboratorEmail = (await collaboratorApi.users.whoami()).email!;\n\n      const { invitationId } = await ownerApi.lists.addCollaborator({\n        listId: list.id,\n        email: collaboratorEmail,\n        role: \"viewer\",\n      });\n\n      // Invited user tries to revoke (should only be able to decline)\n      await expect(\n        collaboratorApi.lists.revokeInvitation({\n          invitationId,\n        }),\n      ).rejects.toThrow(\"Only the list owner can perform this action\");\n    });\n\n    test<CustomTestContext>(\"should not allow non-owner/non-invitee to access invitation\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n      const thirdUserApi = apiCallers[2];\n\n      const list = await ownerApi.lists.create({\n        name: \"Test List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      const collaboratorEmail = (await collaboratorApi.users.whoami()).email!;\n\n      const { invitationId } = await ownerApi.lists.addCollaborator({\n        listId: list.id,\n        email: collaboratorEmail,\n        role: \"viewer\",\n      });\n\n      // Third user (not owner, not invitee) tries to revoke invitation\n      await expect(\n        thirdUserApi.lists.revokeInvitation({\n          invitationId,\n        }),\n      ).rejects.toThrow(\"Invitation not found\");\n\n      // Third user tries to accept invitation\n      await expect(\n        thirdUserApi.lists.acceptInvitation({\n          invitationId,\n        }),\n      ).rejects.toThrow(\"Invitation not found\");\n\n      // Third user tries to decline invitation\n      await expect(\n        thirdUserApi.lists.declineInvitation({\n          invitationId,\n        }),\n      ).rejects.toThrow(\"Invitation not found\");\n    });\n\n    test<CustomTestContext>(\"should not show invitations to collaborators in getCollaborators\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n      const thirdUserApi = apiCallers[2];\n\n      const list = await ownerApi.lists.create({\n        name: \"Test List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      // Owner adds collaborator 1 and they accept\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaboratorApi,\n        list.id,\n        \"viewer\",\n      );\n\n      // Owner invites third user (pending invitation)\n      const thirdUserEmail = (await thirdUserApi.users.whoami()).email!;\n      await ownerApi.lists.addCollaborator({\n        listId: list.id,\n        email: thirdUserEmail,\n        role: \"viewer\",\n      });\n\n      // Owner should see 2 collaborators (1 accepted + 1 pending)\n      const ownerView = await ownerApi.lists.getCollaborators({\n        listId: list.id,\n      });\n      expect(ownerView.collaborators).toHaveLength(2);\n\n      // Collaborator should only see 1 (themselves, no pending invitations)\n      const collaboratorView = await collaboratorApi.lists.getCollaborators({\n        listId: list.id,\n      });\n      expect(collaboratorView.collaborators).toHaveLength(1);\n      expect(collaboratorView.collaborators[0].status).toBe(\"accepted\");\n    });\n\n    test<CustomTestContext>(\"should allow owner to see both accepted collaborators and pending invitations\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n      const thirdUserApi = apiCallers[2];\n\n      const list = await ownerApi.lists.create({\n        name: \"Test List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      // Add and accept one collaborator\n      const collaboratorEmail = (await collaboratorApi.users.whoami()).email!;\n      const { invitationId } = await ownerApi.lists.addCollaborator({\n        listId: list.id,\n        email: collaboratorEmail,\n        role: \"editor\",\n      });\n\n      await collaboratorApi.lists.acceptInvitation({\n        invitationId,\n      });\n\n      // Add pending invitation for third user\n      const thirdUserEmail = (await thirdUserApi.users.whoami()).email!;\n      await ownerApi.lists.addCollaborator({\n        listId: list.id,\n        email: thirdUserEmail,\n        role: \"viewer\",\n      });\n\n      // Owner should see both\n      const { collaborators } = await ownerApi.lists.getCollaborators({\n        listId: list.id,\n      });\n\n      expect(collaborators).toHaveLength(2);\n\n      const acceptedCollaborator = collaborators.find(\n        (c) => c.status === \"accepted\",\n      );\n      const pendingCollaborator = collaborators.find(\n        (c) => c.status === \"pending\",\n      );\n\n      expect(acceptedCollaborator).toBeDefined();\n      expect(acceptedCollaborator?.role).toBe(\"editor\");\n      expect(acceptedCollaborator?.user.email).toBe(collaboratorEmail);\n\n      expect(pendingCollaborator).toBeDefined();\n      expect(pendingCollaborator?.role).toBe(\"viewer\");\n      expect(pendingCollaborator?.user.email).toBe(thirdUserEmail);\n    });\n\n    test<CustomTestContext>(\"should not show invitee name for pending invitations\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Test List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      const collaboratorUser = await collaboratorApi.users.whoami();\n      const collaboratorEmail = collaboratorUser.email!;\n      const collaboratorName = collaboratorUser.name;\n\n      await ownerApi.lists.addCollaborator({\n        listId: list.id,\n        email: collaboratorEmail,\n        role: \"viewer\",\n      });\n\n      // Owner checks pending invitations\n      const { collaborators } = await ownerApi.lists.getCollaborators({\n        listId: list.id,\n      });\n\n      const pendingInvitation = collaborators.find(\n        (c) => c.status === \"pending\",\n      );\n\n      expect(pendingInvitation).toBeDefined();\n      // Name should be masked as \"Pending User\"\n      expect(pendingInvitation?.user.name).toBe(\"Pending User\");\n      // Name should NOT be the actual user's name\n      expect(pendingInvitation?.user.name).not.toBe(collaboratorName);\n      // Email should still be visible to owner\n      expect(pendingInvitation?.user.email).toBe(collaboratorEmail);\n    });\n\n    test<CustomTestContext>(\"should show invitee name after invitation is accepted\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Test List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      const collaboratorUser = await collaboratorApi.users.whoami();\n      const collaboratorEmail = collaboratorUser.email!;\n      const collaboratorName = collaboratorUser.name;\n\n      const { invitationId } = await ownerApi.lists.addCollaborator({\n        listId: list.id,\n        email: collaboratorEmail,\n        role: \"viewer\",\n      });\n\n      // Before acceptance - name should be masked\n      const beforeAccept = await ownerApi.lists.getCollaborators({\n        listId: list.id,\n      });\n      const pendingInvitation = beforeAccept.collaborators.find(\n        (c) => c.status === \"pending\",\n      );\n      expect(pendingInvitation?.user.name).toBe(\"Pending User\");\n\n      // Accept invitation\n      await collaboratorApi.lists.acceptInvitation({\n        invitationId,\n      });\n\n      // After acceptance - name should be visible\n      const afterAccept = await ownerApi.lists.getCollaborators({\n        listId: list.id,\n      });\n      const acceptedCollaborator = afterAccept.collaborators.find(\n        (c) => c.status === \"accepted\",\n      );\n      expect(acceptedCollaborator?.user.name).toBe(collaboratorName);\n      expect(acceptedCollaborator?.user.email).toBe(collaboratorEmail);\n    });\n\n    test<CustomTestContext>(\"should not show invitee name for declined invitations\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaboratorApi = apiCallers[1];\n\n      const list = await ownerApi.lists.create({\n        name: \"Test List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      const collaboratorUser = await collaboratorApi.users.whoami();\n      const collaboratorEmail = collaboratorUser.email!;\n      const collaboratorName = collaboratorUser.name;\n\n      const { invitationId } = await ownerApi.lists.addCollaborator({\n        listId: list.id,\n        email: collaboratorEmail,\n        role: \"viewer\",\n      });\n\n      // Decline the invitation\n      await collaboratorApi.lists.declineInvitation({\n        invitationId,\n      });\n\n      // Owner checks declined invitations\n      const { collaborators } = await ownerApi.lists.getCollaborators({\n        listId: list.id,\n      });\n\n      const declinedInvitation = collaborators.find(\n        (c) => c.status === \"declined\",\n      );\n\n      expect(declinedInvitation).toBeDefined();\n      // Name should still be masked as \"Pending User\" even after decline\n      expect(declinedInvitation?.user.name).toBe(\"Pending User\");\n      expect(declinedInvitation?.user.name).not.toBe(collaboratorName);\n      // Email should still be visible to owner\n      expect(declinedInvitation?.user.email).toBe(collaboratorEmail);\n    });\n\n    test<CustomTestContext>(\"should hide emails from non-owners\", async ({\n      apiCallers,\n    }) => {\n      const ownerApi = apiCallers[0];\n      const collaborator1Api = apiCallers[1];\n      const collaborator2Api = apiCallers[2];\n\n      const list = await ownerApi.lists.create({\n        name: \"Test List\",\n        icon: \"📚\",\n        type: \"manual\",\n      });\n\n      const ownerUser = await ownerApi.users.whoami();\n      const ownerEmail = ownerUser.email!;\n\n      const collaborator1User = await collaborator1Api.users.whoami();\n      const collaborator1Email = collaborator1User.email!;\n\n      const collaborator2User = await collaborator2Api.users.whoami();\n      const collaborator2Email = collaborator2User.email!;\n\n      // Add both collaborators\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaborator1Api,\n        list.id,\n        \"editor\",\n      );\n      await addAndAcceptCollaborator(\n        ownerApi,\n        collaborator2Api,\n        list.id,\n        \"viewer\",\n      );\n\n      // Owner should see all emails\n      const ownerView = await ownerApi.lists.getCollaborators({\n        listId: list.id,\n      });\n\n      expect(ownerView.owner?.email).toBe(ownerEmail);\n\n      const ownerViewCollaborators = ownerView.collaborators.filter(\n        (c) => c.status === \"accepted\",\n      );\n      expect(ownerViewCollaborators).toHaveLength(2);\n\n      const ownerViewCollab1 = ownerViewCollaborators.find(\n        (c) => c.user.email === collaborator1Email,\n      );\n      const ownerViewCollab2 = ownerViewCollaborators.find(\n        (c) => c.user.email === collaborator2Email,\n      );\n\n      expect(ownerViewCollab1?.user.email).toBe(collaborator1Email);\n      expect(ownerViewCollab2?.user.email).toBe(collaborator2Email);\n\n      // Non-owners should NOT see any emails\n      const collaborator1View = await collaborator1Api.lists.getCollaborators({\n        listId: list.id,\n      });\n\n      // Should not see owner email\n      expect(collaborator1View.owner?.email).toBe(null);\n\n      // Should not see other collaborators' emails\n      const collab1ViewCollaborators = collaborator1View.collaborators.filter(\n        (c) => c.status === \"accepted\",\n      );\n      expect(collab1ViewCollaborators).toHaveLength(2);\n\n      collab1ViewCollaborators.forEach((c) => {\n        expect(c.user.email).toBe(null);\n      });\n\n      // Verify collaborator2 also can't see emails\n      const collaborator2View = await collaborator2Api.lists.getCollaborators({\n        listId: list.id,\n      });\n\n      expect(collaborator2View.owner?.email).toBe(null);\n\n      const collab2ViewCollaborators = collaborator2View.collaborators.filter(\n        (c) => c.status === \"accepted\",\n      );\n      expect(collab2ViewCollaborators).toHaveLength(2);\n\n      collab2ViewCollaborators.forEach((c) => {\n        expect(c.user.email).toBe(null);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/trpc/routers/subscriptions.test.ts",
    "content": "import { eq } from \"drizzle-orm\";\nimport { beforeEach, describe, expect, test, vi } from \"vitest\";\n\nimport { assets, AssetTypes, subscriptions, users } from \"@karakeep/db/schema\";\nimport { BookmarkTypes } from \"@karakeep/shared/types/bookmarks\";\n\nimport type { CustomTestContext } from \"../testUtils\";\nimport { defaultBeforeEach, getApiCaller } from \"../testUtils\";\n\n// Mock Stripe using vi.hoisted to ensure it's available during module initialization\nconst mockStripeInstance = vi.hoisted(() => ({\n  customers: {\n    create: vi.fn(),\n  },\n  checkout: {\n    sessions: {\n      create: vi.fn(),\n    },\n  },\n  billingPortal: {\n    sessions: {\n      create: vi.fn(),\n    },\n  },\n  subscriptions: {\n    update: vi.fn(),\n    list: vi.fn(),\n  },\n  webhooks: {\n    constructEvent: vi.fn(),\n  },\n}));\n\nvi.mock(\"stripe\", () => {\n  return {\n    default: vi.fn(() => mockStripeInstance),\n  };\n});\n\n// Mock server config with Stripe settings\nvi.mock(\"@karakeep/shared/config\", async (original) => {\n  const mod = (await original()) as typeof import(\"@karakeep/shared/config\");\n  return {\n    ...mod,\n    default: {\n      ...mod.default,\n      stripe: {\n        secretKey: \"sk_test_123\",\n        priceId: \"price_123\",\n        webhookSecret: \"whsec_123\",\n        isConfigured: true,\n      },\n      publicUrl: \"https://test.karakeep.com\",\n      quotas: {\n        free: {\n          bookmarkLimit: 100,\n          assetSizeBytes: 1000000, // 1MB\n        },\n        paid: {\n          bookmarkLimit: null,\n          assetSizeBytes: null,\n        },\n      },\n    },\n  };\n});\n\nbeforeEach<CustomTestContext>(defaultBeforeEach(false));\n\ndescribe(\"Subscription Routes\", () => {\n  let mockCustomersCreate: ReturnType<typeof vi.fn>;\n  let mockCheckoutSessionsCreate: ReturnType<typeof vi.fn>;\n  let mockBillingPortalSessionsCreate: ReturnType<typeof vi.fn>;\n  let mockWebhooksConstructEvent: ReturnType<typeof vi.fn>;\n  let mockSubscriptionsList: ReturnType<typeof vi.fn>;\n\n  beforeEach(() => {\n    vi.clearAllMocks();\n\n    // Set up mock functions using the global mock instance\n    mockCustomersCreate = mockStripeInstance.customers.create;\n    mockCheckoutSessionsCreate = mockStripeInstance.checkout.sessions.create;\n    mockBillingPortalSessionsCreate =\n      mockStripeInstance.billingPortal.sessions.create;\n    mockWebhooksConstructEvent = mockStripeInstance.webhooks.constructEvent;\n    mockSubscriptionsList = mockStripeInstance.subscriptions.list;\n  });\n\n  describe(\"getSubscriptionStatus\", () => {\n    test<CustomTestContext>(\"returns free tier when no subscription exists\", async ({\n      db,\n      unauthedAPICaller,\n    }) => {\n      const user = await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"test@test.com\",\n        password: \"pass1234\",\n        confirmPassword: \"pass1234\",\n      });\n      const caller = getApiCaller(db, user.id);\n\n      const status = await caller.subscriptions.getSubscriptionStatus();\n\n      expect(status).toEqual({\n        tier: \"free\",\n        status: null,\n        startDate: null,\n        endDate: null,\n        hasActiveSubscription: false,\n        cancelAtPeriodEnd: false,\n      });\n    });\n\n    test<CustomTestContext>(\"returns subscription data when subscription exists\", async ({\n      db,\n      unauthedAPICaller,\n    }) => {\n      const user = await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"test@test.com\",\n        password: \"pass1234\",\n        confirmPassword: \"pass1234\",\n      });\n      const caller = getApiCaller(db, user.id);\n\n      const startDate = new Date(\"2024-01-01\");\n      const endDate = new Date(\"2024-02-01\");\n\n      // Create subscription record\n      await db.insert(subscriptions).values({\n        userId: user.id,\n        stripeCustomerId: \"cus_123\",\n        stripeSubscriptionId: \"sub_123\",\n        status: \"active\",\n        tier: \"paid\",\n        startDate,\n        endDate,\n        cancelAtPeriodEnd: true,\n      });\n\n      const status = await caller.subscriptions.getSubscriptionStatus();\n\n      expect(status).toEqual({\n        tier: \"paid\",\n        status: \"active\",\n        startDate,\n        endDate,\n        hasActiveSubscription: true,\n        cancelAtPeriodEnd: true,\n      });\n    });\n  });\n\n  describe(\"createCheckoutSession\", () => {\n    test<CustomTestContext>(\"creates checkout session for new customer\", async ({\n      db,\n      unauthedAPICaller,\n    }) => {\n      const user = await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"test@test.com\",\n        password: \"pass1234\",\n        confirmPassword: \"pass1234\",\n      });\n      const caller = getApiCaller(db, user.id);\n\n      mockCustomersCreate.mockResolvedValue({\n        id: \"cus_new123\",\n      });\n\n      mockCheckoutSessionsCreate.mockResolvedValue({\n        id: \"cs_123\",\n        url: \"https://checkout.stripe.com/pay/cs_123\",\n      });\n\n      const result = await caller.subscriptions.createCheckoutSession();\n\n      expect(result).toEqual({\n        sessionId: \"cs_123\",\n        url: \"https://checkout.stripe.com/pay/cs_123\",\n      });\n\n      expect(mockCustomersCreate).toHaveBeenCalledWith({\n        email: \"test@test.com\",\n        metadata: {\n          userId: user.id,\n        },\n      });\n\n      expect(mockCheckoutSessionsCreate).toHaveBeenCalledWith({\n        customer: \"cus_new123\",\n        line_items: [\n          {\n            price: \"price_123\",\n            quantity: 1,\n          },\n        ],\n        mode: \"subscription\",\n        success_url:\n          \"https://test.karakeep.com/settings/subscription?success=true\",\n        cancel_url:\n          \"https://test.karakeep.com/settings/subscription?canceled=true\",\n        metadata: {\n          userId: user.id,\n        },\n        customer_update: {\n          address: \"auto\",\n        },\n        allow_promotion_codes: true,\n        managed_payments: {\n          enabled: true,\n        },\n      });\n    });\n\n    test<CustomTestContext>(\"throws error if user already has active subscription\", async ({\n      db,\n      unauthedAPICaller,\n    }) => {\n      const user = await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"test@test.com\",\n        password: \"pass1234\",\n        confirmPassword: \"pass1234\",\n      });\n      const caller = getApiCaller(db, user.id);\n\n      await db.insert(subscriptions).values({\n        userId: user.id,\n        stripeCustomerId: \"cus_123\",\n        stripeSubscriptionId: \"sub_123\",\n        status: \"active\",\n        tier: \"paid\",\n      });\n\n      await expect(\n        caller.subscriptions.createCheckoutSession(),\n      ).rejects.toThrow(/User already has an active subscription/);\n    });\n  });\n\n  describe(\"createPortalSession\", () => {\n    test<CustomTestContext>(\"creates portal session for user with subscription\", async ({\n      db,\n      unauthedAPICaller,\n    }) => {\n      const user = await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"test@test.com\",\n        password: \"pass1234\",\n        confirmPassword: \"pass1234\",\n      });\n      const caller = getApiCaller(db, user.id);\n\n      await db.insert(subscriptions).values({\n        userId: user.id,\n        stripeCustomerId: \"cus_123\",\n        stripeSubscriptionId: \"sub_123\",\n        status: \"active\",\n        tier: \"paid\",\n      });\n\n      mockBillingPortalSessionsCreate.mockResolvedValue({\n        url: \"https://billing.stripe.com/session/123\",\n      });\n\n      const result = await caller.subscriptions.createPortalSession();\n\n      expect(result).toEqual({\n        url: \"https://billing.stripe.com/session/123\",\n      });\n\n      expect(mockBillingPortalSessionsCreate).toHaveBeenCalledWith({\n        customer: \"cus_123\",\n        return_url: \"https://test.karakeep.com/settings/subscription\",\n      });\n    });\n\n    test<CustomTestContext>(\"throws error if user has no subscription\", async ({\n      db,\n      unauthedAPICaller,\n    }) => {\n      const user = await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"test@test.com\",\n        password: \"pass1234\",\n        confirmPassword: \"pass1234\",\n      });\n      const caller = getApiCaller(db, user.id);\n\n      await expect(caller.subscriptions.createPortalSession()).rejects.toThrow(\n        /No Stripe customer found/,\n      );\n    });\n  });\n\n  describe(\"getQuotaUsage\", () => {\n    test<CustomTestContext>(\"returns quota usage for user with no data\", async ({\n      db,\n      unauthedAPICaller,\n    }) => {\n      const user = await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"test@test.com\",\n        password: \"pass1234\",\n        confirmPassword: \"pass1234\",\n      });\n      const caller = getApiCaller(db, user.id);\n\n      const usage = await caller.subscriptions.getQuotaUsage();\n\n      expect(usage).toEqual({\n        bookmarks: {\n          used: 0,\n          quota: 100,\n          unlimited: false,\n        },\n        storage: {\n          used: 0,\n          quota: 1000000,\n          unlimited: false,\n        },\n      });\n    });\n\n    test<CustomTestContext>(\"returns quota usage with bookmarks and assets\", async ({\n      db,\n      unauthedAPICaller,\n    }) => {\n      const user = await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"test@test.com\",\n        password: \"pass1234\",\n        confirmPassword: \"pass1234\",\n      });\n      const caller = getApiCaller(db, user.id);\n\n      // Set user quotas\n      await db\n        .update(users)\n        .set({\n          bookmarkQuota: 100,\n          storageQuota: 1000000, // 1MB\n        })\n        .where(eq(users.id, user.id));\n\n      // Create test bookmarks\n      const bookmark1 = await caller.bookmarks.createBookmark({\n        url: \"https://example.com\",\n        type: BookmarkTypes.LINK,\n      });\n\n      const bookmark2 = await caller.bookmarks.createBookmark({\n        text: \"Test note\",\n        type: BookmarkTypes.TEXT,\n      });\n\n      // Create test assets\n      await db.insert(assets).values([\n        {\n          id: \"asset1\",\n          assetType: AssetTypes.LINK_SCREENSHOT,\n          size: 50000, // 50KB\n          contentType: \"image/png\",\n          bookmarkId: bookmark1.id,\n          userId: user.id,\n        },\n        {\n          id: \"asset2\",\n          assetType: AssetTypes.LINK_BANNER_IMAGE,\n          size: 75000, // 75KB\n          contentType: \"image/jpeg\",\n          bookmarkId: bookmark2.id,\n          userId: user.id,\n        },\n      ]);\n\n      const usage = await caller.subscriptions.getQuotaUsage();\n\n      expect(usage).toEqual({\n        bookmarks: {\n          used: 2,\n          quota: 100,\n          unlimited: false,\n        },\n        storage: {\n          used: 125000, // 50KB + 75KB\n          quota: 1000000,\n          unlimited: false,\n        },\n      });\n    });\n  });\n\n  describe(\"handleWebhook\", () => {\n    test<CustomTestContext>(\"handles customer.subscription.created event\", async ({\n      db,\n      unauthedAPICaller,\n    }) => {\n      const user = await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"test@test.com\",\n        password: \"pass1234\",\n        confirmPassword: \"pass1234\",\n      });\n\n      // Create existing subscription record\n      await db.insert(subscriptions).values({\n        userId: user.id,\n        stripeCustomerId: \"cus_123\",\n        status: \"unpaid\",\n        tier: \"free\",\n      });\n\n      const mockEvent = {\n        type: \"customer.subscription.created\",\n        data: {\n          object: {\n            id: \"sub_123\",\n            customer: \"cus_123\",\n            status: \"active\",\n            current_period_start: 1640995200, // 2022-01-01\n            current_period_end: 1643673600, // 2022-02-01\n            metadata: {\n              userId: user.id,\n            },\n          },\n        },\n      };\n\n      // Mock the Stripe subscriptions.list response\n      mockSubscriptionsList.mockResolvedValue({\n        data: [\n          {\n            id: \"sub_123\",\n            status: \"active\",\n            cancel_at_period_end: false,\n            items: {\n              data: [\n                {\n                  price: { id: \"price_123\" },\n                  current_period_start: 1640995200,\n                  current_period_end: 1643673600,\n                },\n              ],\n            },\n          },\n        ],\n      });\n\n      mockWebhooksConstructEvent.mockReturnValue(mockEvent);\n\n      const result = await unauthedAPICaller.subscriptions.handleWebhook({\n        body: \"webhook-body\",\n        signature: \"webhook-signature\",\n      });\n\n      expect(result).toEqual({ received: true });\n\n      // Verify subscription was updated\n      const subscription = await db.query.subscriptions.findFirst({\n        where: eq(subscriptions.userId, user.id),\n      });\n\n      expect(subscription).toBeTruthy();\n      expect(subscription?.stripeCustomerId).toBe(\"cus_123\");\n      expect(subscription?.stripeSubscriptionId).toBe(\"sub_123\");\n      expect(subscription?.status).toBe(\"active\");\n      expect(subscription?.tier).toBe(\"paid\");\n    });\n\n    test<CustomTestContext>(\"handles customer.subscription.updated event\", async ({\n      db,\n      unauthedAPICaller,\n    }) => {\n      const user = await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"test@test.com\",\n        password: \"pass1234\",\n        confirmPassword: \"pass1234\",\n      });\n\n      // Create existing subscription\n      await db.insert(subscriptions).values({\n        userId: user.id,\n        stripeCustomerId: \"cus_123\",\n        stripeSubscriptionId: \"sub_123\",\n        status: \"active\",\n        tier: \"paid\",\n      });\n\n      const mockEvent = {\n        type: \"customer.subscription.updated\",\n        data: {\n          object: {\n            id: \"sub_123\",\n            customer: \"cus_123\",\n            status: \"past_due\",\n            current_period_start: 1640995200,\n            current_period_end: 1643673600,\n            metadata: {\n              userId: user.id,\n            },\n          },\n        },\n      };\n\n      // Mock the Stripe subscriptions.list response\n      mockSubscriptionsList.mockResolvedValue({\n        data: [\n          {\n            id: \"sub_123\",\n            status: \"past_due\",\n            cancel_at_period_end: false,\n            items: {\n              data: [\n                {\n                  price: { id: \"price_123\" },\n                  current_period_start: 1640995200,\n                  current_period_end: 1643673600,\n                },\n              ],\n            },\n          },\n        ],\n      });\n\n      mockWebhooksConstructEvent.mockReturnValue(mockEvent);\n\n      const result = await unauthedAPICaller.subscriptions.handleWebhook({\n        body: \"webhook-body\",\n        signature: \"webhook-signature\",\n      });\n\n      expect(result).toEqual({ received: true });\n\n      // Verify subscription was updated\n      const subscription = await db.query.subscriptions.findFirst({\n        where: eq(subscriptions.userId, user.id),\n      });\n\n      expect(subscription?.status).toBe(\"past_due\");\n      expect(subscription?.tier).toBe(\"free\"); // past_due status should set tier to free\n    });\n\n    test<CustomTestContext>(\"handles customer.subscription.deleted event\", async ({\n      db,\n      unauthedAPICaller,\n    }) => {\n      const user = await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"test@test.com\",\n        password: \"pass1234\",\n        confirmPassword: \"pass1234\",\n      });\n\n      // Create existing subscription\n      await db.insert(subscriptions).values({\n        userId: user.id,\n        stripeCustomerId: \"cus_123\",\n        stripeSubscriptionId: \"sub_123\",\n        status: \"active\",\n        tier: \"paid\",\n      });\n\n      const mockEvent = {\n        type: \"customer.subscription.deleted\",\n        data: {\n          object: {\n            id: \"sub_123\",\n            customer: \"cus_123\",\n            metadata: {\n              userId: user.id,\n            },\n          },\n        },\n      };\n\n      // Mock the Stripe subscriptions.list response for deleted subscription (empty list)\n      mockSubscriptionsList.mockResolvedValue({\n        data: [],\n      });\n\n      mockWebhooksConstructEvent.mockReturnValue(mockEvent);\n\n      const result = await unauthedAPICaller.subscriptions.handleWebhook({\n        body: \"webhook-body\",\n        signature: \"webhook-signature\",\n      });\n\n      expect(result).toEqual({ received: true });\n\n      // Verify subscription was updated to canceled state\n      const subscription = await db.query.subscriptions.findFirst({\n        where: eq(subscriptions.userId, user.id),\n      });\n\n      expect(subscription).toBeTruthy();\n      expect(subscription?.status).toBe(\"canceled\");\n      expect(subscription?.tier).toBe(\"free\");\n      expect(subscription?.stripeSubscriptionId).toBeNull();\n      expect(subscription?.priceId).toBeNull();\n      expect(subscription?.cancelAtPeriodEnd).toBe(false);\n      expect(subscription?.startDate).toBeNull();\n      expect(subscription?.endDate).toBeNull();\n    });\n\n    test<CustomTestContext>(\"handles unknown webhook event type\", async ({\n      unauthedAPICaller,\n    }) => {\n      const mockEvent = {\n        type: \"unknown.event.type\",\n        data: {\n          object: {},\n        },\n      };\n\n      mockWebhooksConstructEvent.mockReturnValue(mockEvent);\n\n      const result = await unauthedAPICaller.subscriptions.handleWebhook({\n        body: \"webhook-body\",\n        signature: \"webhook-signature\",\n      });\n\n      expect(result).toEqual({ received: true });\n    });\n\n    test<CustomTestContext>(\"handles invalid webhook signature\", async ({\n      unauthedAPICaller,\n    }) => {\n      mockWebhooksConstructEvent.mockImplementation(() => {\n        throw new Error(\"Invalid signature\");\n      });\n\n      await expect(\n        unauthedAPICaller.subscriptions.handleWebhook({\n          body: \"webhook-body\",\n          signature: \"invalid-signature\",\n        }),\n      ).rejects.toThrow(/Invalid signature/);\n    });\n  });\n\n  describe(\"quota updates on tier changes\", () => {\n    test<CustomTestContext>(\"updates quotas to paid limits on tier promotion\", async ({\n      db,\n      unauthedAPICaller,\n    }) => {\n      const user = await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"test@test.com\",\n        password: \"pass1234\",\n        confirmPassword: \"pass1234\",\n      });\n\n      // Set initial free tier quotas\n      await db\n        .update(users)\n        .set({\n          bookmarkQuota: 100,\n          storageQuota: 1000000, // 1MB\n        })\n        .where(eq(users.id, user.id));\n\n      // Create subscription record\n      await db.insert(subscriptions).values({\n        userId: user.id,\n        stripeCustomerId: \"cus_123\",\n        status: \"unpaid\",\n        tier: \"free\",\n      });\n\n      const mockEvent = {\n        type: \"customer.subscription.created\",\n        data: {\n          object: {\n            id: \"sub_123\",\n            customer: \"cus_123\",\n            status: \"active\",\n            current_period_start: 1640995200,\n            current_period_end: 1643673600,\n            metadata: {\n              userId: user.id,\n            },\n          },\n        },\n      };\n\n      // Mock the Stripe subscriptions.list response\n      mockSubscriptionsList.mockResolvedValue({\n        data: [\n          {\n            id: \"sub_123\",\n            status: \"active\",\n            cancel_at_period_end: false,\n            items: {\n              data: [\n                {\n                  price: { id: \"price_123\" },\n                  current_period_start: 1640995200,\n                  current_period_end: 1643673600,\n                },\n              ],\n            },\n          },\n        ],\n      });\n\n      mockWebhooksConstructEvent.mockReturnValue(mockEvent);\n\n      await unauthedAPICaller.subscriptions.handleWebhook({\n        body: \"webhook-body\",\n        signature: \"webhook-signature\",\n      });\n\n      // Verify user quotas were updated to paid limits\n      const updatedUser = await db.query.users.findFirst({\n        where: eq(users.id, user.id),\n        columns: {\n          bookmarkQuota: true,\n          storageQuota: true,\n        },\n      });\n\n      expect(updatedUser?.bookmarkQuota).toBeNull(); // unlimited for paid\n      expect(updatedUser?.storageQuota).toBeNull(); // unlimited for paid\n    });\n\n    test<CustomTestContext>(\"updates quotas to free limits on tier demotion\", async ({\n      db,\n      unauthedAPICaller,\n    }) => {\n      const user = await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"test@test.com\",\n        password: \"pass1234\",\n        confirmPassword: \"pass1234\",\n      });\n\n      // Set initial paid tier quotas (unlimited)\n      await db\n        .update(users)\n        .set({\n          bookmarkQuota: null,\n          storageQuota: null,\n        })\n        .where(eq(users.id, user.id));\n\n      // Create active subscription\n      await db.insert(subscriptions).values({\n        userId: user.id,\n        stripeCustomerId: \"cus_123\",\n        stripeSubscriptionId: \"sub_123\",\n        status: \"active\",\n        tier: \"paid\",\n      });\n\n      const mockEvent = {\n        type: \"customer.subscription.updated\",\n        data: {\n          object: {\n            id: \"sub_123\",\n            customer: \"cus_123\",\n            status: \"past_due\",\n            current_period_start: 1640995200,\n            current_period_end: 1643673600,\n            metadata: {\n              userId: user.id,\n            },\n          },\n        },\n      };\n\n      // Mock the Stripe subscriptions.list response for past_due status\n      mockSubscriptionsList.mockResolvedValue({\n        data: [\n          {\n            id: \"sub_123\",\n            status: \"past_due\",\n            cancel_at_period_end: false,\n            items: {\n              data: [\n                {\n                  price: { id: \"price_123\" },\n                  current_period_start: 1640995200,\n                  current_period_end: 1643673600,\n                },\n              ],\n            },\n          },\n        ],\n      });\n\n      mockWebhooksConstructEvent.mockReturnValue(mockEvent);\n\n      await unauthedAPICaller.subscriptions.handleWebhook({\n        body: \"webhook-body\",\n        signature: \"webhook-signature\",\n      });\n\n      // Verify user quotas were updated to free limits\n      const updatedUser = await db.query.users.findFirst({\n        where: eq(users.id, user.id),\n        columns: {\n          bookmarkQuota: true,\n          storageQuota: true,\n        },\n      });\n\n      expect(updatedUser?.bookmarkQuota).toBe(100); // free tier limit\n      expect(updatedUser?.storageQuota).toBe(1000000); // free tier limit (1MB)\n    });\n\n    test<CustomTestContext>(\"updates quotas to free limits on subscription cancellation\", async ({\n      db,\n      unauthedAPICaller,\n    }) => {\n      const user = await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"test@test.com\",\n        password: \"pass1234\",\n        confirmPassword: \"pass1234\",\n      });\n\n      // Set initial paid tier quotas (unlimited)\n      await db\n        .update(users)\n        .set({\n          bookmarkQuota: null,\n          storageQuota: null,\n        })\n        .where(eq(users.id, user.id));\n\n      // Create active subscription\n      await db.insert(subscriptions).values({\n        userId: user.id,\n        stripeCustomerId: \"cus_123\",\n        stripeSubscriptionId: \"sub_123\",\n        status: \"active\",\n        tier: \"paid\",\n      });\n\n      const mockEvent = {\n        type: \"customer.subscription.deleted\",\n        data: {\n          object: {\n            id: \"sub_123\",\n            customer: \"cus_123\",\n            metadata: {\n              userId: user.id,\n            },\n          },\n        },\n      };\n\n      // Mock the Stripe subscriptions.list response for deleted subscription (empty list)\n      mockSubscriptionsList.mockResolvedValue({\n        data: [],\n      });\n\n      mockWebhooksConstructEvent.mockReturnValue(mockEvent);\n\n      await unauthedAPICaller.subscriptions.handleWebhook({\n        body: \"webhook-body\",\n        signature: \"webhook-signature\",\n      });\n\n      // Verify user quotas were updated to free limits\n      const updatedUser = await db.query.users.findFirst({\n        where: eq(users.id, user.id),\n        columns: {\n          bookmarkQuota: true,\n          storageQuota: true,\n        },\n      });\n\n      expect(updatedUser?.bookmarkQuota).toBe(100); // free tier limit\n      expect(updatedUser?.storageQuota).toBe(1000000); // free tier limit (1MB)\n    });\n  });\n});\n"
  },
  {
    "path": "packages/trpc/routers/subscriptions.ts",
    "content": "// Thanks to @t3dotgg for the recommendations (https://github.com/t3dotgg/stripe-recommendations)!\n\nimport { TRPCError } from \"@trpc/server\";\nimport { count, eq, sum } from \"drizzle-orm\";\nimport Stripe from \"stripe\";\nimport { z } from \"zod\";\n\nimport { assets, bookmarks, subscriptions, users } from \"@karakeep/db/schema\";\nimport serverConfig from \"@karakeep/shared/config\";\n\nimport { authedProcedure, Context, publicProcedure, router } from \"../index\";\n\nconst stripe = serverConfig.stripe.secretKey\n  ? new Stripe(serverConfig.stripe.secretKey, {\n      // @ts-expect-error overrides the pinned API version\n      apiVersion: \"2025-06-30.basil; managed_payments_preview=v1\",\n    })\n  : null;\n\nfunction requireStripeConfig() {\n  if (!stripe || !serverConfig.stripe.priceId) {\n    throw new TRPCError({\n      code: \"PRECONDITION_FAILED\",\n      message: \"Stripe is not configured. Please contact your administrator.\",\n    });\n  }\n  return { stripe, priceId: serverConfig.stripe.priceId };\n}\n\n// Taken from https://github.com/t3dotgg/stripe-recommendations\n\nconst allowedEvents: Stripe.Event.Type[] = [\n  \"checkout.session.completed\",\n  \"customer.subscription.created\",\n  \"customer.subscription.updated\",\n  \"customer.subscription.deleted\",\n  \"customer.subscription.paused\",\n  \"customer.subscription.resumed\",\n  \"customer.subscription.pending_update_applied\",\n  \"customer.subscription.pending_update_expired\",\n  \"customer.subscription.trial_will_end\",\n  \"invoice.paid\",\n  \"invoice.payment_failed\",\n  \"invoice.payment_action_required\",\n  \"invoice.upcoming\",\n  \"invoice.marked_uncollectible\",\n  \"invoice.payment_succeeded\",\n  \"payment_intent.succeeded\",\n  \"payment_intent.payment_failed\",\n  \"payment_intent.canceled\",\n];\n\nasync function syncStripeDataToDatabase(customerId: string, db: Context[\"db\"]) {\n  if (!stripe) {\n    throw new Error(\"Stripe is not configured\");\n  }\n\n  const existingSubscription = await db.query.subscriptions.findFirst({\n    where: eq(subscriptions.stripeCustomerId, customerId),\n  });\n\n  if (!existingSubscription) {\n    console.error(\n      `ERROR: No subscription found for customer with this ID ${customerId}`,\n    );\n    throw new TRPCError({\n      code: \"INTERNAL_SERVER_ERROR\",\n      message: \"No subscription found for customer with this ID\",\n    });\n  }\n\n  try {\n    const subscriptionsList = await stripe.subscriptions.list({\n      customer: customerId,\n      limit: 1,\n      status: \"all\",\n    });\n\n    if (subscriptionsList.data.length === 0) {\n      await db.transaction(async (trx) => {\n        await trx\n          .update(subscriptions)\n          .set({\n            status: \"canceled\",\n            tier: \"free\",\n            stripeSubscriptionId: null,\n            priceId: null,\n            cancelAtPeriodEnd: false,\n            startDate: null,\n            endDate: null,\n          })\n          .where(eq(subscriptions.stripeCustomerId, customerId));\n\n        // Update user quotas to free tier limits and disable browser crawling\n        await trx\n          .update(users)\n          .set({\n            bookmarkQuota: serverConfig.quotas.free.bookmarkLimit,\n            storageQuota: serverConfig.quotas.free.assetSizeBytes,\n            browserCrawlingEnabled:\n              serverConfig.quotas.free.browserCrawlingEnabled,\n          })\n          .where(eq(users.id, existingSubscription.userId));\n      });\n      return;\n    }\n\n    const subscription = subscriptionsList.data[0];\n    const subscriptionItem = subscription.items.data[0];\n\n    const subData = {\n      stripeSubscriptionId: subscription.id,\n      status: subscription.status,\n      tier:\n        subscription.status === \"active\" || subscription.status === \"trialing\"\n          ? (\"paid\" as const)\n          : (\"free\" as const),\n      priceId: subscription.items.data[0]?.price.id || null,\n      cancelAtPeriodEnd: subscription.cancel_at_period_end,\n      startDate: subscriptionItem.current_period_start\n        ? new Date(subscriptionItem.current_period_start * 1000)\n        : null,\n      endDate: subscriptionItem.current_period_end\n        ? new Date(subscriptionItem.current_period_end * 1000)\n        : null,\n    };\n\n    await db.transaction(async (trx) => {\n      await trx\n        .update(subscriptions)\n        .set(subData)\n        .where(eq(subscriptions.stripeCustomerId, customerId));\n\n      if (subData.status === \"active\" || subData.status === \"trialing\") {\n        // Enable paid tier quotas and browser crawling\n        await trx\n          .update(users)\n          .set({\n            bookmarkQuota: serverConfig.quotas.paid.bookmarkLimit,\n            storageQuota: serverConfig.quotas.paid.assetSizeBytes,\n            browserCrawlingEnabled:\n              serverConfig.quotas.paid.browserCrawlingEnabled,\n          })\n          .where(eq(users.id, existingSubscription.userId));\n      } else {\n        // Set free tier quotas and disable browser crawling\n        await trx\n          .update(users)\n          .set({\n            bookmarkQuota: serverConfig.quotas.free.bookmarkLimit,\n            storageQuota: serverConfig.quotas.free.assetSizeBytes,\n            browserCrawlingEnabled:\n              serverConfig.quotas.free.browserCrawlingEnabled,\n          })\n          .where(eq(users.id, existingSubscription.userId));\n      }\n    });\n\n    return subData;\n  } catch (error) {\n    console.error(\"Error syncing Stripe data:\", error);\n    throw error;\n  }\n}\n\nasync function processEvent(event: Stripe.Event, db: Context[\"db\"]) {\n  if (!allowedEvents.includes(event.type)) {\n    return;\n  }\n\n  const { customer: customerId } = event.data.object as {\n    customer: string;\n  };\n\n  if (typeof customerId !== \"string\") {\n    throw new Error(\n      `[STRIPE HOOK] Customer ID isn't string. Event type: ${event.type}`,\n    );\n  }\n\n  return await syncStripeDataToDatabase(customerId, db);\n}\n\nexport const subscriptionsRouter = router({\n  getSubscriptionStatus: authedProcedure.query(async ({ ctx }) => {\n    const subscription = await ctx.db.query.subscriptions.findFirst({\n      where: eq(subscriptions.userId, ctx.user.id),\n    });\n\n    if (!subscription) {\n      return {\n        tier: \"free\" as const,\n        status: null,\n        startDate: null,\n        endDate: null,\n        hasActiveSubscription: false,\n        cancelAtPeriodEnd: false,\n      };\n    }\n\n    return {\n      tier: subscription.tier,\n      status: subscription.status,\n      startDate: subscription.startDate,\n      endDate: subscription.endDate,\n      hasActiveSubscription:\n        subscription.status === \"active\" || subscription.status === \"trialing\",\n      cancelAtPeriodEnd: subscription.cancelAtPeriodEnd || false,\n    };\n  }),\n\n  getSubscriptionPrice: authedProcedure.query(async () => {\n    if (!stripe) {\n      throw new TRPCError({\n        code: \"PRECONDITION_FAILED\",\n        message: \"Stripe is not configured. Please contact your administrator.\",\n      });\n    }\n\n    const { priceId } = requireStripeConfig();\n\n    const price = await stripe.prices.retrieve(priceId);\n\n    return {\n      priceId: price.id,\n      currency: price.currency,\n      amount: price.unit_amount,\n    };\n  }),\n\n  createCheckoutSession: authedProcedure.mutation(async ({ ctx }) => {\n    const { stripe, priceId } = requireStripeConfig();\n\n    const user = await ctx.db.query.users.findFirst({\n      where: eq(users.id, ctx.user.id),\n      columns: {\n        email: true,\n      },\n      with: {\n        subscription: true,\n      },\n    });\n\n    if (!user) {\n      throw new TRPCError({\n        code: \"NOT_FOUND\",\n        message: \"User not found\",\n      });\n    }\n\n    const existingSubscription = user.subscription;\n\n    if (existingSubscription?.status === \"active\") {\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message: \"User already has an active subscription\",\n      });\n    }\n\n    let customerId = existingSubscription?.stripeCustomerId;\n\n    if (!customerId) {\n      const customer = await stripe.customers.create({\n        email: user.email,\n        metadata: {\n          userId: ctx.user.id,\n        },\n      });\n      customerId = customer.id;\n\n      if (!existingSubscription) {\n        await ctx.db.insert(subscriptions).values({\n          userId: ctx.user.id,\n          stripeCustomerId: customerId,\n          status: \"unpaid\",\n        });\n      } else {\n        await ctx.db\n          .update(subscriptions)\n          .set({ stripeCustomerId: customerId })\n          .where(eq(subscriptions.userId, ctx.user.id));\n      }\n    }\n\n    // @ts-expect-error managed_payments is a Stripe preview feature not yet in the SDK types\n    const session = await stripe.checkout.sessions.create({\n      customer: customerId,\n      line_items: [\n        {\n          price: priceId,\n          quantity: 1,\n        },\n      ],\n      mode: \"subscription\",\n      success_url: `${serverConfig.publicUrl}/settings/subscription?success=true`,\n      cancel_url: `${serverConfig.publicUrl}/settings/subscription?canceled=true`,\n      metadata: {\n        userId: ctx.user.id,\n      },\n      customer_update: {\n        address: \"auto\",\n      },\n      allow_promotion_codes: true,\n      managed_payments: {\n        enabled: true,\n      },\n    });\n\n    return {\n      sessionId: session.id,\n      url: session.url,\n    };\n  }),\n\n  syncWithStripe: authedProcedure.mutation(async ({ ctx }) => {\n    const subscription = await ctx.db.query.subscriptions.findFirst({\n      where: eq(subscriptions.userId, ctx.user.id),\n    });\n\n    if (!subscription?.stripeCustomerId) {\n      // No Stripe customer found for user\n      return { success: true };\n    }\n\n    await syncStripeDataToDatabase(subscription.stripeCustomerId, ctx.db);\n    return { success: true };\n  }),\n\n  createPortalSession: authedProcedure.mutation(async ({ ctx }) => {\n    const { stripe } = requireStripeConfig();\n\n    const subscription = await ctx.db.query.subscriptions.findFirst({\n      where: eq(subscriptions.userId, ctx.user.id),\n    });\n\n    if (!subscription?.stripeCustomerId) {\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message: \"No Stripe customer found\",\n      });\n    }\n\n    const session = await stripe.billingPortal.sessions.create({\n      customer: subscription.stripeCustomerId,\n      return_url: `${serverConfig.publicUrl}/settings/subscription`,\n    });\n\n    return {\n      url: session.url,\n    };\n  }),\n\n  getQuotaUsage: authedProcedure.query(async ({ ctx }) => {\n    const user = await ctx.db.query.users.findFirst({\n      where: eq(users.id, ctx.user.id),\n      columns: {\n        bookmarkQuota: true,\n        storageQuota: true,\n      },\n    });\n\n    if (!user) {\n      throw new TRPCError({\n        code: \"NOT_FOUND\",\n        message: \"User not found\",\n      });\n    }\n\n    // Get current bookmark count\n    const [{ bookmarkCount }] = await ctx.db\n      .select({ bookmarkCount: count() })\n      .from(bookmarks)\n      .where(eq(bookmarks.userId, ctx.user.id));\n\n    // Get current storage usage\n    const [{ storageUsed }] = await ctx.db\n      .select({ storageUsed: sum(assets.size) })\n      .from(assets)\n      .where(eq(assets.userId, ctx.user.id));\n\n    return {\n      bookmarks: {\n        used: bookmarkCount,\n        quota: user.bookmarkQuota,\n        unlimited: user.bookmarkQuota === null,\n      },\n      storage: {\n        used: Number(storageUsed) || 0,\n        quota: user.storageQuota,\n        unlimited: user.storageQuota === null,\n      },\n    };\n  }),\n\n  handleWebhook: publicProcedure\n    .input(\n      z.object({\n        body: z.string(),\n        signature: z.string(),\n      }),\n    )\n    .mutation(async ({ input, ctx }) => {\n      if (!stripe || !serverConfig.stripe.webhookSecret) {\n        throw new TRPCError({\n          code: \"PRECONDITION_FAILED\",\n          message: \"Stripe is not configured\",\n        });\n      }\n\n      let event: Stripe.Event;\n\n      try {\n        event = stripe.webhooks.constructEvent(\n          input.body,\n          input.signature,\n          serverConfig.stripe.webhookSecret,\n        );\n      } catch (err) {\n        console.error(\"Webhook signature verification failed:\", err);\n        throw new TRPCError({\n          code: \"BAD_REQUEST\",\n          message: \"Invalid signature\",\n        });\n      }\n\n      try {\n        await processEvent(event, ctx.db);\n        return { received: true };\n      } catch (error) {\n        console.error(\"Error processing webhook:\", error);\n        throw new TRPCError({\n          code: \"INTERNAL_SERVER_ERROR\",\n          message: \"Internal server error\",\n        });\n      }\n    }),\n});\n"
  },
  {
    "path": "packages/trpc/routers/tags.test.ts",
    "content": "import { eq } from \"drizzle-orm\";\nimport { beforeEach, describe, expect, test } from \"vitest\";\n\nimport { bookmarkTags, tagsOnBookmarks } from \"@karakeep/db/schema\";\nimport { BookmarkTypes } from \"@karakeep/shared/types/bookmarks\";\n\nimport type { CustomTestContext } from \"../testUtils\";\nimport { defaultBeforeEach } from \"../testUtils\";\n\nbeforeEach<CustomTestContext>(defaultBeforeEach(true));\n\ndescribe(\"Tags Routes\", () => {\n  test<CustomTestContext>(\"get tag\", async ({ apiCallers }) => {\n    const api = apiCallers[0].tags;\n    const createdTag = await api.create({ name: \"testTag\" });\n\n    const res = await api.get({ tagId: createdTag.id });\n    expect(res.id).toEqual(createdTag.id);\n    expect(res.name).toEqual(\"testTag\");\n    expect(res.numBookmarks).toBeGreaterThanOrEqual(0);\n  });\n\n  test<CustomTestContext>(\"get tag returns bookmark stats\", async ({\n    apiCallers,\n  }) => {\n    const tagsApi = apiCallers[0].tags;\n    const bookmarksApi = apiCallers[0].bookmarks;\n\n    const firstBookmark = await bookmarksApi.createBookmark({\n      url: \"https://example.com/first\",\n      type: BookmarkTypes.LINK,\n    });\n    const secondBookmark = await bookmarksApi.createBookmark({\n      url: \"https://example.com/second\",\n      type: BookmarkTypes.LINK,\n    });\n\n    const firstAttachment = await bookmarksApi.updateTags({\n      bookmarkId: firstBookmark.id,\n      attach: [{ tagName: \"stats-tag\" }],\n      detach: [],\n    });\n\n    const tagId = firstAttachment.attached[0];\n\n    await bookmarksApi.updateTags({\n      bookmarkId: secondBookmark.id,\n      attach: [{ tagId }],\n      detach: [],\n    });\n\n    const stats = await tagsApi.get({ tagId });\n\n    expect(stats.numBookmarks).toBe(2);\n    expect(stats.numBookmarksByAttachedType.human).toBe(2);\n    expect(stats.numBookmarksByAttachedType.ai).toBe(0);\n  });\n\n  test<CustomTestContext>(\"get tag - not found\", async ({ apiCallers }) => {\n    const api = apiCallers[0].tags;\n    await expect(() => api.get({ tagId: \"nonExistentId\" })).rejects.toThrow(\n      /Tag not found/,\n    );\n  });\n\n  test<CustomTestContext>(\"delete tag\", async ({ apiCallers, db }) => {\n    const api = apiCallers[0].tags;\n    const createdTag = await api.create({ name: \"testTag\" });\n\n    await api.delete({ tagId: createdTag.id });\n\n    const res = await db.query.bookmarkTags.findFirst({\n      where: eq(bookmarkTags.id, createdTag.id),\n    });\n    expect(res).toBeUndefined(); // Tag should be deleted\n  });\n\n  test<CustomTestContext>(\"delete tag - unauthorized\", async ({\n    apiCallers,\n  }) => {\n    const user1api = apiCallers[0].tags;\n    const createdTag = await user1api.create({ name: \"testTag\" });\n\n    const api = apiCallers[1].tags;\n    await expect(() => api.delete({ tagId: createdTag.id })).rejects.toThrow(\n      /User is not allowed to access resource/,\n    );\n  });\n\n  test<CustomTestContext>(\"delete unused tags\", async ({ apiCallers }) => {\n    const api = apiCallers[0].tags;\n    await api.create({ name: \"unusedTag\" }); // Create an unused tag\n\n    const res = await api.deleteUnused();\n    expect(res.deletedTags).toBeGreaterThanOrEqual(1); // At least one tag deleted\n  });\n\n  test<CustomTestContext>(\"update tag\", async ({ apiCallers }) => {\n    const api = apiCallers[0].tags;\n    const createdTag = await api.create({ name: \"oldName\" });\n\n    const updatedTag = await api.update({\n      tagId: createdTag.id,\n      name: \"newName\",\n    });\n    expect(updatedTag.name).toEqual(\"newName\");\n  });\n\n  test<CustomTestContext>(\"update tag - conflict\", async ({ apiCallers }) => {\n    const api = apiCallers[0].tags;\n    await api.create({ name: \"existingName\" });\n    const createdTag = await api.create({ name: \"anotherName\" });\n\n    await expect(() =>\n      api.update({ tagId: createdTag.id, name: \"existingName\" }),\n    ).rejects.toThrow(/Tag name already exists/);\n  });\n\n  test<CustomTestContext>(\"merge tags\", async ({ apiCallers }) => {\n    const api = apiCallers[0].tags;\n    const tag1 = await api.create({ name: \"tag1\" });\n    const tag2 = await api.create({ name: \"tag2\" });\n\n    // First, create a bookmark with tag2\n    const bookmarkApi = apiCallers[0].bookmarks;\n    const createdBookmark = await bookmarkApi.createBookmark({\n      url: \"https://example.com\",\n      type: BookmarkTypes.LINK,\n    });\n    await bookmarkApi.updateTags({\n      bookmarkId: createdBookmark.id,\n      attach: [{ tagName: \"tag2\" }],\n      detach: [],\n    });\n\n    // Now perform the merge\n    const result = await api.merge({\n      intoTagId: tag1.id,\n      fromTagIds: [tag2.id],\n    });\n    expect(result.mergedIntoTagId).toEqual(tag1.id);\n    expect(result.deletedTags).toContain(tag2.id);\n\n    // Verify that the bookmark now has tag1 and not tag2\n    const updatedBookmark = await bookmarkApi.getBookmark({\n      bookmarkId: createdBookmark.id,\n      includeContent: false,\n    });\n    const tagNames = updatedBookmark.tags.map((tag) => tag.name);\n    expect(tagNames).toContain(\"tag1\");\n    expect(tagNames).not.toContain(\"tag2\");\n  });\n\n  test<CustomTestContext>(\"merge tags - invalid input\", async ({\n    apiCallers,\n  }) => {\n    const api = apiCallers[0].tags;\n    await expect(() =>\n      api.merge({ intoTagId: \"tag1\", fromTagIds: [\"tag1\"] }),\n    ).rejects.toThrow(/Cannot merge tag into itself/);\n  });\n\n  describe(\"list tags\", () => {\n    test<CustomTestContext>(\"basic list\", async ({ apiCallers }) => {\n      const api = apiCallers[0].tags;\n      await api.create({ name: \"tag1\" });\n      await api.create({ name: \"tag2\" });\n\n      const res = await api.list();\n      expect(res.tags.length).toBeGreaterThanOrEqual(2);\n      expect(res.tags.some((tag) => tag.name === \"tag1\")).toBeTruthy();\n      expect(res.tags.some((tag) => tag.name === \"tag2\")).toBeTruthy();\n    });\n\n    test<CustomTestContext>(\"includes bookmark stats\", async ({\n      apiCallers,\n    }) => {\n      const tagsApi = apiCallers[0].tags;\n      const bookmarksApi = apiCallers[0].bookmarks;\n\n      const firstBookmark = await bookmarksApi.createBookmark({\n        url: \"https://example.com/list-first\",\n        type: BookmarkTypes.LINK,\n      });\n      const secondBookmark = await bookmarksApi.createBookmark({\n        url: \"https://example.com/list-second\",\n        type: BookmarkTypes.LINK,\n      });\n\n      const firstAttachment = await bookmarksApi.updateTags({\n        bookmarkId: firstBookmark.id,\n        attach: [{ tagName: \"list-stats-tag\" }],\n        detach: [],\n      });\n\n      const tagId = firstAttachment.attached[0];\n\n      await bookmarksApi.updateTags({\n        bookmarkId: secondBookmark.id,\n        attach: [{ tagId }],\n        detach: [],\n      });\n\n      const list = await tagsApi.list();\n      const tagStats = list.tags.find((tag) => tag.id === tagId);\n\n      expect(tagStats).toBeDefined();\n      expect(tagStats!.numBookmarks).toBe(2);\n      expect(tagStats!.numBookmarksByAttachedType.human).toBe(2);\n      expect(tagStats!.numBookmarksByAttachedType.ai).toBe(0);\n    });\n\n    test<CustomTestContext>(\"privacy\", async ({ apiCallers }) => {\n      const apiUser1 = apiCallers[0].tags;\n      await apiUser1.create({ name: \"user1Tag\" });\n\n      const apiUser2 = apiCallers[1].tags; // Different user\n      const resUser2 = await apiUser2.list();\n      expect(resUser2.tags.some((tag) => tag.name === \"user1Tag\")).toBeFalsy(); // Should not see other user's tags\n    });\n\n    test<CustomTestContext>(\"search by name\", async ({ apiCallers }) => {\n      const api = apiCallers[0].tags;\n\n      await api.create({ name: \"alpha\" });\n      await api.create({ name: \"beta\" });\n      await api.create({ name: \"alph2\" });\n\n      {\n        const res = await api.list({ nameContains: \"al\" });\n        expect(res.tags.length).toBe(2);\n        expect(res.tags.some((tag) => tag.name === \"alpha\")).toBeTruthy();\n        expect(res.tags.some((tag) => tag.name === \"beta\")).not.toBeTruthy();\n        expect(res.tags.some((tag) => tag.name === \"alph2\")).toBeTruthy();\n      }\n\n      {\n        const res = await api.list({ nameContains: \"beta\" });\n        expect(res.tags.length).toBe(1);\n        expect(res.tags.some((tag) => tag.name === \"beta\")).toBeTruthy();\n      }\n\n      {\n        const res = await api.list({});\n        expect(res.tags.length).toBe(3);\n      }\n    });\n\n    describe(\"pagination\", () => {\n      test<CustomTestContext>(\"basic limit and cursor\", async ({\n        apiCallers,\n      }) => {\n        const api = apiCallers[0].tags;\n\n        // Create several tags\n        await api.create({ name: \"tag1\" });\n        await api.create({ name: \"tag2\" });\n        await api.create({ name: \"tag3\" });\n        await api.create({ name: \"tag4\" });\n        await api.create({ name: \"tag5\" });\n\n        // Test first page with limit\n        const firstPage = await api.list({\n          limit: 2,\n          cursor: { page: 0 },\n        });\n        expect(firstPage.tags.length).toBe(2);\n        expect(firstPage.nextCursor).not.toBeNull();\n\n        // Test second page\n        const secondPage = await api.list({\n          limit: 2,\n          cursor: firstPage.nextCursor!,\n        });\n        expect(secondPage.tags.length).toBe(2);\n        expect(secondPage.nextCursor).not.toBeNull();\n\n        // Test third page (last page)\n        const thirdPage = await api.list({\n          limit: 2,\n          cursor: { page: 2 },\n        });\n        expect(thirdPage.tags.length).toBe(1);\n        expect(thirdPage.nextCursor).toBeNull();\n      });\n\n      test<CustomTestContext>(\"no limit returns all tags\", async ({\n        apiCallers,\n      }) => {\n        const api = apiCallers[0].tags;\n\n        await api.create({ name: \"tag1\" });\n        await api.create({ name: \"tag2\" });\n        await api.create({ name: \"tag3\" });\n\n        const res = await api.list({});\n        expect(res.tags.length).toBe(3);\n        expect(res.nextCursor).toBeNull();\n      });\n\n      test<CustomTestContext>(\"empty page\", async ({ apiCallers }) => {\n        const api = apiCallers[0].tags;\n\n        await api.create({ name: \"tag1\" });\n\n        const emptyPage = await api.list({\n          limit: 2,\n          cursor: { page: 5 }, // Way beyond available data\n        });\n        expect(emptyPage.tags.length).toBe(0);\n        expect(emptyPage.nextCursor).toBeNull();\n      });\n\n      test<CustomTestContext>(\"edge cases\", async ({ apiCallers }) => {\n        const api = apiCallers[0].tags;\n\n        // Test pagination with no tags\n        const emptyResult = await api.list({\n          limit: 10,\n          cursor: { page: 0 },\n        });\n        expect(emptyResult.tags.length).toBe(0);\n        expect(emptyResult.nextCursor).toBeNull();\n\n        // Create exactly one page worth of tags\n        await api.create({ name: \"tag1\" });\n        await api.create({ name: \"tag2\" });\n\n        const exactPage = await api.list({\n          limit: 2,\n          cursor: { page: 0 },\n        });\n        expect(exactPage.tags.length).toBe(2);\n        expect(exactPage.nextCursor).toBeNull();\n\n        // Test with limit larger than available tags\n        const oversizedLimit = await api.list({\n          limit: 100,\n          cursor: { page: 0 },\n        });\n        expect(oversizedLimit.tags.length).toBe(2);\n        expect(oversizedLimit.nextCursor).toBeNull();\n      });\n    });\n\n    describe(\"attachedBy filtering\", () => {\n      test<CustomTestContext>(\"human tags\", async ({ apiCallers, db }) => {\n        const tagsApi = apiCallers[0].tags;\n        const bookmarksApi = apiCallers[0].bookmarks;\n\n        // Create tags attached by humans\n        const bookmark = await bookmarksApi.createBookmark({\n          url: \"https://example.com/human\",\n          type: BookmarkTypes.LINK,\n        });\n\n        await bookmarksApi.updateTags({\n          bookmarkId: bookmark.id,\n          attach: [{ tagName: \"human-tag\" }],\n          detach: [],\n        });\n\n        // Create an unused tag (no attachments)\n        await tagsApi.create({ name: \"unused-tag\" });\n\n        const aiTag = await tagsApi.create({ name: \"ai-tag\" });\n        await db.insert(tagsOnBookmarks).values([\n          {\n            bookmarkId: bookmark.id,\n            tagId: aiTag.id,\n            attachedBy: \"ai\",\n          },\n        ]);\n\n        const humanTags = await tagsApi.list({ attachedBy: \"human\" });\n        expect(humanTags.tags.length).toBe(1);\n        expect(humanTags.tags[0].name).toBe(\"human-tag\");\n      });\n\n      test<CustomTestContext>(\"none (unused tags)\", async ({ apiCallers }) => {\n        const tagsApi = apiCallers[0].tags;\n        const bookmarksApi = apiCallers[0].bookmarks;\n\n        // Create a used tag\n        const bookmark = await bookmarksApi.createBookmark({\n          url: \"https://example.com/used\",\n          type: BookmarkTypes.LINK,\n        });\n\n        await bookmarksApi.updateTags({\n          bookmarkId: bookmark.id,\n          attach: [{ tagName: \"used-tag\" }],\n          detach: [],\n        });\n\n        // Create unused tags\n        await tagsApi.create({ name: \"unused-tag-1\" });\n        await tagsApi.create({ name: \"unused-tag-2\" });\n\n        const unusedTags = await tagsApi.list({ attachedBy: \"none\" });\n        expect(unusedTags.tags.length).toBe(2);\n\n        const tagNames = unusedTags.tags.map((tag) => tag.name);\n        expect(tagNames).toContain(\"unused-tag-1\");\n        expect(tagNames).toContain(\"unused-tag-2\");\n        expect(tagNames).not.toContain(\"used-tag\");\n      });\n\n      test<CustomTestContext>(\"ai tags\", async ({ apiCallers, db }) => {\n        const bookmarksApi = apiCallers[0].bookmarks;\n        const tagsApi = apiCallers[0].tags;\n\n        const tag1 = await tagsApi.create({ name: \"ai-tag\" });\n        const tag2 = await tagsApi.create({ name: \"human-tag\" });\n\n        // Create bookmarks and attach tags to give them usage\n        const bookmark1 = await bookmarksApi.createBookmark({\n          url: \"https://example.com/z\",\n          type: BookmarkTypes.LINK,\n        });\n\n        // Manually attach some tags\n        await db.insert(tagsOnBookmarks).values([\n          {\n            bookmarkId: bookmark1.id,\n            tagId: tag1.id,\n            attachedBy: \"ai\",\n          },\n          {\n            bookmarkId: bookmark1.id,\n            tagId: tag2.id,\n            attachedBy: \"human\",\n          },\n        ]);\n\n        const aiTags = await tagsApi.list({ attachedBy: \"ai\" });\n        expect(aiTags.tags.length).toBe(1);\n        expect(aiTags.tags[0].name).toBe(\"ai-tag\");\n      });\n    });\n\n    describe(\"sortBy\", () => {\n      test<CustomTestContext>(\"name sorting\", async ({ apiCallers }) => {\n        const tagsApi = apiCallers[0].tags;\n        const bookmarksApi = apiCallers[0].bookmarks;\n\n        // Create bookmarks and attach tags to give them usage\n        const bookmark1 = await bookmarksApi.createBookmark({\n          url: \"https://example.com/z\",\n          type: BookmarkTypes.LINK,\n        });\n        const bookmark2 = await bookmarksApi.createBookmark({\n          url: \"https://example.com/a\",\n          type: BookmarkTypes.LINK,\n        });\n        const bookmark3 = await bookmarksApi.createBookmark({\n          url: \"https://example.com/m\",\n          type: BookmarkTypes.LINK,\n        });\n\n        // Attach tags in order: zebra (1 use), apple (2 uses), middle (1 use)\n        await bookmarksApi.updateTags({\n          bookmarkId: bookmark1.id,\n          attach: [{ tagName: \"zebra\" }],\n          detach: [],\n        });\n\n        await bookmarksApi.updateTags({\n          bookmarkId: bookmark2.id,\n          attach: [{ tagName: \"apple\" }],\n          detach: [],\n        });\n\n        await bookmarksApi.updateTags({\n          bookmarkId: bookmark3.id,\n          attach: [{ tagName: \"apple\" }, { tagName: \"middle\" }],\n          detach: [],\n        });\n\n        // Test sorting by name (alphabetical)\n        const nameSort = await tagsApi.list({ sortBy: \"name\" });\n        expect(nameSort.tags.length).toBe(3);\n        expect(nameSort.tags[0].name).toBe(\"apple\");\n        expect(nameSort.tags[1].name).toBe(\"middle\");\n        expect(nameSort.tags[2].name).toBe(\"zebra\");\n      });\n\n      test<CustomTestContext>(\"usage sorting (default)\", async ({\n        apiCallers,\n      }) => {\n        const tagsApi = apiCallers[0].tags;\n        const bookmarksApi = apiCallers[0].bookmarks;\n\n        // Create bookmarks and attach tags with different usage counts\n        const bookmark1 = await bookmarksApi.createBookmark({\n          url: \"https://example.com/usage1\",\n          type: BookmarkTypes.LINK,\n        });\n        const bookmark2 = await bookmarksApi.createBookmark({\n          url: \"https://example.com/usage2\",\n          type: BookmarkTypes.LINK,\n        });\n        const bookmark3 = await bookmarksApi.createBookmark({\n          url: \"https://example.com/usage3\",\n          type: BookmarkTypes.LINK,\n        });\n\n        // single-use: 1 bookmark, high-use: 3 bookmarks, medium-use: 2 bookmarks\n        await bookmarksApi.updateTags({\n          bookmarkId: bookmark1.id,\n          attach: [{ tagName: \"high-use\" }],\n          detach: [],\n        });\n\n        await bookmarksApi.updateTags({\n          bookmarkId: bookmark2.id,\n          attach: [{ tagName: \"high-use\" }, { tagName: \"medium-use\" }],\n          detach: [],\n        });\n\n        await bookmarksApi.updateTags({\n          bookmarkId: bookmark3.id,\n          attach: [\n            { tagName: \"high-use\" },\n            { tagName: \"medium-use\" },\n            { tagName: \"single-use\" },\n          ],\n          detach: [],\n        });\n\n        // Test default sorting (usage) and explicit usage sorting\n        const defaultSort = await tagsApi.list({});\n        expect(defaultSort.tags.length).toBe(3);\n        expect(defaultSort.tags[0].name).toBe(\"high-use\");\n        expect(defaultSort.tags[0].numBookmarks).toBe(3);\n        expect(defaultSort.tags[1].name).toBe(\"medium-use\");\n        expect(defaultSort.tags[1].numBookmarks).toBe(2);\n        expect(defaultSort.tags[2].name).toBe(\"single-use\");\n        expect(defaultSort.tags[2].numBookmarks).toBe(1);\n\n        const usageSort = await tagsApi.list({ sortBy: \"usage\" });\n        expect(usageSort.tags[0].name).toBe(\"high-use\");\n        expect(usageSort.tags[1].name).toBe(\"medium-use\");\n        expect(usageSort.tags[2].name).toBe(\"single-use\");\n      });\n\n      test<CustomTestContext>(\"relevance sorting\", async ({ apiCallers }) => {\n        const tagsApi = apiCallers[0].tags;\n        const bookmarksApi = apiCallers[0].bookmarks;\n\n        // Create bookmarks to give tags usage\n        const bookmark1 = await bookmarksApi.createBookmark({\n          url: \"https://example.com/rel1\",\n          type: BookmarkTypes.LINK,\n        });\n\n        // Create tags with different relevance to search term \"java\"\n        await bookmarksApi.updateTags({\n          bookmarkId: bookmark1.id,\n          attach: [\n            { tagName: \"java\" }, // Exact match - highest relevance\n            { tagName: \"javascript\" }, // Prefix match\n            { tagName: \"java-script\" }, // Prefix match (shorter)\n            { tagName: \"advanced-java\" }, // Substring match\n          ],\n          detach: [],\n        });\n\n        // Test relevance sorting\n        const relevanceSort = await tagsApi.list({\n          nameContains: \"java\",\n          sortBy: \"relevance\",\n        });\n\n        expect(relevanceSort.tags.length).toBe(4);\n\n        // Exact match should be first\n        expect(relevanceSort.tags[0].name).toBe(\"java\");\n\n        // Prefix matches should come next, shorter first (by length)\n        expect(relevanceSort.tags[1].name).toBe(\"javascript\"); // length 10\n        expect(relevanceSort.tags[2].name).toBe(\"java-script\"); // length 11\n\n        // Substring matches should be last\n        expect(relevanceSort.tags[3].name).toBe(\"advanced-java\");\n      });\n\n      test<CustomTestContext>(\"relevance sorting case insensitive\", async ({\n        apiCallers,\n      }) => {\n        const tagsApi = apiCallers[0].tags;\n        const bookmarksApi = apiCallers[0].bookmarks;\n\n        const bookmark1 = await bookmarksApi.createBookmark({\n          url: \"https://example.com/case\",\n          type: BookmarkTypes.LINK,\n        });\n\n        await bookmarksApi.updateTags({\n          bookmarkId: bookmark1.id,\n          attach: [\n            { tagName: \"React\" }, // Exact match (different case)\n            { tagName: \"ReactJS\" }, // Prefix match\n            { tagName: \"my-react\" }, // Substring match\n          ],\n          detach: [],\n        });\n\n        const relevanceSort = await tagsApi.list({\n          nameContains: \"react\",\n          sortBy: \"relevance\",\n        });\n\n        expect(relevanceSort.tags.length).toBe(3);\n        expect(relevanceSort.tags[0].name).toBe(\"React\"); // Exact match first\n        expect(relevanceSort.tags[1].name).toBe(\"ReactJS\"); // Prefix match second\n        expect(relevanceSort.tags[2].name).toBe(\"my-react\"); // Substring match last\n      });\n\n      test<CustomTestContext>(\"relevance sorting without search term is prevented by validation\", async ({\n        apiCallers,\n      }) => {\n        const tagsApi = apiCallers[0].tags;\n\n        // Without nameContains, relevance sorting should throw validation error\n        await expect(() =>\n          tagsApi.list({ sortBy: \"relevance\" }),\n        ).rejects.toThrow(/Relevance sorting requires a nameContains filter/);\n      });\n    });\n\n    describe(\"combination filtering\", () => {\n      test<CustomTestContext>(\"nameContains with attachedBy\", async ({\n        apiCallers,\n      }) => {\n        const tagsApi = apiCallers[0].tags;\n        const bookmarksApi = apiCallers[0].bookmarks;\n\n        // Create bookmarks with tags\n        const bookmark1 = await bookmarksApi.createBookmark({\n          url: \"https://example.com/combo1\",\n          type: BookmarkTypes.LINK,\n        });\n        const bookmark2 = await bookmarksApi.createBookmark({\n          url: \"https://example.com/combo2\",\n          type: BookmarkTypes.LINK,\n        });\n\n        // Attach human tags with \"test\" in name\n        await bookmarksApi.updateTags({\n          bookmarkId: bookmark1.id,\n          attach: [{ tagName: \"test-human\" }],\n          detach: [],\n        });\n\n        await bookmarksApi.updateTags({\n          bookmarkId: bookmark2.id,\n          attach: [{ tagName: \"test-used\" }],\n          detach: [],\n        });\n\n        // Create unused tag with \"test\" in name\n        await tagsApi.create({ name: \"test-unused\" });\n\n        // Create used tag without \"test\" in name\n        await bookmarksApi.updateTags({\n          bookmarkId: bookmark1.id,\n          attach: [{ tagName: \"other-human\" }],\n          detach: [],\n        });\n\n        // Test combination: nameContains + attachedBy human\n        const humanTestTags = await tagsApi.list({\n          nameContains: \"test\",\n          attachedBy: \"human\",\n        });\n        expect(humanTestTags.tags.length).toBe(2);\n\n        const humanTestNames = humanTestTags.tags.map((tag) => tag.name);\n        expect(humanTestNames).toContain(\"test-human\");\n        expect(humanTestNames).toContain(\"test-used\");\n        expect(humanTestNames).not.toContain(\"test-unused\");\n        expect(humanTestNames).not.toContain(\"other-human\");\n\n        // Test combination: nameContains + attachedBy none\n        const unusedTestTags = await tagsApi.list({\n          nameContains: \"test\",\n          attachedBy: \"none\",\n        });\n        expect(unusedTestTags.tags.length).toBe(1);\n        expect(unusedTestTags.tags[0].name).toBe(\"test-unused\");\n      });\n\n      test<CustomTestContext>(\"all parameters together\", async ({\n        apiCallers,\n      }) => {\n        const tagsApi = apiCallers[0].tags;\n        const bookmarksApi = apiCallers[0].bookmarks;\n\n        // Create multiple bookmarks with various tags\n        const bookmark1 = await bookmarksApi.createBookmark({\n          url: \"https://example.com/all1\",\n          type: BookmarkTypes.LINK,\n        });\n        const bookmark2 = await bookmarksApi.createBookmark({\n          url: \"https://example.com/all2\",\n          type: BookmarkTypes.LINK,\n        });\n        const bookmark3 = await bookmarksApi.createBookmark({\n          url: \"https://example.com/all3\",\n          type: BookmarkTypes.LINK,\n        });\n\n        // Create tags with different usage patterns\n        await bookmarksApi.updateTags({\n          bookmarkId: bookmark1.id,\n          attach: [{ tagName: \"filter-high\" }],\n          detach: [],\n        });\n\n        await bookmarksApi.updateTags({\n          bookmarkId: bookmark2.id,\n          attach: [{ tagName: \"filter-high\" }, { tagName: \"filter-low\" }],\n          detach: [],\n        });\n\n        await bookmarksApi.updateTags({\n          bookmarkId: bookmark3.id,\n          attach: [{ tagName: \"filter-high\" }],\n          detach: [],\n        });\n\n        // Test all parameters: nameContains + attachedBy + sortBy + pagination\n        const result = await tagsApi.list({\n          nameContains: \"filter\",\n          attachedBy: \"human\",\n          sortBy: \"usage\",\n          limit: 1,\n          cursor: { page: 0 },\n        });\n\n        expect(result.tags.length).toBe(1);\n        expect(result.tags[0].name).toBe(\"filter-high\"); // Highest usage\n        expect(result.tags[0].numBookmarks).toBe(3);\n        expect(result.nextCursor).not.toBeNull();\n\n        // Get second page\n        const secondPage = await tagsApi.list({\n          nameContains: \"filter\",\n          attachedBy: \"human\",\n          sortBy: \"usage\",\n          limit: 1,\n          cursor: result.nextCursor!,\n        });\n\n        expect(secondPage.tags.length).toBe(1);\n        expect(secondPage.tags[0].name).toBe(\"filter-low\"); // Lower usage\n        expect(secondPage.tags[0].numBookmarks).toBe(1);\n        expect(secondPage.nextCursor).toBeNull();\n      });\n    });\n  });\n\n  test<CustomTestContext>(\"create strips extra leading hashes\", async ({\n    apiCallers,\n    db,\n  }) => {\n    const api = apiCallers[0].tags;\n\n    const created = await api.create({ name: \"##demo\" });\n    expect(created.name).toBe(\"demo\");\n\n    // Confirm DB row too\n    const row = await db.query.bookmarkTags.findFirst({\n      where: eq(bookmarkTags.id, created.id),\n    });\n    expect(row?.name).toBe(\"demo\");\n  });\n\n  test<CustomTestContext>(\"update normalizes leading hashes\", async ({\n    apiCallers,\n    db,\n  }) => {\n    const api = apiCallers[0].tags;\n\n    const created = await api.create({ name: \"#foo\" });\n    const updated = await api.update({ tagId: created.id, name: \"##bar\" });\n\n    expect(updated.name).toBe(\"bar\");\n\n    // Confirm DB row too\n    const row = await db.query.bookmarkTags.findFirst({\n      where: eq(bookmarkTags.id, updated.id),\n    });\n    expect(row?.name).toBe(\"bar\");\n  });\n});\n"
  },
  {
    "path": "packages/trpc/routers/tags.ts",
    "content": "import { experimental_trpcMiddleware } from \"@trpc/server\";\nimport { z } from \"zod\";\n\nimport {\n  zCreateTagRequestSchema,\n  zGetTagResponseSchema,\n  zTagBasicSchema,\n  zTagListResponseSchema,\n  zTagListValidatedRequestSchema,\n  zUpdateTagRequestSchema,\n} from \"@karakeep/shared/types/tags\";\n\nimport type { AuthedContext } from \"../index\";\nimport { authedProcedure, router } from \"../index\";\nimport { Tag } from \"../models/tags\";\n\nexport const ensureTagOwnership = experimental_trpcMiddleware<{\n  ctx: AuthedContext;\n  input: { tagId: string };\n}>().create(async (opts) => {\n  const tag = await Tag.fromId(opts.ctx, opts.input.tagId);\n  return opts.next({\n    ctx: {\n      ...opts.ctx,\n      tag,\n    },\n  });\n});\n\nexport const tagsAppRouter = router({\n  create: authedProcedure\n    .input(zCreateTagRequestSchema)\n    .output(zTagBasicSchema)\n    .mutation(async ({ input, ctx }) => {\n      const tag = await Tag.create(ctx, input);\n      return tag.asBasicTag();\n    }),\n\n  get: authedProcedure\n    .input(\n      z.object({\n        tagId: z.string(),\n      }),\n    )\n    .output(zGetTagResponseSchema)\n    .use(ensureTagOwnership)\n    .query(async ({ ctx }) => {\n      return await ctx.tag.getStats();\n    }),\n  delete: authedProcedure\n    .input(\n      z.object({\n        tagId: z.string(),\n      }),\n    )\n    .use(ensureTagOwnership)\n    .mutation(async ({ ctx }) => {\n      await ctx.tag.delete();\n    }),\n  deleteUnused: authedProcedure\n    .output(\n      z.object({\n        deletedTags: z.number(),\n      }),\n    )\n    .mutation(async ({ ctx }) => {\n      const deletedCount = await Tag.deleteUnused(ctx);\n      return { deletedTags: deletedCount };\n    }),\n  update: authedProcedure\n    .input(zUpdateTagRequestSchema)\n    .output(zTagBasicSchema)\n    .use(ensureTagOwnership)\n    .mutation(async ({ input, ctx }) => {\n      await ctx.tag.update(input);\n      return ctx.tag.asBasicTag();\n    }),\n  merge: authedProcedure\n    .input(\n      z.object({\n        intoTagId: z.string(),\n        fromTagIds: z.array(z.string()),\n      }),\n    )\n    .output(\n      z.object({\n        mergedIntoTagId: z.string(),\n        deletedTags: z.array(z.string()),\n      }),\n    )\n    .mutation(async ({ input, ctx }) => {\n      return await Tag.merge(ctx, input);\n    }),\n  list: authedProcedure\n    .input(\n      // TODO: Remove the optional and default once the next release is out.\n      zTagListValidatedRequestSchema\n        .optional()\n        .default(zTagListValidatedRequestSchema.parse({})),\n    )\n    .output(zTagListResponseSchema)\n    .query(async ({ ctx, input }) => {\n      return await Tag.getAll(ctx, {\n        nameContains: input.nameContains,\n        ids: input.ids,\n        attachedBy: input.attachedBy,\n        sortBy: input.sortBy,\n        pagination: input.limit\n          ? {\n              page: input.cursor?.page ?? 0,\n              limit: input.limit,\n            }\n          : undefined,\n      });\n    }),\n});\n"
  },
  {
    "path": "packages/trpc/routers/users.test.ts",
    "content": "import { eq } from \"drizzle-orm\";\nimport { assert, beforeEach, describe, expect, test, vi } from \"vitest\";\n\nimport {\n  assets,\n  AssetTypes,\n  bookmarks,\n  passwordResetTokens,\n  users,\n} from \"@karakeep/db/schema\";\nimport { BookmarkTypes } from \"@karakeep/shared/types/bookmarks\";\n\nimport type { CustomTestContext } from \"../testUtils\";\nimport * as emailModule from \"../email\";\nimport { defaultBeforeEach, getApiCaller } from \"../testUtils\";\n\n// Mock server config with email settings\nvi.mock(\"@karakeep/shared/config\", async (original) => {\n  const mod = (await original()) as typeof import(\"@karakeep/shared/config\");\n  return {\n    ...mod,\n    default: {\n      ...mod.default,\n      auth: {\n        ...mod.default.auth,\n        emailVerificationRequired: true,\n      },\n      email: {\n        smtp: {\n          host: \"test-smtp.example.com\",\n          port: 587,\n          secure: false,\n          user: \"test@example.com\",\n          password: \"test-password\",\n          from: \"test@example.com\",\n        },\n      },\n    },\n  };\n});\n\n// Mock email functions\nvi.mock(\"../email\", () => ({\n  sendPasswordResetEmail: vi.fn().mockResolvedValue(undefined),\n  sendVerificationEmail: vi.fn().mockResolvedValue(undefined),\n}));\n\nbeforeEach<CustomTestContext>(defaultBeforeEach(false));\n\ndescribe(\"User Routes\", () => {\n  test<CustomTestContext>(\"create user\", async ({ unauthedAPICaller }) => {\n    const user = await unauthedAPICaller.users.create({\n      name: \"Test User\",\n      email: \"test123@test.com\",\n      password: \"pass1234\",\n      confirmPassword: \"pass1234\",\n    });\n\n    expect(user.name).toEqual(\"Test User\");\n    expect(user.email).toEqual(\"test123@test.com\");\n  });\n\n  test<CustomTestContext>(\"first user is admin\", async ({\n    unauthedAPICaller,\n  }) => {\n    const user1 = await unauthedAPICaller.users.create({\n      name: \"Test User\",\n      email: \"test123@test.com\",\n      password: \"pass1234\",\n      confirmPassword: \"pass1234\",\n    });\n\n    const user2 = await unauthedAPICaller.users.create({\n      name: \"Test User\",\n      email: \"test124@test.com\",\n      password: \"pass1234\",\n      confirmPassword: \"pass1234\",\n    });\n\n    expect(user1.role).toEqual(\"admin\");\n    expect(user2.role).toEqual(\"user\");\n  });\n\n  test<CustomTestContext>(\"unique emails\", async ({ unauthedAPICaller }) => {\n    await unauthedAPICaller.users.create({\n      name: \"Test User\",\n      email: \"test123@test.com\",\n      password: \"pass1234\",\n      confirmPassword: \"pass1234\",\n    });\n\n    await expect(() =>\n      unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"test123@test.com\",\n        password: \"pass1234\",\n        confirmPassword: \"pass1234\",\n      }),\n    ).rejects.toThrow(/Email is already taken/);\n  });\n\n  test<CustomTestContext>(\"privacy checks\", async ({\n    db,\n    unauthedAPICaller,\n  }) => {\n    const adminUser = await unauthedAPICaller.users.create({\n      name: \"Test User\",\n      email: \"test123@test.com\",\n      password: \"pass1234\",\n      confirmPassword: \"pass1234\",\n    });\n    const [user1, user2] = await Promise.all(\n      [\"test1234@test.com\", \"test12345@test.com\"].map((e) =>\n        unauthedAPICaller.users.create({\n          name: \"Test User\",\n          email: e,\n          password: \"pass1234\",\n          confirmPassword: \"pass1234\",\n        }),\n      ),\n    );\n\n    assert(adminUser.role == \"admin\");\n    assert(user1.role == \"user\");\n    assert(user2.role == \"user\");\n\n    const user2Caller = getApiCaller(db, user2.id);\n\n    // A normal user can't delete other users\n    await expect(() =>\n      user2Caller.users.delete({\n        userId: user1.id,\n      }),\n    ).rejects.toThrow(/FORBIDDEN/);\n\n    // A normal user can't list all users\n    await expect(() => user2Caller.users.list()).rejects.toThrow(/FORBIDDEN/);\n  });\n\n  test<CustomTestContext>(\"get/update user settings\", async ({\n    db,\n    unauthedAPICaller,\n  }) => {\n    const user = await unauthedAPICaller.users.create({\n      name: \"Test User\",\n      email: \"testupdate@test.com\",\n      password: \"pass1234\",\n      confirmPassword: \"pass1234\",\n    });\n    const caller = getApiCaller(db, user.id);\n\n    const settings = await caller.users.settings();\n    // The default settings\n    expect(settings).toEqual({\n      bookmarkClickAction: \"open_original_link\",\n      archiveDisplayBehaviour: \"show\",\n      timezone: \"UTC\",\n      backupsEnabled: false,\n      backupsFrequency: \"weekly\",\n      backupsRetentionDays: 30,\n\n      // Reader settings\n      readerFontFamily: null,\n      readerFontSize: null,\n      readerLineHeight: null,\n\n      // AI Settings\n      autoSummarizationEnabled: null,\n      autoTaggingEnabled: null,\n      curatedTagIds: null,\n      inferredTagLang: null,\n      tagStyle: \"titlecase-spaces\",\n    });\n\n    // Update settings\n    await caller.users.updateSettings({\n      bookmarkClickAction: \"expand_bookmark_preview\",\n      backupsEnabled: true,\n      backupsFrequency: \"daily\",\n      backupsRetentionDays: 7,\n\n      // Reader settings\n      readerFontFamily: \"serif\",\n      readerFontSize: 12,\n      readerLineHeight: 1.5,\n\n      // AI Settings\n      autoSummarizationEnabled: true,\n      autoTaggingEnabled: true,\n      inferredTagLang: \"en\",\n      tagStyle: \"lowercase-underscores\",\n    });\n\n    // Verify updated settings\n    const updatedSettings = await caller.users.settings();\n    expect(updatedSettings).toEqual({\n      bookmarkClickAction: \"expand_bookmark_preview\",\n      archiveDisplayBehaviour: \"show\",\n      timezone: \"UTC\",\n      backupsEnabled: true,\n      backupsFrequency: \"daily\",\n      backupsRetentionDays: 7,\n\n      // Reader settings\n      readerFontFamily: \"serif\",\n      readerFontSize: 12,\n      readerLineHeight: 1.5,\n\n      // AI Settings\n      autoSummarizationEnabled: true,\n      autoTaggingEnabled: true,\n      curatedTagIds: null,\n      inferredTagLang: \"en\",\n      tagStyle: \"lowercase-underscores\",\n    });\n\n    // Test invalid update (e.g., empty input, if schema enforces it)\n    await expect(() => caller.users.updateSettings({})).rejects.toThrow(\n      /No settings provided/,\n    );\n  });\n\n  test<CustomTestContext>(\"user stats - empty user\", async ({\n    db,\n    unauthedAPICaller,\n  }) => {\n    const user = await unauthedAPICaller.users.create({\n      name: \"Test User\",\n      email: \"stats@test.com\",\n      password: \"pass1234\",\n      confirmPassword: \"pass1234\",\n    });\n    const caller = getApiCaller(db, user.id);\n\n    const stats = await caller.users.stats();\n\n    // All stats should be zero for a new user\n    expect(stats.numBookmarks).toBe(0);\n    expect(stats.numFavorites).toBe(0);\n    expect(stats.numArchived).toBe(0);\n    expect(stats.numTags).toBe(0);\n    expect(stats.numLists).toBe(0);\n    expect(stats.numHighlights).toBe(0);\n    expect(stats.bookmarksByType).toEqual({ link: 0, text: 0, asset: 0 });\n    expect(stats.topDomains).toEqual([]);\n    expect(stats.totalAssetSize).toBe(0);\n    expect(stats.assetsByType).toEqual([]);\n    expect(stats.tagUsage).toEqual([]);\n    expect(stats.bookmarkingActivity.thisWeek).toBe(0);\n    expect(stats.bookmarkingActivity.thisMonth).toBe(0);\n    expect(stats.bookmarkingActivity.thisYear).toBe(0);\n    expect(stats.bookmarkingActivity.byHour).toHaveLength(24);\n    expect(stats.bookmarkingActivity.byDayOfWeek).toHaveLength(7);\n\n    // All hours and days should have 0 count\n    stats.bookmarkingActivity.byHour.forEach((hour, index) => {\n      expect(hour.hour).toBe(index);\n      expect(hour.count).toBe(0);\n    });\n    stats.bookmarkingActivity.byDayOfWeek.forEach((day, index) => {\n      expect(day.day).toBe(index);\n      expect(day.count).toBe(0);\n    });\n  });\n\n  test<CustomTestContext>(\"user stats - with data\", async ({\n    db,\n    unauthedAPICaller,\n  }) => {\n    const user = await unauthedAPICaller.users.create({\n      name: \"Test User\",\n      email: \"statsdata@test.com\",\n      password: \"pass1234\",\n      confirmPassword: \"pass1234\",\n    });\n    const caller = getApiCaller(db, user.id);\n\n    // Create test bookmarks\n    const bookmark1 = await caller.bookmarks.createBookmark({\n      url: \"https://example.com/page1\",\n      type: BookmarkTypes.LINK,\n    });\n\n    const bookmark2 = await caller.bookmarks.createBookmark({\n      url: \"https://google.com/search\",\n      type: BookmarkTypes.LINK,\n    });\n\n    await caller.bookmarks.createBookmark({\n      text: \"Test note content\",\n      type: BookmarkTypes.TEXT,\n    });\n\n    // Create tags\n    const tag1 = await caller.tags.create({ name: \"tech\" });\n    const tag2 = await caller.tags.create({ name: \"work\" });\n\n    // Create lists\n    await caller.lists.create({\n      name: \"Test List\",\n      icon: \"📚\",\n      type: \"manual\",\n    });\n\n    // Archive one bookmark\n    await caller.bookmarks.updateBookmark({\n      bookmarkId: bookmark1.id,\n      archived: true,\n    });\n\n    // Favorite one bookmark\n    await caller.bookmarks.updateBookmark({\n      bookmarkId: bookmark2.id,\n      favourited: true,\n    });\n\n    // Add tags to bookmarks\n    await caller.bookmarks.updateTags({\n      bookmarkId: bookmark1.id,\n      attach: [{ tagId: tag1.id }],\n      detach: [],\n    });\n\n    await caller.bookmarks.updateTags({\n      bookmarkId: bookmark2.id,\n      attach: [{ tagId: tag1.id }, { tagId: tag2.id }],\n      detach: [],\n    });\n\n    // Create highlights\n    await caller.highlights.create({\n      bookmarkId: bookmark1.id,\n      startOffset: 0,\n      endOffset: 10,\n      text: \"highlighted text\",\n      note: \"test note\",\n    });\n\n    // Insert test assets directly into DB\n    await db.insert(assets).values([\n      {\n        id: \"asset1\",\n        assetType: AssetTypes.LINK_SCREENSHOT,\n        size: 1024,\n        contentType: \"image/png\",\n        bookmarkId: bookmark1.id,\n        userId: user.id,\n      },\n      {\n        id: \"asset2\",\n        assetType: AssetTypes.LINK_BANNER_IMAGE,\n        size: 2048,\n        contentType: \"image/jpeg\",\n        bookmarkId: bookmark2.id,\n        userId: user.id,\n      },\n    ]);\n\n    const stats = await caller.users.stats();\n\n    // Verify basic counts\n    expect(stats.numBookmarks).toBe(3);\n    expect(stats.numFavorites).toBe(1);\n    expect(stats.numArchived).toBe(1);\n    expect(stats.numTags).toBe(2);\n    expect(stats.numLists).toBe(1);\n    expect(stats.numHighlights).toBe(1);\n\n    // Verify bookmark types\n    expect(stats.bookmarksByType.link).toBe(2);\n    expect(stats.bookmarksByType.text).toBe(1);\n    expect(stats.bookmarksByType.asset).toBe(0);\n\n    // Verify top domains\n    expect(stats.topDomains).toHaveLength(2);\n    expect(\n      stats.topDomains.find((d) => d.domain === \"example.com\"),\n    ).toBeTruthy();\n    expect(\n      stats.topDomains.find((d) => d.domain === \"google.com\"),\n    ).toBeTruthy();\n\n    // Verify asset stats\n    expect(stats.totalAssetSize).toBe(3072); // 1024 + 2048\n    expect(stats.assetsByType).toHaveLength(2);\n\n    const screenshotAsset = stats.assetsByType.find(\n      (a) => a.type === AssetTypes.LINK_SCREENSHOT,\n    );\n    expect(screenshotAsset?.count).toBe(1);\n    expect(screenshotAsset?.totalSize).toBe(1024);\n\n    const bannerAsset = stats.assetsByType.find(\n      (a) => a.type === AssetTypes.LINK_BANNER_IMAGE,\n    );\n    expect(bannerAsset?.count).toBe(1);\n    expect(bannerAsset?.totalSize).toBe(2048);\n\n    // Verify tag usage\n    expect(stats.tagUsage).toHaveLength(2);\n    const techTag = stats.tagUsage.find((t) => t.name === \"tech\");\n    const workTag = stats.tagUsage.find((t) => t.name === \"work\");\n    expect(techTag?.count).toBe(2); // Used in 2 bookmarks\n    expect(workTag?.count).toBe(1); // Used in 1 bookmark\n\n    // Verify activity stats (should be > 0 since we just created bookmarks)\n    expect(stats.bookmarkingActivity.thisWeek).toBe(3);\n    expect(stats.bookmarkingActivity.thisMonth).toBe(3);\n    expect(stats.bookmarkingActivity.thisYear).toBe(3);\n\n    // Verify hour/day arrays are properly structured\n    expect(stats.bookmarkingActivity.byHour).toHaveLength(24);\n    expect(stats.bookmarkingActivity.byDayOfWeek).toHaveLength(7);\n  });\n\n  test<CustomTestContext>(\"user stats - privacy isolation\", async ({\n    db,\n    unauthedAPICaller,\n  }) => {\n    // Create two users\n    const user1 = await unauthedAPICaller.users.create({\n      name: \"User 1\",\n      email: \"user1@test.com\",\n      password: \"pass1234\",\n      confirmPassword: \"pass1234\",\n    });\n\n    const user2 = await unauthedAPICaller.users.create({\n      name: \"User 2\",\n      email: \"user2@test.com\",\n      password: \"pass1234\",\n      confirmPassword: \"pass1234\",\n    });\n\n    const caller1 = getApiCaller(db, user1.id);\n    const caller2 = getApiCaller(db, user2.id);\n\n    // User 1 creates some bookmarks\n    const bookmark1 = await caller1.bookmarks.createBookmark({\n      url: \"https://user1.com\",\n      type: BookmarkTypes.LINK,\n    });\n\n    const tag1 = await caller1.tags.create({ name: \"user1tag\" });\n\n    // Attach tag to bookmark\n    await caller1.bookmarks.updateTags({\n      bookmarkId: bookmark1.id,\n      attach: [{ tagId: tag1.id }],\n      detach: [],\n    });\n\n    // User 2 creates different bookmarks\n    const bookmark2 = await caller2.bookmarks.createBookmark({\n      url: \"https://user2.com\",\n      type: BookmarkTypes.LINK,\n    });\n\n    const tag2 = await caller2.tags.create({ name: \"user2tag\" });\n\n    // Attach tag to bookmark\n    await caller2.bookmarks.updateTags({\n      bookmarkId: bookmark2.id,\n      attach: [{ tagId: tag2.id }],\n      detach: [],\n    });\n\n    // Get stats for both users\n    const stats1 = await caller1.users.stats();\n    const stats2 = await caller2.users.stats();\n\n    // Each user should only see their own data\n    expect(stats1.numBookmarks).toBe(1);\n    expect(stats1.numTags).toBe(1);\n    expect(stats1.topDomains[0]?.domain).toBe(\"user1.com\");\n    expect(stats1.tagUsage[0]?.name).toBe(\"user1tag\");\n\n    expect(stats2.numBookmarks).toBe(1);\n    expect(stats2.numTags).toBe(1);\n    expect(stats2.topDomains[0]?.domain).toBe(\"user2.com\");\n    expect(stats2.tagUsage[0]?.name).toBe(\"user2tag\");\n\n    // Users should not see each other's data\n    expect(stats1.topDomains.find((d) => d.domain === \"user2.com\")).toBeFalsy();\n    expect(stats2.topDomains.find((d) => d.domain === \"user1.com\")).toBeFalsy();\n  });\n\n  test<CustomTestContext>(\"user stats - activity time patterns\", async ({\n    db,\n    unauthedAPICaller,\n  }) => {\n    const user = await unauthedAPICaller.users.create({\n      name: \"Test User\",\n      email: \"timepatterns@test.com\",\n      password: \"pass1234\",\n      confirmPassword: \"pass1234\",\n    });\n    const caller = getApiCaller(db, user.id);\n\n    // Create bookmarks with specific timestamps\n    const now = new Date();\n    const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);\n    const oneWeekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);\n    const oneMonthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);\n\n    // Insert bookmarks directly with specific timestamps\n    await db\n      .insert(bookmarks)\n      .values([\n        {\n          userId: user.id,\n          type: BookmarkTypes.LINK,\n          createdAt: now,\n          archived: false,\n          favourited: false,\n        },\n        {\n          userId: user.id,\n          type: BookmarkTypes.LINK,\n          createdAt: oneDayAgo,\n          archived: false,\n          favourited: false,\n        },\n        {\n          userId: user.id,\n          type: BookmarkTypes.LINK,\n          createdAt: oneWeekAgo,\n          archived: false,\n          favourited: false,\n        },\n        {\n          userId: user.id,\n          type: BookmarkTypes.LINK,\n          createdAt: oneMonthAgo,\n          archived: false,\n          favourited: false,\n        },\n      ])\n      .returning();\n\n    const stats = await caller.users.stats();\n\n    // Verify activity counts based on time periods\n    expect(stats.bookmarkingActivity.thisWeek).toBeGreaterThanOrEqual(2); // now + oneDayAgo\n    expect(stats.bookmarkingActivity.thisMonth).toBeGreaterThanOrEqual(3); // now + oneDayAgo + oneWeekAgo\n    expect(stats.bookmarkingActivity.thisYear).toBe(4); // All bookmarks\n\n    // Verify that hour and day arrays have proper structure\n    expect(\n      stats.bookmarkingActivity.byHour.every(\n        (h) => typeof h.hour === \"number\" && h.hour >= 0 && h.hour <= 23,\n      ),\n    ).toBe(true);\n\n    expect(\n      stats.bookmarkingActivity.byDayOfWeek.every(\n        (d) => typeof d.day === \"number\" && d.day >= 0 && d.day <= 6,\n      ),\n    ).toBe(true);\n  });\n\n  describe(\"Password Reset\", () => {\n    test<CustomTestContext>(\"forgotPassword - successful email sending\", async ({\n      unauthedAPICaller,\n    }) => {\n      // Create a user first\n      await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"reset@test.com\",\n        password: \"pass1234\",\n        confirmPassword: \"pass1234\",\n      });\n\n      // With mocked email service, this should succeed\n      const result = await unauthedAPICaller.users.forgotPassword({\n        email: \"reset@test.com\",\n      });\n\n      expect(result.success).toBe(true);\n\n      // Verify that the email function was called with correct parameters\n      expect(emailModule.sendPasswordResetEmail).toHaveBeenCalledWith(\n        \"reset@test.com\",\n        \"Test User\",\n        expect.any(String), // token\n      );\n    });\n\n    test<CustomTestContext>(\"forgotPassword - non-existing user\", async ({\n      unauthedAPICaller,\n    }) => {\n      // Should not reveal if user exists or not\n      const result = await unauthedAPICaller.users.forgotPassword({\n        email: \"nonexistent@test.com\",\n      });\n\n      expect(result.success).toBe(true);\n    });\n\n    test<CustomTestContext>(\"forgotPassword - OAuth user (no password)\", async ({\n      db,\n      unauthedAPICaller,\n    }) => {\n      // Create a user without password (OAuth user)\n      await db.insert(users).values({\n        name: \"OAuth User\",\n        email: \"oauth@test.com\",\n        password: null,\n      });\n\n      // Should not send reset email for OAuth users\n      const result = await unauthedAPICaller.users.forgotPassword({\n        email: \"oauth@test.com\",\n      });\n\n      expect(result.success).toBe(true);\n    });\n\n    test<CustomTestContext>(\"resetPassword - valid token\", async ({\n      db,\n      unauthedAPICaller,\n    }) => {\n      // Create a user\n      const user = await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"validreset@test.com\",\n        password: \"oldpass123\",\n        confirmPassword: \"oldpass123\",\n      });\n\n      // Create a password reset token directly in the database\n      const token = \"valid-reset-token\";\n      const expires = new Date(Date.now() + 60 * 60 * 1000); // 1 hour from now\n\n      await db.insert(passwordResetTokens).values({\n        userId: user.id,\n        token,\n        expires,\n      });\n\n      // Reset the password\n      const result = await unauthedAPICaller.users.resetPassword({\n        token,\n        newPassword: \"newpass123\",\n      });\n\n      expect(result.success).toBe(true);\n\n      // Verify the token was consumed (deleted)\n      const remainingTokens = await db\n        .select()\n        .from(passwordResetTokens)\n        .where(eq(passwordResetTokens.token, token));\n\n      expect(remainingTokens).toHaveLength(0);\n\n      // The password reset was successful if we got here without errors\n    });\n\n    test<CustomTestContext>(\"resetPassword - invalid token\", async ({\n      unauthedAPICaller,\n    }) => {\n      await expect(\n        unauthedAPICaller.users.resetPassword({\n          token: \"invalid-token\",\n          newPassword: \"newpass123\",\n        }),\n      ).rejects.toThrow(/Invalid or expired reset token/);\n    });\n\n    test<CustomTestContext>(\"resetPassword - expired token\", async ({\n      db,\n      unauthedAPICaller,\n    }) => {\n      // Create a user\n      const user = await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"expiredtoken@test.com\",\n        password: \"oldpass123\",\n        confirmPassword: \"oldpass123\",\n      });\n\n      // Create an expired password reset token\n      const token = \"expired-reset-token\";\n      const expires = new Date(Date.now() - 60 * 60 * 1000); // 1 hour ago (expired)\n\n      await db.insert(passwordResetTokens).values({\n        userId: user.id,\n        token,\n        expires,\n      });\n\n      await expect(\n        unauthedAPICaller.users.resetPassword({\n          token,\n          newPassword: \"newpass123\",\n        }),\n      ).rejects.toThrow(/Invalid or expired reset token/);\n\n      // Verify the expired token was cleaned up\n      const remainingTokens = await db\n        .select()\n        .from(passwordResetTokens)\n        .where(eq(passwordResetTokens.token, token));\n\n      expect(remainingTokens).toHaveLength(0);\n    });\n\n    test<CustomTestContext>(\"resetPassword - user not found\", async ({\n      db,\n      unauthedAPICaller,\n    }) => {\n      // Create a user first, then delete them to create an orphaned token\n      const user = await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"orphaned@test.com\",\n        password: \"oldpass123\",\n        confirmPassword: \"oldpass123\",\n      });\n\n      // Create a password reset token\n      const token = \"orphaned-token\";\n      const expires = new Date(Date.now() + 60 * 60 * 1000);\n\n      await db.insert(passwordResetTokens).values({\n        userId: user.id,\n        token,\n        expires,\n      });\n\n      // Delete the user to make the token orphaned\n      // Due to foreign key cascade, this will also delete the token\n      // So we expect \"Invalid or expired reset token\" instead of \"User not found\"\n      await db.delete(users).where(eq(users.id, user.id));\n\n      await expect(\n        unauthedAPICaller.users.resetPassword({\n          token,\n          newPassword: \"newpass123\",\n        }),\n      ).rejects.toThrow(/Invalid or expired reset token/);\n    });\n    test<CustomTestContext>(\"resetPassword - password validation\", async ({\n      db,\n      unauthedAPICaller,\n    }) => {\n      // Create a user\n      const user = await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"validation@test.com\",\n        password: \"oldpass123\",\n        confirmPassword: \"oldpass123\",\n      });\n\n      // Create a password reset token\n      const token = \"validation-token\";\n      const expires = new Date(Date.now() + 60 * 60 * 1000);\n\n      await db.insert(passwordResetTokens).values({\n        userId: user.id,\n        token,\n        expires,\n      });\n\n      // Try to reset with a password that's too short\n      await expect(\n        unauthedAPICaller.users.resetPassword({\n          token,\n          newPassword: \"123\", // Too short\n        }),\n      ).rejects.toThrow();\n    });\n\n    test<CustomTestContext>(\"resetPassword - token reuse prevention\", async ({\n      db,\n      unauthedAPICaller,\n    }) => {\n      // Create a user\n      const user = await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"reuse@test.com\",\n        password: \"oldpass123\",\n        confirmPassword: \"oldpass123\",\n      });\n\n      // Create a password reset token\n      const token = \"reuse-token\";\n      const expires = new Date(Date.now() + 60 * 60 * 1000);\n\n      await db.insert(passwordResetTokens).values({\n        userId: user.id,\n        token,\n        expires,\n      });\n\n      // Use the token once\n      await unauthedAPICaller.users.resetPassword({\n        token,\n        newPassword: \"newpass123\",\n      });\n\n      // Try to use the same token again\n      await expect(\n        unauthedAPICaller.users.resetPassword({\n          token,\n          newPassword: \"anotherpass123\",\n        }),\n      ).rejects.toThrow(/Invalid or expired reset token/);\n    });\n  });\n\n  describe(\"Change Password\", () => {\n    test<CustomTestContext>(\"changePassword - successful change\", async ({\n      db,\n      unauthedAPICaller,\n    }) => {\n      const user = await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"changepass@test.com\",\n        password: \"oldpass123\",\n        confirmPassword: \"oldpass123\",\n      });\n      const caller = getApiCaller(db, user.id, user.email, user.role || \"user\");\n\n      await caller.users.changePassword({\n        currentPassword: \"oldpass123\",\n        newPassword: \"newpass456\",\n      });\n\n      // Password change should succeed without throwing\n    });\n\n    test<CustomTestContext>(\"changePassword - wrong current password\", async ({\n      db,\n      unauthedAPICaller,\n    }) => {\n      const user = await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"wrongpass@test.com\",\n        password: \"oldpass123\",\n        confirmPassword: \"oldpass123\",\n      });\n      const caller = getApiCaller(db, user.id, user.email, user.role || \"user\");\n\n      await expect(() =>\n        caller.users.changePassword({\n          currentPassword: \"wrongpassword\",\n          newPassword: \"newpass456\",\n        }),\n      ).rejects.toThrow();\n    });\n\n    test<CustomTestContext>(\"changePassword - OAuth user (no password)\", async ({\n      db,\n    }) => {\n      // Create OAuth user without password\n      await db.insert(users).values({\n        name: \"OAuth User\",\n        email: \"oauth@test.com\",\n        password: null,\n      });\n\n      const oauthUser = await db\n        .select()\n        .from(users)\n        .where(eq(users.email, \"oauth@test.com\"))\n        .then((rows) => rows[0]);\n\n      const caller = getApiCaller(db, oauthUser.id, oauthUser.email, \"user\");\n\n      await expect(() =>\n        caller.users.changePassword({\n          currentPassword: \"anypassword\",\n          newPassword: \"newpass456\",\n        }),\n      ).rejects.toThrow();\n    });\n  });\n\n  describe(\"Delete Account\", () => {\n    test<CustomTestContext>(\"deleteAccount - with password\", async ({\n      db,\n      unauthedAPICaller,\n    }) => {\n      const user = await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"deleteaccount@test.com\",\n        password: \"pass1234\",\n        confirmPassword: \"pass1234\",\n      });\n      const caller = getApiCaller(db, user.id, user.email, user.role || \"user\");\n\n      await caller.users.deleteAccount({\n        password: \"pass1234\",\n      });\n\n      // Verify user is deleted\n      const deletedUser = await db\n        .select()\n        .from(users)\n        .where(eq(users.id, user.id));\n      expect(deletedUser).toHaveLength(0);\n    });\n\n    test<CustomTestContext>(\"deleteAccount - wrong password\", async ({\n      db,\n      unauthedAPICaller,\n    }) => {\n      const user = await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"wrongdeletepass@test.com\",\n        password: \"pass1234\",\n        confirmPassword: \"pass1234\",\n      });\n      const caller = getApiCaller(db, user.id, user.email, user.role || \"user\");\n\n      await expect(() =>\n        caller.users.deleteAccount({\n          password: \"wrongpassword\",\n        }),\n      ).rejects.toThrow();\n    });\n\n    test<CustomTestContext>(\"deleteAccount - OAuth user (no password)\", async ({\n      db,\n    }) => {\n      // Create OAuth user without password\n      await db.insert(users).values({\n        name: \"OAuth User\",\n        email: \"oauthdelete@test.com\",\n        password: null,\n      });\n\n      const oauthUser = await db\n        .select()\n        .from(users)\n        .where(eq(users.email, \"oauthdelete@test.com\"))\n        .then((rows) => rows[0]);\n\n      const caller = getApiCaller(db, oauthUser.id, oauthUser.email, \"user\");\n\n      await caller.users.deleteAccount({});\n\n      // Verify user is deleted\n      const deletedUser = await db\n        .select()\n        .from(users)\n        .where(eq(users.id, oauthUser.id));\n      expect(deletedUser).toHaveLength(0);\n    });\n  });\n\n  describe(\"Update Avatar\", () => {\n    test<CustomTestContext>(\"updateAvatar - promotes unknown asset\", async ({\n      db,\n      unauthedAPICaller,\n    }) => {\n      const user = await unauthedAPICaller.users.create({\n        name: \"Avatar Reject\",\n        email: \"avatar-reject@test.com\",\n        password: \"pass1234\",\n        confirmPassword: \"pass1234\",\n      });\n      const caller = getApiCaller(db, user.id, user.email, user.role || \"user\");\n\n      await db.insert(assets).values({\n        id: \"avatar-asset-2\",\n        assetType: AssetTypes.UNKNOWN,\n        userId: user.id,\n        contentType: \"image/png\",\n        size: 12,\n        fileName: \"avatar.png\",\n        bookmarkId: null,\n      });\n\n      await caller.users.updateAvatar({ assetId: \"avatar-asset-2\" });\n\n      const updatedAsset = await db\n        .select()\n        .from(assets)\n        .where(eq(assets.id, \"avatar-asset-2\"))\n        .then((rows) => rows[0]);\n\n      expect(updatedAsset?.assetType).toBe(AssetTypes.AVATAR);\n    });\n\n    test<CustomTestContext>(\"updateAvatar - deletes avatar asset\", async ({\n      db,\n      unauthedAPICaller,\n    }) => {\n      const user = await unauthedAPICaller.users.create({\n        name: \"Avatar Delete\",\n        email: \"avatar-delete@test.com\",\n        password: \"pass1234\",\n        confirmPassword: \"pass1234\",\n      });\n      const caller = getApiCaller(db, user.id, user.email, user.role || \"user\");\n\n      await db.insert(assets).values({\n        id: \"avatar-asset-3\",\n        assetType: AssetTypes.UNKNOWN,\n        userId: user.id,\n        contentType: \"image/png\",\n        size: 12,\n        fileName: \"avatar.png\",\n        bookmarkId: null,\n      });\n\n      await caller.users.updateAvatar({ assetId: \"avatar-asset-3\" });\n      await caller.users.updateAvatar({ assetId: null });\n\n      const updatedUser = await db\n        .select()\n        .from(users)\n        .where(eq(users.id, user.id))\n        .then((rows) => rows[0]);\n      const remainingAsset = await db\n        .select()\n        .from(assets)\n        .where(eq(assets.id, \"avatar-asset-3\"))\n        .then((rows) => rows[0]);\n\n      expect(updatedUser?.image).toBeNull();\n      expect(remainingAsset).toBeUndefined();\n    });\n  });\n\n  describe(\"Who Am I\", () => {\n    test<CustomTestContext>(\"whoami - returns user info\", async ({\n      db,\n      unauthedAPICaller,\n    }) => {\n      const user = await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"whoami@test.com\",\n        password: \"pass1234\",\n        confirmPassword: \"pass1234\",\n      });\n      const caller = getApiCaller(db, user.id, user.email, user.role || \"user\");\n\n      const whoami = await caller.users.whoami();\n\n      expect(whoami.id).toBe(user.id);\n      expect(whoami.name).toBe(\"Test User\");\n      expect(whoami.email).toBe(\"whoami@test.com\");\n      expect(whoami.localUser).toBe(true);\n    });\n\n    test<CustomTestContext>(\"whoami - OAuth user\", async ({ db }) => {\n      // Create OAuth user\n      await db.insert(users).values({\n        name: \"OAuth User\",\n        email: \"oauthwhoami@test.com\",\n        password: null,\n      });\n\n      const oauthUser = await db\n        .select()\n        .from(users)\n        .where(eq(users.email, \"oauthwhoami@test.com\"))\n        .then((rows) => rows[0]);\n\n      const caller = getApiCaller(db, oauthUser.id, oauthUser.email, \"user\");\n\n      const whoami = await caller.users.whoami();\n\n      expect(whoami.id).toBe(oauthUser.id);\n      expect(whoami.name).toBe(\"OAuth User\");\n      expect(whoami.email).toBe(\"oauthwhoami@test.com\");\n      expect(whoami.localUser).toBe(false);\n    });\n  });\n\n  describe(\"Email Verification\", () => {\n    test<CustomTestContext>(\"verifyEmail - invalid token\", async ({\n      unauthedAPICaller,\n    }) => {\n      await expect(() =>\n        unauthedAPICaller.users.verifyEmail({\n          email: \"nonexistent@test.com\",\n          token: \"invalid-token\",\n        }),\n      ).rejects.toThrow();\n    });\n\n    test<CustomTestContext>(\"verifyEmail - invalid email format\", async ({\n      unauthedAPICaller,\n    }) => {\n      await expect(() =>\n        unauthedAPICaller.users.verifyEmail({\n          email: \"invalid-email\",\n          token: \"some-token\",\n        }),\n      ).rejects.toThrow();\n    });\n  });\n\n  describe(\"Resend Verification Email\", () => {\n    test<CustomTestContext>(\"resendVerificationEmail - existing user\", async ({\n      unauthedAPICaller,\n    }) => {\n      // Create user first\n      await unauthedAPICaller.users.create({\n        name: \"Test User\",\n        email: \"resend@test.com\",\n        password: \"pass1234\",\n        confirmPassword: \"pass1234\",\n      });\n\n      const result = await unauthedAPICaller.users.resendVerificationEmail({\n        email: \"resend@test.com\",\n      });\n\n      expect(result.success).toBe(true);\n\n      // Verify that the email function was called\n      expect(emailModule.sendVerificationEmail).toHaveBeenCalledWith(\n        \"resend@test.com\",\n        \"Test User\",\n        expect.any(String), // token\n        undefined, // redirectUrl\n      );\n    });\n\n    test<CustomTestContext>(\"resendVerificationEmail - non-existing user\", async ({\n      unauthedAPICaller,\n    }) => {\n      // Should not reveal if user exists or not\n      const result = await unauthedAPICaller.users.resendVerificationEmail({\n        email: \"nonexistent@test.com\",\n      });\n      expect(result.success).toBe(true);\n    });\n\n    test<CustomTestContext>(\"resendVerificationEmail - invalid email format\", async ({\n      unauthedAPICaller,\n    }) => {\n      await expect(() =>\n        unauthedAPICaller.users.resendVerificationEmail({\n          email: \"invalid-email\",\n        }),\n      ).rejects.toThrow();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/trpc/routers/users.ts",
    "content": "import { TRPCError } from \"@trpc/server\";\nimport { z } from \"zod\";\n\nimport serverConfig from \"@karakeep/shared/config\";\nimport {\n  zResetPasswordSchema,\n  zSignUpSchema,\n  zUpdateUserSettingsSchema,\n  zUserSettingsSchema,\n  zUserStatsResponseSchema,\n  zWhoAmIResponseSchema,\n  zWrappedStatsResponseSchema,\n} from \"@karakeep/shared/types/users\";\nimport { validateRedirectUrl } from \"@karakeep/shared/utils/redirectUrl\";\n\nimport {\n  adminProcedure,\n  authedProcedure,\n  createRateLimitMiddleware,\n  publicProcedure,\n  router,\n} from \"../index\";\nimport { verifyTurnstileToken } from \"../lib/turnstile\";\nimport { User } from \"../models/users\";\n\nexport const usersAppRouter = router({\n  create: publicProcedure\n    .use(\n      createRateLimitMiddleware({\n        name: \"users.create\",\n        windowMs: 60 * 1000,\n        maxRequests: 3,\n      }),\n    )\n    .input(zSignUpSchema.and(z.object({ redirectUrl: z.string().optional() })))\n    .output(\n      z.object({\n        id: z.string(),\n        name: z.string(),\n        email: z.string(),\n        role: z.enum([\"user\", \"admin\"]).nullable(),\n      }),\n    )\n    .mutation(async ({ input, ctx }) => {\n      if (\n        serverConfig.auth.disableSignups ||\n        serverConfig.auth.disablePasswordAuth\n      ) {\n        const errorMessage = serverConfig.auth.disablePasswordAuth\n          ? \"Local Signups are disabled in the server config. Use OAuth instead!\"\n          : \"Signups are disabled in server config\";\n        throw new TRPCError({\n          code: \"FORBIDDEN\",\n          message: errorMessage,\n        });\n      }\n      if (serverConfig.auth.turnstile.enabled) {\n        const result = await verifyTurnstileToken(\n          input.turnstileToken ?? \"\",\n          ctx.req.ip,\n        );\n        if (!result.success) {\n          throw new TRPCError({\n            code: \"BAD_REQUEST\",\n            message: \"Turnstile verification failed\",\n          });\n        }\n      }\n      const validatedRedirectUrl = validateRedirectUrl(input.redirectUrl);\n      const user = await User.create(ctx, {\n        ...input,\n        redirectUrl: validatedRedirectUrl,\n      });\n      return {\n        id: user.id,\n        name: user.name,\n        email: user.email,\n        role: user.role,\n      };\n    }),\n  list: adminProcedure\n    .output(\n      z.object({\n        users: z.array(\n          z.object({\n            id: z.string(),\n            name: z.string(),\n            email: z.string(),\n            role: z.enum([\"user\", \"admin\"]).nullable(),\n            localUser: z.boolean(),\n            bookmarkQuota: z.number().nullable(),\n            storageQuota: z.number().nullable(),\n          }),\n        ),\n      }),\n    )\n    .query(async ({ ctx }) => {\n      const users = await User.getAll(ctx);\n      return {\n        users: users.map((u) => u.asPublicUser()),\n      };\n    }),\n  changePassword: authedProcedure\n    .input(\n      z.object({\n        currentPassword: z.string(),\n        newPassword: z.string(),\n      }),\n    )\n    .mutation(async ({ input, ctx }) => {\n      const user = await User.fromCtx(ctx);\n      await user.changePassword(input.currentPassword, input.newPassword);\n    }),\n  delete: adminProcedure\n    .input(\n      z.object({\n        userId: z.string(),\n      }),\n    )\n    .mutation(async ({ input, ctx }) => {\n      await User.deleteAsAdmin(ctx, input.userId);\n    }),\n  deleteAccount: authedProcedure\n    .input(\n      z.object({\n        password: z.string().optional(),\n      }),\n    )\n    .mutation(async ({ input, ctx }) => {\n      const user = await User.fromCtx(ctx);\n      await user.deleteAccount(input.password);\n    }),\n  whoami: authedProcedure\n    .output(zWhoAmIResponseSchema)\n    .query(async ({ ctx }) => {\n      const user = await User.fromCtx(ctx);\n      return user.asWhoAmI();\n    }),\n  stats: authedProcedure\n    .output(zUserStatsResponseSchema)\n    .query(async ({ ctx }) => {\n      const user = await User.fromCtx(ctx);\n      return await user.getStats();\n    }),\n  wrapped: authedProcedure\n    .output(zWrappedStatsResponseSchema)\n    .query(async ({ ctx }) => {\n      throw new TRPCError({\n        code: \"BAD_REQUEST\",\n        message: \"This endpoint is currently disabled\",\n      });\n      const user = await User.fromCtx(ctx);\n      return await user.getWrappedStats(2025);\n    }),\n  hasWrapped: authedProcedure.output(z.boolean()).query(async ({ ctx }) => {\n    throw new TRPCError({\n      code: \"BAD_REQUEST\",\n      message: \"This endpoint is currently disabled\",\n    });\n    const user = await User.fromCtx(ctx);\n    return await user.hasWrapped();\n  }),\n  settings: authedProcedure\n    .output(zUserSettingsSchema)\n    .query(async ({ ctx }) => {\n      const user = await User.fromCtx(ctx);\n      return await user.getSettings();\n    }),\n  updateSettings: authedProcedure\n    .input(zUpdateUserSettingsSchema)\n    .mutation(async ({ input, ctx }) => {\n      const user = await User.fromCtx(ctx);\n      await user.updateSettings(input);\n    }),\n  updateAvatar: authedProcedure\n    .input(\n      z.object({\n        assetId: z.string().nullable(),\n      }),\n    )\n    .mutation(async ({ input, ctx }) => {\n      const user = await User.fromCtx(ctx);\n      await user.updateAvatar(input.assetId);\n    }),\n  verifyEmail: publicProcedure\n    .use(\n      createRateLimitMiddleware({\n        name: \"users.verifyEmail\",\n        windowMs: 5 * 60 * 1000,\n        maxRequests: 10,\n      }),\n    )\n    .input(\n      z.object({\n        email: z.string().email(),\n        token: z.string(),\n      }),\n    )\n    .mutation(async ({ input, ctx }) => {\n      await User.verifyEmail(ctx, input.email, input.token);\n      return { success: true };\n    }),\n  resendVerificationEmail: publicProcedure\n    .use(\n      createRateLimitMiddleware({\n        name: \"users.resendVerificationEmail\",\n        windowMs: 5 * 60 * 1000,\n        maxRequests: 3,\n      }),\n    )\n    .input(\n      z.object({\n        email: z.string().email(),\n        redirectUrl: z.string().optional(),\n      }),\n    )\n    .mutation(async ({ input, ctx }) => {\n      const validatedRedirectUrl = validateRedirectUrl(input.redirectUrl);\n      await User.resendVerificationEmail(\n        ctx,\n        input.email,\n        validatedRedirectUrl,\n      );\n      return { success: true };\n    }),\n  forgotPassword: publicProcedure\n    .use(\n      createRateLimitMiddleware({\n        name: \"users.forgotPassword\",\n        windowMs: 15 * 60 * 1000,\n        maxRequests: 3,\n      }),\n    )\n    .input(\n      z.object({\n        email: z.string().email(),\n      }),\n    )\n    .mutation(async ({ input, ctx }) => {\n      await User.forgotPassword(ctx, input.email);\n      return { success: true };\n    }),\n  resetPassword: publicProcedure\n    .use(\n      createRateLimitMiddleware({\n        name: \"users.resetPassword\",\n        windowMs: 5 * 60 * 1000,\n        maxRequests: 10,\n      }),\n    )\n    .input(zResetPasswordSchema)\n    .mutation(async ({ input, ctx }) => {\n      await User.resetPassword(ctx, input);\n      return { success: true };\n    }),\n});\n"
  },
  {
    "path": "packages/trpc/routers/webhooks.test.ts",
    "content": "import { beforeEach, describe, expect, test } from \"vitest\";\n\nimport type { CustomTestContext } from \"../testUtils\";\nimport { defaultBeforeEach } from \"../testUtils\";\n\nbeforeEach<CustomTestContext>(defaultBeforeEach(true));\n\ndescribe(\"Webhook Routes\", () => {\n  test<CustomTestContext>(\"create webhook\", async ({ apiCallers }) => {\n    const api = apiCallers[0].webhooks;\n    const newWebhook = await api.create({\n      url: \"https://example.com/webhook\",\n      events: [\"created\", \"edited\"],\n    });\n\n    expect(newWebhook).toBeDefined();\n    expect(newWebhook.url).toEqual(\"https://example.com/webhook\");\n    expect(newWebhook.events).toEqual([\"created\", \"edited\"]);\n    expect(newWebhook.hasToken).toBe(false); // Assuming token is not set by default\n  });\n\n  test<CustomTestContext>(\"update webhook\", async ({ apiCallers }) => {\n    const api = apiCallers[0].webhooks;\n\n    // First, create a webhook to update\n    const createdWebhook = await api.create({\n      url: \"https://example.com/webhook\",\n      events: [\"created\"],\n    });\n\n    // Update it\n    const updatedWebhook = await api.update({\n      webhookId: createdWebhook.id,\n      url: \"https://updated-example.com/webhook\",\n      events: [\"created\", \"edited\"],\n      token: \"test-token\",\n    });\n\n    expect(updatedWebhook.url).toEqual(\"https://updated-example.com/webhook\");\n    expect(updatedWebhook.events).toEqual([\"created\", \"edited\"]);\n    expect(updatedWebhook.hasToken).toBe(true);\n\n    // Test updating a non-existent webhook\n    await expect(() =>\n      api.update({\n        webhookId: \"non-existent-id\",\n        url: \"https://fail.com\",\n        events: [\"created\"],\n      }),\n    ).rejects.toThrow(/Webhook not found/);\n  });\n\n  test<CustomTestContext>(\"list webhooks\", async ({ apiCallers }) => {\n    const api = apiCallers[0].webhooks;\n\n    // Create a couple of webhooks\n    await api.create({\n      url: \"https://example1.com/webhook\",\n      events: [\"created\"],\n    });\n    await api.create({\n      url: \"https://example2.com/webhook\",\n      events: [\"edited\"],\n    });\n\n    const result = await api.list();\n    expect(result.webhooks).toBeDefined();\n    expect(result.webhooks.length).toBeGreaterThanOrEqual(2);\n    expect(\n      result.webhooks.some((w) => w.url === \"https://example1.com/webhook\"),\n    ).toBe(true);\n    expect(\n      result.webhooks.some((w) => w.url === \"https://example2.com/webhook\"),\n    ).toBe(true);\n  });\n\n  test<CustomTestContext>(\"delete webhook\", async ({ apiCallers }) => {\n    const api = apiCallers[0].webhooks;\n\n    // Create a webhook to delete\n    const createdWebhook = await api.create({\n      url: \"https://example.com/webhook\",\n      events: [\"created\"],\n    });\n\n    // Delete it\n    await api.delete({ webhookId: createdWebhook.id });\n\n    // Verify it's deleted\n    await expect(() =>\n      api.update({\n        webhookId: createdWebhook.id,\n        url: \"https://updated.com\",\n        events: [\"created\"],\n      }),\n    ).rejects.toThrow(/Webhook not found/);\n  });\n\n  test<CustomTestContext>(\"privacy for webhooks\", async ({ apiCallers }) => {\n    const user1Webhook = await apiCallers[0].webhooks.create({\n      url: \"https://user1-webhook.com\",\n      events: [\"created\"],\n    });\n    const user2Webhook = await apiCallers[1].webhooks.create({\n      url: \"https://user2-webhook.com\",\n      events: [\"created\"],\n    });\n\n    // User 1 should not access User 2's webhook\n    await expect(() =>\n      apiCallers[0].webhooks.delete({ webhookId: user2Webhook.id }),\n    ).rejects.toThrow(/User is not allowed to access resource/);\n    await expect(() =>\n      apiCallers[0].webhooks.update({\n        webhookId: user2Webhook.id,\n        url: \"https://fail.com\",\n        events: [\"created\"],\n      }),\n    ).rejects.toThrow(/User is not allowed to access resource/);\n\n    // List should only show the correct user's webhooks\n    const user1List = await apiCallers[0].webhooks.list();\n    expect(user1List.webhooks.some((w) => w.id === user1Webhook.id)).toBe(true);\n    expect(user1List.webhooks.some((w) => w.id === user2Webhook.id)).toBe(\n      false,\n    );\n  });\n\n  test<CustomTestContext>(\"webhook limit enforcement\", async ({\n    apiCallers,\n  }) => {\n    const api = apiCallers[0].webhooks;\n\n    // Create 100 webhooks (the maximum)\n    for (let i = 0; i < 100; i++) {\n      await api.create({\n        url: `https://example${i}.com/webhook`,\n        events: [\"created\"],\n      });\n    }\n\n    // The 101st webhook should fail\n    await expect(() =>\n      api.create({\n        url: \"https://example101.com/webhook\",\n        events: [\"created\"],\n      }),\n    ).rejects.toThrow(/Maximum number of webhooks \\(100\\) reached/);\n  });\n});\n"
  },
  {
    "path": "packages/trpc/routers/webhooks.ts",
    "content": "import { experimental_trpcMiddleware } from \"@trpc/server\";\nimport { z } from \"zod\";\n\nimport {\n  zNewWebhookSchema,\n  zUpdateWebhookSchema,\n  zWebhookSchema,\n} from \"@karakeep/shared/types/webhooks\";\n\nimport type { AuthedContext } from \"../index\";\nimport { authedProcedure, router } from \"../index\";\nimport { Webhook } from \"../models/webhooks\";\n\nexport const ensureWebhookOwnership = experimental_trpcMiddleware<{\n  ctx: AuthedContext;\n  input: { webhookId: string };\n}>().create(async (opts) => {\n  const webhook = await Webhook.fromId(opts.ctx, opts.input.webhookId);\n  return opts.next({\n    ctx: {\n      ...opts.ctx,\n      webhook,\n    },\n  });\n});\n\nexport const webhooksAppRouter = router({\n  create: authedProcedure\n    .input(zNewWebhookSchema)\n    .output(zWebhookSchema)\n    .mutation(async ({ input, ctx }) => {\n      const webhook = await Webhook.create(ctx, input);\n      return webhook.asPublicWebhook();\n    }),\n  update: authedProcedure\n    .input(zUpdateWebhookSchema)\n    .output(zWebhookSchema)\n    .use(ensureWebhookOwnership)\n    .mutation(async ({ input, ctx }) => {\n      await ctx.webhook.update(input);\n      return ctx.webhook.asPublicWebhook();\n    }),\n  list: authedProcedure\n    .output(z.object({ webhooks: z.array(zWebhookSchema) }))\n    .query(async ({ ctx }) => {\n      const webhooks = await Webhook.getAll(ctx);\n      return { webhooks: webhooks.map((w) => w.asPublicWebhook()) };\n    }),\n  delete: authedProcedure\n    .input(\n      z.object({\n        webhookId: z.string(),\n      }),\n    )\n    .use(ensureWebhookOwnership)\n    .mutation(async ({ ctx }) => {\n      await ctx.webhook.delete();\n    }),\n});\n"
  },
  {
    "path": "packages/trpc/stats.ts",
    "content": "import { count, sum } from \"drizzle-orm\";\nimport { Counter, Gauge, Histogram, register } from \"prom-client\";\n\nimport { db } from \"@karakeep/db\";\nimport { assets, bookmarks, users } from \"@karakeep/db/schema\";\nimport {\n  AdminMaintenanceQueue,\n  AssetPreprocessingQueue,\n  BackupQueue,\n  FeedQueue,\n  LinkCrawlerQueue,\n  LowPriorityCrawlerQueue,\n  OpenAIQueue,\n  RuleEngineQueue,\n  SearchIndexingQueue,\n  VideoWorkerQueue,\n  WebhookQueue,\n} from \"@karakeep/shared-server\";\n\n// Queue metrics\nconst queuePendingJobsGauge = new Gauge({\n  name: \"karakeep_queue_jobs\",\n  help: \"Number of jobs in each background queue\",\n  labelNames: [\"queue_name\", \"status\"],\n  async collect() {\n    const queues = [\n      { name: \"link_crawler\", queue: LinkCrawlerQueue },\n      { name: \"low_priority_crawler\", queue: LowPriorityCrawlerQueue },\n      { name: \"backup\", queue: BackupQueue },\n      { name: \"openai\", queue: OpenAIQueue },\n      { name: \"search_indexing\", queue: SearchIndexingQueue },\n      { name: \"admin_maintenance\", queue: AdminMaintenanceQueue },\n      { name: \"video_worker\", queue: VideoWorkerQueue },\n      { name: \"feed\", queue: FeedQueue },\n      { name: \"asset_preprocessing\", queue: AssetPreprocessingQueue },\n      { name: \"webhook\", queue: WebhookQueue },\n      { name: \"rule_engine\", queue: RuleEngineQueue },\n    ];\n\n    const stats = await Promise.all(\n      queues.map(async ({ name, queue }) => {\n        try {\n          return {\n            ...(await queue.stats()),\n            name,\n          };\n        } catch (error) {\n          console.error(`Failed to get stats for queue ${name}:`, error);\n          return { name, pending: 0, pending_retry: 0, failed: 0, running: 0 };\n        }\n      }),\n    );\n\n    stats.forEach(({ name, pending, pending_retry, failed, running }) => {\n      this.set({ queue_name: name, status: \"pending\" }, pending);\n      this.set({ queue_name: name, status: \"pending_retry\" }, pending_retry);\n      this.set({ queue_name: name, status: \"failed\" }, failed);\n      this.set({ queue_name: name, status: \"running\" }, running);\n    });\n  },\n});\n\n// User metrics\nconst totalUsersGauge = new Gauge({\n  name: \"karakeep_total_users\",\n  help: \"Total number of users in the system\",\n  async collect() {\n    try {\n      const result = await db.select({ count: count() }).from(users);\n      this.set(result[0]?.count ?? 0);\n    } catch (error) {\n      console.error(\"Failed to get user count:\", error);\n      this.set(0);\n    }\n  },\n});\n\n// Asset metrics\nconst totalAssetSizeGauge = new Gauge({\n  name: \"karakeep_total_asset_size_bytes\",\n  help: \"Total size of all assets in bytes\",\n  async collect() {\n    try {\n      const result = await db\n        .select({ totalSize: sum(assets.size) })\n        .from(assets);\n      this.set(Number(result[0]?.totalSize ?? 0));\n    } catch (error) {\n      console.error(\"Failed to get total asset size:\", error);\n      this.set(0);\n    }\n  },\n});\n\n// Bookmark metrics\nconst totalBookmarksGauge = new Gauge({\n  name: \"karakeep_total_bookmarks\",\n  help: \"Total number of bookmarks in the system\",\n  async collect() {\n    try {\n      const result = await db.select({ count: count() }).from(bookmarks);\n      this.set(result[0]?.count ?? 0);\n    } catch (error) {\n      console.error(\"Failed to get bookmark count:\", error);\n      this.set(0);\n    }\n  },\n});\n\n// Api metrics\nconst apiRequestsTotalCounter = new Counter({\n  name: \"karakeep_trpc_requests_total\",\n  help: \"Total number of API requests\",\n  labelNames: [\"type\", \"path\", \"is_error\"],\n});\n\nconst apiErrorsTotalCounter = new Counter({\n  name: \"karakeep_trpc_errors_total\",\n  help: \"Total number of API requests\",\n  labelNames: [\"type\", \"path\", \"code\"],\n});\n\nconst apiRequestDurationSummary = new Histogram({\n  name: \"karakeep_trpc_request_duration_seconds\",\n  help: \"Duration of tRPC requests in seconds\",\n  labelNames: [\"type\", \"path\"],\n  buckets: [\n    5e-3, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10,\n  ],\n});\n\n// Register all metrics\nregister.registerMetric(queuePendingJobsGauge);\nregister.registerMetric(totalUsersGauge);\nregister.registerMetric(totalAssetSizeGauge);\nregister.registerMetric(totalBookmarksGauge);\nregister.registerMetric(apiRequestsTotalCounter);\nregister.registerMetric(apiErrorsTotalCounter);\nregister.registerMetric(apiRequestDurationSummary);\n\nexport {\n  queuePendingJobsGauge,\n  totalUsersGauge,\n  totalAssetSizeGauge,\n  totalBookmarksGauge,\n  apiRequestsTotalCounter,\n  apiErrorsTotalCounter,\n  apiRequestDurationSummary,\n};\n"
  },
  {
    "path": "packages/trpc/testUtils.ts",
    "content": "import { vi } from \"vitest\";\n\nimport { getInMemoryDB } from \"@karakeep/db/drizzle\";\nimport { users } from \"@karakeep/db/schema\";\n\nimport { createCallerFactory } from \"./index\";\nimport { appRouter } from \"./routers/_app\";\n\nexport function getTestDB() {\n  return getInMemoryDB(true);\n}\n\nexport type TestDB = ReturnType<typeof getTestDB>;\n\nexport async function seedUsers(db: TestDB) {\n  return await db\n    .insert(users)\n    .values([\n      {\n        name: \"Test User 1\",\n        email: \"test1@test.com\",\n      },\n      {\n        name: \"Test User 2\",\n        email: \"test2@test.com\",\n      },\n      {\n        name: \"Test User 3\",\n        email: \"test3@test.com\",\n      },\n    ])\n    .returning();\n}\n\nexport function getApiCaller(\n  db: TestDB,\n  userId?: string,\n  email?: string,\n  role: \"user\" | \"admin\" = \"user\",\n) {\n  const createCaller = createCallerFactory(appRouter);\n  return createCaller({\n    user: userId\n      ? {\n          id: userId,\n          email,\n          role,\n        }\n      : null,\n    db,\n    req: {\n      ip: null,\n    },\n  });\n}\n\nexport type APICallerType = ReturnType<typeof getApiCaller>;\n\nexport interface CustomTestContext {\n  apiCallers: APICallerType[];\n  unauthedAPICaller: APICallerType;\n  db: TestDB;\n}\n\nexport async function buildTestContext(\n  seedDB: boolean,\n): Promise<CustomTestContext> {\n  const db = getTestDB();\n  let users: Awaited<ReturnType<typeof seedUsers>> = [];\n  if (seedDB) {\n    users = await seedUsers(db);\n  }\n  const callers = users.map((u) => getApiCaller(db, u.id, u.email));\n\n  return {\n    apiCallers: callers,\n    unauthedAPICaller: getApiCaller(db),\n    db,\n  };\n}\n\nexport function defaultBeforeEach(seedDB = true) {\n  return async (context: object) => {\n    vi.mock(\"@karakeep/shared-server\", async (original) => {\n      const mod =\n        (await original()) as typeof import(\"@karakeep/shared-server\");\n      return {\n        ...mod,\n        LinkCrawlerQueue: {\n          enqueue: vi.fn(),\n        },\n        OpenAIQueue: {\n          enqueue: vi.fn(),\n        },\n        SearchIndexingQueue: {\n          enqueue: vi.fn(),\n        },\n        triggerRuleEngineOnEvent: vi.fn(),\n        triggerSearchReindex: vi.fn(),\n        triggerWebhook: vi.fn(),\n      };\n    });\n    Object.assign(context, await buildTestContext(seedDB));\n  };\n}\n"
  },
  {
    "path": "packages/trpc/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@karakeep/tsconfig/node.json\",\n  \"include\": [\"**/*.ts\"],\n  \"exclude\": [\"node_modules\"],\n  \"compilerOptions\": {\n    \"tsBuildInfoFile\": \"node_modules/.cache/tsbuildinfo.json\"\n  }\n}\n"
  },
  {
    "path": "packages/trpc/vitest.config.ts",
    "content": "/// <reference types=\"vitest\" />\n\nimport tsconfigPaths from \"vite-tsconfig-paths\";\nimport { defineConfig } from \"vitest/config\";\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  plugins: [tsconfigPaths()],\n  test: {\n    alias: {\n      \"@/*\": \"./*\",\n    },\n  },\n});\n"
  },
  {
    "path": "patches/playwright-extra@4.3.6.patch",
    "content": "diff --git a/dist/puppeteer-compatiblity-shim/index.js b/dist/puppeteer-compatiblity-shim/index.js\nindex fd1297952369011b711a83124209f1cd3789978e..71eba6bf608da93f6e2adf0faf648bb7cd75d2fa 100644\n--- a/dist/puppeteer-compatiblity-shim/index.js\n+++ b/dist/puppeteer-compatiblity-shim/index.js\n@@ -116,8 +116,14 @@ function createPageShim(page) {\n             if (prop === '_client') {\n                 return () => ({\n                     send: async (method, params) => {\n-                        const session = await getPageCDPSession(page);\n-                        return await session.send(method, params);\n+                        try {\n+                            const session = await getPageCDPSession(page);\n+                            return await session.send(method, params);\n+                        }\n+                        catch (err) {\n+                            debug('page shim: error when sending:', err.message);\n+                            return Promise.resolve();\n+                        }\n                     },\n                     on: (event, listener) => {\n                         getPageCDPSession(page).then(session => {\n"
  },
  {
    "path": "patches/xcode@3.0.1.patch",
    "content": "diff --git a/lib/pbxProject.js b/lib/pbxProject.js\nindex 068548ab89dfd2d39f90d46d881c17dc86f90bf4..8ee4b8b30788ad057cd5f1b1efe41fa51478d4ce 100644\n--- a/lib/pbxProject.js\n+++ b/lib/pbxProject.js\n@@ -1679,7 +1679,7 @@ function correctForFrameworksPath(file, project) {\n function correctForPath(file, project, group) {\n     var r_group_dir = new RegExp('^' + group + '[\\\\\\\\/]');\n \n-    if (project.pbxGroupByName(group).path)\n+    if (project.pbxGroupByName(group)&&project.pbxGroupByName(group).path)\n         file.path = file.path.replace(r_group_dir, '');\n \n     return file;\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "packages:\n  - \"packages/*\"\n  - \"apps/*\"\n  - \"tooling/*\"\n  - \"tools/*\"\n  - \"docs\"\n"
  },
  {
    "path": "start-dev.sh",
    "content": "#!/usr/bin/env bash\n\n# Function to check if a command exists\ncommand_exists() {\n    command -v \"$1\" >/dev/null 2>&1\n}\n\n# Function to check if a port is in use\nport_in_use() {\n    lsof -i :\"$1\" >/dev/null 2>&1\n}\n\n# Check if Docker is installed\nif ! command_exists docker; then\n    echo \"Error: Docker is not installed. Please install Docker first.\"\n    exit 1\nfi\n\n# Check if pnpm is installed\nif ! command_exists pnpm; then\n    echo \"Error: pnpm is not installed. Please install pnpm first.\"\n    exit 1\nfi\n\n# Start Meilisearch if not already running\nif ! port_in_use 7700; then\n    echo \"Starting Meilisearch...\"\n    docker run -d -p 7700:7700 --name karakeep-meilisearch getmeili/meilisearch:v1.37.0\nelse\n    echo \"Meilisearch is already running on port 7700\"\nfi\n\n# Start Chrome if not already running\nif ! port_in_use 9222; then\n    echo \"Starting headless Chrome...\"\n    docker run -d -p 9222:9222 --name karakeep-chrome gcr.io/zenika-hub/alpine-chrome:124 \\\n        --no-sandbox \\\n        --disable-gpu \\\n        --disable-dev-shm-usage \\\n        --remote-debugging-address=0.0.0.0 \\\n        --remote-debugging-port=9222 \\\n        --hide-scrollbars\nelse\n    echo \"Chrome is already running on port 9222\"\nfi\n\n# Install dependencies if node_modules doesn't exist\nif [ ! -d \"node_modules\" ]; then\n    echo \"Installing dependencies...\"\n    pnpm install\nfi\n\n# Get DATA_DIR from environment or .env file\nif [ -z \"$DATA_DIR\" ] && [ -f \".env\" ]; then\n    DATA_DIR=$(grep \"^DATA_DIR=\" .env | cut -d'=' -f2)\nfi\n\n# Create DATA_DIR if it doesn't exist\nif [ -n \"$DATA_DIR\" ] && [ ! -d \"$DATA_DIR\" ]; then\n    echo \"Creating DATA_DIR at $DATA_DIR...\"\n    mkdir -p \"$DATA_DIR\"\nfi\n\necho \"Running database migrations...\"\npnpm run db:migrate\n\necho \"Starting web app and workers...\"\npnpm web & WEB_PID=$!\npnpm workers & WORKERS_PID=$!\n\n# Function to handle script termination\ncleanup() {\n    echo \"Shutting down services...\"\n    kill $WEB_PID $WORKERS_PID 2>/dev/null\n    docker stop karakeep-meilisearch karakeep-chrome 2>/dev/null\n    docker rm karakeep-meilisearch karakeep-chrome 2>/dev/null\n    exit 0\n}\n\n# Wait for web app to be ready (max 30 seconds)\necho \"Waiting for web app to start...\"\nATTEMPT=0\nwhile [ $ATTEMPT -lt 30 ]; do\n    if nc -z localhost 3000 2>/dev/null; then\n        break\n    fi\n    sleep 1\n    ATTEMPT=$((ATTEMPT + 1))\n    if [ $ATTEMPT -eq 30 ]; then\n        echo \"Warning: Web app may not have started properly after 30 seconds\"\n    fi\ndone\n\n# Set up trap to catch termination signals\ntrap cleanup SIGINT SIGTERM\n\necho \"Development environment is running!\"\necho \"Web app: http://localhost:3000\"\necho \"Meilisearch: http://localhost:7700\"\necho \"Chrome debugger: http://localhost:9222\"\necho \"Press Ctrl+C to stop all services\"\n\n# Wait for user interrupt\nwait \n"
  },
  {
    "path": "tooling/github/package.json",
    "content": "{\n  \"name\": \"@karakeep/github\"\n}\n"
  },
  {
    "path": "tooling/github/setup/action.yml",
    "content": "name: \"Setup and install\"\ndescription: \"Common setup steps for Actions\"\n\nruns:\n  using: composite\n  steps:\n    - uses: pnpm/action-setup@v2\n    - uses: actions/setup-node@v4\n      with:\n        node-version: 24\n        cache: \"pnpm\"\n        registry-url: https://registry.npmjs.org\n\n    - shell: bash\n      run: pnpm add -g turbo\n\n    - shell: bash\n      run: npm install -g corepack@0.31.0 && corepack enable\n\n    - shell: bash\n      run: pnpm install\n      env:\n        PUPPETEER_SKIP_DOWNLOAD: \"true\"\n        PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: \"true\"\n"
  },
  {
    "path": "tooling/oxlint/oxlint-base.json",
    "content": "{\n  \"$schema\": \"../../node_modules/oxlint/configuration_schema.json\",\n  \"plugins\": [\n    \"typescript\",\n    \"import\",\n    \"unicorn\",\n    \"oxc\"\n  ],\n  \"rules\": {\n    \"for-direction\": \"error\",\n    \"no-async-promise-executor\": \"error\",\n    \"no-case-declarations\": \"error\",\n    \"no-class-assign\": \"error\",\n    \"no-compare-neg-zero\": \"error\",\n    \"no-cond-assign\": \"error\",\n    \"no-const-assign\": \"error\",\n    \"no-constant-binary-expression\": \"error\",\n    \"no-constant-condition\": \"error\",\n    \"no-control-regex\": \"error\",\n    \"no-debugger\": \"error\",\n    \"no-delete-var\": \"error\",\n    \"no-dupe-class-members\": \"error\",\n    \"no-dupe-else-if\": \"error\",\n    \"no-dupe-keys\": \"error\",\n    \"no-duplicate-case\": \"error\",\n    \"no-empty\": \"error\",\n    \"no-empty-character-class\": \"error\",\n    \"no-empty-pattern\": \"error\",\n    \"no-empty-static-block\": \"error\",\n    \"no-ex-assign\": \"error\",\n    \"no-extra-boolean-cast\": \"error\",\n    \"no-fallthrough\": \"error\",\n    \"no-func-assign\": \"error\",\n    \"no-global-assign\": \"error\",\n    \"no-import-assign\": \"error\",\n    \"no-invalid-regexp\": \"error\",\n    \"no-irregular-whitespace\": \"error\",\n    \"no-loss-of-precision\": \"error\",\n    \"no-new-native-nonconstructor\": \"error\",\n    \"no-nonoctal-decimal-escape\": \"error\",\n    \"no-obj-calls\": \"error\",\n    \"no-prototype-builtins\": \"error\",\n    \"no-redeclare\": \"error\",\n    \"no-regex-spaces\": \"error\",\n    \"no-self-assign\": \"error\",\n    \"no-setter-return\": \"error\",\n    \"no-shadow-restricted-names\": \"error\",\n    \"no-sparse-arrays\": \"error\",\n    \"no-this-before-super\": \"error\",\n    \"no-unexpected-multiline\": \"error\",\n    \"no-unsafe-finally\": \"error\",\n    \"no-unsafe-negation\": \"error\",\n    \"no-unsafe-optional-chaining\": \"error\",\n    \"no-unused-labels\": \"error\",\n    \"no-unused-private-class-members\": \"error\",\n    \"no-unused-vars\": [\n      \"error\",\n      {\n        \"argsIgnorePattern\": \"^_\",\n        \"varsIgnorePattern\": \"^_\"\n      }\n    ],\n    \"no-useless-backreference\": \"error\",\n    \"no-useless-catch\": \"error\",\n    \"no-useless-escape\": \"error\",\n    \"no-with\": \"error\",\n    \"require-yield\": \"error\",\n    \"use-isnan\": \"error\",\n    \"valid-typeof\": \"error\",\n    \"@typescript-eslint/ban-ts-comment\": \"error\",\n    \"no-array-constructor\": \"error\",\n    \"@typescript-eslint/no-duplicate-enum-values\": \"error\",\n    \"@typescript-eslint/no-empty-object-type\": \"error\",\n    \"@typescript-eslint/no-explicit-any\": \"error\",\n    \"@typescript-eslint/no-extra-non-null-assertion\": \"error\",\n    \"@typescript-eslint/no-misused-new\": \"error\",\n    \"@typescript-eslint/no-namespace\": \"error\",\n    \"@typescript-eslint/no-non-null-asserted-optional-chain\": \"error\",\n    \"@typescript-eslint/no-require-imports\": \"error\",\n    \"@typescript-eslint/no-this-alias\": \"error\",\n    \"@typescript-eslint/no-unnecessary-type-constraint\": \"error\",\n    \"@typescript-eslint/no-unsafe-declaration-merging\": \"error\",\n    \"@typescript-eslint/no-unsafe-function-type\": \"error\",\n    \"no-unused-expressions\": \"error\",\n    \"@typescript-eslint/no-wrapper-object-types\": \"error\",\n    \"no-throw-literal\": \"off\",\n    \"@typescript-eslint/prefer-as-const\": \"error\",\n    \"@typescript-eslint/prefer-namespace-keyword\": \"error\",\n    \"prefer-promise-reject-errors\": \"off\",\n    \"require-await\": \"off\",\n    \"@typescript-eslint/triple-slash-reference\": \"error\",\n    \"@typescript-eslint/adjacent-overload-signatures\": \"error\",\n    \"@typescript-eslint/array-type\": \"error\",\n    \"@typescript-eslint/ban-tslint-comment\": \"error\",\n    \"@typescript-eslint/consistent-generic-constructors\": \"error\",\n    \"@typescript-eslint/consistent-indexed-object-style\": \"error\",\n    \"@typescript-eslint/consistent-type-definitions\": \"error\",\n    \"@typescript-eslint/no-confusing-non-null-assertion\": \"error\",\n    \"no-empty-function\": \"error\",\n    \"@typescript-eslint/no-inferrable-types\": \"error\",\n    \"@typescript-eslint/prefer-for-of\": \"error\",\n    \"@typescript-eslint/prefer-function-type\": \"error\",\n    \"@typescript-eslint/consistent-type-imports\": \"off\",\n    \"import/consistent-type-specifier-style\": [\n      \"error\",\n      \"prefer-top-level\"\n    ]\n  },\n  \"overrides\": [\n    {\n      \"files\": [\"**/*\"],\n      \"rules\": {\n        \"no-var\": \"error\",\n        \"prefer-rest-params\": \"error\",\n        \"prefer-spread\": \"error\"\n      }\n    }\n  ]\n}\n"
  },
  {
    "path": "tooling/oxlint/oxlint-nextjs.json",
    "content": "{\n  \"$schema\": \"../../node_modules/oxlint/configuration_schema.json\",\n  \"plugins\": [\n    \"nextjs\"\n  ],\n  \"rules\": {\n    \"@next/next/google-font-display\": \"warn\",\n    \"@next/next/google-font-preconnect\": \"warn\",\n    \"@next/next/next-script-for-ga\": \"warn\",\n    \"@next/next/no-async-client-component\": \"warn\",\n    \"@next/next/no-before-interactive-script-outside-document\": \"warn\",\n    \"@next/next/no-css-tags\": \"warn\",\n    \"@next/next/no-head-element\": \"warn\",\n    \"@next/next/no-img-element\": \"warn\",\n    \"@next/next/no-page-custom-font\": \"warn\",\n    \"@next/next/no-styled-jsx-in-document\": \"warn\",\n    \"@next/next/no-sync-scripts\": \"error\",\n    \"@next/next/no-title-in-document-head\": \"warn\",\n    \"@next/next/no-typos\": \"warn\",\n    \"@next/next/no-unwanted-polyfillio\": \"warn\",\n    \"@next/next/inline-script-id\": \"error\",\n    \"@next/next/no-assign-module-variable\": \"error\",\n    \"@next/next/no-document-import-in-page\": \"error\",\n    \"@next/next/no-duplicate-head\": \"error\",\n    \"@next/next/no-head-import-in-document\": \"error\",\n    \"@next/next/no-script-component-in-head\": \"error\"\n  }\n}\n"
  },
  {
    "path": "tooling/oxlint/oxlint-react.json",
    "content": "{\n  \"$schema\": \"../../node_modules/oxlint/configuration_schema.json\",\n  \"plugins\": [\n    \"react\",\n    \"jsx-a11y\"\n  ],\n  \"rules\": {\n    \"react/jsx-key\": \"warn\",\n    \"react/jsx-no-comment-textnodes\": \"warn\",\n    \"react/jsx-no-duplicate-props\": \"warn\",\n    \"react/jsx-no-target-blank\": \"warn\",\n    \"react/jsx-no-undef\": \"warn\",\n    \"react/no-children-prop\": \"warn\",\n    \"react/no-danger-with-children\": \"warn\",\n    \"react/no-direct-mutation-state\": \"warn\",\n    \"react/no-find-dom-node\": \"warn\",\n    \"react/no-is-mounted\": \"warn\",\n    \"react/no-render-return-value\": \"warn\",\n    \"react/no-string-refs\": \"warn\",\n    \"react/no-unescaped-entities\": \"warn\",\n    \"react/no-unknown-property\": \"warn\",\n    \"react-hooks/rules-of-hooks\": \"error\",\n    \"react-hooks/exhaustive-deps\": \"off\",\n    \"jsx-a11y/alt-text\": \"error\",\n    \"jsx-a11y/anchor-ambiguous-text\": \"off\",\n    \"jsx-a11y/anchor-has-content\": \"error\",\n    \"jsx-a11y/anchor-is-valid\": \"error\",\n    \"jsx-a11y/aria-activedescendant-has-tabindex\": \"error\",\n    \"jsx-a11y/aria-props\": \"error\",\n    \"jsx-a11y/aria-role\": \"error\",\n    \"jsx-a11y/aria-unsupported-elements\": \"error\",\n    \"jsx-a11y/autocomplete-valid\": \"error\",\n    \"jsx-a11y/click-events-have-key-events\": \"error\",\n    \"jsx-a11y/heading-has-content\": \"error\",\n    \"jsx-a11y/html-has-lang\": \"error\",\n    \"jsx-a11y/iframe-has-title\": \"off\",\n    \"jsx-a11y/img-redundant-alt\": \"error\",\n    \"jsx-a11y/label-has-associated-control\": \"off\",\n    \"jsx-a11y/media-has-caption\": \"error\",\n    \"jsx-a11y/mouse-events-have-key-events\": \"error\",\n    \"jsx-a11y/no-access-key\": \"error\",\n    \"jsx-a11y/no-autofocus\": \"off\",\n    \"jsx-a11y/no-distracting-elements\": \"error\",\n    \"jsx-a11y/no-noninteractive-tabindex\": [\n      \"error\",\n      {\n        \"tags\": [],\n        \"roles\": [\n          \"tabpanel\"\n        ],\n        \"allowExpressionValues\": true\n      }\n    ],\n    \"jsx-a11y/no-redundant-roles\": \"error\",\n    \"jsx-a11y/role-has-required-aria-props\": \"off\",\n    \"jsx-a11y/role-supports-aria-props\": \"error\",\n    \"jsx-a11y/scope\": \"error\",\n    \"jsx-a11y/tabindex-no-positive\": \"error\"\n  }\n}\n"
  },
  {
    "path": "tooling/oxlint/package.json",
    "content": "{\n  \"name\": \"oxlint\",\n  \"version\": \"0.0.0\",\n  \"private\": true\n}"
  },
  {
    "path": "tooling/prettier/index.js",
    "content": "import { fileURLToPath } from \"url\";\n\n/** @typedef {import(\"prettier\").Config} PrettierConfig */\n/** @typedef {import(\"prettier-plugin-tailwindcss\").PluginOptions} TailwindConfig */\n/** @typedef {import(\"@ianvs/prettier-plugin-sort-imports\").PluginConfig} SortImportsConfig */\n\n/** @type { PrettierConfig | SortImportsConfig | TailwindConfig } */\nconst config = {\n  plugins: [\n    \"@ianvs/prettier-plugin-sort-imports\",\n    \"prettier-plugin-tailwindcss\",\n  ],\n  tailwindConfig: fileURLToPath(\n    new URL(\"../../tooling/tailwind/web.ts\", import.meta.url),\n  ),\n  tailwindFunctions: [\"cn\", \"cva\"],\n  importOrder: [\n    \"<TYPES>\",\n    \"^(react/(.*)$)|^(react$)|^(react-native(.*)$)\",\n    \"^(next/(.*)$)|^(next$)\",\n    \"^(expo(.*)$)|^(expo$)\",\n    \"<THIRD_PARTY_MODULES>\",\n    \"\",\n    \"<TYPES>^@karakeep\",\n    \"^@karakeep/(.*)$\",\n    \"\",\n    \"<TYPES>^[.|..|~]\",\n    \"^~/\",\n    \"^[../]\",\n    \"^[./]\",\n  ],\n  importOrderParserPlugins: [\"typescript\", \"jsx\", \"decorators-legacy\"],\n  importOrderTypeScriptVersion: \"4.4.0\",\n  endOfLine: \"auto\",\n};\n\nexport default config;\n"
  },
  {
    "path": "tooling/prettier/package.json",
    "content": "{\n  \"name\": \"@karakeep/prettier-config\",\n  \"private\": true,\n  \"version\": \"0.1.0\",\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": \"./index.js\"\n  },\n  \"scripts\": {\n    \"clean\": \"rm -rf .turbo node_modules\",\n    \"format\": \"oxfmt --check .\",\n    \"format:fix\": \"oxfmt .\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@ianvs/prettier-plugin-sort-imports\": \"^4.1.1\",\n    \"prettier-plugin-tailwindcss\": \"^0.5.11\"\n  },\n  \"devDependencies\": {\n    \"@karakeep/tsconfig\": \"workspace:^0.1.0\",\n    \"typescript\": \"^5.9\"\n  }\n}\n"
  },
  {
    "path": "tooling/prettier/tsconfig.json",
    "content": "{\n  \"extends\": \"@karakeep/tsconfig/base.json\",\n  \"compilerOptions\": {\n    \"tsBuildInfoFile\": \"node_modules/.cache/tsbuildinfo.json\"\n  },\n  \"include\": [\".\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "tooling/tailwind/.oxlintrc.json",
    "content": "{\n  \"$schema\": \"../../node_modules/oxlint/configuration_schema.json\",\n  \"extends\": [\"../../tooling/oxlint/oxlint-base.json\"],\n  \"categories\": {\n    \"correctness\": \"warn\"\n  },\n  \"env\": {\n    \"builtin\": true,\n    \"commonjs\": true\n  },\n  \"ignorePatterns\": [\n    \"**/*.config.js\",\n    \"**/*.config.cjs\",\n    \"**/.eslintrc.cjs\",\n    \"**/.next\",\n    \"**/dist\",\n    \"**/build\",\n    \"**/pnpm-lock.yaml\"\n  ]\n}\n"
  },
  {
    "path": "tooling/tailwind/base.ts",
    "content": "import type { Config } from \"tailwindcss\";\n\nexport default {\n  darkMode: [\"class\"],\n  content: [\n    \"pages/**/*.{ts,tsx}\",\n    \"components/**/*.{ts,tsx}\",\n    \"app/**/*.{ts,tsx}\",\n    \"src/**/*.{ts,tsx}\",\n  ],\n  theme: {\n    container: {\n      center: true,\n      padding: \"2rem\",\n      screens: {\n        \"2xl\": \"1400px\",\n      },\n    },\n    extend: {\n      colors: {\n        border: \"hsl(var(--border))\",\n        input: \"hsl(var(--input))\",\n        ring: \"hsl(var(--ring))\",\n        background: \"hsl(var(--background))\",\n        foreground: \"hsl(var(--foreground))\",\n        primary: {\n          DEFAULT: \"hsl(var(--primary))\",\n          foreground: \"hsl(var(--primary-foreground))\",\n        },\n        secondary: {\n          DEFAULT: \"hsl(var(--secondary))\",\n          foreground: \"hsl(var(--secondary-foreground))\",\n        },\n        destructive: {\n          DEFAULT: \"hsl(var(--destructive))\",\n          foreground: \"hsl(var(--destructive-foreground))\",\n        },\n        muted: {\n          DEFAULT: \"hsl(var(--muted))\",\n          foreground: \"hsl(var(--muted-foreground))\",\n        },\n        accent: {\n          DEFAULT: \"hsl(var(--accent))\",\n          foreground: \"hsl(var(--accent-foreground))\",\n        },\n        popover: {\n          DEFAULT: \"hsl(var(--popover))\",\n          foreground: \"hsl(var(--popover-foreground))\",\n        },\n        card: {\n          DEFAULT: \"hsl(var(--card))\",\n          foreground: \"hsl(var(--card-foreground))\",\n        },\n      },\n      borderColor: {\n        DEFAULT: \"hsl(var(--border))\",\n      },\n    },\n  },\n} satisfies Config;\n"
  },
  {
    "path": "tooling/tailwind/globals.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n  :root {\n    --background: 0 0% 100%;\n    --foreground: 222.2 84% 4.9%;\n\n    --card: 0 0% 100%;\n    --card-foreground: 222.2 84% 4.9%;\n\n    --popover: 0 0% 100%;\n    --popover-foreground: 222.2 84% 4.9%;\n\n    --primary: 222.2 47.4% 11.2%;\n    --primary-foreground: 210 40% 98%;\n\n    --secondary: 210 40% 96.1%;\n    --secondary-foreground: 222.2 47.4% 11.2%;\n\n    --muted: 210 40% 96.1%;\n    --muted-foreground: 215.4 16.3% 46.9%;\n\n    --accent: 210 40% 96.1%;\n    --accent-foreground: 222.2 47.4% 11.2%;\n\n    --destructive: 0 84.2% 60.2%;\n    --destructive-foreground: 210 40% 98%;\n\n    --border: 214.3 31.8% 91.4%;\n    --input: 214.3 31.8% 91.4%;\n    --ring: 222.2 84% 4.9%;\n\n    --radius: 0.5rem;\n  }\n\n  .dark {\n    --background: 222.2 84% 4.9%;\n    --foreground: 210 40% 98%;\n\n    --card: 222.2 84% 4.9%;\n    --card-foreground: 210 40% 98%;\n\n    --popover: 222.2 84% 4.9%;\n    --popover-foreground: 210 40% 98%;\n\n    --primary: 210 40% 98%;\n    --primary-foreground: 222.2 47.4% 11.2%;\n\n    --secondary: 217.2 32.6% 17.5%;\n    --secondary-foreground: 210 40% 98%;\n\n    --muted: 217.2 32.6% 17.5%;\n    --muted-foreground: 215 20.2% 65.1%;\n\n    --accent: 217.2 32.6% 17.5%;\n    --accent-foreground: 210 40% 98%;\n\n    --destructive: 0 80% 45%;\n    --destructive-foreground: 210 40% 98%;\n\n    --border: 217.2 32.6% 17.5%;\n    --input: 217.2 32.6% 17.5%;\n    --ring: 212.7 26.8% 83.9%;\n  }\n}\n\n@layer base {\n  * {\n    @apply border-border;\n  }\n  body {\n    @apply bg-background text-foreground;\n  }\n}\n\n@layer components {\n  /* Sleek scrollbar for sidebar only */\n  .sidebar-scrollbar::-webkit-scrollbar {\n    width: 6px;\n  }\n\n  .sidebar-scrollbar::-webkit-scrollbar-track {\n    background: transparent;\n  }\n\n  .sidebar-scrollbar::-webkit-scrollbar-thumb {\n    background: hsl(var(--muted-foreground) / 0.15);\n    border-radius: 3px;\n  }\n\n  .sidebar-scrollbar::-webkit-scrollbar-thumb:hover {\n    background: hsl(var(--muted-foreground) / 0.25);\n  }\n\n  /* Firefox scrollbar for sidebar */\n  .sidebar-scrollbar {\n    scrollbar-width: thin;\n    scrollbar-color: hsl(var(--muted-foreground) / 0.15) transparent;\n  }\n}\n"
  },
  {
    "path": "tooling/tailwind/native.ts",
    "content": "import type { Config } from \"tailwindcss\";\nimport { hairlineWidth } from \"nativewind/theme\";\n\nimport base from \"./base\";\n\nexport default {\n  content: base.content,\n  presets: [base],\n  theme: {\n    extend: {\n      borderWidth: {\n        // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-assignment\n        hairline: hairlineWidth(),\n      },\n    },\n  },\n} satisfies Config;\n"
  },
  {
    "path": "tooling/tailwind/package.json",
    "content": "{\n  \"name\": \"@karakeep/tailwind-config\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"exports\": {\n    \"./native\": \"./native.ts\",\n    \"./web\": \"./web.ts\",\n    \"./globals.css\": \"./globals.css\"\n  },\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"clean\": \"rm -rf .turbo node_modules\",\n    \"format\": \"oxfmt --check .\",\n    \"format:fix\": \"oxfmt .\",\n    \"lint\": \"oxlint .\",\n    \"lint:fix\": \"oxlint . --fix\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@tailwindcss/typography\": \"^0.5.10\",\n    \"nativewind\": \"^4.2.1\",\n    \"postcss\": \"^8.4.35\",\n    \"tailwindcss\": \"^3.4.1\",\n    \"tailwindcss-animate\": \"^1.0.7\"\n  },\n  \"devDependencies\": {\n    \"@karakeep/tsconfig\": \"workspace:^0.1.0\",\n    \"typescript\": \"^5.9\"\n  }\n}\n"
  },
  {
    "path": "tooling/tailwind/tsconfig.json",
    "content": "{\n  \"extends\": \"@karakeep/tsconfig/base.json\",\n  \"compilerOptions\": {\n    \"tsBuildInfoFile\": \"node_modules/.cache/tsbuildinfo.json\"\n  },\n  \"include\": [\".\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "tooling/tailwind/web.ts",
    "content": "import type { Config } from \"tailwindcss\";\nimport typography from \"@tailwindcss/typography\";\nimport animate from \"tailwindcss-animate\";\n\nimport base from \"./base\";\n\nexport default {\n  content: base.content,\n  presets: [base],\n  theme: {\n    extend: {\n      borderRadius: {\n        lg: \"var(--radius)\",\n        md: \"calc(var(--radius) - 2px)\",\n        sm: \"calc(var(--radius) - 4px)\",\n      },\n      keyframes: {\n        \"accordion-down\": {\n          from: { height: \"0\" },\n          to: { height: \"var(--radix-accordion-content-height)\" },\n        },\n        \"accordion-up\": {\n          from: { height: \"var(--radix-accordion-content-height)\" },\n          to: { height: \"0\" },\n        },\n        \"pulse-border\": {\n          \"0%, 100%\": {\n            \"box-shadow\": \"0 0 0 0 gray\",\n          },\n          \"50%\": {\n            \"box-shadow\": \"0 0 0 2px gray\",\n          },\n        },\n      },\n      animation: {\n        \"accordion-down\": \"accordion-down 0.2s ease-out\",\n        \"accordion-up\": \"accordion-up 0.2s ease-out\",\n        \"pulse-border\": \"pulse-border 1s ease-in-out infinite\",\n      },\n    },\n  },\n  plugins: [animate, typography],\n} satisfies Config;\n"
  },
  {
    "path": "tooling/typescript/base.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"ES2022\"],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"Bundler\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"moduleDetection\": \"force\",\n    \"jsx\": \"preserve\",\n    \"incremental\": true,\n    \"noUncheckedIndexedAccess\": false\n  },\n  \"exclude\": [\"node_modules\", \"build\", \"dist\", \".next\", \".expo\"]\n}\n"
  },
  {
    "path": "tooling/typescript/node.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@tsconfig/node22/tsconfig.json\",\n  \"include\": [\"**/*.ts\"],\n  \"exclude\": [\"node_modules\", \"build\", \"dist\", \".next\", \".expo\"],\n  \"compilerOptions\": {\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"node\",\n    \"incremental\": true,\n    \"isolatedModules\": true,\n    \"strict\": true,\n    \"esModuleInterop\": true\n  }\n}\n"
  },
  {
    "path": "tooling/typescript/package.json",
    "content": "{\n  \"name\": \"@karakeep/tsconfig\",\n  \"private\": true,\n  \"version\": \"0.1.0\",\n  \"files\": [\n    \"base.json\",\n    \"node.json\"\n  ],\n  \"devDependencies\": {\n    \"@tsconfig/node22\": \"^22.0.0\"\n  }\n}\n"
  },
  {
    "path": "tools/compare-models/.gitignore",
    "content": "node_modules\ndist\n.env\n"
  },
  {
    "path": "tools/compare-models/README.md",
    "content": "# Model Comparison Tool\n\nA standalone CLI tool to compare the tagging performance of AI models using your existing Karakeep bookmarks.\n\n## Features\n\n- **Two comparison modes:**\n  - **Model vs Model**: Compare two AI models against each other\n  - **Model vs Existing**: Compare a new model against existing AI-generated tags on your bookmarks\n- Fetches existing bookmarks from your Karakeep instance\n- Runs tagging inference with AI models\n- **Random shuffling**: Models/tags are randomly assigned to \"Model A\" or \"Model B\" for each bookmark to eliminate bias\n- Blind comparison: Model names are hidden during voting (only shown as \"Model A\" and \"Model B\")\n- Interactive voting interface\n- Shows final results with winner\n\n## Setup\n\n### Environment Variables\n\nRequired environment variables:\n\n```bash\n# Karakeep API configuration\nKARAKEEP_API_KEY=your_api_key_here\nKARAKEEP_SERVER_ADDR=https://your-karakeep-instance.com\n\n# Comparison mode (default: model-vs-model)\n# - \"model-vs-model\": Compare two models against each other\n# - \"model-vs-existing\": Compare a model against existing AI tags\nCOMPARISON_MODE=model-vs-model\n\n# Models to compare\n# MODEL1_NAME: The new model to test (always required)\n# MODEL2_NAME: The second model to compare against (required only for model-vs-model mode)\nMODEL1_NAME=gpt-4o-mini\nMODEL2_NAME=claude-3-5-sonnet\n\n# OpenAI/OpenRouter API configuration (for running inference)\nOPENAI_API_KEY=your_openai_or_openrouter_key\nOPENAI_BASE_URL=https://openrouter.ai/api/v1  # Optional, defaults to OpenAI\n\n# Optional: Number of bookmarks to test (default: 10)\nCOMPARE_LIMIT=10\n```\n\n### Using OpenRouter\n\nFor OpenRouter, set:\n```bash\nOPENAI_BASE_URL=https://openrouter.ai/api/v1\nOPENAI_API_KEY=your_openrouter_key\n```\n\n### Using OpenAI Directly\n\nFor OpenAI directly:\n```bash\nOPENAI_API_KEY=your_openai_key\n# OPENAI_BASE_URL can be omitted for direct OpenAI\n```\n\n## Usage\n\n### Run with pnpm (Recommended)\n\n```bash\ncd tools/compare-models\npnpm install\npnpm run\n```\n\n### Run with environment file\n\nCreate a `.env` file:\n\n```env\nKARAKEEP_API_KEY=your_api_key\nKARAKEEP_SERVER_ADDR=https://your-karakeep-instance.com\nMODEL1_NAME=gpt-4o-mini\nMODEL2_NAME=claude-3-5-sonnet\nOPENAI_API_KEY=your_openai_key\nCOMPARE_LIMIT=10\n```\n\nThen run:\n```bash\npnpm run\n```\n\n### Using directly with node\n\nIf you prefer to run the compiled JavaScript directly:\n\n```bash\npnpm build\nexport KARAKEEP_API_KEY=your_api_key\nexport KARAKEEP_SERVER_ADDR=https://your-karakeep-instance.com\nexport MODEL1_NAME=gpt-4o-mini\nexport MODEL2_NAME=claude-3-5-sonnet\nexport OPENAI_API_KEY=your_openai_key\nnode dist/index.js\n```\n\n## Comparison Modes\n\n### Model vs Model Mode\n\nCompare two different AI models against each other:\n\n```bash\nCOMPARISON_MODE=model-vs-model\nMODEL1_NAME=gpt-4o-mini\nMODEL2_NAME=claude-3-5-sonnet\n```\n\nThis mode runs inference with both models on each bookmark and lets you choose which tags are better.\n\n### Model vs Existing Mode\n\nCompare a new model against existing AI-generated tags on your bookmarks:\n\n```bash\nCOMPARISON_MODE=model-vs-existing\nMODEL1_NAME=gpt-4o-mini\n# MODEL2_NAME is not required in this mode\n```\n\nThis mode is useful for:\n- Testing if a new model produces better tags than your current model\n- Evaluating whether to switch from one model to another\n- Quality assurance on existing AI tags\n\n**Note:** This mode only compares bookmarks that already have AI-generated tags (tags with `attachedBy: \"ai\"`). Bookmarks without AI tags are automatically filtered out.\n\n## Usage Flow\n\n1. The tool fetches your latest link bookmarks from Karakeep\n   - In **model-vs-existing** mode, only bookmarks with existing AI tags are included\n2. For each bookmark, it randomly assigns the options to \"Model A\" or \"Model B\" and runs tagging\n3. You'll see a side-by-side comparison (randomly shuffled each time):\n   ```\n   === Bookmark 1/10 ===\n   How to Build Better AI Systems\n   https://example.com/article\n   This article explores modern approaches to...\n\n   ─────────────────────────────────────\n\n   Model A (blind):\n     • ai\n     • machine-learning\n     • engineering\n\n   Model B (blind):\n     • artificial-intelligence\n     • ML\n     • software-development\n\n   ─────────────────────────────────────\n\n   Which tags do you prefer? [1=Model A, 2=Model B, s=skip, q=quit] >\n   ```\n\n4. Choose your preference:\n   - `1` - Vote for Model A\n   - `2` - Vote for Model B\n   - `s` or `skip` - Skip this comparison\n   - `q` or `quit` - Exit early and show current results\n\n5. After completing all comparisons (or quitting early), results are displayed:\n   ```\n   ───────────────────────────────────────\n   === FINAL RESULTS ===\n   ───────────────────────────────────────\n   gpt-4o-mini: 6 votes\n   claude-3-5-sonnet: 3 votes\n   Skipped: 1\n   Errors: 0\n   ───────────────────────────────────────\n   Total bookmarks tested: 10\n\n   🏆 WINNER: gpt-4o-mini\n   ───────────────────────────────────────\n   ```\n\n6. The actual model names are only shown in the final results - during voting you see only \"Model A\" and \"Model B\"\n\n## Bookmark Filtering\n\nThe tool currently tests only:\n- **Link-type bookmarks** (not text notes or assets)\n- **Non-archived** bookmarks\n- **Latest N bookmarks** (where N is COMPARE_LIMIT)\n- **In model-vs-existing mode**: Only bookmarks with existing AI tags (tags with `attachedBy: \"ai\"`)\n\n## Architecture\n\nThis tool leverages Karakeep's shared infrastructure:\n- **API Client**: Uses `@karakeep/sdk` for type-safe API interactions with proper authentication\n- **Inference**: Reuses `@karakeep/shared/inference` for OpenAI client with structured output support\n- **Prompts**: Uses `@karakeep/shared/prompts` for consistent tagging prompt generation with token management\n- No code duplication - all core functionality is shared with the main Karakeep application\n\n\n## Error Handling\n\n- If a model fails to generate tags for a bookmark, an error is shown and comparison continues\n- Errors are counted separately in final results\n- Missing required environment variables will cause the tool to exit with a clear error message\n\n## Build\n\nTo build a standalone binary:\n\n```bash\npnpm build\n```\n\nThe built binary will be in `dist/index.js`.\n\n## Notes\n\n- The tool is designed for manual, human-in-the-loop evaluation\n- No results are persisted - they're only displayed in console\n- Content is fetched with `includeContent=true` from Karakeep API\n- Uses Karakeep SDK (`@karakeep/sdk`) for type-safe API interactions\n- Inference runs sequentially to keep state management simple\n- Recommended to use `pnpm run` for the best experience (uses tsx for development)\n- **Random shuffling**: For each bookmark, models are randomly assigned to \"Model A\" or \"Model B\" to eliminate position bias. The actual model names are only revealed in the final results.\n"
  },
  {
    "path": "tools/compare-models/package.json",
    "content": "{\n  \"name\": \"@karakeep/compare-models\",\n  \"version\": \"0.1.0\",\n  \"description\": \"Standalone tool to compare tagging performance between AI models\",\n  \"bin\": {\n    \"compare-models\": \"dist/index.js\"\n  },\n  \"scripts\": {\n    \"build\": \"tsc && chmod +x dist/index.js\",\n    \"run\": \"tsx --env-file=./.env src/index.ts\",\n    \"typecheck\": \"tsc --noEmit\"\n  },\n  \"dependencies\": {\n    \"@karakeep/sdk\": \"workspace:^\",\n    \"@karakeep/shared\": \"workspace:^\",\n    \"chalk\": \"^5.3.0\",\n    \"zod\": \"^3.24.2\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^24\",\n    \"tsx\": \"^4.8.1\",\n    \"typescript\": \"^5.9\"\n  }\n}\n"
  },
  {
    "path": "tools/compare-models/src/apiClient.ts",
    "content": "import { createKarakeepClient } from \"@karakeep/sdk\";\n\nimport type { Bookmark } from \"./types\";\nimport { config } from \"./config\";\n\nexport class KarakeepAPIClient {\n  private readonly client: ReturnType<typeof createKarakeepClient>;\n\n  constructor() {\n    this.client = createKarakeepClient({\n      baseUrl: `${config.KARAKEEP_SERVER_ADDR}/api/v1/`,\n      headers: {\n        \"Content-Type\": \"application/json\",\n        authorization: `Bearer ${config.KARAKEEP_API_KEY}`,\n      },\n    });\n  }\n\n  async fetchBookmarks(limit: number): Promise<Bookmark[]> {\n    const bookmarks: Bookmark[] = [];\n    let cursor: string | null = null;\n    let hasMore = true;\n\n    while (hasMore && bookmarks.length < limit) {\n      const params: {\n        limit: number;\n        includeContent: true;\n        archived?: boolean;\n        cursor?: string;\n      } = {\n        limit: Math.min(limit - bookmarks.length, 50),\n        includeContent: true,\n        archived: false,\n      };\n\n      if (cursor) {\n        params.cursor = cursor;\n      }\n\n      const { data, response, error } = await this.client.GET(\"/bookmarks\", {\n        params: {\n          query: params,\n        },\n      });\n\n      if (error) {\n        throw new Error(`Failed to fetch bookmarks: ${String(error)}`);\n      }\n\n      if (!response.ok) {\n        throw new Error(`Failed to fetch bookmarks: ${response.status}`);\n      }\n\n      const batchBookmarks = (data?.bookmarks || [])\n        .filter((b) => b.content?.type === \"link\")\n        .map((b) => ({\n          ...b,\n          tags: (b.tags || []).map((tag) => ({\n            name: tag.name,\n            attachedBy: tag.attachedBy,\n          })),\n        })) as Bookmark[];\n\n      bookmarks.push(...batchBookmarks);\n      cursor = data?.nextCursor || null;\n      hasMore = !!cursor;\n    }\n\n    return bookmarks.slice(0, limit);\n  }\n}\n"
  },
  {
    "path": "tools/compare-models/src/bookmarkProcessor.ts",
    "content": "import type { InferenceClient } from \"@karakeep/shared/inference\";\nimport { buildTextPrompt } from \"@karakeep/shared/prompts.server\";\n\nimport { inferTags } from \"./inferenceClient\";\nimport type { Bookmark } from \"./types\";\n\nexport async function extractBookmarkContent(\n  bookmark: Bookmark,\n): Promise<string> {\n  if (bookmark.content.type === \"link\") {\n    const parts = [];\n\n    if (bookmark.content.url) {\n      parts.push(`URL: ${bookmark.content.url}`);\n    }\n\n    if (bookmark.title) {\n      parts.push(`Title: ${bookmark.title}`);\n    }\n\n    if (bookmark.content.description) {\n      parts.push(`Description: ${bookmark.content.description}`);\n    }\n\n    if (bookmark.content.htmlContent) {\n      parts.push(`Content: ${bookmark.content.htmlContent}`);\n    }\n\n    return parts.join(\"\\n\");\n  }\n\n  if (bookmark.content.type === \"text\" && bookmark.content.text) {\n    return bookmark.content.text;\n  }\n\n  return \"\";\n}\n\nexport async function runTaggingForModel(\n  bookmark: Bookmark,\n  inferenceClient: InferenceClient,\n  lang: string = \"english\",\n  contextLength: number = 8000,\n): Promise<string[]> {\n  const content = await extractBookmarkContent(bookmark);\n\n  if (!content) {\n    return [];\n  }\n\n  try {\n    // Use the shared prompt builder with empty custom prompts and default tag style\n    const prompt = await buildTextPrompt(\n      lang,\n      [], // No custom prompts for comparison tool\n      content,\n      contextLength,\n      \"as-generated\", // Use tags as generated by the model\n    );\n\n    const tags = await inferTags(inferenceClient, prompt);\n    return tags;\n  } catch (error) {\n    throw new Error(\n      `Failed to generate tags: ${error instanceof Error ? error.message : String(error)}`,\n    );\n  }\n}\n"
  },
  {
    "path": "tools/compare-models/src/config.ts",
    "content": "import { z } from \"zod\";\n\n// Local config schema for compare-models tool\nconst envSchema = z.object({\n  KARAKEEP_API_KEY: z.string().min(1),\n  KARAKEEP_SERVER_ADDR: z.string().url(),\n  MODEL1_NAME: z.string().min(1),\n  MODEL2_NAME: z.string().min(1).optional(),\n  OPENAI_API_KEY: z.string().min(1),\n  OPENAI_BASE_URL: z.string().url().optional(),\n  OPENAI_SERVICE_TIER: z.enum([\"auto\", \"default\", \"flex\"]).optional(),\n  COMPARISON_MODE: z\n    .enum([\"model-vs-model\", \"model-vs-existing\"])\n    .default(\"model-vs-model\"),\n  COMPARE_LIMIT: z\n    .string()\n    .optional()\n    .transform((val) => (val ? parseInt(val, 10) : 10)),\n  INFERENCE_CONTEXT_LENGTH: z\n    .string()\n    .optional()\n    .transform((val) => (val ? parseInt(val, 10) : 8000)),\n  INFERENCE_MAX_OUTPUT_TOKENS: z\n    .string()\n    .optional()\n    .transform((val) => (val ? parseInt(val, 10) : 2048)),\n  INFERENCE_USE_MAX_COMPLETION_TOKENS: z\n    .string()\n    .optional()\n    .transform((val) => val === \"true\")\n    .default(\"false\"),\n});\n\nexport const config = envSchema.parse(process.env);\n"
  },
  {
    "path": "tools/compare-models/src/index.ts",
    "content": "import chalk from \"chalk\";\n\nimport type { ComparisonResult } from \"./types\";\nimport { KarakeepAPIClient } from \"./apiClient\";\nimport { runTaggingForModel } from \"./bookmarkProcessor\";\nimport { config } from \"./config\";\nimport { createInferenceClient } from \"./inferenceClient\";\nimport {\n  askQuestion,\n  clearProgress,\n  close,\n  displayComparison,\n  displayError,\n  displayFinalResults,\n  displayProgress,\n} from \"./interactive\";\n\ninterface VoteCounters {\n  model1Votes: number;\n  model2Votes: number;\n  skipped: number;\n  errors: number;\n  total: number;\n}\n\ninterface ShuffleResult {\n  modelA: string;\n  modelB: string;\n  modelAIsModel1: boolean;\n}\n\nasync function main() {\n  console.log(chalk.cyan(\"\\n🚀 Karakeep Model Comparison Tool\\n\"));\n\n  const isExistingMode = config.COMPARISON_MODE === \"model-vs-existing\";\n\n  if (isExistingMode) {\n    console.log(\n      chalk.yellow(\n        `Mode: Comparing ${config.MODEL1_NAME} against existing AI tags\\n`,\n      ),\n    );\n  } else {\n    if (!config.MODEL2_NAME) {\n      console.log(\n        chalk.red(\n          \"\\n✗ Error: MODEL2_NAME is required for model-vs-model comparison mode\\n\",\n        ),\n      );\n      process.exit(1);\n    }\n    console.log(\n      chalk.yellow(\n        `Mode: Comparing ${config.MODEL1_NAME} vs ${config.MODEL2_NAME}\\n`,\n      ),\n    );\n  }\n\n  const apiClient = new KarakeepAPIClient();\n\n  displayProgress(\"Fetching bookmarks from Karakeep...\");\n  let bookmarks = await apiClient.fetchBookmarks(config.COMPARE_LIMIT);\n  clearProgress();\n\n  // Filter bookmarks with AI tags if in existing mode\n  if (isExistingMode) {\n    bookmarks = bookmarks.filter(\n      (b) => b.tags.some((t) => t.attachedBy === \"ai\"),\n    );\n    console.log(\n      chalk.green(\n        `✓ Fetched ${bookmarks.length} link bookmarks with existing AI tags\\n`,\n      ),\n    );\n  } else {\n    console.log(chalk.green(`✓ Fetched ${bookmarks.length} link bookmarks\\n`));\n  }\n\n  if (bookmarks.length === 0) {\n    console.log(\n      chalk.yellow(\n        \"\\n⚠ No bookmarks found with AI tags. Please add some bookmarks with AI tags first.\\n\",\n      ),\n    );\n    return;\n  }\n\n  const counters: VoteCounters = {\n    model1Votes: 0,\n    model2Votes: 0,\n    skipped: 0,\n    errors: 0,\n    total: bookmarks.length,\n  };\n\n  const detailedResults: ComparisonResult[] = [];\n\n  for (let i = 0; i < bookmarks.length; i++) {\n    const bookmark = bookmarks[i];\n\n    displayProgress(\n      `[${i + 1}/${bookmarks.length}] Running inference on: ${bookmark.title || bookmark.content.title || \"Untitled\"}`,\n    );\n\n    let model1Tags: string[] = [];\n    let model2Tags: string[] = [];\n\n    // Get tags for model 1 (new model)\n    try {\n      const model1Client = createInferenceClient(config.MODEL1_NAME);\n      model1Tags = await runTaggingForModel(\n        bookmark,\n        model1Client,\n        \"english\",\n        config.INFERENCE_CONTEXT_LENGTH,\n      );\n    } catch (error) {\n      clearProgress();\n      displayError(\n        `${config.MODEL1_NAME} failed: ${error instanceof Error ? error.message : String(error)}`,\n      );\n      counters.errors++;\n      continue;\n    }\n\n    // Get tags for model 2 or existing AI tags\n    if (isExistingMode) {\n      // Use existing AI tags from the bookmark\n      model2Tags = bookmark.tags\n        .filter((t) => t.attachedBy === \"ai\")\n        .map((t) => t.name);\n    } else {\n      // Run inference with model 2\n      try {\n        const model2Client = createInferenceClient(config.MODEL2_NAME!);\n        model2Tags = await runTaggingForModel(\n          bookmark,\n          model2Client,\n          \"english\",\n          config.INFERENCE_CONTEXT_LENGTH,\n        );\n      } catch (error) {\n        clearProgress();\n        displayError(\n          `${config.MODEL2_NAME} failed: ${error instanceof Error ? error.message : String(error)}`,\n        );\n        counters.errors++;\n        continue;\n      }\n    }\n\n    clearProgress();\n\n    const model2Label = isExistingMode\n      ? \"Existing AI Tags\"\n      : config.MODEL2_NAME!;\n\n    const shuffleResult: ShuffleResult = {\n      modelA: config.MODEL1_NAME,\n      modelB: model2Label,\n      modelAIsModel1: Math.random() < 0.5,\n    };\n\n    if (!shuffleResult.modelAIsModel1) {\n      shuffleResult.modelA = model2Label;\n      shuffleResult.modelB = config.MODEL1_NAME;\n    }\n\n    const comparison: ComparisonResult = {\n      bookmark,\n      modelA: shuffleResult.modelA,\n      modelATags: shuffleResult.modelAIsModel1 ? model1Tags : model2Tags,\n      modelB: shuffleResult.modelB,\n      modelBTags: shuffleResult.modelAIsModel1 ? model2Tags : model1Tags,\n    };\n\n    displayComparison(i + 1, bookmarks.length, comparison, true);\n\n    const answer = await askQuestion(\n      \"Which tags do you prefer? [1=Model A, 2=Model B, s=skip, q=quit] > \",\n    );\n\n    const normalizedAnswer = answer.toLowerCase();\n\n    if (normalizedAnswer === \"q\" || normalizedAnswer === \"quit\") {\n      console.log(chalk.yellow(\"\\n⏸ Quitting early...\\n\"));\n      break;\n    }\n\n    if (normalizedAnswer === \"1\") {\n      comparison.winner = \"modelA\";\n      if (shuffleResult.modelAIsModel1) {\n        counters.model1Votes++;\n      } else {\n        counters.model2Votes++;\n      }\n      detailedResults.push(comparison);\n    } else if (normalizedAnswer === \"2\") {\n      comparison.winner = \"modelB\";\n      if (shuffleResult.modelAIsModel1) {\n        counters.model2Votes++;\n      } else {\n        counters.model1Votes++;\n      }\n      detailedResults.push(comparison);\n    } else {\n      comparison.winner = \"skip\";\n      counters.skipped++;\n      detailedResults.push(comparison);\n    }\n  }\n\n  close();\n\n  displayFinalResults({\n    model1Name: config.MODEL1_NAME,\n    model2Name: isExistingMode ? \"Existing AI Tags\" : config.MODEL2_NAME!,\n    model1Votes: counters.model1Votes,\n    model2Votes: counters.model2Votes,\n    skipped: counters.skipped,\n    errors: counters.errors,\n    total: counters.total,\n  });\n}\n\nmain().catch((error) => {\n  console.error(chalk.red(`\\n✗ Fatal error: ${error}\\n`));\n  process.exit(1);\n});\n"
  },
  {
    "path": "tools/compare-models/src/inferenceClient.ts",
    "content": "import type { InferenceClient } from \"@karakeep/shared/inference\";\nimport {\n  OpenAIInferenceClient,\n  type OpenAIInferenceConfig,\n} from \"@karakeep/shared/inference\";\nimport { z } from \"zod\";\n\nimport { config } from \"./config\";\n\nexport function createInferenceClient(modelName: string): InferenceClient {\n  const inferenceConfig: OpenAIInferenceConfig = {\n    apiKey: config.OPENAI_API_KEY,\n    baseURL: config.OPENAI_BASE_URL,\n    serviceTier: config.OPENAI_SERVICE_TIER,\n    textModel: modelName,\n    imageModel: modelName, // Use same model for images if needed\n    contextLength: config.INFERENCE_CONTEXT_LENGTH,\n    maxOutputTokens: config.INFERENCE_MAX_OUTPUT_TOKENS,\n    useMaxCompletionTokens: config.INFERENCE_USE_MAX_COMPLETION_TOKENS,\n    outputSchema: \"structured\",\n  };\n\n  return new OpenAIInferenceClient(inferenceConfig);\n}\n\nexport async function inferTags(\n  inferenceClient: InferenceClient,\n  prompt: string,\n): Promise<string[]> {\n  const tagsSchema = z.object({\n    tags: z.array(z.string()),\n  });\n\n  const response = await inferenceClient.inferFromText(prompt, {\n    schema: tagsSchema,\n  });\n\n  const parsed = tagsSchema.safeParse(JSON.parse(response.response));\n  if (!parsed.success) {\n    throw new Error(\n      `Failed to parse model response: ${parsed.error.message}`,\n    );\n  }\n\n  return parsed.data.tags;\n}\n"
  },
  {
    "path": "tools/compare-models/src/interactive.ts",
    "content": "import * as readline from \"node:readline\";\nimport chalk from \"chalk\";\n\nimport type { ComparisonResult } from \"./types\";\n\nconst rl = readline.createInterface({\n  input: process.stdin,\n  output: process.stdout,\n});\n\nexport async function askQuestion(question: string): Promise<string> {\n  return new Promise((resolve) => {\n    rl.question(question, (answer) => {\n      resolve(answer.trim());\n    });\n  });\n}\n\nexport function displayComparison(\n  index: number,\n  total: number,\n  result: ComparisonResult,\n  blind: boolean = true,\n): void {\n  const divider = chalk.gray(\"─\".repeat(80));\n  const header = chalk.bold.cyan(`\\n=== Bookmark ${index}/${total} ===`);\n  const title = chalk.bold.white(result.bookmark.title || \"Untitled\");\n  const url = result.bookmark.content.url\n    ? chalk.gray(result.bookmark.content.url)\n    : \"\";\n  const content = chalk.gray(\n    result.bookmark.content.description\n      ? result.bookmark.content.description.substring(0, 200) + \"...\"\n      : \"\",\n  );\n\n  const modelAName = blind ? \"Model A\" : result.modelA;\n  const modelBName = blind ? \"Model B\" : result.modelB;\n\n  const modelATags = result.modelATags\n    .map((tag) => chalk.green(`  • ${tag}`))\n    .join(\"\\n\");\n  const modelBTags = result.modelBTags\n    .map((tag) => chalk.yellow(`  • ${tag}`))\n    .join(\"\\n\");\n\n  console.log(header);\n  console.log(title);\n  if (url) console.log(url);\n  if (content) console.log(content);\n  console.log(divider);\n  console.log();\n  console.log(chalk.green(`${modelAName}:`));\n  if (modelATags) {\n    console.log(modelATags);\n  } else {\n    console.log(chalk.gray(\"  (no tags)\"));\n  }\n  console.log();\n  console.log(chalk.yellow(`${modelBName}:`));\n  if (modelBTags) {\n    console.log(modelBTags);\n  } else {\n    console.log(chalk.gray(\"  (no tags)\"));\n  }\n  console.log();\n}\n\nexport function displayError(message: string): void {\n  console.log(chalk.red(`\\n✗ Error: ${message}\\n`));\n}\n\nexport function displayProgress(message: string): void {\n  process.stdout.write(chalk.gray(message));\n}\n\nexport function clearProgress(): void {\n  process.stdout.write(\"\\r\\x1b[K\");\n}\n\nexport function close(): void {\n  rl.close();\n}\n\nexport function displayFinalResults(results: {\n  model1Name: string;\n  model2Name: string;\n  model1Votes: number;\n  model2Votes: number;\n  skipped: number;\n  errors: number;\n  total: number;\n}): void {\n  const winner =\n    results.model1Votes > results.model2Votes\n      ? results.model1Name\n      : results.model2Votes > results.model1Votes\n        ? results.model2Name\n        : \"TIE\";\n\n  const divider = chalk.gray(\"─\".repeat(80));\n  const header = chalk.bold.cyan(\"\\n=== FINAL RESULTS ===\");\n  const model1Line = chalk.green(\n    `${results.model1Name}: ${results.model1Votes} votes`,\n  );\n  const model2Line = chalk.yellow(\n    `${results.model2Name}: ${results.model2Votes} votes`,\n  );\n  const skippedLine = chalk.gray(`Skipped: ${results.skipped}`);\n  const errorsLine = chalk.red(`Errors: ${results.errors}`);\n  const totalLine = chalk.bold(`Total bookmarks tested: ${results.total}`);\n  const winnerLine =\n    winner === \"TIE\"\n      ? chalk.bold.cyan(`\\n🏁 RESULT: TIE`)\n      : chalk.bold.green(`\\n🏆 WINNER: ${winner}`);\n\n  console.log(divider);\n  console.log(header);\n  console.log(divider);\n  console.log(model1Line);\n  console.log(model2Line);\n  console.log(skippedLine);\n  console.log(errorsLine);\n  console.log(divider);\n  console.log(totalLine);\n  console.log(winnerLine);\n  console.log(divider);\n}\n"
  },
  {
    "path": "tools/compare-models/src/types.ts",
    "content": "export interface Bookmark {\n  id: string;\n  title: string | null;\n  content: {\n    type: string;\n    title: string;\n    url?: string;\n    text?: string;\n    htmlContent?: string;\n    description?: string;\n  };\n  tags: Array<{ name: string; attachedBy?: \"ai\" | \"human\" }>;\n}\n\nexport interface ModelConfig {\n  name: string;\n  apiKey: string;\n  baseUrl?: string;\n}\n\nexport interface ComparisonResult {\n  bookmark: Bookmark;\n  modelA: string;\n  modelATags: string[];\n  modelB: string;\n  modelBTags: string[];\n  winner?: \"modelA\" | \"modelB\" | \"skip\";\n}\n\nexport interface FinalResults {\n  model1Name: string;\n  model2Name: string;\n  model1Votes: number;\n  model2Votes: number;\n  skipped: number;\n  errors: number;\n  total: number;\n}\n"
  },
  {
    "path": "tools/compare-models/tsconfig.json",
    "content": "{\n  \"$schema\": \"https://json.schemastore.org/tsconfig\",\n  \"extends\": \"@tsconfig/node22/tsconfig.json\",\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"CommonJS\",\n    \"lib\": [\"ES2022\"],\n    \"outDir\": \"dist\",\n    \"rootDir\": \"src\",\n    \"moduleResolution\": \"node\",\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"allowJs\": true,\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": false,\n    \"declaration\": false,\n    \"sourceMap\": false\n  },\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "turbo.json",
    "content": "{\n  \"$schema\": \"https://turborepo.org/schema.json\",\n  \"globalDependencies\": [\n    \"**/.env\"\n  ],\n  \"tasks\": {\n    \"topo\": {\n      \"dependsOn\": [\n        \"^topo\"\n      ]\n    },\n    \"build\": {\n      \"dependsOn\": [\n        \"^build\"\n      ],\n      \"outputs\": [\n        \".next/**\",\n        \"!.next/cache/**\",\n        \"next-env.d.ts\",\n        \".expo/**\",\n        \".output/**\",\n        \".vercel/output/**\",\n        \"dist/**\"\n      ]\n    },\n    \"dev\": {\n      \"persistent\": true,\n      \"cache\": false\n    },\n    \"format\": {\n      \"outputLogs\": \"new-only\"\n    },\n    \"format:fix\": {\n      \"outputLogs\": \"new-only\"\n    },\n    \"lint\": {\n      \"dependsOn\": [\n        \"^topo\"\n      ]\n    },\n    \"lint:fix\": {\n      \"dependsOn\": [\n        \"^topo\"\n      ]\n    },\n    \"typecheck\": {\n      \"dependsOn\": [\n        \"^topo\"\n      ],\n      \"outputs\": [\n        \"node_modules/.cache/tsbuildinfo.json\"\n      ]\n    },\n    \"test\": {\n      \"dependsOn\": [\n        \"^topo\"\n      ],\n      \"cache\": false\n    },\n    \"clean\": {\n      \"cache\": false\n    },\n    \"//#clean\": {\n      \"cache\": false\n    }\n  }\n}\n"
  }
]